tty 及其在远程登录(SSH,telnet等)中的应用

[功能]



打印连接到标准输入的终端的文件名。



[描述]



命令项:



-s, –silent, –quiet: 什么也不打印,只是返回退出状态码。



–help: 打印帮助信息。



–version: 打印版本信息并退出。



返回的状态码:



0 表示标准输入是一个终端。



1 表示标准输入不是一个终端。



2 表示给定的参数是不正确的参数。



3 表示发生了写错误。



[举例]



*查看当前的标准终端文件描述符号:



[quietheart@lv-k etc]$ tty



/dev/pts/6



这里,输入tty之后,输出”/dev/pts/6”就是当前连接的终端对应的文件描述符号,我们可以通过向这个文件,这样会看到当前终端会输出我们写入的数据,例如:



[quietheart@lv-k etc]$ echo “mytest” >/dev/pts/6



mytest



可见,向/dev/pts/6文件写入的内容,直接显示到我们的终端上面了。如果我在机器上面开了多个终端,然后我在别的终端中输入上面的echo命令,那么将会看到对应”/dev/pts/6”的终端上面会显示相应的字符了。



*运行tty什么也不输出,然后查看其退出码:



[quietheart@lv-k etc]$ tty -s



[quietheart@lv-k etc]$ echo $?



0



这里,使用shell的命令”echo $?”来显示上一条命令的退出状态码。状态码的含义如下:



0 表示标准输入是一个终端。



1 表示标准输入不是一个终端。



2 表示给定的参数是不正确的参数。



3 表示发生了写错误。

1,tty(终端设备的统称):



tty一词源于Teletypes,或者teletypewriters,原来指的是电传打字机,是通过串行线用打印机键盘通过阅读和发送信息的东西,后来这东西被键盘与显示器取代,所以现在叫终端比较合适。终端是一种字符型设备,它有多种类型,通常使用tty来简称各种类型的终端设备。



2,pty(虚拟终端):



如果我们远程telnet到主机或使用xterm时,也需要一个终端交互,这就是虚拟终端pty(pseudo-tty)



3,pts/ptmx(pts/ptmx结合使用,进而实现pty):



pts(pseudo-terminal slave)是pty的实现方法,与ptmx(pseudo-terminal master)配合使用实现pty。



*Linux终端:



在Linux系统的设备特殊文件目录/dev/下,终端特殊设备文件一般有以下几种:



1,串行端口终端(/dev/ttySn)



串行端口终端(Serial Port Terminal)是使用计算机串行端口连接的终端设备。计算机把每个串行端口都看作是一个字符设备。有段时间这些串行端口设备通常被称为终端设备,因为那时它的最大用途就是用来连接终端。这些串行端口所对应的设备名称是/dev/tts/0(或/dev/ttyS0), /dev/tts/1(或/dev/ttyS1)等,设备号分别是(4,0), (4,1)等,分别对应于DOS系统下的COM1、COM2等。若要向一个端口发送数据,可以在命令行上把标准输出重定向到这些特殊文件名上即可。例如,在命令行提示符下键入:echo test > /dev/ttyS1会把单词”test”发送到连接在ttyS1(COM2)端口的设备上。可接串口来实验。



2,伪终端(/dev/pty/)



伪终端(Pseudo Terminal)是成对的逻辑终端设备(即master和slave设备, 对master的操作会反映到slave上)。



例如/dev/ptyp3和/dev/ttyp3(或者在设备文件系统中分别是/dev/pty/m3和 /dev/pty/s3)。它们与实际物理设备并不直接相关。如果一个程序把ptyp3(master设备)看作是一个串行端口设备,则它对该端口的读/ 写操作会反映在该逻辑终端设备对应的另一个ttyp3(slave设备)上面。而ttyp3则是另一个程序用于读写操作的逻辑设备。



这样,两个程序就可以通过这种逻辑设备进行互相交流,而其中一个使用ttyp3的程序则认为自己正在与一个串行端口进行通信。这很象是逻辑设备对之间的管道操作。对于ttyp3(s3),任何设计成使用一个串行端口设备的程序都可以使用该逻辑设备。但对于使用ptyp3的程序,则需要专门设计来使用 ptyp3(m3)逻辑设备。



例如,如果某人在网上使用telnet程序连接到你的计算机上,则telnet程序就可能会开始连接到设备 ptyp2(m2)上(一个伪终端端口上)。此时一个getty程序就应该运行在对应的ttyp2(s2)端口上。当telnet从远端获取了一个字符时,该字符就会通过m2、s2传递给 getty程序,而getty程序就会通过s2、m2和telnet程序往网络上返回”login:”字符串信息。这样,登录程序与telnet程序就通过“伪终端”进行通信。通过使用适当的软件,就可以把两个甚至多个伪终端设备连接到同一个物理串行端口上。



在使用设备文件系统 (device filesystem)之前,为了得到大量的伪终端设备特殊文件,使用了比较复杂的文件名命名方式。因为只存在16个ttyp(ttyp0—ttypf) 的设备文件,为了得到更多的逻辑设备对,就使用了象q、r、s等字符来代替p。例如,ttys8和ptys8就是一个伪终端设备对。不过这种命名方式目前仍然在RedHat等Linux系统中使用着。



但Linux系统上的Unix98并不使用上述方法,而使用了”pty master”方式,例如/dev/ptm3。它的对应端则会被自动地创建成/dev/pts/3。这样就可以在需要时提供一个pty伪终端。目录 /dev/pts是一个类型为devpts的文件系统,并且可以在被加载文件系统列表中看到。虽然“文件”/dev/pts/3看上去是设备文件系统中的一项,但其实它完全是一种不同的文件系统。



即: TELNET —> TTYP3(S3: slave) —> PTYP3(M3: master) —> GETTY



*实践:



以下过程是在ubuntu上面的实践结果。



1,虚拟终端/dev/tty和伪终端/dev/pts/



假设5个xterm终端的话,会发现/dev下面有一个/dev/ptmx /和5个/dev/pts/*



而如果用”[Ctrl][Alt]F1”进入一个终端的话,就会发现/dev下面多出一个/dev/tty1



2,查看终端的方法:



使用命令”tty”可以查看当前对应的终端,命令”ps -ax”可以查看所有程序以及对应的终端。通过这个命令,发现如果多开一个screen程序,或者用telnet登陆之后,那么机器上面就会多一个/dev/pts/*。可知,一般伪终端用户模拟终端程序。



3,向指定终端发送信息:



如果运行”echo hello >/dev/tty2”,



那么”[Ctrl][Alt]F2”打开”/dev/tty2”对应的终端,就会发现刚才发送的hello了。



注意,如果发送”echo -n ls >/dev/tty2”那么会显示ls,但是回车之后,并没有执行ls。可见:ttyN是代表linux的纯命令行终端,而写到这个终端的字符串只是做为这个终端的输出,并没有当做终端的输入处理。



如果运行”echo hello >/dev/pts/2”,



那么,就可以在xterm的第二个窗口看到hello了。可知,/dev/pts/n是Xwindows模式下的伪终端.



嵌入式linux操作系统,可以通过串口(Console)登录。为了方便使用,需要寻找通过网线远程登录的方法。最初的想法是SSH,不过板子的ROM太小,存不了体积庞大庞大的OpenSSH套装。后来换用了telnet,直接拿busybox的telnetd做服务器,效果很好。



后来有一天,发现了Linux中有一个直接建立TCP连接的工具:nc 。在服务端使用nc -l 端口号 来进行监听,在客户端使用nc IP地址 端口号来建立连接。建立连接后,nc会把从stdin读入的字节流发送给另一方,把接收到的字节流写入stdout中。配合方便的管道操作,不正可以将shell的输入/输出传送到远端机器上吗?于是在Ubuntu中实验操作如下(之后发现这种操作叫做“反弹shell”):



打开一个终端A,输入命令



mkfifo /tmp/p # 创建临时管道
sh -i </tmp/p |& nc -l 2333 >/tmp/p



确实,它的标准输入输出都是管道。这会带来一个问题,需要操纵tty的一些命令,比如vi、less、sudo等都无法正常使用(可以动手试试效果怎么样)。更为要命的是,在终端B中按下Ctrl+C这样的控制键,内核把结束信号发送给了客户端nc,而不是远程的程序!



Ctrl+C直接杀死nc,结束了会话。对比telnet,我们的登录系统还缺少什么东西。这就是伪终端(pseudoterminal)。



二、了解伪终端



  1. 终端和它的作用



历史上,终端(有时被成为tty,tele typewriter)是用户访问计算机主机的硬件设备,可以理解为一个显示器和一个键盘的组合。



现代Linux里面比较接近此概念的是(一系列)虚拟控制台(virtual console)。在Ubuntu等发行版本中按下Ctrl+Alt+F1(或F2, F3, …)即可切换到相应控制台下。/dev/tty1等文件是这些硬件在linux下的设备文件。程序通过这些文件的读写实现对控制台的读写,通过ioctl实现对硬件参数的设置。
终端还可以指代设备文件,实现软件接口。比如常见的/dev/tty1文件,还有/dev/pts目录下的所有文件。



对终端设备文件进行读写,能够从键盘读取输入,从显示器进行输出,实现交互式的输入输出
linux中的每个进程有一个“控制终端(control terminal)”的属性(取值为设备文件),用于实现作业控制。在终端上输入Ctrl+C、Ctrl+Z,则以该终端为控制终端的前台进程组会收到终止、暂停的信号。
对终端设备进行ioctl操作,可以实现终端相关的硬件参数设置。login、sudo的不显示密码,都离不开对终端设备的操作。
终端还可以指代“终端模拟器”。终端模拟器是应用程序,用于模拟一个终端。它一般是GUI程序,带有窗口。从窗口输入的字符作为模拟键盘的输入,在窗口上打印的字符作为模拟显示器的输出。终端模拟器还需要创建模拟的终端设备(如/dev/pts/1),用于当做命令行进程(CLI进程)的输入输出、控制终端。当键盘键入一个字符,它要让CLI进程从终端设备中读到这个字符,当CLI进程写入终端设备时,终端模拟器要读到并显示出来。



终端模拟器的这个需求,恰恰和telnet这种远程登录服务器的需求相似。telnet服务器也要创建模拟的终端设备,用于当做命令行进程(CLI进程)的输入输出、控制终端。当从网络收到一个字符,它要让CLI进程从终端设备中读到这个字符,当CLI进程写入终端设备时,telnet要把输出发送到网络。



这种共同的需求在linux中有一个统一实现——伪终端(pseudoterminal)。没错,上面的/dev/pts/文件夹里的以数字命名的文件就是伪终端的设备文件。




  1. 伪终端的介绍
    通过man pts可以查阅linux对伪终端的介绍。伪终端是伪终端master和伪终端slave(终端设备文件)这一对字符设备。/dev/ptmx是用于创建一对master、slave的文件。当一个进程打开它时,获得了一个master的文件描述符(file descriptor),同时在/dev/pts下创建了一个slave设备文件。



master端是更接近用户显示器、键盘的一端,slave端是在虚拟终端上运行的CLI(Command Line Interface,命令行接口)程序。Linux的伪终端驱动程序,会把“master端(如键盘)写入的数据”转发给slave端供程序输入,把“程序写入slave端的数据”转发给master端供(显示器驱动等)读取。



我们打开的“终端”桌面程序,其实是一种终端模拟器。当终端模拟器运行时,它通过/dev/ptmx打开master端,创建了一个伪终端对,并让shell运行在slave端。当用户在终端模拟器中按下键盘按键时,它产生字节流并写入master中,shell便可从slave中读取输入;shell和它的子程序,将输出内容写入slave中,由终端模拟器负责将字符打印到窗口中。



(终端模拟器的显示原理就不在这里展开了,这里认为键盘按键形成一列字节流、向显示器输出字节流后便打印到屏幕上)



linux中为什么要提出伪终端这个概念呢?shell等命令行程序不可以直接从显示器和键盘读取数据吗?为了同屏运行多个终端模拟器、并实现远程登录,还真不能让bash直接跨过伪终端这一层。在操作系统的一大思想——虚拟化的指导下,为多个终端模拟器、远程用户分配多个虚拟的终端是有必要的。上图中的shell使用的slave端就是一个虚拟化的终端。master端是模拟用户一端的交互。之所以称为虚拟化的终端,它除了转发数据流外,还要有点终端的样子。




  1. 作为终端的伪终端
    最为一个虚拟的终端,每一个伪终端里面封装了一个终端驱动,让它能做到这些事情:



为程序提供一些输入输出模式的帮助,比如输入密码时隐藏字符
为用户提供对进程的控制,比如按下Ctrl+C结束前台进程
对,这些就是转发数据之外的控制。



终端的属性:回显控制和行控制
当用户按下一个按键时,字符会出现在屏幕上。这可不是CLI进程写回来的。不信的话可以在终端里运行cat,随便输入些什么按回车。第二行是cat返回来的,第一行正是终端的特性。



终端驱动里存储了一个状态——回显控制:是否将写入master的字符再次送回master的读端(显示器)。默认情况下这个是启用的。在命令行里可以使用stty来更改终端的状态。比如在终端中运行



stty -echo
则会关掉当前终端的回显。这时按下按键,已经没有字符显示出来了。输入ls等命令,能够看到shell正常接收到我们的命令(此时回车并没有显示出来)。这时cat后,盲打一些文字,按下回车后看到只有一条文字了。



除了用户通过命令行方式,CLI的程序还能通过系统调用来设置终端的回显,比如login,sudo等程序就是通过暂时关闭回显来隐藏密码的。具体方式是在slave的文件描述符上调用ioctl函数(参考man tty_ioctl),不过推荐使用更友好的tcsetattr函数。详细设置可查阅man tcsetattr。



另外,终端驱动还提供有行缓冲功能。还是以cat为例:当我们输入文字,在键入回车之前,cat并不能读取到我们输入的字符。这里的cat的行为可以理解为逐字符读写:



while(read(0, &c, 1) > 0) //read from stdin, while not EOF
write(1, &c, 1); //write to stdout
是谁阻止cat及时读入字符了呢?其实是终端驱动。它默认开启了一个行缓冲区,这样等程序要调用read系统调用时,先让程序阻塞着(blocked),等用户输入一整行后,才解除阻塞。我们可以使用下列命令将行缓存大小设置为1:



stty min 1 -icanon
这时,运行cat,尝试输入文字。每输入一个字符,能够立即返回一个字符。(把min改为time,还能设置输入字符最长被阻塞1秒)



这些终端的状态属性信息还有很多,比如设置终端的宽度、高度等。具体可以参考man stty。



特殊控制字符
特殊控制字符,是指Ctrl和其他键的组合。如Ctrl+C、Ctrl+Z等等。用户按下这些按键,终端模拟器(键盘)会在master端写入一个字节。规则是:Ctrl+字母得到的字节是(大写)字母的ascii码减去0x40。比如Ctrl+C是0x03,Ctrl+Z是0x1A



驱动收到这些特殊字符,并不会像收到正常字节那样处理。在echo的时候,它返回两个可见字符。比如键入Ctrl+C(0x03),就会回显^和C(0x5E 0x03)两个字符。更重要的是,驱动将会拦截某些控制字符,他们不会被转发给slave端,而是触发作业控制(job control)的规则:向前台进程组发送SIGINT信号。



要想绕过这一机制,我们可以使用stty的一些设置。下面的命令能够同时关闭控制字符的特殊语义、设置行缓冲大小为1:



stty raw
然后,运行cat命令,我们键入的所有字符,包括控制字符Ctrl+C(0x03),都会成功传递给cat,并且被原样返回。(可以试试上下左右、回车键的效果)



telnet和SSH等远程登录的原理了。每次用户通过客户端连接服务端的时候,服务端创建一个伪终端master、slave字符设备对,在slave端运行login程序,将master端的输入输出通过网络传送至客户端。至于客户端,则将从网络收到的信息直接关联到键盘/显示器上。



服务端②:创建伪终端,并将master重定向至nc
按照man pts中的介绍,要创建master、slave对,只需要用open系统调用打开/dev/ptmx文件,即可得到master的文件描述符。同时,在/dev/pts中已经创建了一个设备文件,表示slave端。但是,为了能让其他进程(login,shell)打开slave端,需要按照手册介绍来调用两个函数:



Before opening the pseudoterminal slave, you must pass the master’s file descriptor to grantpt(3) and unlockpt(3).



具体信息可以查阅man 3 grantpt,man 3 unlockpt文档。



我们可以直接关闭(man 2 close)终端创建进程的0和1号文件描述符,把master端的文件描述符拷贝(man 2 dup)到0和1号,然后把当前进程刷成nc(man 3 exec)。这虽然是比较优雅的做法,但比较复杂。而且当没有进程打开slave的时候,nc从master处读不到数据(read返回0),会认为是EOF而结束连接。所以这里用一个笨办法:将所有从master读到的数据通过管道送给nc,将所有从nc得到的数据写入master。我们需要两个线程完成这件事。



//ptmxtest.c



//先是一些头文件和函数声明
#include
#define _XOPEN_SOURCE
#include
#include
#include<sys/types.h>
#include<sys/stat.h>
#include
#include<sys/ioctl.h>



/* Chown the slave to the calling user. */
extern int grantpt (int __fd) __THROW;



/* Release an internal lock so the slave can be opened.
Call after grantpt(). */
extern int unlockpt (int __fd) __THROW;



/* Return the pathname of the pseudo terminal slave associated with
the master FD is open on, or NULL on errors.
The returned storage is good until the next call to this function. */
extern char *ptsname (int __fd) __THROW __wur;



char buf[1]={‘\0’}; //创建缓冲区,这里只需要大小为1字节
int main()
{
//创建master、slave对并解锁slave字符设备文件
int mfd = open(“/dev/ptmx”, O_RDWR);
grantpt(mfd);
unlockpt(mfd);
//查询并在控制台打印slave文件位置
fprintf(stderr,”%s\n”,ptsname(mfd));



int pid=fork();//分为两个进程
if(pid)//父进程从master读字节,并写入标准输出中
{
while(1)
{
if(read(mfd,buf,1)>0)
write(1,buf,1);
else
sleep(1);
}
}
else//子进程从标准输入读字节,并写入master中
{
while(1)
{
if(read(0,buf,1)>0)
write(mfd,buf,1);
else
sleep(1);
}
}

return 0; } 将文件保存后,打开一个终端(称为终端A),运行下列命令,在命令行中建立此程序与nc的通道:


gcc -o ptmxtest ptmxtest.c
mkfifo /tmp/p
nc -l 2333 </tmp/p | ./ptmxtest >/tmp/p
至此,图中的②构建完毕,已经有一个nc在监听2333端口,它的输入输出通过管道送到ptmxtest程序中,ptmxtest又将这些信息搬运给master端。



在我的Ubuntu中运行命令后显示,创建的slave设备文件是/dev/pts/20。



服务端①:将login程序与终端关联起来
在图中①处的地方,需要将login与伪终端的输入输出关联起来。这一点通过输入输出重定向即可完成。不过,想要实现Ctrl+C等作业控制,还需要更多的设置。这涉及到一些Linux的进程管理的知识(感兴趣的可以去搜索“进程、进程组、会话、控制终端”等关键字)。



一个进程与终端的联系,不仅取决于它的输入输出,还有它的控制终端(Controlling terminal,可通过tty命令查询,通过/dev/tty打开)。简单地说,进程控制终端是谁,谁才能向进程发送控制信号。这里要将login的控制终端设为伪终端,具体说是slave设备文件才行。



设置控制终端需要使用终端设备的ioctl来实现。查看man tty_ioctl,可以找到相关信息:



Controlling terminal



TIOCSCTTY int arg
Make the given terminal the controlling terminal of the calling process. The calling process must be a session leader and not have a controlling terminal already. For this case, arg should be specified as zero.





TIOCNOTTY void
If the given terminal was the controlling terminal of the calling process, give up this controlling terminal. …



比较重要的信息是,我们可以指定TIOCSCTTY参数来设置控制终端,但它要求调用者是没有控制终端的会话组长(Session leader)。所以要先指定TIOCNOTTY参数来放弃当前控制终端,并用setsid函数(man 2 setsid)创建新的会话并设置自己为组长。



我们将login包装一层,完成上面的操作,得到新的程序mylogin:



//mylogin.c



#include
#define _XOPEN_SOURCE
#include
#include
#include<sys/types.h>
#include<sys/stat.h>
#include
#include
#include<sys/ioctl.h>



int main(int argc, char *argv[])
{
int old=open(“/dev/tty”,O_RDWR); //打开当前控制终端
ioctl(old, TIOCNOTTY); //放弃当前控制终端



//根据"man 2 setsid"的说明,调用setsid的进程不能是进程组组长(从bash中运行的命令是组长),故fork出一个子进程,让组长结束,子进程脱离进程组成为新的会话组长
int pid=fork();
if(pid==0){
setsid(); //子进程成为会话组长
perror("setsid"); //显示setsid是否成功
ioctl(0, TIOCSCTTY, 0); //这时可以设置新的控制终端了,设置控制终端为stdin
execv("/bin/login", argv); //把当前进程刷成login
}
return 0; } 保存文件后,打开一个终端(称为终端B),编译运行:


gcc -o mylogin mylogin.c
#假设这里的slave设备是/dev/pts/20
#因为login要读取密码文件,需要用root权限执行
sudo ./mylogin </dev/pts/20 >/dev/pts/20 2>&1
该命令将实验图中①处的slave设备,重定向至mylogin的stdin、stdout和stderr。在程序执行时,会将控制终端设置为伪终端,然后执行login。至此,服务端全部建立完毕。



客户端:连接远程机器,配置本地终端
客户端处于实验图的③处。打开新的终端(终端C),这里简单地使用nc连接远程socket,并且nc的输入输出重定向至键盘、显示器即可。但是要注意,nc是运行在终端C上的,而终端C的默认属性会拦截字符Ctrl+C、使用行缓冲区域。这样nc的输入输出其实并不直接是键盘、显示器。为此,我们先设置终端C的属性,再运行nc:



stty raw -echo
nc localhost 2333 #该行没有回显,要摸黑输入
然后,在终端C中出现了我们打印的setsid的信息,和login的提示符。在终端C中,使用键盘可以正常登录,得到shell的提示符。使用tty命令能够看到当前shell使用的控制终端是/dev/pts/20,也就是我们创建的伪终端。输入w命令可以看到系统中登录的用户和登录终端。
至此为止,我们实现了类似telnet的远程登录。



结语
linux中终端驱动本身有回显、行缓存、作业控制等丰富的属性,在此基础上实现的伪终端在终端模拟器、远程登录等场合下能够得到多种应用。



在实验过程中也牵扯到进程控制、输入输出重定向、网络通信这么多的知识,更体现出linux的复杂精致的结构。我感觉,linux 就像一个包罗万象、又自成体统的小宇宙,它采用独特的虚拟化技术,灵活的模块化和重用机制,虚拟出各种设备,实现了驱动程序的随意拼插。在这里,所有模块都得到了充分的利用,并能够像变形金刚那样对各类需求提出面面俱到的解决方案。



//ptmxtest.c



//先是一些头文件和函数声明
#include
#define _XOPEN_SOURCE
#include
#include
#include<sys/types.h>
#include<sys/stat.h>
#include
#include<sys/ioctl.h>



/* Chown the slave to the calling user. */
extern int grantpt (int __fd) __THROW;



/* Release an internal lock so the slave can be opened.
Call after grantpt(). */
extern int unlockpt (int __fd) __THROW;



/* Return the pathname of the pseudo terminal slave associated with
the master FD is open on, or NULL on errors.
The returned storage is good until the next call to this function. */
extern char *ptsname (int __fd) __THROW __wur;



char buf[1]={‘\0’}; //创建缓冲区,这里只需要大小为1字节
int main()
{
//创建master、slave对并解锁slave字符设备文件
int mfd = open(“/dev/ptmx”, O_RDWR);
grantpt(mfd);
unlockpt(mfd);
//查询并在控制台打印slave文件位置
fprintf(stderr,”%s\n”,ptsname(mfd));



int pid=fork();//分为两个进程
if(pid)//父进程从master读字节,并写入标准输出中
{
while(1)
{
if(read(mfd,buf,1)>0)
write(1,buf,1);
else
sleep(1);
}
}
else//子进程从标准输入读字节,并写入master中
{
while(1)
{
if(read(0,buf,1)>0)
write(mfd,buf,1);
else
sleep(1);
}
}

return 0; } 将文件保存后,打开一个终端(称为终端A),运行下列命令,在命令行中建立此程序与nc的通道:


gcc -o ptmxtest ptmxtest.c
mkfifo /tmp/p
nc -l 2333 </tmp/p | ./ptmxtest >/tmp/p
至此,图中的②构建完毕,已经有一个nc在监听2333端口,它的输入输出通过管道送到ptmxtest程序中,ptmxtest又将这些信息搬运给master端。



在我的Ubuntu中运行命令后显示,创建的slave设备文件是/dev/pts/20。



服务端①:将login程序与终端关联起来
在图中①处的地方,需要将login与伪终端的输入输出关联起来。这一点通过输入输出重定向即可完成。不过,想要实现Ctrl+C等作业控制,还需要更多的设置。这涉及到一些Linux的进程管理的知识(感兴趣的可以去搜索“进程、进程组、会话、控制终端”等关键字)。



一个进程与终端的联系,不仅取决于它的输入输出,还有它的控制终端(Controlling terminal,可通过tty命令查询,通过/dev/tty打开)。简单地说,进程控制终端是谁,谁才能向进程发送控制信号。这里要将login的控制终端设为伪终端,具体说是slave设备文件才行。



设置控制终端需要使用终端设备的ioctl来实现。查看man tty_ioctl,可以找到相关信息:



Controlling terminal



TIOCSCTTY int arg
Make the given terminal the controlling terminal of the calling process. The calling process must be a session leader and not have a controlling terminal already. For this case, arg should be specified as zero.





TIOCNOTTY void
If the given terminal was the controlling terminal of the calling process, give up this controlling terminal. …



比较重要的信息是,我们可以指定TIOCSCTTY参数来设置控制终端,但它要求调用者是没有控制终端的会话组长(Session leader)。所以要先指定TIOCNOTTY参数来放弃当前控制终端,并用setsid函数(man 2 setsid)创建新的会话并设置自己为组长。



我们将login包装一层,完成上面的操作,得到新的程序mylogin:



//mylogin.c



#include
#define _XOPEN_SOURCE
#include
#include
#include<sys/types.h>
#include<sys/stat.h>
#include
#include
#include<sys/ioctl.h>



int main(int argc, char *argv[])
{
int old=open(“/dev/tty”,O_RDWR); //打开当前控制终端
ioctl(old, TIOCNOTTY); //放弃当前控制终端



//根据"man 2 setsid"的说明,调用setsid的进程不能是进程组组长(从bash中运行的命令是组长),故fork出一个子进程,让组长结束,子进程脱离进程组成为新的会话组长
int pid=fork();
if(pid==0){
setsid(); //子进程成为会话组长
perror("setsid"); //显示setsid是否成功
ioctl(0, TIOCSCTTY, 0); //这时可以设置新的控制终端了,设置控制终端为stdin
execv("/bin/login", argv); //把当前进程刷成login
}
return 0; } 保存文件后,打开一个终端(称为终端B),编译运行:


gcc -o mylogin mylogin.c
#假设这里的slave设备是/dev/pts/20
#因为login要读取密码文件,需要用root权限执行
sudo ./mylogin </dev/pts/20 >/dev/pts/20 2>&1
该命令将实验图中①处的slave设备,重定向至mylogin的stdin、stdout和stderr。在程序执行时,会将控制终端设置为伪终端,然后执行login。至此,服务端全部建立完毕。



客户端:连接远程机器,配置本地终端
客户端处于实验图的③处。打开新的终端(终端C),这里简单地使用nc连接远程socket,并且nc的输入输出重定向至键盘、显示器即可。但是要注意,nc是运行在终端C上的,而终端C的默认属性会拦截字符Ctrl+C、使用行缓冲区域。这样nc的输入输出其实并不直接是键盘、显示器。为此,我们先设置终端C的属性,再运行nc:



stty raw -echo
nc localhost 2333 #该行没有回显,要摸黑输入
然后,在终端C中出现了我们打印的setsid的信息,和login的提示符。在终端C中,使用键盘可以正常登录,得到shell的提示符。使用tty命令能够看到当前shell使用的控制终端是/dev/pts/20,也就是我们创建的伪终端。输入w命令可以看到系统中登录的用户和登录终端。



至此为止,我们实现了类似telnet的远程登录。



结语
linux中终端驱动本身有回显、行缓存、作业控制等丰富的属性,在此基础上实现的伪终端在终端模拟器、远程登录等场合下能够得到多种应用。



在实验过程中也牵扯到进程控制、输入输出重定向、网络通信这么多的知识,更体现出linux的复杂精致的结构。我感觉,linux 就像一个包罗万象、又自成体统的小宇宙,它采用独特的虚拟化技术,灵活的模块化和重用机制,虚拟出各种设备,实现了驱动程序的随意拼插。在这里,所有模块都得到了充分的利用,并能够像变形金刚那样对各类需求提出面面俱到的解决方案。



用户通过terminal输入,通过电线连到UART (Universal Asynchronous Receiver and Transmitter,通用异步收发器),然后由它的driver来处理一些包括奇偶校验,流控制在内的数据传输。之后会有两个步骤:



Line discipline: 提供了一个editing buffer和一些基本的编辑命令(如backspace, erase word, clear line, reprint等)。当然这些在line discipline中设置的指令可以在应用程序中被disable。这里有两种mode:前面一种默认的叫cooked (or canonical) mode,它规定了一些默认的对backspace等指令的行为;而一些和用户交互的应用(如编辑器,shell这些依赖于光标和readline的应用)则会把其设置成raw mode,然后由自己来处理这些行编辑命令。Line discipline还有一些控制echo等的选项,这些会在之后更详细地描述。另外,对于每一种串行设备只有一种相应的line discipline(默认的叫N_TTY),其它还有一些用于处理网络包(ppp, IrDA, serial mice)的line discipline,这里就不介绍了。



TTY driver: 主要用于处理比如kill、suspend进程,以及限制输入只能被定向到foreground进程等,其被实现在drivers/char/tty_io.c中。另外和line discipline相似的,TTY driver是一种被动模式,只会通过其它进程或者中断处理函数来调用。



于是我们平常所说的TTY设备其实主要就是由UART driver, line discipline和TTY driver这三部分组成的。用户可以通过login来成为某个tty的owner,然后通过对/dev下tty文件的读写来操作相应的TTY设备。



一个进程可能会有5种状态:



R Running or runnable (on run queue)
D Uninterruptible sleep (waiting for some event)
S Interruptible sleep (waiting for some event or signal)
T Stopped, either by a job control signal or because it is being traced by a debugger.
Z Zombie process, terminated but not yet reaped by its parent.



运行命令ps



可以通过STAT来查看当前的状态。其中s表示这是一个session leader(一般是一个shell),+表示这是一个foreground进程。另外还可以看到每个进程都对应了一个tty(TTY那列),这里显示的都是pts(后面会提到)。



一般driver中的TTY是通过signal来和process进行异步的通信的,我们可以通过运行kill -l来查看系统中有哪些signal:



有关的signals:SIGHUP, SIGINT, SIGQUIT, SIGPIPE, SIGCHLD, SIGSTOP, SIGCONT, SIGTSTP, SIGTTIN, SIGTTOU and SIGWINCH.



假设你在vim上编辑一个文件,光标停在屏幕的中间某个位置,而vim这时正在运行着一些操作(比如查找等)。这个时候你按下一个Ctrl-z,由于vim的line discipline有一条规则来拦截这个Ctrl-z,你不需要等待vim运行完它的查找操作再从TTY中读取Ctrl-z这个字符(ASCII 26),line discipline会直接发一个SIGTSTP到foreground进程组(包含了vim这个进程)。由于vim本身注册了SIGTSTP的处理函数,于是内核就会开始运行这个处理函数,它可能会把光标移到屏幕的最后,往TTY设备中写一些相关的控制信息,再向自己的进程组发送一个SIGSTOP,这个时候vim进程就被stop了。session leader(可能是/bin/bash)通过SIGCHLD收到vim进程被suspend的信息,向TTY设备读取当前的一些配置文件(之后恢复foreground进程用的),然后通过ioctl使自己成为该TTY的foreground进程,在屏幕上打印一些提示信息,比如



[1] + suspended vim xx
告诉用户这个vim进程被suspend了。



这个时候,如果你运行ps,你会看到vim进程是stopped的状态(“T”)。如果我们通过bg或者kill SIGCONT pid来唤醒它,vim会开始执行它的SIGCONT处理函数,该处理函数可能会需要通过写TTY设备重新画GUI界面,但是由于vim这个时候是一个background的进程,TTY设备不允许一个background进程的输出,相反,它会发送一个SIGTTOU给vim,再次stop它,再通过SIGCHLD把执行权交给session leader,打印提示信息:



[1] + continued vim xx
[1] + suspended (tty output) vim xx
而如果我们用fg命令,shell就会首先恢复之前保存的配置文件,告诉TTY driver从现在开始要把vim作为foreground进程来看待,最后通过发送SIGCONT来重新开始运行vim。



TTY设备的配置
可以通过命令tty来查看你当前的shell对应于那个tty:



shell tty



另外,我们也可以通过命令行工具stty来操作TTY设备:



stty -a



-a告诉stty打印所有当前shell对应的TTY的所有设置,当然也可以通过-F来打印其它TTY的设置(参看stty文档)。在这些设置中有一些是和UART相关的参数(比如speed),一些是和line discipline相关的(比如第二行的输出),还有一些是用于job control。我们来一一解释:



speed 9600 baud; rows 58; columns 204; line = 0;
Attribute Related part Description
speed UART The baud rate. Ignored for pseudo terminals.
rows,columns TTY driver Somebody’s idea of the size, in characters, of the terminal attached to this TTY device.
line Line discipline The line discipline attached to the TTY device. 0 is N_TTY. Listed in /proc/tty/ldiscs.
比如我们可以尝试下:开一个xterm(通过tty得到其TTY设备为/dev/pts/2),运行stty -a得到它对应的row(59)和columns(207),打开一个vim,vim会根据TTY设备当前的rows和columns设置来填充窗口。然后我们在另外一个shell中输入:



stty -F /dev/pts/2 rows 30
它会更新内核内存中相应TTY(/dev/pts/2)的数据结构,然后发送一个SIGWINCH给vim,使得其重画界面。然后你再去看原来那个vim的窗口,发现它的row变成了原来的一半!



stty -a的第二行列出了一些特殊字符:



intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = M-^?; eol2 = M-^?; swtch = ; start = ^Q; stop = ; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O; min = 1; time = 0;
我们再来做一个实验:



打开一个新的xterm,输入:



stty intr o
我们用o,代替了Ctrl-c,来向foreground发送SIGINT,于是之后你按Ctrl-c将不会中断foreground进程,而如果你开一个cat,输入”hello”,则会在按下’o’的时候发现cat被中断了!



最后stty -a列出了一系列的”switch”,它们的排序是没有特定顺序的,一些是UART相关的,一些是会影响line discipline的,等等。-表示这个switch是被关闭的。这里简单介绍几个:



icanon表示使用canonical(line-based)模式,如果你把它关掉:



stty -icanon; cat
那么你现在输入一些字符(比如backspace)将会不再和之前那样work了。



echo表示开启echo模式,如果你把它给关了:



stty -echo; cat
你如果再输入,由于echo模式没了,屏幕上将不会同步打印出你输入的字符了。不过由于我们现在处于cooked(canonical)模式,和line editing相关的东西还是有用的,一旦你敲了enter键,line discipline会把编辑好的buffer传递给cat,再整行打印出来。



tostop可以控制background的job是否被允许向终端写:



stty tostop; (sleep 5; echo hello, world) &
5秒之后, 该job会尝试着向TTY进行写,由于tostop被设置了,所以不允许background向TTY进行写,像前面提到的那样,shell输出:



[1] + continued (; sleep 5; echo hello, world; )
[1] + suspended (tty output) (; sleep 5; echo hello, world)
而如果我们运行:



stty -tostop; (sleep 5; echo hello, world) &
5秒后将会打印出”hello world“,并输出:



[1] + done (; sleep 5; echo hello, world; )
前面讲了一些关于TTY的历史,原理,配置等,接下来我们来说下tty,pty,pts,ttys,console等的区别,主要参考和摘录了Tekkaman Ninja的博文:



现在的个人计算机一般只有一个控制台,没有终端。当然愿意的话,可以在串口上连一两台物理终端。但是Linux按POSIX标准把个人计算机当成小型机来用,在控制台上通过getty软件虚拟了六个字符哑终端(或者叫虚拟控制台终端tty1-tty6,数量可以在/etc/inittab里自己调整)和一个图型终端,在虚拟图形终端中又可以通过软件(如screen,tmux)再虚拟无限多个伪终端(pts/0等)。但这全是虚拟的,虽然用起来一样,但实际上没有物理实体。所以在个人计算机上,只有一个实际的控制台,没有终端,所有终端都是在控制台上用软件模拟的。



系统控制台 —— /dev/console



/dev/console是系统控制台,是与操作系统交互的设备。系统所产生的信息会发送到该设备上。平时我们看到的PC只有一个屏幕和键盘,它其实就是控制台。目前只有在单用户模式下,才允许用户登录控制台/dev/console。(可以在单用户模式下输入tty命令进行确认)。



console有缓冲的概念,为内核提供打印输出。内核把要打印的内容装入缓冲区,然后由console来决定打印到哪里(比如是tty0还是ttySn等)。



某些情况下console和tty0是一致的,就是当前所使用的是虚拟终端,所以有些资料中称/dev/console是到/dev/tty0的符号链接,但是这样说现在看来是不对的:根据内核文档,在2.1.71之前,/dev/console根据不同系统设定,符号链接到/dev/tty0或者其他tty上,在2.1.71版本之后则完全由内核代码内部控制它的映射。



当前控制台 —— /dev/tty
这是应用程序中的概念,如果当前进程有控制终端,那么/dev/tty就是当前进程控制台的设备文件。对于你登录的shell,/dev/tty就是你使用的控制台,设备号是(5,0)。不过它并不指任何物理意义上的控制台,/dev/tty会映射到当前设备(使用命令“tty”可以查看它具体对应哪个实际物理控制台设备)。输出到/dev/tty的内容只会显示在当前工作终端上(无论是登录在ttyn中还是pty中)。你如果在控制台界面下(即字符界面下)那么dev/tty就是映射到dev/tty1-6之间的一个(取决于你当前的控制台号),但是如果你现在是在图形界面(Xwindows),那么你会发现现在的/dev/tty映射到的是/dev/pts的伪终端上。



你可以输入命令tty,将显示当前映射终端如:/dev/tty1或者/dev/pts/0等。也可以使用命令ps l来查看其他进程与哪个控制终端相连。



虚拟控制台 —— /dev/ttyn
/dev/ttyn是进程虚拟控制台,他们共享同一个真实的物理控制台。



如果在进程里打开一个这样的文件且该文件不是其他进程的控制台时,那该文件就是这个进程的控制台。进程printf数据会输出到这里。在PC上,用户可以使用alt+Fn切换控制台,看起来感觉存在多个屏幕,这种虚拟控制台对应tty1~n。



还有一个比较特殊的/dev/tty0,他代表当前虚拟控制台,是当前所使用虚拟控制台的一个别名。因此不管当前正在使用哪个虚拟控制台(注意:这里是虚拟控制台,不包括伪终端),系统信息都会发送到/dev/tty0上。只有系统或超级用户root可以向/dev/tty0进行写操作。tty0是系统自动打开的,但不用于用户登录。



伪终端 —— pty(pseudo-tty)
伪终端(Pseudo Terminal)是终端的发展,为满足现在需求(比如网络登陆、xwindow窗口的管理)。它是成对出现的逻辑终端设备(即master和slave设备,对master的操作会反映到slave上)。它多用于模拟终端程序,是远程登陆(telnet、ssh、xterm等)后创建的控制台设备。



历史上,有两套伪终端软件接口:



BSD接口:较简单,master为/dev/pty[p-za-e][0-9a-f];slave为/dev/tty[p-za-e][0-9a-f],它们都是配对的出现的。例如/dev/ptyp3和/dev/ttyp3。但由于在编程时要找到一个合适的终端需要逐个尝试,所以逐渐被放弃。
Unix 98接口:使用一个/dev/ptmx作为master设备,在每次打开操作时会得到一个master设备fd,并在/dev/pts/目录下得到一个slave设备(如/dev/pts/3),这样就避免了逐个尝试的麻烦。由于可能有好几千个用户登陆,所以/dev/pts/是动态生成的。第一个用户登陆,设备文件为/dev/pts/0,第二个为/dev/pts/1,以此类推。它们并不与实际物理设备直接相关。现在大多数系统是通过此接口实现pty。
我们在X Window下打开的终端或使用telnet或ssh等方式登录Linux主机,此时均通过pty设备。例如,如果某人在网上使用telnet程序连接到你的计算机上,则telnet程序就可能会打开/dev/ptmx设备获取一个fd。此时一个getty程序就应该运行在对应的/dev/pts/
上。当telnet从远端获取了一个字符时,该字符就会通过ptmx、pts/传递给 getty程序,而getty程序就会通过pts/、ptmx和telnet程序往网络上返回“login:”字符串信息。这样,登录程序与telnet程序就通过伪终端进行通信。



telnet<—>/dev/ptmx(master)<—>pts/(slave)<—>getty
如果一个程序把pts/
看作是一个串行端口设备,则它对该端口的读/写操作会反映在该逻辑终端设备对的另一个/dev/ptmx上,而/dev/ptmx则是另一个程序用于读写操作的逻辑设备。这样,两个程序就可以通过这种逻辑设备进行互相交流,这很象是逻辑设备对之间的管道操作。对于pts/*,任何设计成使用一个串行端口设备的程序都可以使用该逻辑设备。但对于使用/dev/ptmx的程序,则需要专门设计来使用/dev/ptmx逻辑设备。



串口终端 —— /dev/ttySn
串行端口终端是使用计算机串行端口连接的终端设备。计算机把每个串行端口都看作是一个字符设备。有段时间串行端口设备通常被称为终端设备,那时它的最大用途就是用来连接终端,所以这些串行端口所对应的设备名称是/dev/tts/0(或/dev/ttyS0)、/dev/tts/1(或/dev/ttyS1)等,设备号分别是(4,0)、(4,1)等(对应于win系统下的COM1、COM2等)。若要向一个端口发送数据,可以在命令行上把标准输出重定向到这些特殊文件名上即可。



例如,在命令行提示符下键入echo hello > /dev/ttyS1会把“hello”发送到连接在ttyS1(COM2)端口的设备上。



Category linux