正在缓冲的符号复制
你可能听说过TTY和PTY这些缩写,在/dev目录下也看到过/dev/tty[n]设备,它们与Linux终端有关。但你清楚TTY、PTY具体指的是什么,它们与shell的关系,以及它们是如何工作的吗?为了理解这些,我们需要先回顾一下历史。
在计算机诞生之前,人们发明了电传打字机(Teleprinter),通过长长的电线点对点连接,发送和接收打印的信息,用于远距离传输电报信息。
电传打印机也可以写作teletypewriter或teletype。
后来人们将电传打印机连接到早期的大型计算机上,作为输入和输出设备,将输入的数据发送到计算机,并打印出响应。
如今,我们用电传打字机终端(TTY)代表计算机终端,只是沿用了历史习惯。电传打字机曾经是计算机的终端,它的缩写便是TTY(Teletypewriter)。
为了将不同型号的电传打字机接入计算机,需要在操作系统内核安装驱动,为上层应用所有的低层细节。电传打字机通过两根电缆连接:一根用于向计算机发送指令,一根用于接收计算机的输出。这两根电缆插入UART(Universal Asynchronous Receiver and Transmitter,通用异步接收和发送器)的串行接口连接到计算机。
操作系统包含一个UART驱动程序,管理字节的物理传输,包括奇偶校验和流量控制。然后输入的字符序列被传递给TTY驱动。该驱动包含一个叫做line discipline的组件。
line discipline负责转换特殊字符(如退格、擦除字、清空行),并将收到的内容回传给电传打字机,以便用户可以看到输入的内容。同时它还负责对字符进行缓冲,当按下回车键时,缓冲的数据被传递给与TTY相关的前台用户进程。用户可以并行执行几个进程,但每次只与一个进程交互,其他进程在后台工作。
如今电传打字机已经进了博物馆,但Linux/Unix仍然保留了当初TTY驱动和line discipline的设计和功能。终端不再是一个需要通过UART连接到计算机上的物理设备。终端成为内核的一个模块,它可以直接向TTY驱动发送字符,并从TTY驱动读取响应然后打印到屏幕上。也就是说,用内核模块模拟物理终端设备,因此被称为终端模拟器(terminal emulator)。
上图展示了一个典型的Linux桌面系统。终端模拟器就像过去的物理终端一样,它来自键盘的事件并将其发送到TTY驱动,并从TTY驱动读取响应,通过显卡驱动将结果渲染到显示器上。TTY驱动和line discipline的行为与原先一样,但不再有UART和物理终端参与。
要查看一个终端模拟器吗?在Ubuntu 20桌面系统上,按Ctrl+Alt+F3就可以得到一个由内核模拟的TTY。Linux上这种模拟的文本终端也被称为虚拟终端(Virtual consoles)。每个虚拟终端都由一个特殊的设备文件/dev/tty[n]表示,与这个虚拟终端的交互是通过对这个设备文件的读写操作以及使用ioctl系统调用进行的。执行tty命令可以查看代表当前虚拟终端的设备文件。
复制代码在这里不方便展示具体命令和操作过程。你可以通过Ctrl+Alt+F3到Ctrl+Alt+F6在几个虚拟终端之间切换。按Ctrl+Alt+F2回到桌面环境。X系统也是运行在一个终端模拟器上,在Ubuntu 20上它对应的设备是/dev/tty2这也是为什么使用Ctrl+Alt+F2可以切换到X系统的原因。我们还可以查看X系统打开的文件中是否包含了设备文件/dev/tty2通过查找X系统的PID再查看这个进程打开了哪些文件就可以得知。此外也可以通过实验验证例如同时在tty3下以root用户身份执行echo命令再按Ctrl+Alt+F4切换到tty4就能看到从tty3发送来的信息。
终端模拟器是运行在内核的模块我们也可以让终端模拟程序运行在用户区这时就称为伪终端(Pseudo Terminal PTY)。PTY运行在用户区更加安全和灵活同时仍然保留了TTY驱动和line discipline的功能常用的伪终端有xtermgnome-terminal以及远程终端ssh我们以Ubuntu桌面版提供的gnome-terminal为例介绍伪终端如何与TTY驱动交互。
用户在客户端的终端中输入ssh命令,这条命令会通过PTY master、TTY驱动,最终到达PTY slave。Bash的标准输入已经被设置为PTY slave,它会从标准输入读取字符序列并解释执行。当需要启动ssh客户端时,它会请求与远程服务器建立TCP连接。
服务器端接收到客户端的TCP连接请求后,会向内核申请创建PTY,并获得一对设备文件描述符。然后让ssh server持有PTY master,而ssh server fork出的子进程bash持有PTY slave。bash的标准输入、标准输出和标准错误都被设置为PTY slave。
当用户在客户端的终端中输入命令如ls -l并按下回车键,这些字符会经过PTY master到达TTY驱动。我们需要让客户端的line discipline暂时失效,也就是说,客户端不会对特殊字符回车键进行处理,而是让命令ls -l和回车键一起到达PTY slave。ssh client会从PTY slave读取字符序列,然后通过网络发送给ssh server。
ssh server将接收到的字节写入PTY master。TTY驱动会对字节进行缓冲,直到收到特殊字符回车键。由于服务器端的line discipline并未禁用echo规则,所以TTY驱动还会将收到的字符写回PTY master。ssh server从PTY master读取字符,将这些字符通过TCP连接发回客户端。这些发回的字符是命令本身的回显,让客户端能看到自己的输入。
在服务器端,TTY驱动将字符序列传给PTY slave。bash从PTY slave读取字符,解释并执行命令如ls -l。bash会fork出ls子进程,该子进程的标准输入、标准输出和标准错误同样设置为了PTY slave。命令的执行结果会写入标准输出PTY slave,然后通过TTY驱动到达PTY master,再由ssh server通过TCP连接发送给ssh client。
值得注意的是,在客户端我们看到的所有字符都来自于远程服务器。我们输入的内容也是远程服务器上的line discipline应用echo规则的结果。虽然表面看似简单的在远程终端上执行了一条命令,但实际上背后涉及了复杂的终端模拟和数据传输过程。
简单回顾总结一下,本文主要讲述了电传打字机(TTY)和伪终端(PTY)的区别和联系。终端是物理设备,而伪终端则是运行在用户区的终端模拟程序。Shell是由terminal fork出来的子进程,负责解释执行用户输入的字符。可以使用stty命令对TTY设备进行配置。远程终端ssh也是一种伪终端PTY。
相信通过阅读本文,你已经对终端、终端模拟器和伪终端有了更深入的理解。如果你对底层实现感兴趣,可以进一步阅读TTY驱动的源码以及line discipline的源码。