第六章结尾留了个扣子:程序会问内核「我的输出那头是终端吗?」在电传打字机时代这问题很好回答——是就是,不是就不是。可今天根本没有硬件终端,你打开的只是一个 mac 窗口。内核拿什么回答?
答案是:内核伪造了一台终端。这个伪造品叫假终端(pseudo-terminal,简称 pty),它是全书最关键的一块积木——SSH、tmux、expect、script 录制命令行,全站在它上面。
一对孪生的门
pty 的构造很朴素:内核里开一对连在一起的「字符设备」,像一扇门的两面:
- 主面(master):给「扮演终端的那一方」用——iTerm2 就拿着这一面。
- 从面(slave):给「使用终端的那一方」用——shell 和你的程序拿着这一面当自己的 stdin/stdout。从面有个设备文件,就是
tty命令打出来的那个/dev/ttys003或/dev/pts/0。
两面之间的规则对称得漂亮:写进主面的字节,能从从面读出来;写进从面的字节,能从主面读出来。就像那根串口线,只不过两端都是程序。
于是整个结构一目了然:iTerm2 拿着主面,假装自己是一台 VT100;shell 拿着从面,真心实意地以为自己连着一台真终端。shell 调用 isatty,内核看了看——是 pty 的从面——回答「是」。谎言天衣无缝。
一个实验就能看见这扇门:开两个终端窗口。窗口 A 敲 tty,得到比如 /dev/ttys003;窗口 B 敲 echo hello > /dev/ttys003——hello 出现在窗口 A 的屏幕上游荡。你刚刚徒手把字节写进了别人终端的「从面」,被 A 的模拟器画了出来。
pty 不只是根管子,它还带着「打字机行为」
注意 pty 和第六章的管道不是一回事。管道是纯字节通道;pty 的从面还附赠一整套第一章以来讲的打字机行为——回显、行缓冲、把 Ctrl-C 翻译成信号(第十章)、记录窗口大小。这些行为由内核里 pty 自带的一层代码(就是第二章说的 tty 层,规范模式/原始模式那些设置)提供。
这就是为什么「管道不够用」:如果 iTerm2 只用普通管道连 shell,shell 一 isatty 发现对面不是终端,行编辑、颜色、Ctrl-C 全都没有了——你会得到一个残废的命令行。pty 存在的全部意义,就是让「假装是终端」这件事连行为细节都像。
谁都能开一对:pty 是公共积木
一旦明白 pty 是「任何程序都能向内核申请的一对门」,一堆工具立刻褪掉神秘色彩:
- 终端模拟器(iTerm2、Windows Terminal):申请一对 pty,主面给自己画屏幕,从面启动 shell。这是标准开机流程。
- SSH 服务器:你远程登录时,服务器上的 sshd 为你的会话申请一对 pty,让你的程序以为连着真终端。下一章详谈。
- tmux:为每个窗格申请一对 pty。第九章的主角。
- script 命令:录制命令行会话的工具,原理就是开一对 pty 坐中间,一边转述一边抄写下来。
- expect 类自动化工具:要自动化一个「只肯跟终端说话」的程序(比如要输密码的),就开一对 pty 骗它。
还有个日常痕迹:每个 pty 从面都是 /dev/ 下的一个文件(mac 上 ttys00X,Linux 上 pts/N),你开几个终端窗口/tmux 窗格就有几个。who 命令列出来的那一列,就是这些文件名。
为什么要绕这么大一圈?
值得停下来问一句:图形界面时代,为什么还要让内核费事伪造打字机?直接让 shell 跟 iTerm2 用一种新协议说话不行吗?
不行,而且原因深刻:pty 让四十年来所有为「真终端」写的程序无需修改就能跑在图形界面里。vim、less、你的 shell、以及 1980 年代编译出来的老古董,它们只认「终端」这个抽象。内核把抽象保留下来、把硬件抽掉,新老世界就接上了。这是 Unix 最典型的生存策略:抽象不动,实现偷换。
一句话总结:pty 是内核提供的一对「门」,主面给冒充终端的程序,从面给使用终端的程序,中间还附赠回显、行缓冲、信号翻译等全套打字机行为。图形界面的一切终端体验,都建立在这个美丽的谎言上。
现在,把「冒充终端的程序」从你的电脑搬到千里之外:主面不再连本地窗口,而是连一根网线。这就是 SSH——下一章。