第60期 | 复印清单,拆成了一把旋钮
一、复印清单,拆成了一把旋钮
fork 只会一种复印——全套。task_struct 复印一份,页表复印一份(再标成写时复制)。1991 年够用了,可很快有人发现不对劲:服务器要并发接成千上万个连接,只想多干一份活,不想为每个连接多要一套房子。整单复印,太贵了。
1996 年,Linux 2.0 带来了一个新的系统调用:clone()¹。它不预设复印什么,而是递给你一张勾选清单:CLONE_VM,地址空间共享,不复印;CLONE_FILES,文件描述符表共享;CLONE_FS,根目录与当前目录共享;CLONE_SIGHAND,信号处理表共享。什么都不勾、只带一个 SIGCHLD,clone 就退化成 fork。
于是有了一个反常识的事实:在 Linux 内核眼里,进程和线程是同一种东西——都是被调度器调度的 task。fork 与 pthread_create 走的是同一个内核入口,区别只在那张清单上勾了什么²。strace 跟一下便知:fork 那行是 clone(SIGCHLD),干干净净,没有 CLONE_VM;pthread_create 那行拖着一长串 CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD。
同一年,Xavier Leroy 写出了 LinuxThreads³——第一个吃螃蟹的:线程,就是"共享了地址空间的进程"。可它有个硬伤:每个线程顶着独立的 PID,POSIX 却要求同一进程的线程共享一个 PID;信号处理也乱了套,库还偷用了 SIGUSR1 和 SIGUSR2 做线程间协调,用户程序就别想再碰这两个信号。凑合用了七年。
2003 年,NPTL 随内核 2.6 进场³:CLONE_THREAD 让同一线程组的线程共享一个线程组 ID(tgid),POSIX 的"同一 PID"要求终于落地²。线程从此名正言顺。
回头看,Ritchie 与 Thompson 1974 年的论文讲进程的"映像"(image)⁴——代码、数据、栈三段。二十年后,Linux 把这"三段"拆成了一张可勾选的清单。进程与线程的边界不是天堑,是旋钮拧到哪一格。而这一切没有发明任何新原理——并发的原理早熟了,赢的是把"整单复印"工程化成"按项勾选"的中间层。发展性研究,长的就是这个样子。
范式之眼补一句:CLONE_VM 共享的仍是线性地址空间——旧抽象。今天的算力主体是向量阵列,共享的单位迟早要从"一段线性内存"变成"一片张量空间"。旋钮还在,刻度要换了。
二、agent 分叉,借记忆不借命
如果 AI 的操作系统先出现,它派生子任务的方式,不会是 fork 整套。派一个子 agent 去查资料:它需要父 agent 的世界模型(共享,像 CLONE_VM)、工具连接池(共享,像 CLONE_FILES),但推理上下文必须独立(像私有的栈),token 账本也要独立结算。
今天的多智能体框架派生子任务,常常是整段 prompt 复制——贵且笨。Linux 二十年前走过的路摆在那:从"整单复印"到"按项勾选"。agent 的 clone,也该有一张清单:SHARE_WORLD_MODEL / SHARE_TOOLSET / PRIVATE_REASONING_CONTEXT / PRIVATE_BUDGET。
更妙的是,agent 分叉可以比进程分叉更便宜。模型权重本就只读,天然共享;真正要 fork 的只是注意力状态——KV cache 按页管理、写时共享,正是 PagedAttention(SOSP 2023)⁵干的事:把操作系统的分页思想,用进了大模型的内存。fork 的哲学,在向量时代借尸还魂。
照见那根隐形的线:Linux 用 flags 把"什么共享、什么隔离"说清楚,才有了线程,后来才有了容器(CLONE_NEWNS 一家)。AI OS 的第一张设计图,也该从这样一张"共享清单"画起——先回答什么可共享,再谈智能。
三、今晚,亲眼看一次 fork 与线程的分岔
手边任何一台 Linux 机器,写二十行 C:
int x = 0;
fork 版:子进程里 x = 100,父进程打印 x;
pthread 版:线程里 x = 100,主线程打印 x。
跑起来看:fork 版打印 0——子进程写进了自己的副本,父进程看不见;pthread 版打印 100——线程写的是唯一的正本。同一行 x = 100,命运完全不同,区别不在代码,在 clone 那一刻勾了什么。
再跑一遍 strace -f -e trace=clone:fork 那行没有 CLONE_VM,pthread_create 那行带着 CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD。一分钟,亲眼看完"进程与线程在内核里是同一种东西"。
映射到普遍场景:做多智能体编排的团队,派生子任务前先列一张"共享清单":模型权重、工具连接池这类只读的东西只管共享;推理上下文、token 账本这类会变的东西各自记账。先列清单,再写代码——这是 clone 教的工程顺序。
觉 然 小 注
「应无所住而生其心。」⁶ fork 是相,线程是相,"进程"这个词本身也是相。clone 把相拆成了清单——不住于任何一种生法,系统才有弹性。无住,不是没有章法,是章法活成了旋钮。
引 用 与 出 处
- [1] Leroy, Xavier. LinuxThreads README (release 0.7). glibc 源码存档, 1996. https://gitlab.opencode.de/CoDE_Admin/glibc/-/tree/c3966b88eeb1e0b5726ca16a70a94269d40ee152/linuxthreads
- [2] Bendersky, Eli. Launching Linux threads and processes with clone. 个人技术博客, 2018. https://eli.thegreenplace.net/2018/launching-linux-threads-and-processes-with-clone.html
- [3] Wikipedia contributors. LinuxThreads. Wikipedia(在线百科,持续更新). https://en.wikipedia.org/wiki/LinuxThreads
- [4] Ritchie, Dennis M. & Thompson, Ken. The UNIX Time-Sharing System. Communications of the ACM, 1974. https://doi.org/10.1145/361011.361061
- [5] Kwon et al. Efficient Memory Management for Large Language Model Serving with PagedAttention. SOSP, 2023.
- [6] 《金刚般若波罗蜜经》. 鸠摩罗什译. (思想化用)
· · ·