上一章说,终端的祖传规矩是「键盘出字节、屏幕收字节、中间一根线」。这一章把这根线彻底看清楚——因为理解终端的一切迷惑行为,最后都会回到这一句:终端里没有图形、没有界面、没有「字」,只有字节在流。
做一个三十秒的实验
打开终端,敲 cat,回车。然后随便打几个字,比如 hello,再回车。你会看到 hello 出现了两次。
这两次是谁干的?这是理解终端的关键一幕:
- 第一次是你敲键的瞬间就出现的——这叫回显(echo)。注意:不是你的键盘直接点亮了屏幕,也不是
cat干的(它还没拿到你的输入)。是内核里管终端的那层代码(tty,第一章的打字机幽灵)干的:它收到你敲的字节后,顺手把字节原样发回屏幕,好让你看见自己敲了什么。这是从电传打字机继承的规矩——当年你不亲眼看到纸上打出字,怎么知道线没断? - 第二次是你按回车之后才出现的。回车告诉内核「这行齐了」,内核才把攒下的一整行交给
cat;cat的唯一工作就是把读到的字节原样写回去,于是屏幕上又打了一遍。
所以一行字显示两次,是两个人各打了一遍:内核的 tty 层负责「让你看见自己在敲什么」,程序负责「处理完之后输出」。你可以用 stty -echo 把回显关掉再试——敲什么都看不见了,但 cat 照样工作(记得 stty echo 改回来)。顺便说,输入密码时屏幕一片空白,就是程序临时把回显关了。
字节就是数字,字符只是约定
线上跑的字节,每个就是一个 0 到 255 的数字。「字符」是对数字的一套约定:ASCII 码表说 104 是 h、105 是 i。想亲眼看看?试试 echo hi | od -c,你会看到 h i \n——三个字节,最后那个 \n 就是你敲的回车。
中文稍微绕一点:UTF-8 编码里一个汉字通常占三个字节。你可以用 echo 你 | od -x 看到十六进制的 e4 bd a0。这解释了终端里一个经典怪事:为什么删一个汉字有时要按两三下退格,或者删掉半个字留下乱码?因为终端按字节处理,一个汉字是三个字节,粗暴地按字节删就会把一个汉字撕碎。
stdin 和 stdout:程序嘴边的两根管子
每个程序一出生,操作系统就在它嘴边接好两根管子:标准输入(stdin)和标准输出(stdout)。在终端里启动程序时,这两根管子的另一头就是终端:stdin 连着「你敲键盘产生的那条字节流」,stdout 连着「内核送去显示的那条字节流」。还有第三根管子 stderr(标准错误),专走报错——第六章讲管道时它很重要。
要点是:程序根本不知道屏幕的存在。cat 不认识光标、不认识颜色、不知道窗口多大,它只会往 stdout 这根管子里塞字节。把字节变成屏幕上发光的字符,是管子另一头的事。这个「程序只吐字节」的设计是 Unix 最深远的决定之一,后面几章——管道、重定向、SSH、tmux——全部建立在这两个字节流的朴素之上。
行缓冲:为什么回车之前程序什么都不知道
刚才实验里还有一个细节值得停下来:你敲 h、e、l、l、o 的时候,cat 完全没有收到任何东西——内核把字节攒着,直到你按回车才整行交出。这叫规范模式(canonical mode),又叫「行模式」:内核替你管一行的编辑,所以你可以用退格键改掉打错的字,程序收到的永远是干净的成品。
但 vim、less、以及一切要「按一个键立刻反应」的程序显然不能等回车。它们启动时会通知内核切换成原始模式(raw mode):别攒行、别回显、来一个字节立刻给我。这就是为什么 vim 里按 j 光标立刻动,而你退出 vim 的瞬间世界又恢复正常——vim 把模式切回去了。当程序异常退出忘了切回来,你的终端就会「发疯」:敲字不回显、回车不换行。老手都知道此时盲敲 reset 回车就能救回来——它干的活就是把 tty 恢复出厂设置。
这一章记住三件事:线上只有字节;你看到的回显是内核打的,程序输出是程序打的,两拨人;程序默认按行拿输入,vim 这类程序会把终端切成「来一个字节给一个」的原始模式。
字节流到程序这一头讲完了。另一头——字节流到了屏幕那边,是谁把它变成发光的字的?那些彩色的字、满屏的进度条又是谁画的?下一章讲终端模拟器。