理解 Linux 终端、终端模拟器和伪终端
你可能听说过 TTY 和 PTY 这些缩写,也在 /dev
目录下看到过 /dev/tty[n]
设备,大概知道它们和 Linux 终端的概念有关。可是你清楚 TTY
、PTY
具体指的是什么,它们有什么区别,以及它们和 shell
又是什么关系呢?为了理解这些,我们需要先回顾一下历史。
回顾历史
在计算机诞生之前,人们发明了 Teleprinter(电传打字机),通过长长的电线点对点连接,发送和接收打印的信息,用于远距离传输电报信息。
在今天你很难想象程序的运行结果需要等到打印出来才能看到,Teleprinter
设备已经进了计算机博物馆。现在我们用 TTY 代表计算机终端(terminal),只是沿用了历史习惯,电传打字机(teletypewriter)曾经是计算机的终端,它的缩写便是 TTY(TeleTYpewriter)。
为了把不同型号的电传打字机接入计算机,需要在操作系统内核安装驱动,为上层应用屏蔽所有的低层细节。
上图是一个典型的 Linux 桌面系统。终端模拟器就像过去的物理终端一样,它监听来自键盘的事件将其发送到 TTY 驱动,并从 TTY 驱动读取响应,通过显卡驱动将结果渲染到显示器上。TTY 驱动 和 line discipline 的行为与原先一样,但不再有 UART 和 物理终端参与。
如何看到一个终端模拟器呢?在 Ubuntu 20
桌面系统上,按 Ctrl+Alt+F3
就会得到一个由内核模拟的 TTY。Linux 上这种模拟的文本终端也被称为虚拟终端(Virtual consoles)。每个虚拟终端都由一个特殊的设备文件 /dev/tty[n]
所表示,与这个虚拟终端的交互,是通过对这个设备文件的读写操作,以及使用ioctl
系统调用操作这个设备文件进行的。通过执行 tty
命令可以查看代表当前虚拟终端的设备文件:
$ tty /dev/tty3
可以看到,当前终端的设备文件是 /dev/tty3
,也就是通过 Ctrl+Alt+F3
得到的虚拟终端。
你可以通过 Ctrl+Alt+F3
到 Ctrl+Alt+F6
在几个虚拟终端之间切换。按 Ctrl+Alt+F2
回到桌面环境。X 系统也是运行在一个终端模拟器上,在 Ubuntu 20
上它对应的设备是 /dev/tty2
,这也是为什么使用 Ctrl+Alt+F2
可以切换到 X 系统的原因。
我们可以看看 X 系统打开的文件中是否包含了设备文件 /dev/tty2
。先查找 X 系统的 PID:
# ps aux | grep Xorg mazhen 1404 0.1 0.6 741884 49996 tty2 Sl+ 08:07 0:13 /usr/lib/xorg/Xorg vt2 -displayfd 3 -auth /run/user/1000/gdm/Xauthority -background none -noreset -keeptty -verbose 3
再看看这个进程(1404)打开了哪些文件:
# ll /proc/1404/fd 总用量 0 dr-x------ 2 mazhen mazhen 0 7月 10 08:07 ./ dr-xr-xr-x 9 mazhen mazhen 0 7月 10 08:07 ../ lrwx------ 1 mazhen mazhen 64 7月 10 08:07 0 -> /dev/tty2 lrwx------ 1 mazhen mazhen 64 7月 10 08:07 1 -> 'socket:[39965]' lrwx------ 1 mazhen mazhen 64 7月 10 10:09 10 -> 'socket:[34615]' ...
可以看到,X 系统确实打开了 /dev/tty2
。
再做一个有趣的实验,在 tty3 下以 root 用户身份执行 echo 命令:
# echo "hello from tty3" > /dev/tty4
再按 Ctrl+Alt+F4
切换到 tty4,能看到从 tty3 发送来的信息。
伪终端(pseudo terminal, PTY)
终端模拟器(terminal emulator) 是运行在内核的模块,我们也可以让终端模拟程序运行在用户区。运行在用户区的终端模拟程序,就被称为伪终端(pseudo terminal, PTY)。
PTY 运行在用户区,更加安全和灵活,同时仍然保留了 TTY 驱动和 line discipline 的功能。常用的伪终端有 xterm,gnome-terminal,以及远程终端 ssh。我们以 Ubuntu 桌面版提供的 gnome-terminal 为例,介绍伪终端如何与 TTY 驱动交互。
我们简单梳理一下远程终端是如何执行命令的。
-
用户在客户端的 terminal 中输入 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
。 -
当用户在客户端的 terminal 中输入命令
ls -l
和回车键,这些字符经过PTY master
到达 TTY 驱动。我们需要禁用客户端 line discipline 的所有规则,也就是说客户端的 line discipline 不会对特殊字符回车键做处理,而是让命令ls -l
和回车键一起到达PTY slave
。ssh client
从PTY slave
读取字符序列,通过网络,发送给ssh server
。 -
ssh server
将从 TCP 连接上接收到的字节写入PTY master
。TTY 驱动对字节进行缓冲,直到收到特殊字符回车键。 -
由于服务器端的 line discipline 没有禁用
echo
规则,所以 TTY 驱动还会将收到的字符写回PTY master
,ssh server
从PTY master
读取字符,将这些字符通过 TCP 连接发回客户端。注意,这是发回的字符不是ls -l
命令的执行结果,而是ls -l
本身的回显,让客户端能看到自己的输入。 -
在服务器端 TTY 驱动将字符序列传送给
PTY slave
,bash 从PTY slave
读取字符,解释并执行命令ls -l
。bash fork 出ls
子进程,该子进程的标准输入、标准输出和标准错误同样设置为了PTY slave
。ls -l
命令的执行结果写入标准输出PTY slave
,然后执行结果通过 TTY 驱动到达PTY master
,再由ssh server
通过 TCP 连接发送给ssh client
。
注意在客户端,我们在屏幕上看到的所有字符都来自于远程服务器。包括我们输入的内容,也是远程服务器上的 line discipline 应用 echo
规则的结果,将这些字符回显了回来。表面看似简单的在远程终端上执行了一条命令,实际上底下确是波涛汹涌。
写在最后
简单回顾总结一下本文的主要内容:
-
电传打字机(TTY)是物理设备,最初是为电报设计的,后来被连接到计算机上,发送输入和获取输出。
-
电传打字机(TTY)现在被运行在内核中的模块所模拟,被称为终端模拟器(terminal emulator)。
-
伪终端(pseudo terminal, PTY) 是运行在用户区的终端模拟程序。
-
Shell
由terminal
fork 出来,是terminal
的子进程。Shell
不处理键盘事件,也不负责字符的显示,这些是由terminal
处理。Shell
负责解释执行用户输入的字符。 -
可以使用
stty
命令对 TTY 设备进行配置。 -
远程终端
ssh
也是一种伪终端 PTY。
相信通过这篇文章,你已经能够理解终端、终端模拟器和伪终端的区别和联系。如果想进一步探究低层实现,可以阅读 TTY 驱动的源码 drivers/tty/tty_io.c和 line discipline
的源码 drivers/tty/n_tty.c。
本文文字及图片出自 InfoQ
你也许感兴趣的:
- 【外评】电脑从哪里获取时间?
- 【外评】为什么 Stack Overflow 正在消失?
- Android 全力押注 Rust,Linux 却在原地踏步?谷歌:用 Rust 重写固件太简单了!
- 【外评】哪些开源项目被广泛使用,但仅由少数人维护?
- 【外评】好的重构与不好的重构
- C 语言老将从中作梗,Rust for Linux 项目内讧升级!核心维护者愤然离职:不受尊重、热情被消耗光
- 【外评】代码审查反模式
- 我受够了维护 AI 生成的代码
- 【外评】Linux 桌面市场份额升至 4.45
- 【外评】作为全栈开发人员如何跟上 AI/ML 的发展?
你对本文的反应是: