fork、exec、wait 的关系是什么
这一组系统调用是 Linux / Unix 进程管理里最经典的一组面试题。很多同学一开始会把它们混在一起,觉得都和“创建进程、执行程序”有关,但其实它们三者分别负责完全不同的事情:
fork:创建子进程exec:让当前进程执行新的程序wait:父进程等待并回收子进程
最典型的组合关系是:
父进程先
fork出子进程,子进程再exec执行新程序,父进程最后通过wait回收子进程。
一、什么是 fork
fork 的作用是:
复制当前进程,创建一个新的子进程。
调用 fork() 之后,系统里会出现两个几乎一样的进程:
- 父进程
- 子进程
它们会从 fork() 返回处继续往下执行。
fork 的返回值
这是最常考的点:
- 在父进程中,
fork()返回 子进程的 PID - 在子进程中,
fork()返回 0 - 如果创建失败,返回 -1
因此通常会写成:
pid_t pid = fork();
if (pid == 0) {
// 子进程逻辑
} else if (pid > 0) {
// 父进程逻辑
} else {
// fork 失败
}fork 后父子进程的特点
父子进程在刚开始时看起来非常像:
- 代码看起来一样
- 数据看起来一样
- 地址空间内容也很像
- 打开的文件描述符也会继承
但这不意味着系统真的马上把父进程所有物理内存完整复制一遍。
二、为什么 fork 后不会立刻完整复制内存
因为这样代价太大了。
现代系统通常会使用:
写时复制(Copy On Write, COW)
也就是说:
- 父子进程先各自拥有独立页表
- 但很多页表项先共同指向同一批物理页
- 这些共享页通常会被标记为只读
这样一来:
- 如果父子进程都只是读,就根本不用复制
- 只有某一方真正要写某一页时,系统才复制出新的物理页并修改映射
所以 fork 的本质是:
创建一个新的进程实体,但用 COW 避免了立刻把全部物理内存复制一遍。
三、什么是 exec
exec 的作用是:
用一个新程序替换当前进程的程序映像。
注意,这句话里最重要的点是:
exec不会创建新进程。
它不会产生新的 PID,也不会建立新的父子关系。当前这个进程还是原来的那个进程,只不过它运行的程序内容被替换掉了。
可以怎么理解 exec
你可以把它理解成:
fork是“生了一个新进程”exec是“让这个进程换一个新程序来跑”
也可以记成一句更形象的话:
exec是换壳不换号。
exec 后会发生什么
执行 exec 后,当前进程通常会:
- 装载新的程序代码
- 替换原来的代码段、数据段、堆、栈等内容
- 从新程序的入口开始执行
所以:
- PID 通常不变
- 进程身份还是原来的
- 只是程序映像变了
exec 成功后还会返回吗
通常不会。
因为一旦 exec 成功,当前进程已经开始执行新程序了。只有在 exec 失败时,它才会返回错误。
四、为什么 fork 后常常紧跟 exec
这是 Unix 设计里非常经典的一套组合。
典型做法是:
- 父进程
fork - 子进程创建成功后,在子进程里调用
exec - 子进程变成一个全新的程序去执行
- 父进程继续保留原来的逻辑
这样就能达到一个非常灵活的效果:
- 父进程不变
- 子进程去执行别的程序
这也是 shell 执行命令时最常见的方式。
五、什么是 wait
wait 的作用是:
让父进程等待子进程结束,并回收子进程的退出状态。
这里“回收”是非常重要的。
因为子进程结束后,并不会立刻从系统里彻底消失。内核还需要保留它的一些信息,例如:
- 退出状态码
- 资源使用统计
- 必要的进程表项
因为父进程后面可能还要通过 wait / waitpid 来获取这些信息。
六、为什么父进程必须 wait
因为如果父进程一直不 wait,那么子进程退出后,这些退出信息就没人来收。
这时子进程就会变成:
僵尸进程(Zombie Process)
什么是僵尸进程
僵尸进程表示:
- 子进程已经结束执行
- 不再占用 CPU
- 大部分资源已经释放
- 但退出状态还没被父进程回收
所以它会以“残留进程表项”的形式继续存在。
为什么会有僵尸进程
因为内核不能在子进程刚退出时立刻把所有信息全删掉,必须给父进程一个获取退出状态的机会。
所以只有当父进程调用 wait 或 waitpid 后,这部分残留信息才会真正被清理。
七、wait 和 waitpid 的区别
wait
等待任意一个子进程结束。
waitpid
可以更精细地指定:
- 等待哪个子进程
- 是否阻塞等待
- 是否只取某些状态
所以 waitpid 比 wait 更灵活。
八、fork、exec、wait 的典型配合流程
最经典的过程可以画成这样:
父进程
├── fork() → 创建子进程
│ └── 子进程 exec() → 执行新程序
└── wait() → 等待并回收子进程这就是最标准的一套流程:
fork:先把子进程生出来exec:让子进程改跑新程序wait:父进程最后回收它
九、为什么要设计成三步,而不是一个调用全做完
Unix 把这件事拆成三步,是因为这样非常灵活。
比如父进程可以:
fork后不马上exec- 子进程在
exec前先做重定向 - 父进程决定
wait或不wait - 做后台执行、管道、重定向等各种组合
也就是说,Unix 风格不是“一步到位”,而是:
用多个小而清晰的系统调用,拼出复杂行为。
这就是它很经典的地方。
十、小结
fork用来创建子进程,它会复制当前进程的基本执行环境;exec用来在当前进程中装载并执行新的程序,它不会创建新进程,而是替换当前进程的程序映像;wait则用于父进程等待子进程结束并回收其退出状态,避免产生僵尸进程。它们的典型配合方式是:父进程先fork出子进程,子进程再exec执行新的程序,父进程最后通过wait或waitpid回收子进程。
十一、总结
fork、exec、wait是 Unix 进程管理中最经典的一组调用。fork负责创建子进程,exec负责让当前进程换一个新程序执行,wait负责让父进程等待并回收子进程。理解它们的关系后,像僵尸进程、孤儿进程、shell 执行命令、进程创建与回收这些问题都会顺很多。
