C 语言 fork 与进程实践
更新时间:2026-08-26。本文是
languages/c/主题专家层文档。多进程是 C 程序并发的经典方式——fork一份变两份、exec换身子、waitpid收尸。这些 API 简单,但背后的"僵尸进程""退出码""写时复制"处处是坑。本文用实测讲透。
本文要回答的问题
fork调用一次,为什么返回两次?怎么区分父进程和子进程?- 子进程的退出码怎么传给父进程?
waitpid和僵尸进程是什么关系? fork之后接exec,内存是完整拷贝一份吗?
一、fork:一次调用,两次返回
pid_t pid = fork(); // 从这里开始,程序"分叉"成两份fork 的神奇之处:调用一次,返回两次——
| 返回值 | 含义 |
|---|---|
pid > 0 | 当前是父进程,pid 是子进程的 PID |
pid == 0 | 当前是子进程 |
pid < 0 | fork 失败 |
实测:
[child] pid=6240, ppid=6239 ← 子进程:自己的 pid 是 6240
[parent] child pid=6240 ← 父进程:子进程 pid 是 6240父进程和子进程从 fork 之后各自继续执行相同的代码,靠返回值区分身份。
二、退出码与回收
2.1 子进程退出码
if (pid == 0) {
return 42; // 子进程退出码 42
}2.2 waitpid 回收
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
WEXITSTATUS(status); // 提取退出码 42
}实测输出:[parent] child 正常退出, 退出码=42。
状态码需要用宏解析,不能直接读 status:
| 宏 | 含义 |
|---|---|
WIFEXITED(status) | 是否正常退出 |
WEXITSTATUS(status) | 正常退出的退出码 |
WIFSIGNALED(status) | 是否被信号杀死 |
WTERMSIG(status) | 杀死的信号编号 |
2.3 僵尸进程(zombie)
关键坑:子进程退出后,如果父进程不调用 waitpid 回收,子进程就变成僵尸进程——它已结束,但内核还保留它的 task_struct 和退出状态,占用资源。
// 父进程不 waitpid → 子进程变僵尸
// 用 ps 看:STAT 列为 Z(zombie)僵尸进程的本质:内核需要"留个口"让父进程能查到子进程的退出状态,父进程不查,这个口就一直留着。父进程死了,僵尸由 init(PID 1)收养回收。详见 僵尸进程案例。
三、exec:换掉进程映像
fork 复制当前进程,exec 则用新程序替换当前进程:
pid_t pid = fork();
if (pid == 0) {
// 子进程:换成 ls 程序
execl("/bin/ls", "ls", "-l", NULL);
// exec 成功则不会走到这里
perror("execl failed");
_exit(1);
}exec 族函数(execl/execv/execle/execve...)的区别只是参数形式不同,核心都是"加载新程序、替换当前进程映像"。
注意:
exec成功后原进程的代码不再执行,所以 exec 后面通常接perror + _exit(失败处理)。
四、写时复制(COW)
fork 后父子进程共享同一份内存,直到一方修改时才真正复制:

所以 fork 本身很快(只是复制页表),内存开销是懒的——只有真正写某页才复制。这就是为什么 fork + exec 是高效组合:子进程马上 exec,几乎没触发 COW 复制。
五、与本站主线衔接
- fork 内核机制:clone、task_struct、写时复制的内核实现,见 进程创建。
- 僵尸进程案例:真实生产事故复盘,见 僵尸进程案例。
- 进程 vs 线程:fork 与 pthread_create 的选择,见 线程创建。
- 信号:子进程退出向父进程发 SIGCHLD,见 信号处理。
一句话总结
fork 一次调用两次返回、靠返回值分辨父子;子进程退出码用 waitpid + 宏解析、不回收就变僵尸;fork 后接 exec 是高效组合、靠 COW 省内存;掌握 fork/wait/exec 三件套,就掌握了 C 多进程的核心。