UNIX 1974 — 用极小内核做出能用的分时系统
待复核UNIX 1974 是 Ritchie 与 Thompson 在贝尔实验室一台 PDP-11 小机器上做出来的分时操作系统。日常类比:那个年代主流是「把一栋写字楼当一台电脑」(Multics),他们做的事相当于「在一台二手摩托上跑出一辆能上路的小汽车」——同样接多用户、同样能写代码,但代码量小到一两个人维护得过来。
论文的核心不是某一个新点子,而是一组互相配合的简单抽象:
- 层次化文件系统(一棵目录树)
- 把设备 / 管道 / 进程间通信都当成普通文件
- shell 是一个普通的用户态程序
- 进程靠
fork+exec创建 - 所有 I/O 都是字节流
加在一起,就是后来所有类 Unix 系统(Linux、macOS、BSD)共同的祖宗模板。
不读这篇,下面这些”理所当然”其实没法解释:
- 为什么 Linux / macOS 的
ls -l | grep '^d' | wc -l能跑——它由三个独立程序串成 - 为什么
/dev/null、/dev/random长得像文件、用得也像文件 - 为什么 shell 脚本能用
<>|把任意程序拼起来,无须程序作者协商 - 为什么 macOS 的 Terminal、Linux 的终端、WSL 用起来差不多——都顺着同一份模板演化
这套抽象一旦学进脑子,再读 Plan 9、Docker、io_uring、Rust for Linux 都是在同一张图上加补丁。
UNIX 设计的灵魂是 「小、组合、对称」,落到机制上是 4 个互相支撑的抽象:
-
一切皆文件:普通文件、目录、终端、磁盘、管道,全部用同一组系统调用
open / read / write / close操作。新设备进来只要写驱动暴露成/dev/xxx,所有老程序自动能用。 -
fork + exec 拆成两步:
fork复制当前进程得到子进程,exec把子进程替换成新程序。两步分开的好处是——子进程在fork之后、exec之前还是父进程的副本,可以重定向 stdin / stdout、关无关 fd、改环境变量;父进程通常只负责等待。这就是 shell 实现cmd > out.txt的关键。 -
管道(pipe):内核里的一个有限缓冲区,一端写、一端读。把两个进程的 fd 接到管道两端,就实现了进程间数据流。
A | B在 shell 里等价于「fork A、fork B、把 A 的 stdout 和 B 的 stdin 都接到同一根管道」。 -
shell 是用户程序:登录后跑的 shell 不是内核的一部分,是一个普通可执行文件。这意味着「换一个 shell」不需要改内核——zsh / fish / nushell 都可以无缝替换。
关键事实:1974 年的 UNIX 第 4 版内核大约 九千行 C 代码(CACM 原文约 9000 lines),能在 144KB 内存的 PDP-11/45 上同时支持十几个登录用户。今天 Linux 6.x 主线超过 3000 万行,但用户接触到的核心抽象——文件描述符、fork/exec、管道、信号——和这篇论文里的几乎一样。
案例 1:一行 shell 命令背后发生了什么
Section titled “案例 1:一行 shell 命令背后发生了什么”ls -l | grep '^d' | wc -l这条命令统计当前目录下有几个子目录。shell 在你按下回车后做的事:
- 创建两根管道 P1、P2
- fork 三次,得到三个子进程
- 子进程 1 把 stdout 接到 P1 的写端,exec 成
ls -l - 子进程 2 把 stdin 接到 P1 的读端、stdout 接到 P2 的写端,exec 成
grep '^d' - 子进程 3 把 stdin 接到 P2 的读端,exec 成
wc -l - shell 等三个孩子全部结束
ls、grep、wc 三个程序的作者从未协商过对方存在——他们只读 stdin、写 stdout,剩下交给内核。这就是 1974 年论文最深的洞见:协议(字节流)+ 内核胶水(管道)= 任意两个程序都能拼。
案例 2:/dev/null 是怎么”既是文件又什么都不是”的
Section titled “案例 2:/dev/null 是怎么”既是文件又什么都不是”的”some-noisy-command 2> /dev/null/dev/null 是一个特殊文件。在内核里它对应一个驱动:write 时丢弃数据并返回成功,read 时立刻返回 EOF。但它在 ABI 上完全像普通文件——可以 open、可以 dup2、可以 chmod。
这就是「一切皆文件」的威力——shell 不需要懂”丢弃 stderr”是个特殊操作,它只需要做「把 fd 2 重定向到 /dev/null 这个文件」,剩下的内核自己分发。
案例 3:fork 成本与现代替代
Section titled “案例 3:fork 成本与现代替代”fork 在小内存进程上极便宜:内核先「假装复制」(写时复制 / COW)——像复印机只登记「要印」,真改页才真印。但大内存 / 多线程进程里会变重:要抄大量「页目录」,还要通知各 CPU 丢掉旧地址缓存(TLB shootdown,像让全班擦掉过期板书)。所以现代场景看到这些替代:
posix_spawn/vfork:像「直接办新工牌」——跳过整本复印,直接构造新进程clone(Linux):fork 的可定制版,可选共享哪些资源(隔离舱 / 内存 / 文件句柄);容器、线程都基于它- Windows
CreateProcess:一次系统调用直接创建+加载,没有 fork 这一步
理解 1974 年 fork 的设计动机(分两步是为了让子进程在中间插入重定向),才能判断现代场景哪种更合适。
-
把”一切皆文件”当绝对真理:后来补上的
ioctl(给设备发「特殊指令」)、fcntl(改文件句柄开关)、socket 的bind/listen/accept(先占地址、再等人连、再接客)都是往字节流模型里打的补丁。网络、GPU、异步 I/O 这种有连接状态、有非阻塞需求的场景,统一抽象就开始漏。Plan 9 的 9P 把原则推到极致;io_uring 则承认「字节流不够用,要给异步 I/O 做新接口」。 -
误以为 fork+exec 理所当然:fork 在 1974 年很优雅,因为进程很小。今天大堆 + 多线程 + 大量 fd 的服务进程里,fork 的开销和正确性问题都在变大(父进程持锁时 fork 出的子进程可能死锁)。
-
把 shell 当稳健的程序语言:shell 最初是用户态命令解释器,不是写大型自动化的语言。CI / 安装脚本里反复踩到的引号、空格、错误码不传播、
set -e诡异,根因都是「shell 是 glue 不是引擎」。原始论文里 shell 篇幅很小,作者从没承诺它能扛复杂逻辑。 -
C 不等于”系统语言唯一选择”:论文真正的论点是「用高级语言写 OS 是可行的」,不是「必须用 C」。Rust for Linux、Plan 9 的 Alef、研究型 OS 都在挑战这个误读。
适用 vs 不适用场景
Section titled “适用 vs 不适用场景”适用:
- 通用分时 / 多用户操作系统的设计模板(Linux / macOS / BSD 的祖宗)
- 文本流为主的工具组合(命令行工具、CI、构建系统)
- 需要”协议 + 胶水”实现可扩展性的场景
不适用:
- 硬实时(毫秒级 deadline 不能抖):QNX、VxWorks 用了不一样的进程模型
- 单机百万级 IOPS / 超低延迟 I/O:io_uring、SPDK 已放弃纯 read/write 字节流
- 大规模分布式 OS 抽象(Plan 9 用 9P 把文件抽象网络化、容器走 namespace,都是在 UNIX 之外加层)
- GUI 密集应用(像素 / 事件循环 / 焦点塞进字节流不自然,X11、Wayland 另起炉灶)
历史小故事(可跳过)
Section titled “历史小故事(可跳过)”- 1969 年:Bell Labs 退出 Multics 项目,Thompson 在闲置的 PDP-7 上写出第一版 UNIX
- 1971 年:第一版 UNIX 手册公开
- 1973 年:Ritchie 用 C 重写内核(之前是汇编),从此 UNIX 能跨硬件移植——这是论文之前最关键的一步
- 1974 年:CACM 发表本论文,第一次把 UNIX 完整介绍给学术圈
- 1977 年:Berkeley 拿到源码开始改,催生 BSD
- 1983 年:Ritchie 与 Thompson 凭 UNIX 拿图灵奖
- 1991 年:Linus 在 Minix 上读到 UNIX 文档,开始写 Linux
40 年后,世界上几乎所有服务器、几乎所有手机、几乎所有超算都在跑这篇 1974 年论文的孙辈。
- 简单抽象 + 可组合 = 长寿:每个抽象都不复杂,但拼起来能表达极多场景。这是 UNIX 活 50 年的关键
- 协议比工具重要:
ls / grep / wc谁先谁后都行,因为协议(字节流 + 退出码)固定了 - 机制与策略分离:内核给 fork+exec 机制,shell 决定怎么用——这条原则后来被 X11、HTTP、容器全部继承
- 小而硬比大而全更难:1974 论文的反方案 Multics 在工程上更宏大,但 UNIX 的”刚好够用”才是赢家
- 可移植性来自抽象选对:因为内核被 C 写、用户接口被字节流抽象,UNIX 可以从 PDP-11 跳到 VAX、再跳到 x86、ARM、RISC-V,几乎不用改用户代码
- 零基础读者的实操路径:在自己电脑上打开终端、跑
man fork、man pipe、man 2 open,对照本论文里的章节看——70% 的内容可以一行一行映射回 1974 年的设计
- 论文原文 PDF:Ritchie & Thompson 1974 CACM(10 页,密度高但易读)
- 视频:The UNIX System: Making Computers Easier to Use (AT&T 1982)(25 分钟,作者本人讲解)
- Brian Kernighan 的 《UNIX: A History and a Memoir》(亲历者回忆,技术 + 八卦都有)
- 实操:在 Linux / macOS 上用
strace/dtruss跟踪ls | wc整个 fork+exec+pipe 流程 - algol-60 —— UNIX 出现前的高级语言代表,C 的远亲
- algol-60 —— 同时代的”高级语言写系统”路线,C 在 ALGOL 家族影响下诞生
- tcp —— UNIX 把套接字塞进文件抽象,让网络编程”看起来像读写文件”
- amdahl-law-1967 —— 1960 年代分时系统设计的核心约束之一
- case-for-risc-1980 —— UNIX 在 PDP-11 上跑通后,硬件人开始反思”软件简单 = 硬件可以更简单”
- bigtable-2006 —— Google 把 UNIX 的”文件即接口”推到分布式存储
- csp-hoare-1978 —— Hoare 提出的进程通信抽象,后来影响 Go 的 channel;与管道走的是不同设计路线
- dijkstra-goto —— 同期程序设计纪律的一面,UNIX 工具集是这种”小而清晰”哲学的工程化体现
- gray-1978-notes —— 1970 年代另一条系统设计主线(事务/数据库),与 UNIX 工具流形成补充而非冲突