1 进程等待
1-1 进程等待必要性
父进程必须对子进程进行等待,核心原因有三点:
- 避免僵尸进程与内存泄漏 子进程退出后,如果父进程完全不管,子进程会进入僵尸进程 状态,PCB 会一直留在系统中,造成内存泄漏,而且即使
kill -9也无法杀死僵尸进程。 - 回收子进程资源 只有父进程通过
wait/waitpid等待,内核才会释放子进程退出后占用的系统资源。 - **获取子进程的退出信息(可选但常用)**父进程需要知道子进程的执行结果:是正常完成、出错退出,还是被信号杀死,这在多进程协作中非常关键。
结论:进程等待的本质就是父进程通过系统调用,回收子进程资源 + 获取退出信息。
1-2 进程等待的方法
-
基础版:
waitpid_t wait(int *status);
-
升级版:
waitpid(更灵活)pid_t waitpid(pid_t pid, int *status, int options);
- 返回值:
- 成功:返回退出子进程的 PID;
- 设置了
WNOHANG且没有已退出子进程:返回 0; - 出错:返回 -1,并设置
errno。
参数详解
pid:指定要等待的子进程pid == -1:等待任意子进程(和wait等价);pid > 0:等待 PID 等于pid的特定子进程。
status:输出型参数,保存子进程的退出状态(和wait共用一套解析宏)。options:选项参数- 默认
0:阻塞等待(子进程没退出,父进程就一直卡在waitpid这里); WNOHANG:非阻塞等待。如果指定的子进程还没退出,waitpid直接返回 0,父进程可以继续做别的事,之后再轮询。
- 默认
总结:
- 如果子进程已经退出,调用 wait/waitpid 时,wait/waitpid 会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用 wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即出错返回。

1-3 获取子进程status
- wait 和 waitpid,都有一个 status 参数,该参数是一个输出型参数,由操作系统填充。
- 如果传递 NULL,表示不关心子进程的退出状态信息。
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
- status 不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究 status 低 16 比特位):

1. status 的本质:位图结构
status 是一个 int 类型,里面的二进制位被划分为两部分,用来保存两个关键信息:
- 低 7 位 :终止信号编号(
signumber),表示进程是不是被信号杀死的; - 第 8-15 位 :退出码(
exit_code),表示进程正常退出时的返回值(exit(code)或main函数return code里的code)。
2. 常用解析宏
WIFEXITED(status):判断进程是否正常终止 。- 非 0:正常退出(没有被信号杀死);
- 0:被信号杀死。
WEXITSTATUS(status):提取进程的退出码 。- 只有
WIFEXITED(status)为真时,这个值才有意义; - 比如
exit(1)或return 1,这里就能拿到 1(注意图里的status:256就是1 << 8,对应退出码 1)。
- 只有
3. 三种退出场景总结
- 正常终止(代码跑完) :
signumber = 0,exit_code是进程的返回值(如return 0对应exit_code=0); - 正常终止但出错退出 :
signumber = 0,exit_code != 0(如exit(1)对应exit_code=1); - 被信号杀死 :
signumber != 0,此时exit_code无意义。
1-4 关键细节补充
- 阻塞 vs 非阻塞等待
- 阻塞等待(
wait或waitpid传options=0):父进程会一直卡着,直到有子进程退出,适合 "必须等子进程完成才能继续" 的场景; - 非阻塞等待(
waitpid传WNOHANG):父进程不会卡住,可以边做自己的事边轮询子进程状态,需要写循环反复调用waitpid。
- 阻塞等待(
- 僵尸进程的解决 只要父进程调用
wait/waitpid,不管是否关心退出码,都会回收子进程的 PCB,从而解决僵尸进程问题。 exit()和return的区别exit(code):直接终止整个进程,code就是进程的退出码;main函数里的return code:等价于exit(code),其他函数里的return只是返回函数值,不会终止进程。
1-5 阻塞等待和非阻塞等待
一、基础背景:fork 父子进程的运行与退出
- 谁先运行?
fork()创建子进程后,父子进程谁先被调度执行,完全由操作系统的调度器决定,代码中无法控制顺序。 - 谁先退出? 通常是子进程先退出 ,然后由父进程通过
wait/waitpid负责回收子进程的资源,避免产生僵尸进程。
二、进程等待的两种模式:阻塞 vs 非阻塞
1. 核心系统调用:waitpid 的 options 参数
pid_t waitpid(pid_t pid, int *status, int options);
options = 0:阻塞等待options = WNOHANG:非阻塞等待
2. 阻塞等待(对应图里的 "李四进程 → 阻塞等待 → 操作系统")
- 工作方式 :父进程调用
waitpid后,如果子进程还没退出,父进程会直接被挂起(阻塞),什么都不做,一直等到子进程退出才继续运行。 - 缺点:等待期间父进程完全 "卡住",无法处理其他任务,时间被白白浪费。
- 图中比喻:就像李四一直等张三,啥也不干,只能干等着。
3. 非阻塞等待(对应图里的 "非阻塞轮询 → 手机、C、新闻")
- 工作方式 :父进程调用
waitpid时设置WNOHANG,如果子进程还没退出,waitpid会直接返回0,父进程不会被挂起,可以继续执行自己的代码。之后父进程可以通过循环,反复调用waitpid轮询子进程状态。 - 优点:等待期间父进程可以处理其他任务(比如图里的看手机、写代码、看新闻),CPU 时间被充分利用,效率更高。
- 图中比喻:就像李四每隔一会儿问一下张三完事没,没问的时候可以做自己的事。
三、两种等待方式的核心对比
| 对比维度 | 阻塞等待 | 非阻塞等待(WNOHANG) |
|---|---|---|
| options 参数 | 0 | WNOHANG |
| 子进程未退出时的行为 | 父进程挂起,停止运行 | 父进程返回 0,继续运行 |
| CPU 利用率 | 低(父进程空等) | 高(父进程可处理其他任务) |
| 实现复杂度 | 简单,直接调用一次即可 | 稍复杂,需要写循环轮询 |
| 适用场景 | 父进程没有其他任务,只需要等子进程 | 父进程需要同时处理多个任务 |
四、关键理解点
- 等待时间由子进程决定:父进程需要等待多久,完全取决于子进程什么时候退出,父进程无法控制这个时间。
- 效率差异的本质:阻塞等待是 "被动等待",非阻塞等待是 "主动轮询 + 并行处理",所以非阻塞等待的资源利用率更高。
WNOHANG的作用 :它只是让waitpid不阻塞,并不是 "不等待",父进程还是需要通过循环轮询,最终完成子进程的回收。
2 进程程序替换
程序替换是通过特定的接口,加载磁盘上的⼀个全新的程序(代码和数据),加载到调用进程的地址空间中!
一、核心现象:exec 程序替换的直观效果
-
示例现象
- 代码中调用
printf("我变成了一个进程:%d\n", getpid());后,紧接着调用exec系列函数。 - 运行结果:只会打印这一行,
exec后面的所有代码(后续的printf)都不会执行。 - 关键结论:exec 成功调用后,原进程的代码和数据会被新程序完全替换,原程序后续代码不再执行。
- 代码中调用
-
是否创建了新进程?
- 没有!
exec只是替换了进程的代码段、数据段、堆和栈,进程的 PID(进程 ID)、PCB(进程控制块)这些内核信息完全不变。- 所以
getpid()拿到的 PID 还是原来的进程号,只是跑的程序变了。
二、程序替换的本质原理
-
替换的核心动作
- 把磁盘上可执行文件(ELF 格式)的代码段和数据段,加载到当前进程的内存空间中。
- 同时更新进程的页表,让虚拟内存映射到新程序的物理内存。
- 原进程的代码段、数据段会被直接覆盖,栈和堆也会被重置,所以原程序后续代码无法执行。
-
为什么能做到?
- 程序运行的前提是加载到内存,这个加载过程必须由操作系统完成。
exec系列函数本质上是操作系统提供的系统调用,只有内核有权限修改进程的内存映射,完成程序的加载与替换。
三、exec 函数的关键特性
-
返回值的特殊含义
- 调用成功:没有返回值,因为原程序已经被替换,后续代码不会执行,程序会直接从新程序的入口点开始运行。
- 调用失败:返回
-1,此时原程序的代码还在,可以继续执行后续逻辑。
-
和
fork()的经典搭配- 实际开发中,很少直接在父进程里调用
exec,通常的流程是:fork()创建子进程(复制父进程的 PCB 和内存空间);- 在子进程中调用
exec替换程序; - 父进程通过
wait/waitpid回收子进程资源。
- 这样父进程的代码不会被破坏,同时实现了 "子进程执行新程序" 的效果。
- 实际开发中,很少直接在父进程里调用
四、补充:常见的 exec 系列函数
exec 系列有多个变体,核心功能一致,只是传参方式不同:
| 函数名 | 传参方式 | 路径查找 |
|---|---|---|
execl |
列表传参,以 NULL 结尾 |
需指定完整路径 |
execlp |
列表传参,以 NULL 结尾 |
可通过 PATH 环境变量查找 |
execv |
数组传参 | 需指定完整路径 |
execvp |
数组传参 | 可通过 PATH 环境变量查找 |