跳转到内容

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 个互相支撑的抽象:

  1. 一切皆文件:普通文件、目录、终端、磁盘、管道,全部用同一组系统调用 open / read / write / close 操作。新设备进来只要写驱动暴露成 /dev/xxx,所有老程序自动能用。

  2. fork + exec 拆成两步fork 复制当前进程,exec 把复制体替换成新程序。两步分开的好处是——父进程在 fork 之后、exec 之前还是自己,可以重定向 stdin / stdout、关无关 fd、改环境变量。这就是 shell 实现 cmd > out.txt 的关键。

  3. 管道(pipe):内核里的一个有限缓冲区,一端写、一端读。把两个进程的 fd 接到管道两端,就实现了进程间数据流。A | B 在 shell 里等价于「fork A、fork B、把 A 的 stdout 和 B 的 stdin 都接到同一根管道」。

  4. shell 是用户程序:登录后跑的 shell 不是内核的一部分,是一个普通可执行文件。这意味着「换一个 shell」不需要改内核——zsh / fish / nushell 都可以无缝替换。

关键事实:1974 年的 UNIX 第 4 版内核大约 一万行 C 代码,能在 144KB 内存的 PDP-11/45 上同时支持十几个登录用户。今天 Linux 6.x 主线超过 3000 万行,但用户接触到的核心抽象——文件描述符、fork/exec、管道、信号——和这篇论文里的几乎一样。

实践案例

案例 1:一行 shell 命令背后发生了什么

Terminal window
ls -l | grep '^d' | wc -l

这条命令统计当前目录下有几个子目录。shell 在你按下回车后做的事:

  1. 创建两根管道 P1、P2
  2. fork 三次,得到三个子进程
  3. 子进程 1 把 stdout 接到 P1 的写端,exec 成 ls -l
  4. 子进程 2 把 stdin 接到 P1 的读端、stdout 接到 P2 的写端,exec 成 grep '^d'
  5. 子进程 3 把 stdin 接到 P2 的读端,exec 成 wc -l
  6. shell 等三个孩子全部结束

lsgrepwc 三个程序的作者从未协商过对方存在——他们只读 stdin、写 stdout,剩下交给内核。这就是 1974 年论文最深的洞见:协议(字节流)+ 内核胶水(管道)= 任意两个程序都能拼

案例 2:/dev/null 是怎么”既是文件又什么都不是”的

Terminal window
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 能在中间插入重定向),才能判断现代场景哪种更合适。

踩过的坑

  1. 把”一切皆文件”当绝对真理ioctlfcntl、各种 mode flag、socket 的 bind/listen/accept 都是后来为了塞进字节流模型打的补丁。遇到网络、GPU、异步 I/O 这种有连接状态、有非阻塞需求的场景,统一抽象就开始漏。Plan 9 的 9P 协议是把这条原则推到极致的尝试,io_uring 则是承认”字节流不够用、要给异步 I/O 做新接口”。

  2. 误以为 fork+exec 理所当然:fork 模型在 1974 年很优雅,因为进程很小。今天大堆 + 多线程 + 大量 fd 的服务进程里,fork 的开销和正确性问题都在变大(比如父进程持有锁时 fork 出的子进程可能死锁)。

  3. 把 shell 当稳健的程序语言:shell 最初定位是用户态命令解释器,不是写大型自动化的语言。CI 脚本、安装脚本里反复踩到的引号 / 空格 / 错误码不传播 / set -e 行为诡异,根因都是「shell 是 glue 不是引擎」。原始论文里 shell 的篇幅其实很小,作者从没承诺它能扛复杂逻辑。

  4. 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 年论文的孙辈。

学到什么

  1. 简单抽象 + 可组合 = 长寿:每个抽象都不复杂,但拼起来能表达极多场景。这是 UNIX 活 50 年的关键
  2. 协议比工具重要ls / grep / wc 谁先谁后都行,因为协议(字节流 + 退出码)固定了
  3. 机制与策略分离:内核给 fork+exec 机制,shell 决定怎么用——这条原则后来被 X11、HTTP、容器全部继承
  4. 小而硬比大而全更难:1974 论文的反方案 Multics 在工程上更宏大,但 UNIX 的”刚好够用”才是赢家
  5. 可移植性来自抽象选对:因为内核被 C 写、用户接口被字节流抽象,UNIX 可以从 PDP-11 跳到 VAX、再跳到 x86、ARM、RISC-V,几乎不用改用户代码
  6. 零基础读者的实操路径:在自己电脑上打开终端、跑 man forkman pipeman 2 open,对照本论文里的章节看——70% 的内容可以一行一行映射回 1974 年的设计

延伸阅读

关联

  • 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 工具流形成补充而非冲突