进程等待(解决僵尸进程)
一、进程等待
1.1 进程等待的必要性

其中 , 回收子进程资源是最重要的 , 获取子进程的退出信息是可选的(可能只希望它执行,而不需要它执行后的信息)!!!
通过父进程等待解决僵尸进程!!!
1.2 进程等待的两种方法
Linux系统提供了 wait 和 waitpid 两种函数来实现进程等待 。
在介绍进程等待之前 , 可能会疑惑 , 为什么进程等待能够解决僵尸进程 ?
首先 , 需要明确僵尸进程的本质 , 再分析进程等待的作用机制!!!
1)僵尸进程的本质 :
当子进程执行完毕后,它的代码、数据等资源会被释放,但进程控制块(PCB)会暂时保留(PCB 中记录了进程的退出状态、PID 等关键信息)。此时子进程就变成了 “僵尸进程”—— 它已经 “死亡”,但 PCB 还在系统中占用资源。
如果大量僵尸进程存在,会导致系统进程表项被耗尽,无法创建新进程,引发资源泄漏。
2)进程等待的作用机制
父进程通过wait或waitpid函数进行进程等待,主要做两件事:
- 回收子进程的 PCB 资源:父进程等待子进程退出后,会从系统中移除子进程的 PCB,彻底释放它占用的进程表项等资源。
- 获取子进程的退出信息:父进程可以通过
status参数获取子进程的退出码、终止信号等信息,从而判断子进程的执行结果。
3)进程等待如何解决僵尸进程
僵尸进程的核心问题是 PCB 未被回收 。而进程等待的过程,就是父进程主动去回收子进程 PCB 的过程。
当父进程调用wait/waitpid时:
- 如果子进程已经是僵尸进程,父进程会立即回收它的 PCB,僵尸进程被彻底清除。
- 如果子进程还在运行,父进程会阻塞等待(或非阻塞轮询),直到子进程退出后再回收 PCB,避免子进程变成僵尸进程。
1.2.1 wait方法


解决僵尸进程:
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{pid_t id = fork();if (id == 0){// 子进程int cnt = 5;while (cnt){printf("我是一个子进程,pid: %d ,ppid: %d\n", getpid(), getppid());sleep(1);cnt--;}exit(0);}// 父进程sleep(10);pid_t rid = wait(NULL);if (rid > 0){printf("wait success,rid: %d\n", rid);}sleep(10);return 0;
}代码效果解释:
1. 进程创建:
- 程序运行后,通过
fork()创建一个子进程。fork()调用成功后,父进程和子进程会同时执行后续代码。- 子进程中
fork()返回0,父进程中fork()返回子进程的 PID。
2. 子进程行为:
- 子进程进入
while循环,循环 5 次。每次循环打印子进程的 PID 和父进程的 PID(通过getpid()和getppid()获取),然后睡眠 1 秒,cnt递减。- 循环结束后,子进程通过
exit(0)正常退出。3. 父进程行为:
- 父进程先执行
sleep(10),睡眠 10 秒。- 睡眠结束后,调用
wait(NULL)等待子进程退出。wait(NULL)会阻塞父进程,直到子进程退出。- 子进程退出后,
wait(NULL)返回子进程的 PID,父进程打印 “wait success, rid: 子进程 PID”。
相关函数解释:
1. fork() 函数
- 用于创建子进程,是 Linux 中进程创建的核心函数。
- 调用后,父进程和子进程拥有独立的地址空间,代码从
fork()之后开始各自执行。 - 子进程会复制父进程的代码、数据等资源(通过写时拷贝技术优化)。
2. 进程 ID 相关函数
1)getpid():获取当前进程的 PID。
2)getppid():获取当前进程的父进程 PID。
3. 进程退出函数 exit()
1)用于使进程正常退出,参数为退出码(0 表示正常退出,非 0 表示异常退出)。
2)进程退出时,内核会回收其大部分资源,但进程控制块(PCB)会暂时保留,直到父进程进行进程等待。
4. 进程等待函数 wait()
1)父进程调用 wait(NULL) 会阻塞自己,直到子进程退出。
2) wait() 返回子进程的 PID,用于确认是哪个子进程退出。
3) 参数 NULL 表示不需要获取子进程的退出状态(若需要,可通过非 NULL 的 int* 指针获取)。
4) 进程等待的核心作用是回收子进程的 PCB 资源,避免僵尸进程的产生。
5. 僵尸进程
1) 子进程退出后,若父进程未及时调用 wait() 或 waitpid() 进行进程等待,子进程的 PCB 会残留于系统中,形成僵尸进程。
2) 僵尸进程会占用系统的进程表项资源,若大量存在会导致系统无法创建新进程。本代码中,父进程通过 wait(NULL) 解决了僵尸进程问题。
6. 阻塞与睡眠
1)sleep(10) 使父进程睡眠 10 秒,此期间父进程不执行任何操作。
2)wait(NULL) 使父进程阻塞,直到子进程退出才继续执行。两者都是使进程暂停执行的方式,但 wait() 是为了等待子进程事件,sleep() 是单纯的时间延迟。
1.2.2 waitpid方法



什么情况下,wait() 或者 waitpid() 会失败?
1)wait() 失败:父进程没有子进程 , wait就会失败
2) waitpid() 失败 : PID的值传错了



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

| 位区间(低 16 位) | 含义 |
|---|---|
| 第 0~6 位 | 终止信号(若进程是被信号杀死的,这里存信号编号;正常退出则这 7 位全为 0) |
| 第 7 位 | core dump 标志(进程异常终止时,是否生成核心转储文件) |
| 第 8~15 位 | 退出码(仅当进程 “正常退出” 时有效,即第 0~6 位全为 0 时) |
理解正常退出:
当子进程通过 return、exit 或 _exit 正常退出时:
- 第 0~6 位全为
0(表示没有被信号杀死)。 - 退出码存在第 8~15 位。
如何拿到退出状态?
将 status 右移 8 位,再与 0xFF(二进制 11111111)按位与,提取第 8~15 位的数值。

理解“异常退出”
当子进程被信号杀死(比如 kill -9 子进程PID)时:
- 第 0~6 位存储终止信号的编号(比如
9对应SIGKILL信号)。 - 此时第 8~15 位的退出码 “无意义”,因为进程是被信号强制终止的
如何拿到终止信号?
将 status 与 0X7F(二进制 0000000001111111)按位与,提取第 0~6 位的信号编号。

“退出信息” 为什么不能用全局变量传递?

子进程是通过 fork 创建的,它和父进程之间是 写时拷贝(Copy-On-Write) 的关系 —— 子进程会复制父进程的内存空间,但之后父子进程的内存是独立的(父子进程相互独立)。
如果定义一个全局变量 int exicode = 0; 子进程修改它时只会修改自己的副本,父进程无法感知。因此,必须通过操作系统提供的 “进程等待” 机制(wait/waitpid)来获取子进程的退出信息—— 操作系统会统一管理进程的退出状态,父进程通过系统调用从内核中读取这些信息。

1.2.3 如何做到的

- 退出信息存储在哪里?
在Linux系统中 , 每个进程都有对应的 "task_struct"结构体 , 里面记录了进程的所有关键信息 , 包括 : 进程(PID) 、 父进程(PPID);退出码(exit_code)、终止信号(exit_signal)等退出信息。
子进程退出后 , 它的task_struct 不会被立即销毁,而是暂存这些退出信息,直到父进程来取走
- 父进程怎么拿到这些信息?
因为父进程和子进程是相互独立的进程,父进程不能直接访问子进程的内存或数据。所以必须通过系统调用(比如
waitpid)来向操作系统 “请求” 子进程的退出信息 —— 操作系统会从子进程的task_struct中提取这些信息,再返回给父进程。这和我们调用
getpid()、getppid()的原理是一样的:这些函数也是通过系统调用,从task_struct中读取进程 ID 信息后返回给我们。
我们通过一段代码来演示这个过程(获取退出信息):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>int main() {pid_t pid = fork();if (pid == 0) {// 子进程:睡眠3秒后正常退出,退出码设为10sleep(3);exit(10);} else {// 父进程:等待子进程退出,并获取退出信息int status;waitpid(pid, &status, 0);// 判断是否正常退出if (WIFEXITED(status)) {// 提取正常退出码int exit_code = WEXITSTATUS(status);printf("子进程正常退出,退出码:%d\n", exit_code);}}return 0;
}流程拆解:
- 子进程执行:子进程调用
exit(10)后,退出码10被存入它的task_struct中。 - 父进程调用
waitpid:父进程通过waitpid(pid, &status, 0)发起系统调用,向操作系统请求子进程的退出信息。 - 操作系统提取并返回信息:操作系统从子进程的
task_struct中取出退出码10,按照 “位图规则” 填充到status变量中。 - 父进程解析
status:通过WIFEXITED(status)判断子进程是 “正常退出”,再通过WEXITSTATUS(status)提取出退出码10,最终打印结果。
1.3 关于WIFEXITED和WEXITSTATUS的作用
这两个是系统提供的宏函数,用于简化status的解析逻辑:
WIFEXITED(status):判断子进程是否 “正常退出”(即通过return、exit、_exit退出,而非被信号杀死)。如果返回true,说明可以安全地提取退出码。WEXITSTATUS(status):在WIFEXITED为true时,提取子进程的退出码。
1.4 阻塞与非阻塞等待(option)
1.4.1 生活例子
我们用 “等外卖” 这个场景,就能轻松区分两者的本质差异:
| 场景 | 阻塞等待(对应waitpid(..., 0)) | 非阻塞等待(对应waitpid(..., WNOHANG)) |
|---|---|---|
| 行为 | 你下单后,放下手机啥也不做,盯着门口等外卖员敲门,期间不能刷视频、做家务。 | 你下单后,继续刷视频、做家务,每隔 5 分钟去门口看一眼 “外卖到了没”,没到就继续忙自己的事。 |
| 核心特点 | “死等”:等待期间无法做其他事,直到目标事件(外卖到)发生。 | “忙等 + 轮询”:不死等,期间能处理其他任务,通过 “定期检查” 确认目标事件是否发生。 |
| 对应程序中的逻辑 | 父进程调用waitpid后,暂停执行,直到子进程退出才继续往下跑。 | 父进程调用waitpid后,不管子进程是否退出都会立即返回,返回后可以去执行其他代码,之后再循环调用waitpid检查子进程状态。 |

1.4.2 返回值
waitpid的返回值(板书重点)板书里说 “返回值> 0:等待结束;=0:子进程没退出;<0:等待失败”,对应代码中的三个分支:
- >0:子进程已退出,返回子进程 PID(等待成功,回收资源)。
- =0:子进程还在运行,调用成功但没拿到结果(非阻塞的核心,父进程可以去忙别的)。
- <0:等待出错(比如子进程已经被回收过,或 ID 不存在)。
例如:
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>typedef void (*Func_t)();#define NUM 5
Func_t handlers[NUM + 1];void Download()
{printf("我是一个下载的任务...\n");
}void Flush()
{printf("我是一个刷新的任务...\n");
}void Log()
{printf("我是一个记录日志的任务...\n");
}void RegisterHandler(Func_t h[], Func_t f)
{int i = 0;for (; i < NUM; i++){if (h[i] == NULL) break;}if (i == NUM) return;h[i] = f;h[i + 1] = NULL;
}int main()
{RegisterHandler(handlers, Download);RegisterHandler(handlers, Flush);RegisterHandler(handlers, Log);pid_t id = fork();if (id == 0){int cnt = 3;while (1){sleep(3);printf("我是一个子进程, PID : %d, PPID : %d\n", getpid(), getppid());sleep(1);cnt--;}exit(10);}while (1){int status = 0;pid_t rid = waitpid(id, &status, WNOHANG);if (rid > 0){printf("WAIT SUCCESS, RID: %d, EXIT CODE: %d, EXIT SIGNAL: %d\n", rid, (status >> 8) & 0xFF, status & 0x7F);break;}else if (rid == 0){int i = 0;for (; handlers[i]; i++){handlers[i]();}printf("本轮调用结束,子进程没有退出\n");sleep(1);}else{printf("等待失败\n");break;}}return 0;
}
父进程非阻塞等待与任务执行模块
pid_t rid = waitpid(id, &status, WNOHANG);:waitpid的WNOHANG选项表示非阻塞等待 —— 如果子进程未退出,waitpid立即返回0;如果子进程已退出,返回子进程PID;如果等待失败,返回-1。if (rid > 0):子进程已退出,解析status获取退出码((status >> 8) & 0xFF)和终止信号(status & 0x7F),打印信息后跳出循环。else if (rid == 0):子进程未退出,此时执行handlers数组中的所有任务函数(遍历handlers,依次调用注册的Download、Flush、Log函数),打印 “本轮调用结束,子进程没有退出” 后睡眠 1 秒,继续下一轮循环。else:等待失败,打印 “等待失败” 后跳出循环。
该代码通过函数指针数组实现任务注册与回调,结合进程创建(fork)和非阻塞进程等待(waitpid+WNOHANG),实现了父进程在等待子进程的同时,周期性执行 “下载、刷新、记录日志” 等任务的功能。核心是利用非阻塞等待的特性,让父进程在子进程运行期间不 “空等”,而是持续执行注册的任务,提高了 CPU 资源的利用率。
1.4.3 非阻塞轮询意义
非阻塞的核心价值:提高 CPU 利用率
板书里用 “打电话” 举例:“非阻塞调用,像打电话催李四(子进程),挂了电话可以做别的,不用一直占线等”。
- 如果父进程用阻塞等待(
waitpid(id, &status, 0)),那 3 秒内父进程什么都干不了,只能空等(CPU 空闲浪费)。- 用非阻塞等待,父进程在这 3 秒里可以处理日志、读写文件、甚至创建其他子进程 —— 充分利用 CPU 资源,这在服务器开发(比如同时处理多个客户端请求)中非常重要。
1.4.4 对比阻塞与非阻塞
| 维度 | 阻塞等待(options=0) | 非阻塞等待(options=WNOHANG) |
|---|---|---|
| 核心逻辑 | 父进程调用后暂停,直到子进程退出才继续。 | 父进程调用后立即返回,通过循环 “轮询” 子进程状态。 |
waitpid返回值 | 成功返回子进程 PID,失败返回 - 1(不会返回 0)。 | 成功返回 PID,子进程未退出返回 0,失败返回 - 1。 |
| CPU 利用率 | 低(等待期间 CPU 空闲)。 | 高(等待期间父进程可处理其他任务)。 |
| 适用场景 | 父进程不需要做其他事,只需等子进程(比如简单的命令执行)。 | 父进程需要并发处理多个任务(比如服务器同时处理多个客户端)。 |
| 代码复杂度 | 简单(不用循环,调用一次即可)。 | 稍复杂(需要循环轮询,处理 3 种返回值情况)。 |
