编程语言研究书库

进程创建与终止 — fork/exec/exit/wait 四原语

进程创建与终止 — 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:

  1. stdout 连终端 → C 库行缓冲,带 \n 立即 flush,fork 时 缓冲区是空的;
  2. stdout 重定向到管道/文件 → 全缓冲(≥256 字节一块),那一行 留在缓冲区里没被冲走;
  3. 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:

int flags = fcntl(fd, F_GETFL);
flags |= O_APPEND;
fcntl(fd, F_SETFL, flags);    /* F_SETFL 需要第三参数 */

用信号做生产者-消费者同步(过渡方案)

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:

  1. atexit 注册的函数按注册逆序跑(LIFO)——做「离开时总要 做的事」(记日志、还终端)最干净;上限 sysconf(_SC_ATEXIT_MAX) (demo 里打印出 2,147,483,647)。孩子继承父的 atexit 清单14;
  2. 冲刷并关闭所有 stdio 流;
  3. 调 _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:

  1. SIGCHLD 不排队——四个孩子同时死,最多来两个信号, handler 必须 waitpid(si_pid, ...) 按 si_pid 点名收;
  2. 保存 errno——handler 开头 saved_errno = errno, 结尾恢复(waitpid 会改 errno);
  3. 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 位)。

  1. fork 一次调用两个返回:父得子 PID、子得 0;-1 是失败。
  2. 孩子=内存副本(COW 优化),不是别名;但 fd 指向同一 OFD—— 文件偏移共享,交错写要小心。
  3. fork 前 fflush(stdout)!——不冲的话全缓冲模式下每个孩子都 带着父的缓冲区存货。
  4. 多进程写同一文件:write + O_APPEND(fcntl F_GETFL/F_SETFL); printf 竞争概率随进程数指数上升。
  5. exit():atexit 逆序 → 冲流 → _exit;内核关 fd、放内存、过继 孩子、留 8 位遗言、发 SIGCHLD。
  6. zombie = 死了没人 wait;服务器必须收割,否则进程表满。
  7. exec 成功不返回;身份(PID/目录/fd/掩码)全留,程序全换, handler 重置默认、atexit 作废。
  8. exec 家族:l=列表、v=数组、p=PATH 搜、e=自定义环境; 参数个数不定只能用 v 系。
  9. wait 循环到 ECHILD;遗言用宏解析(WIFEXITED/WEXITSTATUS/ WIFSIGNALED/WTERMSIG/WCOREDUMP),别自己拆位。
  10. waitpid:WNOHANG 轮询、等指定 pid、WUNTRACED/WCONTINUED 检测停/继续——作业控制的检测原语。
  11. SIGCHLD handler 收割:SA_SIGINFO 拿 si_pid 点名 waitpid、 存 errno、计数兜底、退出前 BLOCK+WNOHANG 清仓。
  12. shell = while 循环{ getline → strtok → fork → 子 execvp → 父 waitpid };内建命令不 fork。

11. 原文地图

主题原书章原文位置
fork 语义与返回值范式Chapter 11: Process Creation and Terminationtext/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」)

Footnotes

  1. 出处:「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」)。 ↩

  2. 出处:「Chapter 11: Process Creation and Termination」第 96–117 段(text/17-ch11-chapter-11-process-creation-and-termination.txt:111,搜「mitosis」)。 ↩

  3. 出处:「Chapter 11: Process Creation and Termination」第 274–298 段(text/17-ch11-chapter-11-process-creation-and-termination.txt:290,搜「unique PID」) · :293(搜「pending signals」)。 ↩ ↩2

  4. 出处:「Chapter 11: Process Creation and Termination」第 185–188 段(text/17-ch11-chapter-11-process-creation-and-termination.txt:188,搜「copy-on-write」)。 ↩

  5. 出处:「Chapter 11: Process Creation and Termination」第 199–272 段(text/17-ch11-chapter-11-process-creation-and-termination.txt:253,搜「fork_demo2」) · :263(搜「xxxxxxxxxxxx」)。 ↩ ↩2

  6. 出处:「Chapter 11: Process Creation and Termination」第 299–405 段(text/17-ch11-chapter-11-process-creation-and-termination.txt:399,搜「pcpcpc」)。 ↩ ↩2

  7. 出处:「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」)。 ↩ ↩2

  8. 出处:「Chapter 11: Process Creation and Termination」第 520–538 段(text/17-ch11-chapter-11-process-creation-and-termination.txt:530,搜「isatty」)。 ↩

  9. 出处:「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」)。 ↩ ↩2

  10. 出处:「Chapter 11: Process Creation and Termination」第 621–789 段(text/17-ch11-chapter-11-process-creation-and-termination.txt:678,搜「sync_io_demo」) · :704(搜「SIGUSR1」)。 ↩

  11. 出处:「Chapter 11: Process Creation and Termination」第 791–816 段(text/17-ch11-chapter-11-process-creation-and-termination.txt:809,搜「architectural」)。 ↩ ↩2

  12. 出处:「Chapter 11: Process Creation and Termination」第 813–816 段(text/17-ch11-chapter-11-process-creation-and-termination.txt:813,搜「clone」)。 ↩ ↩2 ↩3

  13. 出处:「Chapter 11: Process Creation and Termination」第 818–896 段(text/17-ch11-chapter-11-process-creation-and-termination.txt:854,搜「run in the reverse」)。 ↩

  14. 出处:「Chapter 11: Process Creation and Termination」第 878–882 段(text/17-ch11-chapter-11-process-creation-and-termination.txt:878,搜「inherit」) · :954(搜「2147483647」)。 ↩

  15. 出处:「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」)。 ↩

  16. 出处:「Chapter 11: Process Creation and Termination」第 1450–1462 段(text/17-ch11-chapter-11-process-creation-and-termination.txt:1452,搜「zombie」)。 ↩

  17. 出处:「Chapter 11: Process Creation and Termination」第 989–1055 段(text/17-ch11-chapter-11-process-creation-and-termination.txt:993,搜「execve」) · :1046(搜「does not return」)。 ↩

  18. 出处:「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」)。 ↩

  19. 出处:「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」)。 ↩

  20. 出处:「Chapter 11: Process Creation and Termination」第 1091–1183 段(text/17-ch11-chapter-11-process-creation-and-termination.txt:1177,搜「AUTHOR=stewart」)。 ↩

  21. 出处:「Chapter 11: Process Creation and Termination」第 1467–1530 段(text/17-ch11-chapter-11-process-creation-and-termination.txt:1476,搜「ECHILD」) · :1519(搜「while」)。 ↩

  22. 出处:「Chapter 11: Process Creation and Termination」第 1531–1578 段(text/17-ch11-chapter-11-process-creation-and-termination.txt:1566,搜「WIFEXITED」)。 ↩ ↩2

  23. 出处:「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」)。 ↩ ↩2

  24. 出处:「Chapter 11: Process Creation and Termination」第 1644–1686 段(text/17-ch11-chapter-11-process-creation-and-termination.txt:1655,搜「waitpid」) · :1675(搜「WNOHANG」)。 ↩ ↩2

  25. 出处:「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」)。 ↩ ↩2

  26. 出处:「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」)。 ↩ ↩2 ↩3

  27. 出处:「Chapter 11: Process Creation and Termination」第 2369–2497 段(text/17-ch11-chapter-11-process-creation-and-termination.txt:2433,搜「spl_sh.c」) · :2465(搜「execvp」)。 ↩ ↩2

  28. 出处:「Chapter 11: Process Creation and Termination」第 2499–2538 段(text/17-ch11-chapter-11-process-creation-and-termination.txt:2527,搜「isn't efficient」) · :2533(搜「setuid」)。 ↩ ↩2

On this page