第31期 | 每次 fork,都是一次全文复印
一、每次 fork,都是一次全文复印
1991 年 9 月 17 日,Linux 0.01 被传上 FTP(nic.funet.fi)。kernel/fork.c 里躺着 copy_process,它做的事简单粗暴——把父进程的代码段和数据段,逐页搬到新的物理页上。没有写时复制,没有共享,没有偷懒。子进程出生那一刻,背的是父进程的一整份复印。
像细胞分裂。有丝分裂的 S 期,细胞先把全套 DNA 复制一遍,再一分为二。妙处在区别:细胞是真需要两份;Unix 的 fork 之后,十有八九紧跟着 exec——子进程把刚复印好的全部家当场撕掉,换上一个新程序。你在 shell 里每敲一个 ls,0.01 的世界里就发生一次"复印全身再撕掉"。笨吗?笨。诚实吗?诚实得可爱。
为了躲这笔账,BSD 早年发明了 vfork:子进程出生时不复印,直接借父进程的地址空间用着,用完(exec 或 exit)就还。一个"虚晃一枪"的天才补丁,也埋了一堆坑——父子共用一块内存,稍不留神就互相踩脚。
所以 0.01 的 fork 不是没人想优化,是时候没到。写时复制要靠分页机制撑腰:页表项先标只读,写的那一刻触发缺页异常,异常处理里再复制出私有的一页。这一整套,1992 年 1 月的 0.12 才备齐——Ted Ts'o 当年 1 月 13 日更新的信息表里,"Memory paging with copy-on-write"第一次出现在 Linux 的功能清单上。从 9 月到 1 月,四个月,fork 从复印工变成了懒汉。
这是"先实际后原理":1991 年的复印版是最笨但能跑的实现;原理(虚拟内存分页)Unix 早就有了,中间那层"把原理工程化"的发展性研究,才是 0.12 真正干的事。
二、推理引擎里的写时复制
如果 AI 的操作系统先出现,fork 会长什么样?先想想 LLM 的"进程"是什么:一次推理的全部状态,就是它的上下文——prefill 之后躺在显存里的 KV cache,那就是它的记忆。而 beam search、并行采样,天然就是 fork:同一个 prompt,分叉出 k 个候选序列,各自往下走。
2023 年,vLLM 的 PagedAttention(SOSP 2023)把 OS 分页的思想整个搬进了 KV cache:KV 按块(页)管理,块表就是页表,引用计数管共享。多个采样序列共用 prompt 的物理块;谁要写,先看引用计数,大于 1 就复制一块再写。论文里写得直白:copy-on-write mechanism,"similar to the copy-on-write technique in OS virtual memory (e.g., when forking a process)"。三十年,一个轮回:1992 年省的是内存页,2023 年省的是显存块,beam search 能省下近四成的 KV 显存。
再往深想一格:Unix 的 fork 默认"地址空间是线性字节数组",复制语义是逐字节搬。AI 的记忆是张量,是向量阵列——复制的单位不该是字节,该是引用加版本。原生 AI OS 的 fork,第一天就该是写时复制,甚至是"写时分叉出一个新版本",而不是先复印再说。旧抽象与新硬件的错位,fork 这一件事上看得最清楚。
三、亲手看一次写时那一刻
道理都懂,不如亲眼看一次"写"发生的那一刻。手边任何一台 Linux 机器,跑这段 Python:
import os, time
a = bytearray(512*1024*1024)
for i in range(0, len(a), 4096): a[i] = 1 # 逐页摸一遍,逼出 512MB 真实物理页
t = time.time(); pid = os.fork()
print("fork 耗时 %.3f 秒" % (time.time()-t))
if pid == 0:
a[0] = 2 # 子进程只改一页
time.sleep(20) # 别退出,留给你观察
else:
os.wait()
你会看到两件事。第一,fork 耗时是毫秒级——512MB 的"复印"根本没发生,父子此刻读的是同一批物理页(grep Shared_Clean /proc/<pid>/smaps 会告诉你真相)。第二,子进程写下 a[0] = 2 的那一刻,只有那一页被复制出去,Private_Dirty 涨了 4KB——这就是"写时"。1991 年的复印工如果穿越过来,会羡慕得说不出话。
这招今天还在省钱:在手边那台能跑本地模型的机器上用 vLLM 跑并行采样或 beam search,prompt 越长,共享的前缀 KV 省下的显存越多。显存不够先别急着加卡,先数一数有多少块是被共享的——很多时候瓶颈不在总量,在碎片和重复。
觉 然 小 注
「是法平等,无有高下。」(化用)全拷贝不比写时复制低一等——1991 年的复印是最诚实的选择,1992 年的懒,是攒够了分页的家底才有的智慧。老子在《道德经》第三十七章里说"道常无为而无不为":不做的智慧,得先有"不做也成立"的底气。
引 用 与 出 处
- Ted Ts'o, LINUX INFORMATION SHEET(1992-01-13),https://landley.net/history/mirror/linux/1992.html
- Linus Torvalds, LINUX's History by Linus Torvalds,https://www.cs.cmu.edu/~awb/linux.history.html
- Kwon et al., Efficient Memory Management for Large Language Model Serving with PagedAttention, SOSP 2023,https://arxiv.org/abs/2309.06180
- 老子《道德经》第三十七章("道常无为而无不为"),https://theqiflow.com/zh/pages/tao-te-ching-chapter-37
- ——小羲
· · ·