进程创建与终止 — fork/exec/exit/wait 四原语¶
这一章讲什么: 上一章是进程的「解剖学」,这一章是「生理学」: 进程怎么诞生(fork 复制自己)、怎么换灵魂(exec 换装程序但 不换身份)、怎么死(exit 留遗言)、父亲怎么收尸(wait 读遗言 并释放资源)。四者合称 Unix 进程控制的四支柱——shell、服务器、 守护进程,全是它们四个的组合。shell 的神秘性在这一章彻底 瓦解:它只是个 while 循环 + fork + execvp + waitpid。
1. 顶层全景:进程的一生与四个原语¶
fork() 复制出几乎一模一样的子进程(返回值分岔:父得子 PID,子得 0)
│ 复制什么:全部内存(写时才真拷,COW)、描述符表(指向同一
│ OFD——文件偏移共享!)、进程描述符(改 PID/PPID/清挂起信号)
▼
exec() 子进程换程序:内存全换、身份全留(PID/目录/fd/信号掩码……)
│ 成功不返回;六个库函数(execl/execle/execlp/execv/execvpe/execvp)
│ 按 l=列表/v=数组、p=PATH 搜索、e=自定义环境 组合命名
▼
exit() 死前收拾:atexit 注册的函数(LIFO)→ 冲刷 stdio → _exit 进内核
│ 内核:关 fd、释放内存、把孩子过继给 init、留 8 位遗言、给父发 SIGCHLD
│ 没人收的尸体 = zombie(占着进程表);父先死 = orphan(过继 init)
▼
wait() 父亲阻塞收尸:拿遗言(8 位退出码 / 几号信号杀的 / 是否转储)、
让内核释放子进程最后资源;waitpid 可加 WNOHANG 轮询、
等指定孩子、收 stop/continue;SIGCHLD handler 里收割 = 异步版
图说:前两步是「生和换装」,后两步是「死和收尸」;每一步都有
一个经典的坑在等着。 主线例子:fork_demo2 的变量实验——
子进程把三个变量全改了,父进程的一个都没变;wait_demo2 的
遗言实验——exit(356) 传出去的是 100。
2. fork():一次调用,两个返回¶
pid_t fork(void) 无参数;成功时父进程得到子 PID、子进程得到 0
(PID 恒不为 0,所以 0 就是孩子的身份证),失败返 -11。标准范式:
pid_t r = fork();
if ( -1 == r ) /* 出错 */
else if ( 0 == r ) /* 孩子走这条 */
else /* 父亲走这条,r 是孩子的 PID */
两个进程从 fork 返回后的下一条指令开始各自执行——「像路上 的分叉,像细胞的有丝分裂」2。克隆的不只是内存:进程描述符 也复制,但改几处——孩子有自己的 PID、PPID=父 PID、挂起信号 清零、定时器一个不继承3。现代 Linux 用 copy-on-write: 内存页先共享,谁写谁才真正复制4。
主线例子走查:fork_demo2 变量实验¶
程序追踪三个变量:全局 globalvar=10(data 段)、局部 localvar=0
(栈)、堆上字符串 heapvar="On the heap."。fork 后孩子把三个
全改(localvar→1、globalvar→自己的 PID、heapvar 涂成 x),
父亲睡 2 秒让孩子先输出,然后各自打印。运行结果5:
fork 前(父): pid=3686, localvar=0, globalvar=10, heapvar="On the heap."
孩子先打印: Child PID=3687, localvar=0, globalvar=10, heapvar="On the heap."
孩子改完后: localvar=1, globalvar=3687, heapvar="xxxxxxxxxxxx"
父亲最后打: localvar=0, globalvar=10, heapvar="On the heap." ← 原封不动
图说:孩子是「副本」不是「别名」——堆、栈、数据段各拷贝一份, 互不可见。 想真共享内存,那是第 12 章的共享内存。
但文件描述符不是副本——偏移共享!¶
fork 复制描述符表,但表项指向内核里同一个 OFD——文件偏移
共享。fork_demo3 里父子各写 10 个字符(子写 c、父写 p),
cat newfile 看到 pcpcpcppcpcppcpcpccc 交错的乱序——
两个进程的字节你插我我插你,谁也不覆盖谁,因为偏移只有一个6。
这是第 13 章管道的伏笔:fork 后的共享 fd 是进程间通信的地基。
坑二连:stdio 缓冲与竞争¶
fork_demo4 的诡异输出:./fork_demo4 正常,| cat 后
「About to create many processes...」打印了 5 遍。机理链7:
- stdout 连终端 → C 库行缓冲,带
\n立即 flush,fork 时 缓冲区是空的; - stdout 重定向到管道/文件 → 全缓冲(≥256 字节一块),那一行 留在缓冲区里没被冲走;
- fork 把孩子诞生前的缓冲区内容也复制了——4 个孩子每人 吐出一份存货。
修复:if (!isatty(fileno(stdout))) fflush(stdout); 在 fork 前
冲掉8。再往深是竞争:N 个进程无同步地 printf,p=0.05、N=64 时
「至少两个同时写」的概率约 0.84——行丢失几乎必然。对策:
write()(Linux 3.14 起原子)+ fcntl 给 fd 加 O_APPEND
(每次写前原子地 seek 到尾)9:
用信号做生产者-消费者同步(过渡方案)¶
sync_io_demo:fd 开成 O_RDWR 当共享缓冲区,子产父消。
因为偏移共享、读写位置又各自需要,两个进程必须轮流来——
用 SIGUSR1(该你读了)/SIGUSR2(写完了)+ pause() 互相点名,
每人操作前 lseek 回自己存的 pos,操作完把 pos 存下来。
输出八字符一行、与 fold -w8 的文件内容完全一致——无丢失
无重复10。但这方案脆弱(信号不排队、pause 竞态),第 12、
13 章有更好的。
3. 其他生进程的方式(知道即可)¶
vfork():不复制内存让孩子共享,省但危险——man 页自己引用
4.2BSD 的话「这个系统调用终将被消除」;POSIX.1-2008 已移除11。
clone():Linux 特有,可共享地址空间、传函数给孩子跑——线程
的底层(glibc 的 pthread_create 用它)12。
4. exit() 与 _exit():死前的仪式¶
main 里 return n(int 型)== exit(n)。exit() 三步,顺序
固定13:
- atexit 注册的函数按注册逆序跑(LIFO)——做「离开时总要
做的事」(记日志、还终端)最干净;上限
sysconf(_SC_ATEXIT_MAX)(demo 里打印出 2,147,483,647)。孩子继承父的 atexit 清单14; - 冲刷并关闭所有 stdio 流;
- 调
_exit(status)进内核。
内核侧 _exit 五动作:关所有 fd → 释放内存 → 在世的孩子们
过继给 init → 把 exit 值的低 8 位存成「遗言」→ 给父发 SIGCHLD15。
父先死的孩子叫孤儿(orphan),由 init 收养——孤儿不惨,
zombie(僵尸)才麻烦:死了没人 wait 的尸体,内核不敢清进程表
(怕父还来收尸),占着 PID 和表项;服务器不收割的话表会满、
再也 fork 不动16。
5. exec():换程序不换身份¶
execve(path, argv, envp) 三个参数全是指针:程序路径(不搜
PATH)、参数向量、环境向量——ve = vector。成功不返回;
返回了必错(-1/errno)——所以 canonical 写法是调用后面直接跟
fatal_error(errno, "execve")17。
换什么、留什么(和第 10 章 task_struct 的分类呼应)18:
| 换掉 | 留下 |
|---|---|
| text/data/BSS/堆/栈全换 | PID、PPID、PGID、SID 全留 |
| 信号处置有 handler 的重置为默认 | 挂起信号、信号掩码、umask |
| atexit 清单作废 | 打开的 fd(目录流除外) |
| locale 重置为 C | 当前/根目录、控制终端、alarm 余量、itimer(POSIX timer 不留!) |
六个库函数一张表:l=参数逐个列/v=参数数组;p=按 PATH 搜;
e=自己传环境。execlp("ls","ls","-l",NULL) 最常用;
参数个数运行时才知的场合只能用 v 系(列表型要写死参数个数,
没法用)19。execve_demo2 还演示了环境可以整个换掉——只给
两条 AUTHOR=...,print_args_env 打印出来真就只有那两条20。
6. wait/waitpid:收尸与遗言¶
wait(&status):有 waitable 孩子就立即收一个(返回其 PID),
没有就阻塞;没有孩子可调 ECHILD——收多个孩子就 while
((pid=wait(NULL)) != -1) 循环到 ECHILD21。
遗言的位布局(Linux 传统格式,移植代码用宏别用位运算):
正常退出 → 第 2 字节是退出码;被信号杀 → 低 7 位是信号号、
bit7 是 core dump 标志22。配套宏:WIFEXITED / WEXITSTATUS
/ WIFSIGNALED / WTERMSIG / WCOREDUMP。
主线例子第二程:wait_demo2 的遗言23:
$ ./wait_demo2 32 → Child 17176 terminated with exit status 32.
$ ./wait_demo2 356 → Child 17193 terminated with exit status 100. ← 只有低 8 位!
$ (另开终端 kill -1) → terminated as a result of signal 1.
$ (kill -3) → terminated as a result of signal 3 and a core dump took place.
waitpid(pid, &status, options) 三个升级:pid>0 等指定孩子 / 0 等本组 / -1 等任意 / <-1 等进程组;options 有 WNOHANG (没死立刻返 0,不阻塞——轮询模式)、WUNTRACED(孩子也 stop 时返回)、WCONTINUED(SIGCONT 恢复时也返回——作业 控制的检测原语)24。
异步收割:SIGCHLD handler 的设计难题¶
父进程不想傻等?装 SIGCHLD handler,信号一到就地收尸。但 三个坑25:
- SIGCHLD 不排队——四个孩子同时死,最多来两个信号,
handler 必须
waitpid(si_pid, ...)按 si_pid 点名收; - 保存 errno——handler 开头
saved_errno = errno, 结尾恢复(waitpid 会改 errno); - SA_NOCLDWAIT 是陷阱——设了它子进程直接销尸,wait 会 ECHILD;handler 里还要 wait 就别设,自相矛盾。
sighandler_wait_demo 的兜底设计:handler 里收一个、
sigchld_count++;main 退出前先 BLOCK SIGCHLD 再
WNOHANG 循环清仓——把 handler 没赶上的 zombie 扫掉
(四个孩子同时退出的运行记录里,恰有一个是 main 收的)26。
7. 落点:spl_sh——60 行的 shell¶
do {
printf("spl_sh$ "); /* 提示符 */
getline(&line, &len, stdin); /* 读一行 */
strtok 切成 argvec,末尾补 NULL; /* 解析 */
if (argvec[0]=="exit") exit; /* 内建命令:shell 自己做 */
else if (argvec[0]=="help") { ...; continue; }
if ( fork() == 0 ) /* 孩子: */
execvp(argvec[0], argvec); /* 换装成命令 */
else /* 父亲: */
waitpid(pid, &status, 0); /* 等它干完 */
} while (TRUE);
图说:bash 的骨架就是这个循环,只是多了重定向、管道、作业 控制、脚本。内建命令(exit/help)的本质:不 fork,shell 自己 执行27。
system("cmd") 一句话跑 shell 命令——fork + execl("/bin/sh")
+ wait 的封装。两个问题:不快(shell 里再 fork)且危险
(用户输入直接拼命令、setuid 程序里绝对禁用)28。
8. 作者的判断与证据¶
有明证的:
- fork 的复制语义(COW、偏移共享、描述符差异清单)有 man 页 和 fork_demo2/3 的运行记录双重证据356。
- 缓冲区分叉的解释(行缓冲 vs 全缓冲、fork 复制缓冲区)机理完整, 可复现7。
- 竞争概率 0.19/0.48/0.84(p=0.05,N=16/32/64)是二项分布的 直接计算9。
- wait 的遗言位布局与宏、356→100 的截断,有运行记录2223。
作者的判断:
- 「vfork 别用、clone 是线程的底层」——与 POSIX 的废弃声明一致, 立场稳1112。
- SIGCHLD handler 五要素(不 NODEFER、存 errno、SA_SIGINFO 拿 si_pid、sigchld_count 兜底、退出前 BLOCK+WNOHANG 清仓)是 一条「诚实的缺陷清单」驱动的设计——承认信号不排队救不了全部, 用计数器 + 兜底循环补2526。
- 「system() 图省事可以、认真写别用」——安全界共识,作者给了 setuid 禁令的具体场景28。
9. 边界与局限¶
- 管道没讲——spl_sh 不能
ls | wc(要 dup2 重定向,第 13 章); 后台作业(&)留作习题27。 - vfork/clone 只有导览级介绍,线程化的细节全归第 15 章12。
- waitid 只在 SYNOPSIS 出现,没展开24。
- 进程优先级/调度(nice、SCHED_*)完全没碰——本书是编程接口 导向,调度策略不在范围。
- SIGCHLD 异步收割的「信号丢失」只能缓解不能根除——原书明说 「仍可能丢,所以要有兜底」;彻底的解法是 signalfd(本书未讲)26。
10. 可带走的¶
主线例子,一行复述: fork 后孩子改 localvar/globalvar/heapvar, 父亲一个不变(内存是副本);exit(356) 父亲只收到 100(遗言 8 位)。
- fork 一次调用两个返回:父得子 PID、子得 0;-1 是失败。
- 孩子=内存副本(COW 优化),不是别名;但 fd 指向同一 OFD—— 文件偏移共享,交错写要小心。
- fork 前 fflush(stdout)!——不冲的话全缓冲模式下每个孩子都 带着父的缓冲区存货。
- 多进程写同一文件:write + O_APPEND(fcntl F_GETFL/F_SETFL); printf 竞争概率随进程数指数上升。
- exit():atexit 逆序 → 冲流 → _exit;内核关 fd、放内存、过继 孩子、留 8 位遗言、发 SIGCHLD。
- zombie = 死了没人 wait;服务器必须收割,否则进程表满。
- exec 成功不返回;身份(PID/目录/fd/掩码)全留,程序全换, handler 重置默认、atexit 作废。
- exec 家族:l=列表、v=数组、p=PATH 搜、e=自定义环境; 参数个数不定只能用 v 系。
- wait 循环到 ECHILD;遗言用宏解析(WIFEXITED/WEXITSTATUS/ WIFSIGNALED/WTERMSIG/WCOREDUMP),别自己拆位。
- waitpid:WNOHANG 轮询、等指定 pid、WUNTRACED/WCONTINUED 检测停/继续——作业控制的检测原语。
- SIGCHLD handler 收割:SA_SIGINFO 拿 si_pid 点名 waitpid、 存 errno、计数兜底、退出前 BLOCK+WNOHANG 清仓。
- shell = while 循环{ getline → strtok → fork → 子 execvp → 父 waitpid };内建命令不 fork。
11. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| fork 语义与返回值范式 | Chapter 11: Process Creation and Termination | text/17-ch11-chapter-11-process-creation-and-termination.txt:87(搜「pid_t fork」) · :111(搜「mitosis」) · :124(搜「typical coding paradigm」) |
| 孩子描述符差异 | 同上 | :290(搜「unique PID」) · :293(搜「pending signals」) |
| copy-on-write | 同上 | :188(搜「copy-on-write」) |
| fork_demo2 变量实验 | 同上 | :253(搜「fork_demo2」) · :263(搜「xxxxxxxxxxxx」) |
| 文件偏移共享 | 同上 | :399(搜「pcpcpc」) |
| 缓冲区分叉与 isatty 修复 | 同上 | :478(搜「copy of the parent's buffer」) · :530(搜「isatty」) |
| 竞争概率与 O_APPEND/fcntl | 同上 | :561(搜「0.84」) · :592(搜「F_GETFL」) · :571(搜「atomically」) |
| sync_io_demo 信号同步 | 同上 | :678(搜「sync_io_demo」) · :704(搜「SIGUSR1」) |
| vfork/clone | 同上 | :809(搜「architectural」) · :813(搜「clone」) |
| exit/atexit/_exit | 同上 | :854(搜「run in the reverse」) · :878(搜「inherit」) · :889(搜「init the parent」) · :954(搜「2147483647」) |
| zombie/orphan | 同上 | :1452(搜「zombie」) |
| execve 与保留清单 | 同上 | :993(搜「execve」) · :1036(搜「reset to their defaults」) · :1044(搜「Exit functions are not preserved」) |
| exec 家族表与 l/v/p/e | 同上 | :1230(搜「Table 11-1」) · :1264(搜「no easy way」) · :1177(搜「AUTHOR=stewart」) |
| wait 与 ECHILD 循环 | 同上 | :1476(搜「ECHILD」) · :1519(搜「while」) |
| 遗言位布局与宏 | 同上 | :1566(搜「WIFEXITED」) |
| wait_demo2 运行记录 | 同上 | :1628(搜「exit status 32」) · :1641(搜「356 = 256 + 100」) |
| waitpid 参数与 flags | 同上 | :1655(搜「waitpid」) · :1675(搜「WNOHANG」) |
| SIGCHLD handler 设计 | 同上 | :2090(搜「SA_NOCLDWAIT」) · :2153(搜「CLD_EXITED」) · :2175(搜「saved_errno」) |
| 兜底收割运行记录 | 同上 | :2318(搜「sighandler_wait_demo」) · :2339(搜「main() reaping」) |
| spl_sh 迷你 shell | 同上 | :2433(搜「spl_sh.c」) · :2465(搜「execvp」) |
| system() 的问题 | 同上 | :2527(搜「isn't efficient」) · :2533(搜「setuid」) |
-
出处:「Chapter 11: Process Creation and Termination」第 81–135 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:87,搜「pid_t fork」) · :124(搜「typical coding paradigm」)。 ↩ -
出处:「Chapter 11: Process Creation and Termination」第 96–117 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:111,搜「mitosis」)。 ↩ -
出处:「Chapter 11: Process Creation and Termination」第 274–298 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:290,搜「unique PID」) · :293(搜「pending signals」)。 ↩↩ -
出处:「Chapter 11: Process Creation and Termination」第 185–188 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:188,搜「copy-on-write」)。 ↩ -
出处:「Chapter 11: Process Creation and Termination」第 199–272 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:253,搜「fork_demo2」) · :263(搜「xxxxxxxxxxxx」)。 ↩↩ -
出处:「Chapter 11: Process Creation and Termination」第 299–405 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:399,搜「pcpcpc」)。 ↩↩ -
出处:「Chapter 11: Process Creation and Termination」第 441–539 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:478,搜「copy of the parent's buffer」) · :499(搜「full buffering」)。 ↩↩ -
出处:「Chapter 11: Process Creation and Termination」第 520–538 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:530,搜「isatty」)。 ↩ -
出处:「Chapter 11: Process Creation and Termination」第 541–620 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:561,搜「0.84」) · :592(搜「F_GETFL」) · :571(搜「atomically」)。 ↩↩ -
出处:「Chapter 11: Process Creation and Termination」第 621–789 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:678,搜「sync_io_demo」) · :704(搜「SIGUSR1」)。 ↩ -
出处:「Chapter 11: Process Creation and Termination」第 791–816 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:809,搜「architectural」)。 ↩↩ -
出处:「Chapter 11: Process Creation and Termination」第 813–816 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:813,搜「clone」)。 ↩↩↩ -
出处:「Chapter 11: Process Creation and Termination」第 818–896 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:854,搜「run in the reverse」)。 ↩ -
出处:「Chapter 11: Process Creation and Termination」第 878–882 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:878,搜「inherit」) · :954(搜「2147483647」)。 ↩ -
出处:「Chapter 11: Process Creation and Termination」第 884–896 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:889,搜「init the parent」) · :890(搜「low-order 8 bits」)。 ↩ -
出处:「Chapter 11: Process Creation and Termination」第 1450–1462 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:1452,搜「zombie」)。 ↩ -
出处:「Chapter 11: Process Creation and Termination」第 989–1055 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:993,搜「execve」) · :1046(搜「does not return」)。 ↩ -
出处:「Chapter 11: Process Creation and Termination」第 1025–1045 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:1036,搜「reset to their defaults」) · :1044(搜「Exit functions are not preserved」)。 ↩ -
出处:「Chapter 11: Process Creation and Termination」第 1185–1277 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:1230,搜「Table 11-1」) · :1264(搜「no easy way」)。 ↩ -
出处:「Chapter 11: Process Creation and Termination」第 1091–1183 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:1177,搜「AUTHOR=stewart」)。 ↩ -
出处:「Chapter 11: Process Creation and Termination」第 1467–1530 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:1476,搜「ECHILD」) · :1519(搜「while」)。 ↩ -
出处:「Chapter 11: Process Creation and Termination」第 1531–1578 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:1566,搜「WIFEXITED」)。 ↩↩ -
出处:「Chapter 11: Process Creation and Termination」第 1583–1642 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:1628,搜「exit status 32」) · :1641(搜「356 = 256 + 100」)。 ↩↩ -
出处:「Chapter 11: Process Creation and Termination」第 1644–1686 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:1655,搜「waitpid」) · :1675(搜「WNOHANG」)。 ↩↩ -
出处:「Chapter 11: Process Creation and Termination」第 2095–2166 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:2090,搜「SA_NOCLDWAIT」) · :2175(搜「saved_errno」) · :2153(搜「CLD_EXITED」)。 ↩↩ -
出处:「Chapter 11: Process Creation and Termination」第 2167–2367 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:2318,搜「sighandler_wait_demo」) · :2339(搜「main() reaping」)。 ↩↩↩ -
出处:「Chapter 11: Process Creation and Termination」第 2369–2497 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:2433,搜「spl_sh.c」) · :2465(搜「execvp」)。 ↩↩ -
出处:「Chapter 11: Process Creation and Termination」第 2499–2538 段(
text/17-ch11-chapter-11-process-creation-and-termination.txt:2527,搜「isn't efficient」) · :2533(搜「setuid」)。 ↩↩