第64期 | 投胎换骨:exec 的另一半人生
一、投胎换骨
exec 的另一半人生:Unix 运行一个程序,要分两步走。fork 先复印一个自己,exec 再把复印件的"灵魂"整个换掉——这对拆分,是 Unix 的标志性手笔¹。
妙处在不对称:fork 复制一切,exec 扔掉一切。代码、数据、堆栈,地址空间原地清空,换一具全新的身体;但 PID 不变,PPID 不变,打开的文件一个不关。投胎换的是骨,留的是名分和关系。
冷知识:内核里真正的 exec 只有一个——execve(pathname, argv, envp)。execl、execv、execvp 这些,全是 glibc 在用户态替你拼好参数、再调 execve 的包装²。连 PATH 搜索都是 libc 干的:execvp 先在 PATH 里逐个目录试,试中了才把绝对路径递给内核。内核不认"大概在哪",只认确切路径。
exec 成功之后不返回——手册原话²。走进去,就没打算回来;失败才回来,带着 -1。被捕获的信号处理函数会被重置回默认,但信号屏蔽字、打开的文件描述符原样保留。这条"换身体不换关系"的规矩,藏着 shell 最大的秘密:`ls > out.txt` 能生效,是因为 shell 先 open、再 dup2 到标准输出,然后 exec——重定向是爹妈铺好的关系,孩子只管换骨。
脚本的 #! 也是 exec 的活儿。1980 年 1 月,Dennis Ritchie 给贝尔实验室的 Version 8 Unix 加了内核级解释器指令³。他的邮件写得像产品发布:"系统改了:文件以魔术字符 #! 开头,剩下的那行就是解释器的名字。"此前这活是 shell 顺手干的;整行最初只许 16 个字符。灵感他自己承认是借来的——可能来自某次 BSD 会议⁴。
Linux 里 #! 归 fs/binfmt_script.c 管,解释器还能再套脚本,最多 4 层,再深内核报 ELOOP⁵。Linux 0.01 的 fs/exec.c 里,do_execve 只认 a.out 一种格式——ELF、脚本、misc,全是后来一层层加上去的。
好奇心停在最妙处:fork+exec 拆成两步,不是原理要求,是工程选择。Unix 赢在把"复印"和"换骨"拆成两个可组合的原语——shell 才有机会在中间插一脚:重定向、管道、环境变量,全是 fork 之后、exec 之前那条缝里塞进去的。不发明新原理,只把老原理拆成趁手的形状——这就是发展性研究的长相。
二、换任务不必换身体
如果 AI 的操作系统先出现,"运行一个程序"可能根本不需要两步走。Unix 把世界拆成两样东西:程序是死的(磁盘上的文件),进程是活的(内存里的执行)。fork+exec 是"让死的东西活过来"的仪式:先造个活的空壳,再把死的灵魂灌进去。
原生 AI OS 里没有"死的程序"——智能体本身就是持续的执行流,记忆、意图、上下文全在线。换任务不需要扔掉地址空间再重载,只需要转移注意力:把当前意图挂起,把新意图的上下文装进来。人不会为了"开始做另一件事"先把自己格式化一遍。
推演一步:AI OS 里"启动"是个动词,不是名词。没有 exec 那一瞬间的换骨,只有意图的连续流转。spawn 一个子智能体,带的不是 argv、envp 三件套,而是一份完整的意图包:要办的事、已知的上下文、办成的标准、办砸的退路。父子之间传的不是文件描述符,是记忆指针。
照见一层平时看不见的:fork+exec 默认了一个没人问过的前提——"程序和进程是两种东西"。1970 年代天衣无缝:程序小,地址空间扔了重载不心疼。今天一个大模型的权重几十 GB,"换骨"的代价大到没人敢在推理时 exec 一个新模型——于是有了常驻进程、热加载、LoRA 切换,全是绕着"换骨太贵"打的补丁。旧抽象的账,迟到了五十年才寄到。
落地还差几步:缺一个"意图即进程"的原语。今天的容器、serverless、常驻推理服务,都在用 1974 年的 fork+exec 语义硬套"长期存活、随时换任务"的智能体。posix_spawn 已经把两步并成一步;下一步,是把"换程序"换成"换意图"。
范式之眼:exec 扔掉的是线性地址空间——顺序指令流的身体。AI 时代的主体是权重矩阵和 KV cache,是向量空间。"换任务"该是权重的热切换、上下文的流转,而不是把整片线性内存倒掉重来。这条线,第 61 章收束。
三、一条命令,看 exec 换骨
手边任何一台 Linux 机器,先看"成功不返回"和"PID 不变":
sh -c 'echo "换骨前 PID=$$"; exec /bin/echo "换骨后 PID=$$"'
两个 PID 一模一样——身体换了,人没换。再看 PATH 搜索是谁干的:
strace -f -e trace=execve sh -c 'ls /no-such-dir' 2>&1 | grep execve
你会看到一串 ENOENT 的试探,最后一次命中绝对路径——PATH 的腿,全是 sh 在用户态替你跑的²。
容器时代最实用的 exec 姿势:entrypoint 脚本的最后一行写 `exec "$@"`。脚本把自己原地换成业务进程,PID 1 直接就是你的应用——信号送得进来,第 63 期的 tini 收尸逻辑也接得上。不写 exec,PID 1 是个 sh,SIGTERM 先敲 sh 的门,经常石沉大海。
落到九羲:边缘盒与容器里的推理服务,凡是"脚本启动业务进程"的结构,结尾都值得一个 exec。换骨不换名分,进程树少一层,信号链路短一截——1974 年的原语,在 2026 年的容器里依然趁手。
觉 然 小 注
「过去心不可得,现在心不可得,未来心不可得。」⁶(思想化用)
execve 是最彻底的断舍离:前世的代码、数据、堆栈一概不留;但"谁在走"——PID、打开的文件、爹是谁——一件不丢。不住于过去,不等于失忆。该忘的忘,该留的留,这才是换骨的火候。
引 用 与 出 处
- [1] D. M. Ritchie & K. Thompson. The UNIX Time-Sharing System. Communications of the ACM, 1974. https://doi.org/10.1145/361011.361061
- [2] execve(2). Linux man-pages:内核层面唯一的 exec 系统调用,成功不返回;被捕获的信号重置为默认,文件描述符保留. https://man7.org/linux/man-pages/man2/execve.2.html
- [3] Dennis Ritchie. 1980 年 1 月 10 日邮件:为 Version 8 Unix 引入 #! 解释器指令;此前由 shell 代劳;整行暂限 16 字符. https://en.wikipedia.org/wiki/Interpreter_directive
- [4] Sven Mascheck. The #! magic, details about the shebang mechanism:Ritchie 承认灵感来自别处(或为 BSD 会议);BSD 系实现史. https://www.in-ulm.de/~mascheck/various/shebang/
- [5] Cristian Regolo. Linux internals: how interpreter scripts work:Linux 下 #! 嵌套上限 4 层,超限报 ELOOP. https://cristian.regolo.cc/2017/01/29/linux-internals-how-interpreter-scripts-work.html
- [6] 《金刚般若波罗蜜经》第十七品,鸠摩罗什译.(思想化用)
· · ·