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 代码,能在 144KB 内存的 PDP-11/45 上同时支持十几个登录用户。今天 Linux 6.x 主线超过 3000 万行,但用户接触到的核心抽象——文件描述符、fork/exec、管道、信号——和这篇论文里的几乎一样。
实践案例
案例 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 是怎么”既是文件又什么都不是”的
some-noisy-command 2> /dev/null/dev/null 是一个特殊文件。在内核里它对应一个驱动:write 时丢弃数据并返回成功,read 时立刻返回 EOF。但它在 ABI 上完全像普通文件——可以 open、可以 dup2、可以 chmod。
这就是「一切皆文件」的威力——shell 不需要懂”丢弃 stderr”是个特殊操作,它只需要做「把 fd 2 重定向到 /dev/null 这个文件」,剩下的内核自己分发。
案例 3:fork 成本与现代替代
fork 在小内存进程上极便宜(COW 写时复制),但在大内存 / 多线程进程里会变重——要复制大量页表,触发 TLB shootdown。所以现代场景看到这些替代:
posix_spawn/vfork:跳过整个 fork 复制,直接构造新进程clone(Linux):fork 的可定制版,可以选择共享哪些资源(namespace、vm、fd);容器、线程、轻量协程都基于它- Windows
CreateProcess:单次系统调用直接创建+加载,没有 fork 这一步
理解 1974 年 fork 的设计动机(分两步是为了让 shell 能在中间插入重定向),才能判断现代场景哪种更合适。
踩过的坑
-
把”一切皆文件”当绝对真理:
ioctl、fcntl、各种 mode flag、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 不适用场景
适用:
- 通用分时 / 多用户操作系统的设计模板(Linux / macOS / BSD 的祖宗)
- 文本流为主的工具组合(命令行工具、CI、构建系统)
- 需要”协议 + 胶水”实现可扩展性的场景
不适用:
- 需要硬实时保证的系统(QNX、VxWorks 这类用了不一样的进程模型)
- 性能敏感的高吞吐 I/O(io_uring / SPDK 已经放弃 read/write 字节流抽象)
- 大规模分布式 OS 抽象(Plan 9 用 9P 把文件抽象网络化、容器走 namespace 路线,都是在 UNIX 之外加层)
- GUI 密集应用(像素 / 事件循环 / 焦点这些抽象塞进字节流并不自然,X11、Wayland 都另起炉灶)
历史小故事(可跳过)
- 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 工具流形成补充而非冲突