第8讲 · 终端和 UNIX Shell:从打字机到一门编程语言
2026 南京大学《操作系统原理》第八讲(Aside)· 课堂实录重构
| 主讲 | 蒋炎岩(Yanyan Jiang) |
| 视频 | https://www.bilibili.com/video/BV1nQXsBuEUz/ |
| 课程主页 | https://jyywiki.cn/OS/2026/ |
| 本讲讲义 | https://jyywiki.cn/OS/2026/lect8.md |
| 讲义 / 幻灯片作者 | 蒋炎岩,https://jyywiki.cn/ |
| 讲义 / 幻灯片许可 | CC BY-NC 4.0 |
| 图片来源 | 书中插图为课堂演示与讲义渲染图;演示类截图为视频画面 |
对应讲次:第8讲(南京大学 操作系统原理)
制作模型:deepseek-flash(coding-agent / pi;据 ~/.pi/agent/settings.json)
编者说明:本书由课堂录像的语音字幕重构而成,术语、章节骨架与参考链接已按课程官方讲义(
lect8.md)校准。讲师的口语风格、类比与案例被尽量保留;编辑性的补充一律以「编者说明」或「编者拓展」标出。
引子:一个「非常傻」的设备,撑起整个交互世界
(参考时间: 00:00)
终端(terminal)是我们每天工作都要用的东西。这一讲要回到 1970 年代——那时候还没有高像素的图形显示器——去看看人们是如何完成今天我们能做的几乎一切事情的。答案的核心,是一个极其简单的接口:
终端只做一件事:和操作系统交换字符。 你送一个字节流给它,它把字节流送回来。
而围绕这个「傻设备」,UNIX 演化出了一整套令人叹为观止的机制:伪终端、信号、会话(session)、进程组(process group)、作业控制(job control),最终还有一门编程语言——Shell。
一、从键盘到终端:一段平滑的演化史
1.1 「键盘」的老祖宗
(参考时间: 00:00)
「键盘」的历史比大家想象的长得多:
- 最早可以追溯到公元前 3 世纪的水风琴(Water Organ, Hydraulis)——它已经有「按键」的概念了;
- 后来是管风琴(Pipe Organ);
- 1500 年代,出现了今天钢琴的前身——「击奏弦鸣乐器」:你按下键,一个锤子打在琴弦上,发出悦耳的声音。
这条线索的关键在于:按键—机械—发声这套「擒纵机构」会被不断复用。
📷 水风琴与管风琴:键盘的老祖宗
回到原视频 (00:01:00)
1.2 打字机:终端所有「怪键」的由来
(参考时间: 00:03)
把锤子从「打琴弦」换成「打色带」,你就得到了打字机(typewriter):
- 擒纵机构:按键时蓄力,松开时带着字车走一个字符的距离;
- QWERTY 键盘(1860s):它其实是为降低打字速度设计的——因为两个字模同时推向色带、击打同一位置时机械结构容易卡死,把常用字母拆开可以减小卡壳的概率;
- 讲师还分享:他 2000 年时玩过真货——可以换黑/红双色色带,打字是真的费力(要用很大力气才能把字母推到色带上并推动字车)。
打字机给今天的终端留下了大量遗产,几乎解释了键盘上每一个怪键:
| 遗产 | 含义 |
|---|---|
| Shift / Caps Lock | shift 把「字锤/字模」向上移动一段距离,切换字符集(打大写);Caps Lock 就是把这个档位锁住 |
| CR / LF | \r(Carriage Return,回车)= 把打印头移回行首;\n(Line Feed,换行)= 把纸向上移一行 |
| Space / Backspace | 互逆操作:向右 / 向左移一格(打错了就 backspace 再打个 - 划掉) |
| Tab | 移到下一个 tabstop 以实现垂直对齐,所以叫「制表符」 |
| 等宽字体(monospace) | 因为每次移动的距离完全确定 |
编者拓展|为什么 UNIX 用
\n、Windows 用\r\n:打字机上「回车」和「换行」是两个独立的机械动作:先把打印头拉回行首(CR),再把纸卷上一行(LF)。早期电传打字机需要显式发两个控制字符\r\n。后来 UNIX 选择只用一个\n,由终端驱动/程序去完成「回到行首 + 换行」的语义;而 DOS/Windows 保留了\r\n。这个差异至今仍是跨平台文本处理的一大坑(dos2unix、Git 的core.autocrlf都为此而生)。你甚至可以在 Python 里验证 CR 的行为:
print('Hel\rlo')会打印出llo——\r把光标移回行首,后面的字符覆盖了前面的。编者拓展|打字机上的「所见即所得」与 emoji 的归宿:讲师提了个有趣的问题——emoji 现在像个「词」,有含义、在 token space 里有自己的 embedding,可你很难把它念出来。如果有一天 emoji 被收进新华字典,拼音该怎么标?这其实是「符号」与「语言」关系的现代注脚。
1.3 电气化:电传打字机与 Baudot 码
(参考时间: 00:11)
从机械打字机走到电气化,就有了电传打字机(Teletypewriter)——功能还是一样的「打字」,但有一个关键区别:按下一个键,字会打在导线的另一端。这根导线可以非常远,甚至可以跨越大陆。
- Telex(teleprinter exchange):1920 年代,早于计算机;使用 Baudot Code(5 位编码);
- 5 位能编很多常用字符(字母、数字、一些符号),还留了特殊编码用来「进入大写/小写模式」;有趣的是这套码里没有美元符号——因为它不是美国人设计的;
- Teletype Model 28(1951):讲师特别推荐大家读它的 technical data sheet——1951 年的产品,机械结构、电路波形、零件构成写得清清楚楚,「读这些东西会有一点小小的震撼:1950 年代人类的科技已经发展到这样的程度」。
📷 Baudot Code:5 位编码的电传打字机字符集
回到原视频 (00:12:00)
1.4 Video Terminal:VT100 与 ASCII
(参考时间: 00:13)
计算机早期直接以机器码运行、把输出戳成二进制孔(穿孔纸带/卡片),对人类极不友好。于是我们希望计算机的输出能以人眼可读的方式呈现——这就诞生了:
- Video Teletypewriter:从打字机继承的核心功能只有一个——
putchar。计算机生成一个字符,送进去,它就打出来。这也就是今天 TTY(teletypewriter)这个名字的由来; - VT100(DEC, 1978):里程碑式产品,成为事实上的行业标准。它是首个完整实现 ANSI Escape Sequence 的终端,让终端能做颜色、屏幕控制、滚动等等;80×24 字符的显示也成为标准布局;
📷 VT100(DEC, 1978):ANSI Escape Sequence 与 80×24 成为标准
回到原视频 (00:17:30)
- ASCII:这套编码是和电传打字机共同设计的。这解释了初学时那些困惑——为什么
'A'是 65、'a'是 97?前面那些编码的是什么? 答案就在下面(终端作为输入设备)里。
编者拓展|ANSI Escape Sequence 与「终端里的图形」:VT100 引入的转义序列以
ESC(ASCII 27,八进制 033)开头,用来告诉终端「接下来不是要打印的字符,而是一条指令」——比如移动光标、改变颜色、滚动屏幕。今天你在终端里看到的一切「炫酷界面」,本质上都是在字符流里夹带转义序列。有了 Unicode 之后,终端能玩的花活就更多了:用
▁▂▃▄▅▆▇█拼出柱状图、用方框/线条字符画出窗口、用 Powerline 拼出带箭头的状态栏。讲师甚至能用 Unicode 字符叠出动物图案和 3D 效果。
1.5 终端:非常「傻」的输入设备
(参考时间: 00:20)
终端不仅能输出,也能输入。但它的输入模型同样简单:
- 你按下
A,终端就把A的编码送到计算机; - 至于「要不要显示这个
A、显示成 A 还是 B」——由操作系统/应用程序决定; - 按下 Shift 时,终端不会送任何字符给计算机——shift 只是在终端内部「换挡」(就像打字机上字锤没敲下去),直到你真正按下一个字母,才把大写字母送进去;
- 你甚至可以读到原始的字节流:讲师演示了一个程序,把终端送来的所有按键原样打印(包括
ESC、箭头键的转义序列)。按下ESC,得到的是 ASCII 27。
按 Ctrl-C 会发生什么? 你会得到一个编码为 03 的字符——这正是 ASCII 里 ETX(End of Text)的编码!同理:
📷 用 Unicode 字符拼出的图形与 Powerline 状态栏
回到原视频 (00:19:30)
Ctrl-C → 0x03 (ETX, End of Text)
Ctrl-D → 0x04 (EOT, End of Transmission)
回车 → \r (CR)
这就是 ASCII 里那些「不知道干什么」的控制字符的来历——它们是为电传打字机设计的。讲师还演示了一个「退不出去」的程序:它把终端配置成「所有字符都直接给我」,于是 Ctrl-C、Ctrl-Z 都失效了;幸好代码里留了「按 Q 退出」的后门,否则只能再开一个终端把进程杀掉。
编者拓展|箭头键为什么会送来一串字符:ASCII 里没有上下左右。于是终端只好把它们编码成转义序列:
↑是ESC [ A、↓是ESC [ B、→是ESC [ C、←是ESC [ D。所以「方向键」在程序看来就是一串以 ESC 开头的字节——这也是为什么早期程序处理方向键会出各种怪问题。
二、伪终端:想要多少就有多少
(参考时间: 00:28)
真实的 VT100 只有一台。而在现代系统里,终端想要多少就有多少——每次你开一个新窗口,操作系统就给你变出一台「虚拟终端」。这就是伪终端(pseudo terminal, PTY)。
graph LR
App["终端模拟器<br>(如 foot / iTerm / Windows Terminal)"] -->|"open /dev/ptmx"| M["PTY Master<br>(主设备)"]
M <-->|"双向字符通道"| S["PTY Slave<br>(从设备,/dev/pts/N)"]
S -->|"stdin/stdout/stderr"| Sh["shell / 其它程序"]
- 一对 PTY 提供双向通信通道:主设备(PTY Master)连接到终端模拟器,从设备(PTY Slave)连接到 shell 或其它程序;
- 你看到的
/dev/pts/0、/dev/pts/5就是这么来的; - 创建一个新 PTY 的入口是特殊文件
/dev/ptmx(PTY master multiplexer):打开它、grantpt、unlockpt,再拿到从设备的路径和文件描述符。
📷 mini-tty:终端中的终端(向操作系统申请一个新的 PTY)
回到原视频 (00:29:30)
讲师现场用 mini-tty(让 AI 写的)演示了「终端中的终端」:在自己的终端里再创建一个 PTY,把输入转发进去、把输出取出来再显示到自己的终端——「这不就是 tmux 吗」。他还提到 ttyrec / ttyplay:因为终端之间只是交换字符,所以只要记录下所有流出 PTY 的字符和时间戳,就能把终端改造成一个「录屏器」,甚至渲染成网页里可回放的 SVG。
编者拓展|PTY 的内部一串操作:在 Linux 上创建一个 PTY 的典型流程是: 1.
int mfd = open("/dev/ptmx", O_RDWR);——/dev/ptmx是「多路复用」入口; 2.grantpt(mfd)、unlockpt(mfd); 3.ptsname(mfd)得到从设备路径(如/dev/pts/7); 4.open(ptsname)打开从设备,把它设为子进程的0/1/2。这一整套,正是
sshd、tmux、script(1)、以及你用的终端模拟器在背后做的事。
如果我们不满足于「只能显示字符」呢? DEC 早在 VT200 时代就做了扩展——Sixel:一个字符编码一个 6×1 的「是/否」像素块,配合调色板就能拼出图形。今天的许多终端(Kitty、Windows Terminal 等)都支持它。讲师现场演示:在不支持的终端上什么也看不到,在支持的终端上确实出现了一小块像素图像。
📷 Sixel:在终端里显示像素图像
回到原视频 (00:38:30)
三、终端与操作系统
3.1 登录:getty、session 与控制终端
(参考时间: 00:42)
终端是「人机交互的第一个设备」。系统启动时:
graph LR
K["内核初始化<br>(扫描并初始化终端设备)"] --> I["init 进程"]
I -->|"为每个终端启动一个 getty"| G["getty<br>(open tty,提示输入用户名)"]
G --> L["login<br>(校验并建立会话)"]
- 系统刚启动时,第一个进程的
0/1/2是「什么都没有」的;它也不知道自己在哪个终端上。因此getty程序负责「打开某个终端、提示输入登录名、调用login」; - Linux 里按 Ctrl+Alt+F1/F2/…,每一个虚拟控制台都是一个
getty;讲师自己的图形界面就跑在 TTY7 上; - 远程登录则不同:
sshd会分配一个伪终端(openpty),fork一个子进程并完成初始化,再切换成用户的登录程序; - 无论哪种登录方式,你都会得到一个会话(session),并关联一个控制终端(controlling terminal)。
📷 在虚拟控制台上由 getty 启动的 login 提示(Ctrl+Alt+F1…)
回到原视频 (00:45:00)
编者拓展|getty 与波特率:
getty打开终端时会配置一系列参数,其中包括 波特率(baud rate)——如果是真的 VT100,这个值必须是对方能握手的有效值。今天终端是模拟出来的,波特率其实无关紧要,但你仍然能在当前伪终端上看到它模拟出来的38400。
3.2 canonical mode:为什么 read 没有立刻返回?
(参考时间: 00:40)
你很熟悉的这个行为,其实不是 C 库做的,而是操作系统在「搞鬼」:
- 你运行
cat,它调用read(STDIN_FILENO, buf, size); - 你按
ABC……read并没有立即返回; - 只有当你按下回车,
read才返回——而且你还能用退格键修正输入。
这是因为终端默认工作在 canonical mode(规范模式):终端驱动内部维护一个行缓冲区,并模拟出行编辑:
- 你按
A,字符进入驱动的缓冲区(还没到进程); - 你按
Backspace,驱动把上一个字符从缓冲区删掉; - 驱动还可以回显(echo)字符到终端(所以 sudo 输密码时要关闭回显);
- 只有遇到换行符(或
Ctrl-D),才把整行交给进程。
编者拓展|
stty -a里藏着你的终端配置:stty -a会列出当前终端的全部配置,包括按键绑定:intr = ^C、quit = ^\、eof = ^D、susp = ^Z……这些都可以改。它还显示icanon(是否规范模式)、echo(是否回显)等标志位。讲师建议:善用stty -a和Ctrl-\——当Ctrl-C退不出去时,试试Ctrl-\(SIGQUIT)。
3.3 Ctrl-C 到底发生了什么:信号机制
(参考时间: 00:59)
前面 demo 里,Ctrl-C 只是送来了一个字符 0x03。那为什么它常常能「终止程序」?答案是操作系统把某些字符「翻译」成了信号:
- signal / sigaction:注册一个「信号处理程序(handler)」。当进程收到信号时,会强行跳转到 handler 执行——哪怕进程正在
while(1)或printf; - kill:可以由另一个进程向目标进程发送信号;
- 因此,你完全可以写出「
Ctrl-C不能退出」的程序(比如把 handler 写成「什么都不做」),也可以把它改成「收到信号后执行清理代码再退出」。
讲师演示了:注册 SIGINT / SIGQUIT 的 handler 后,Ctrl-C 会触发 receive SIGINT,而 Ctrl-\ 触发 receive SIGQUIT 并退出;也可以用另一个进程 kill -SIGINT <pid>,效果与在终端里按 Ctrl-C 完全一样——因为走的是同一份代码。
编者拓展|常见的终端相关信号: | 信号 | 触发 | 默认行为 | |---|---|---| |
SIGINT|Ctrl-C(^C) | 终止 | |SIGQUIT|Ctrl-\(^\) | 终止并 core dump | |SIGTSTP|Ctrl-Z(^Z) | 暂停进程 | |SIGCONT|fg/bg/kill -CONT| 恢复已暂停的进程 | |SIGHUP| 控制终端关闭 / 会话 leader 退出 | 终止 |现代编程应优先用
sigaction(2)(signal(2)的语义在不同系统上不一致)。这也是为什么「写一个能捕获信号、优雅退出的服务」是系统编程的基本功。
3.4 信号发给谁?会话与进程组
(参考时间: 01:04)
真正麻烦的问题来了:
你按下
Ctrl-C时,操作系统里的进程是一棵大树(还有「托孤」行为)。到底该杀谁? vs code 为什么安然无恙?如果yes又fork了一堆子进程,是杀一个还是全部?后台的进程又怎么办?
UNIX 为此做了一个大费周章的设计:
- 会话(Session):给进程引入
Session ID(大分组)。子进程继承父进程的 session;一个 session 关联一个控制终端;leader 退出时,全 session 的进程收到SIGHUP。 - 进程组(Process Group):再引入
PGID(小分组)。一个会话里只能有一个前台进程组;操作系统收到Ctrl-C,就向前台进程组的所有进程发送SIGINT。
graph TD
S["Session(一个控制终端)"] --> G1["Process Group 1(前台)<br>← Ctrl-C 只发给它"]
S --> G2["Process Group 2(后台)<br>← 不受 Ctrl-C 影响"]
S --> G3["Process Group 3(后台)"]
对应的 API 是:
getsid/setsid(setsid会脱离控制终端);getpgid/setpgid;tcgetpgrp/tcsetpgrp(设置/查询前台进程组,讲义吐槽「迷惑 API」)。
编者拓展|
SIGHUP、nohup与 tmux,以及「云主机上跑长任务」的坑:因为「会话 leader 退出时全 session 收到SIGHUP」,所以你的 SSH 连接一断,正在跑的长任务就会收到 SIGHUP 而终止。这就是为什么: - 要用nohup(忽略 SIGHUP,把输出重定向)来跑长任务; - 或者用tmux/screen:它们在服务器上维护一个不随 SSH 断开而退出的 session(tmux里的程序不回收到那个 SIGHUP),你重新连上再attach回去。租云主机训练模型、结果网络一抖程序就没了——根源就在这里。
3.5 Job Control:终端里的「窗口管理器」
(参考时间: 01:06)
有了 session / process group,终端就能实现作业控制(job control)——本质上是在只有一个屏幕的终端里,模拟 Windows 的窗口管理:
| Windows | 终端 |
|---|---|
| 双击打开程序 | shell 为命令分配一个新的进程组 |
| 最小化 | Ctrl-Z(SIGTSTP,暂停) |
| Alt-Tab 切换窗口 | jobs 列出作业;fg / bg 前后台切换(tcsetpgrp) |
| 关闭窗口 | Ctrl-C(向前台进程组发 SIGINT) |
| 关闭全部窗口 | 关掉整个 session / 杀进程组 |
📷 job control:jobs / fg / bg / Ctrl-Z 在终端里模拟窗口管理
回到原视频 (01:07:30)
讲师现场演示了:Ctrl-Z 把程序「最小化」、jobs 查看、bg 让它到后台继续跑、fg 拉回前台、Ctrl-C 杀掉。他感叹:「这背后就是那个很讨厌的 session / process group 机制。」
编者拓展|这是一段「历史的糟粕」,但它是 POSIX 的一部分:讲师毫不客气地评价这套设计「很不优雅」「是历史的糟粕」,但它成了 POSIX 标准——任何声称兼容 POSIX 的系统都必须实现它,否则跑不了依赖这套接口的软件(如 bash)。回头看,其实并不需要「把进程绑定到设备」: - Android:每个 app 是不同的用户,借 Linux 权限隔离;「强行终止」就是遍历并杀掉该用户的所有进程; - Snap:程序跑在隔离沙箱里(AppArmor + seccomp + namespaces); - Windows Window Manager:只需要「进程组」——关窗口就全杀掉。
发明 session/pg 时,人们还没想到容器、cgroups、namespaces。讲师说:如果有机会重新设计系统,可以做得更好——这也是 AI 时代「有胆量破坏旧秩序、建新秩序」的机会。
编者拓展|
uid/euid/saved uid的坑:讲义在这里顺带提了一嘴uid、effective uid、saved uid——这是 UNIX 权限模型里最容易被误用的一组概念。USENIX 有一篇经典论文 Setuid Demystified 专门讲「为什么setuid系列调用在不同平台上语义不一致、极易写出提权漏洞」。任何软件都很难逃脱千疮百孔的设计。
四、UNIX Shell:一门「只有字符串」的编程语言
(参考时间: 01:18)
有了终端,我们就可以讲 Shell——你面前那个 $ 提示符后面等命令的小程序。
4.1 Shell 真的很好写
(参考时间: 01:18)
while (1) {
printf("$ ");
read(0, buf, size); // 从 stdin 读一行命令
execve(command, argv, envp); // 执行它
}
最早的 shell 真的就这么简单——一个 while 循环、一次 read、一次 execve。讲师展示了一份零依赖的 UNIX Shell(从 xv6 里「偷」出来的),用 -ffreestanding 编译,连一个库函数都不依赖——这正是「可以在系统调用之上,创建整个操作系统应用世界」的真正含义。
编者拓展|Shebang
#!:为了让脚本能像可执行文件一样直接运行,UNIX 早期实现了以#!开头的「可执行文件」。内核看到文件开头是#!,就会执行第一行里写的程序(如/bin/bash),并把脚本文件路径作为参数传进去。这就是为什么脚本开头要写#!/bin/bash、#!/usr/bin/env python3。
4.2 Shell 是 UNIX Philosophy 背后的编程语言
(参考时间: 01:20)
UNIX Philosophy 的 KISS 三原则(做一件事并做好、协同工作、用文本流作通用接口)——背后很大程度上是靠 Shell 这门编程语言支撑的。
有一个程序员笑话很传神:
当你坐在 VT100 面前,你按下的每一个字符都是在编程。哪怕你只是在整理文件,你输出的每一条命令在 shell 看来都是一个表达式或程序。
Shell 是一门基于文本替换的极简编程语言,它甚至连整数都没有:
- 要算
1 + 2?在早期 shell 里你得调用外部命令expr 1 + 2——注意expr是一个进程,它从命令行参数里拿到表达式再算给你; - 而
1+2(没有空格)不会被算出来——因为 shell 匹配的是argv里的第二个字符串是不是+;只有空格才会把它分成expr、1、2。
它做的事情只有一件:把你的一条命令,翻译成一组系统调用的序列(fork / execve / waitpid / pipe / open / dup2……)。它的语法也都围绕这个思路:
graph TD
C["一条命令"] --> P["预处理 / 命令替换<br>$()、<()"]
C --> R["重定向<br>>、<、2>/dev/null"]
C --> S["顺序与逻辑<br>;、&&、||"]
C --> PP["管道<br>|"]
P --> SC["系统调用序列"]
R --> SC
S --> SC
PP --> SC
编者拓展|从命令到系统调用:重定向是怎么实现的:利用「子进程继承文件描述符」这一特性——父进程先打开好文件,在子进程里「腾挪」:
c int fd_in = open("in.txt", O_RDONLY | O_CLOEXEC); int fd_out = open("out.txt", O_WRONLY | O_CLOEXEC); int pid = fork(); if (pid == 0) { dup2(fd_in, 0); // 把 0 号(stdin)换成 fd_in dup2(fd_out, 1); // 把 1 号(stdout)换成 fd_out execve(...); // 执行命令;它写 1 号就是写文件 } else { close(fd_in); close(fd_out); waitpid(pid, &status, 0); }$()命令替换、<()进程替换、&&/||短路、|管道,最终都会被翻译成类似这样的一组系统调用。讲师甚至用<()配合diff(因为diff需要两个文件,没法把两个都塞进标准输入)——「这也是 UNIX 设计的一个缺陷,但被各种 workaround 解决了」。
4.3 有优点就有缺点
(参考时间: 01:31)
Shell 的设计被1970 年代的算力、算法和工程能力束缚了,后人只好「将错就错」:
- 优先级歧义:
ls > a.txt | cat到底是什么意思?重定向给了文件、cat还能收到输入吗?——bash/zsh 的行为不同(zsh 会像 tee 一样既写文件又输出,bash/fish 则什么也看不到)。所以脚本要写#!/bin/bash甚至#!/bin/sh来保持兼容; - 文本数据「责任自负」:文件名里的空格就是灾难(讲师说 UNIX 用户都有强烈倾向用
-而不是空格;GNU make 至今不能很好地处理带空格的文件名); - 一个经典例子:
bash $ echo hello > /etc/a.txt bash: /etc/a.txt: Permission denied $ sudo echo hello > /etc/a.txt bash: /etc/a.txt: Permission denied为什么加了sudo还是不行? 因为重定向是 shell 做的,不是sudo做的——sudo echo只是让echo以 root 运行,但真正打开/etc/a.txt的是你的 shell(没有权限)。这就是所谓的「sudo sandwich」问题。
编者拓展|AI 时代为什么还要读手册:讲师强烈推荐大家读
man sh(dash 的command interpreter (shell))。理由是:AI 时代「学什么」成了难题,你容易进入「什么都能学、于是什么都不用学」的虚无状态;而真正稀缺的是「知道什么事能办到」——因为人无法做成自己想象不到的事。扩展想象力的方式,就是去看别人想到了什么。他的具体做法堪称「AI 时代的 UNIX Philosophy」:
bash man -P cat sh | claude "Make a comprehensive summary"「Read the friendly manual!」——把手册管道给 AI,让它在几秒内帮你建立起清晰的概念。UNIX 的文本接口设计,再一次发挥余热。
总结
- 终端是一个极其简单的设备:它只和操作系统交换字符;它像打字机一样「傻」,所有「聪明」的行为都由操作系统/应用程序定义。
- 打字机留下了大量遗产:Shift/Caps Lock、CR/LF、Space/Backspace、Tab、等宽字体;ASCII 的控制字符(
Ctrl-C=03、Ctrl-D=04)也是为电传打字机设计的。 - VT100 + ANSI Escape Sequence 奠定了现代终端;Unicode 与 Sixel 让终端能画图、能显示像素。
- 伪终端(PTY) 让「终端想要多少就有多少」:
/dev/ptmx+/dev/pts/N;ttyrec、tmux都建立在这个抽象上。 - 登录从 getty/login 开始,给你一个 session + 控制终端;终端默认工作在 canonical mode(行编辑、回显都在驱动里)。
Ctrl-C是把字符翻译成信号:SIGINT(SIGQUIT、SIGTSTP、SIGCONT、SIGHUP……)通过sigaction处理、kill发送。- 信号发给「前台进程组」:UNIX 用 session + process group 来解决「
Ctrl-C杀谁」的问题,并在此基础上实现 job control——这是「历史的糟粕」,却成了 POSIX 标准;nohup/tmux正是为了绕开SIGHUP。 - Shell 是一门编程语言:只有字符串、基于文本替换,把命令翻译成系统调用序列(重定向、管道、短路、命令替换)。它是 UNIX Philosophy 的载体,也意外地非常适合大模型。
- 第一性原理:回到终端,它只是在和操作系统交换字符;其他所有行为,都是设计者可以定义的。
附:官方参考与延伸阅读
本讲的讲义见课程主页,以下链接按书中出现顺序整理:
阅读材料:man sh(dash 的手册)——讲师建议把它非交互式地打印出来再管道给 AI 做总结,「在 AI 的帮助下读手册,建立正确的概念」。
版权说明:本讲讲义与幻灯片作者为蒋炎岩,采用 Creative Commons BY-NC 4.0 许可,来源于 https://jyywiki.cn/OS/2026/。视频画面与字幕版权归原作者所有。本书为课堂内容的学习性重构。