本文介绍了linux中伪终端的创建,介绍了终端的回显、行缓存、控制字符等特性,并在此基础上解释和模拟了telnet、SSH开启远程会话的过程。
轻量级远程登录之前制作的一块嵌入式板子,安装了嵌入式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
该命令将bash的标准输入输出与nc的标准输出输入连接起来,并由nc将其与socket连接起来。同时,nc监听2333端口(如果使用小于1024的端口,需要root权限),等待远程连接。现在打开另一个终端B,准备连接:
nc localhost 2333
这时,在终端B中出现了sh的提示符。输入一般的shell命令后可以执行并得到结果。看来linux自带的工具已经灵活、强大到足够搭建一个小型的远程登录系统。这个过程可以使用下面的图来描述:
过tty命令,我们看到,此时的shell并没有一个tty终端。确实,它的标准输入输出都是管道。这会带来一个问题,需要操纵tty的一些命令,比如vi、less、sudo等都无法正常使用(可以动手试试效果怎么样)。更为要命的是,在终端B中按下Ctrl+C这样的控制键,内核把结束信号发送给了客户端nc,而不是远程的程序!
Ctrl+C直接杀死nc,结束了会话。对比telnet,我们的登录系统还缺少什么东西。这就是伪终端(pseudoterminal)。
了解伪终端终端和它的作用
终端(terminal)这个词拥有很多含义,这里尽量将其分开说明。
历史上,终端(有时被成为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/
文件夹里的以数字命名的文件就是伪终端的设备文件。
伪终端的介绍
通过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端是模拟用户一端的交互。之所以称为虚拟化的终端,它除了转发数据流外,还要有点终端的样子。
作为终端的伪终端
最为一个虚拟的终端,每一个伪终端里面封装了一个终端驱动,让它能做到这些事情:
- 为程序提供一些输入输出模式的帮助,比如输入密码时隐藏字符
- 为用户提供对进程的控制,比如按下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
,并且被原样返回。(可以试试上下左右、回车键的效果)
linux中终端驱动本身有回显、行缓存、作业控制等丰富的属性,在此基础上实现的伪终端在终端模拟器、远程登录等场合下能够得到多种应用。
在实验过程中也牵扯到进程控制、输入输出重定向、网络通信这么多的知识,更体现出linux的复杂精致的结构。我感觉,linux 就像一个包罗万象、又自成体统的小宇宙,它采用独特的虚拟化技术,灵活的模块化和重用机制,虚拟出各种设备,实现了驱动程序的随意拼插。在这里,所有模块都得到了充分的利用,并能够像变形金刚那样对各类需求提出面面俱到的解决方案。