引言
在Linux进程管理中,子进程退出后若父进程未及时回收其资源,会产生僵尸进程并引发内存泄漏问题,而wait与waitpid作为核心的进程等待系统调用,不仅能解决僵尸进程问题,还能让父进程获取子进程的退出状态;其中wait仅支持阻塞等待任意子进程,waitpid则扩展了指定子进程等待、非阻塞等待等能力,本文将详细讲解进程等待的必要性、两种等待方法的使用及核心原理,帮助理解Linux进程资源回收的底层逻辑。


- 引言
- 目录
-
- 一、进程等待的必要性
- 二、进程等待的方法
-
- [2.1 wait](#2.1 wait)
-
- [2.1.1 **认识wati**](#2.1.1 认识wati)
- [2.1.2 wait的使用](#2.1.2 wait的使用)
- [2.2 waitpid](#2.2 waitpid)
-
- [2.2.1 认识waitpid](#2.2.1 认识waitpid)
- [2.2.2 使用前提](#2.2.2 使用前提)
- [2.2.3 waitpid的使用](#2.2.3 waitpid的使用)
- 2.2.4获取退出信息
- 三、非阻塞轮询
- 总结
目录
一、进程等待的必要性
- 子进程退出,父进程如果不管不顾,就可能造成'僵尸进程'的问题,进而造成内存泄漏。
- 另外,进程一旦变成僵尸状态,那就刀枪不入,"杀人不眨眼"的
kill -9也无能为力,因为谁也没有办法杀死一个已经死去的进程。 - 最后,父进程派给子进程的任务完成的如何,我们需要知道。
如,⼦进程运⾏完成,结果对还是不对,或者是否正常退出。
- ⽗进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
二、进程等待的方法
当子进程执行完毕后,如果父进程没有及时回收(例如父进程处于死循环中),子进程会进入僵尸状态(Z状态),等待父进程读取其退出状态。僵尸进程无法被杀死,其占用的内核资源(如进程描述符)会一直保留,导致资源泄露。因此,我们需要通过进程等待(如 wait() 或 waitpid())来回收僵尸进程。
- 如果父进程先于子进程结束,子进程会被
init进程(PID=1)接管并自动回收,不会永久僵尸。- 僵尸进程的清理必须由父进程完成,这是 Unix/Linux 进程管理的设计特点。
接下来讲解两种进程等待的方法!
2.1 wait
2.1.1 认识wati

- 调用的时候需要包含头文件
#include <sys/types.h>、#include <sys/wait.h> - 使用
wait()或waitpid()系统调用。如果不需要知道子进程的死亡原因,直接wait(NULL)即可完成回收;如果需要处理退出状态,则使用wait(&status)获取详细信息。 wait的返回值是判断等待子进程是否成功的核心依据:若等待成功,返回子进程的 PID (大于 0);单进程场景下,可直接对比该返回值与fork给父进程的子进程 PID 来验证匹配性(因为fork给父进程返回的是子进程的pid )。若当前进程无任何子进程,wait调用直接失败,返回 -1。
记住:好父进程,从不留下僵尸孩子。
2.1.2 wait的使用
代码演示
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
if (pid < 0)
{
perror("fork failed"); // fork失败时打印错误信息
return 1;
}
// 子进程逻辑
if (pid == 0)
{
int cnt; // 把循环变量声明移到循环外,兼容C89标准
for (cnt = 2; cnt > 0; cnt--)
{
// 打印子进程PID、父进程PID、循环计数
printf("子进程 pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt);
sleep(1); // 子进程每次循环休眠1秒
}
exit(0); // 子进程正常退出
}
// 父进程逻辑
sleep(4); // 父进程休眠4秒,确保子进程先运行完毕
pid_t ret = wait(NULL); // 回收子进程资源(不关心退出状态)
// 打印回收结果:成功则输出子进程PID,失败则提示错误
if (ret == pid)
{
printf("父进程成功回收子进程:%d\n", ret);
}
else
{
printf("回收子进程失败,wait返回值:%d\n", ret);
}
sleep(2); // 父进程最后休眠2秒,观察输出
return 0;
}
我们可以观察到这样的进程状态变化过程:
- 前 2 秒内,子进程处于运行状态(循环打印信息并休眠),父进程则进入 4 秒的休眠阶段,二者均处于活跃状态;
- 2 秒后子进程执行完循环逻辑并退出,但此时父进程仍在休眠中,无法立即调用wait()系统调用回收子进程资源,因此子进程会转变为僵尸进程(Zombie Process)(仅保留 PID 等内核级信息,用户级资源已释放);
- 又过 2 秒后,父进程的休眠周期结束,随即执行wait()操作并成功回收该子进程 ------ 此时子进程的所有内核资源被彻底释放,僵尸进程状态消失;
- 父进程在完成子进程回收后,会继续执行 2 秒的休眠逻辑,因此这一阶段能观察到系统中仅有父进程处于运行状态的场景。
思考:
当我们让子进程进入死循环无法退出时,父进程调用
wait()后会一直停在该调用处,持续等待子进程退出事件的发生;在此期间父进程完全无法执行后续任何任务,这种现象在操作系统中被称为什么?
wait()的本质不是"轮询检测状态",而是阻塞等待子进程退出的内核事件(无轮询、不占用CPU)。
这种现象称之为阻塞状态,接下来让我们来看看如何解决这种状态!
2.2 waitpid
2.2.1 认识waitpid
waitpid 是 Linux 下用于回收子进程资源 的系统调用,是 wait 函数的增强版。它解决了 wait 的两大痛点:
wait只能阻塞等待任意一个子进程退出,无法指定子进程;wait只能阻塞等待,无法实现非阻塞式查询。
waitpid 既兼容 wait 的所有功能,又支持"指定子进程等待""非阻塞等待"等扩展能力,是进程资源回收的更灵活选择。
2.2.2 使用前提
- 头文件(必须包含)
c
#include <sys/types.h> // 提供pid_t等类型定义
#include <sys/wait.h> // 提供waitpid函数声明和宏定义(如WNOHANG)
- 函数原型
c
pid_t waitpid(pid_t pid, int *wstatus, int options);
返回值、三个参数的含义和用法是核心,下面逐一拆解:
- 第一个参数
| pid 值 | 含义 |
|---|---|
pid > 0 |
等待PID等于该值的指定子进程(精准回收某个子进程) |
pid = -1 |
等待当前进程的任意一个子进程退出(完全等价于 wait 的行为) |
pid = 0 |
等待和当前进程同进程组的任意子进程(多进程组场景用) |
pid < -1 |
等待进程组ID等于该值绝对值的任意子进程(小众场景,一般不用) |
- 第二个参数
- 本质是
int*类型的指针,用于接收子进程的退出状态/终止原因;- 若传入
NULL:表示不关心子进程的退出信息(和wait(NULL)一样);- 若传入有效指针:需配合
<sys/wait.h>中的宏解析状态(后续会讲)。
- 第三个参数
控制 waitpid 的等待行为,最常用的两个值:
| options 值 | 含义 |
|---|---|
0 |
阻塞等待(等价于 wait 的行为):父进程暂停执行,直到指定子进程退出 |
WNOHANG |
非阻塞等待:父进程调用后立即返回,不管子进程是否退出(核心扩展) |
- 返回值
| 返回值 | 含义 |
|---|---|
> 0 |
成功回收子进程,返回值是被回收子进程的PID |
0 |
仅当 options=WNOHANG 时出现:子进程还在运行,未退出(非阻塞特征) |
-1 |
等待失败(如指定的 pid 不是当前进程的子进程、被信号中断等),同时设置 errno 标识错误原因 |
2.2.3 waitpid的使用
代码如下:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
// 子进程执行逻辑
void child_task()
{
for (int cnt = 2; cnt > 0; cnt--)
{
printf("子进程 pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt);
sleep(1);
}
}
int main()
{
pid_t pid = fork();
if (pid < 0)
{
perror("fork失败");
return 1;
}
// 子进程逻辑
if (pid == 0)
{
child_task();
exit(0);
}
// 父进程逻辑
sleep(4); // 等待子进程退出成僵尸进程
// waitpid(-1, NULL, 0) 等价于 wait(NULL),阻塞等待任意子进程
pid_t ret = waitpid(-1, NULL, 0);
// 结果判断与打印
printf(ret == pid ? "父进程成功回收子进程:%d\n" : "回收子进程失败\n", ret);
sleep(2);
return 0;
}
1. 前2秒:子进程运行、父进程休眠(二者均存活)
- 代码执行后,
fork()创建子进程:- 子进程进入
child_task(),循环2次(cnt=2→1),每秒打印一次进程信息,全程占用CPU执行逻辑; - 父进程跳过子进程分支后,直接执行
sleep(4):此时父进程进入可中断阻塞状态(S状态),放弃CPU使用权,仅等待4秒计时结束,期间不执行任何逻辑。
- 子进程进入
- 这2秒内,子进程处于"运行/短暂休眠"状态,父进程处于"4秒休眠的前2秒",系统中能看到父子两个进程均为活跃状态(无僵尸进程)。
- 第2~4秒:子进程退出→变为僵尸进程
- 子进程完成2次循环后调用
exit(0):- 子进程的用户级资源(代码、数据、文件描述符等)会立即释放;
- 但Linux为了让父进程获取子进程的退出状态,会保留子进程的内核级资源 (PID、退出码、进程状态等),此时子进程变为僵尸进程(Z状态) (用
ps命令可看到状态为Z+)。
- 此时父进程仍在
sleep(4)的后2秒休眠中,无法执行waitpid回收子进程,因此僵尸进程会持续存在。
- 第4秒后:父进程回收子进程→仅父进程运行
- 父进程4秒休眠结束,执行
waitpid(-1, NULL, 0):waitpid是阻塞式系统调用,但此时子进程已退出,因此会立即回收子进程的内核级资源(PID被释放、僵尸状态消失);- 父进程打印"回收成功"的提示,证明子进程已被彻底清理。
- 最后父进程执行
sleep(2):此时子进程已完全消失,系统中仅父进程处于阻塞休眠状态,因此能观察到"只有父进程运行"的场景;2秒后父进程休眠结束,代码执行完毕退出。
2.2.4获取退出信息
- 我们首先先来研究一下status,在这里我们只研究低16比特位,高16比特位目前不关心。
status是一个整型变量,但status不能简单的当作整型来看待,status的不同比特位所代表的信息不同

在status的低16比特位当中,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,则低7位表示终止信号,而第8位比特位是core dump标志(也不用关心)。

exitSignal = status & 0x7F; //退出信号
-
0x7F 换算成十进制是 127,转化为二进制为低 7 个比特位上都为 1(高位均为 0),可以通过按位与操作将 status 的低 7 位(0~6 位) 提取出来,得到子进程的退出信号。
exitCode = (status >> 8) & 0xFF; //退出码
-
将 status右移 8 位,把原本存储在 8 ~ 15 位的退出码移动到 0~7 位(低 8 位),再与 0xFF 执行按位与操作(0xFF 二进制是低 8 位全为 1),过滤高位后即可提取出子进程的退出码。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
void RunChild()
{
int cnt = 2;
while(cnt)
{
printf("子进程 pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
}
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork失败"); // 补充文案,明确错误类型
return 1;
}
else if(id == 0)
{
RunChild();
exit(2);
}
else
{
sleep(4);
int status = 0;
pid_t ret = waitpid(-1, &status, 0);
// 优化:先判断waitpid是否失败(-1),覆盖更多错误场景
if(ret == -1)
{
perror("waitpid失败"); // 精准打印错误原因
return 1;
}
else if(ret == id)
{
// 提取退出信号/退出码,用临时变量简化打印行,提升可读性
int exit_signal = status & 0x7F;
int exit_code = (status >> 8) & 0xFF;
printf("父进程成功回收子进程: %d, 退出信号: %d, 退出码: %d\n", ret, exit_signal, exit_code);
}
else
{
printf("等待子进程失败,回收的PID: %d\n", ret); // 补充错误PID,便于排查
}
sleep(2);
}
return 0;
}

对于此,系统当中提供了两个宏来获取退出码和退出信号。
WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号。
WEXITSTATUS(status):用于获取进程的退出码。

思考:思考:子进程退出后,父进程为何无法直接读取其退出状态、资源使用等数据,必须通过wait/waitpid等系统调用才能获取?
- 进程隔离:父子进程拥有独立地址空间,子进程的退出状态(退出码、终止信号)存在于内核维护的子进程专属数据结构中,父进程作为用户态程序,无权直接访问内核态的这些私有数据;
- 内核资源管理 :子进程退出后会变为僵尸进程,其退出状态仅暂存于内核进程表中,需通过
wait/waitpid触发内核操作,才能将状态从内核态拷贝到父进程的用户态内存,同时完成僵尸进程的资源回收。
简单来说,wait/waitpid是内核提供的"中介接口",既解决了进程隔离导致的访问限制,又能让内核统一管理子进程的退出资源。
三、非阻塞轮询
-
上述所给例子中,当子进程未退出时,父进程都在一直等待子进程退出,在等待期间,父进程不能做任何事情,这种等待叫做阻塞等待。
-
实际上我们可以让父进程不要一直等待子进程退出,而是当子进程未退出时父进程可以做一些自己的事情,当子进程退出时再读取子进程的退出信息,即非阻塞等待。
-
做法很简单,向waitpid函数的第三个参数potions传入WNOHANG,这样一来,等待的子进程若是没有结束,那么waitpid函数将直接返回0,不予以等待。而等待的子进程若是正常结束,则返回该子进程的pid。
-
例如,父进程可以隔一段时间调用一次waitpid函数,若是等待的子进程尚未退出,则父进程可以先去做一些其他事,过一段时间再调用waitpid函数读取子进程的退出信息。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (id == 0){
//child
int count = 3;
while (count--){
printf("child do something...PID:%d, PPID:%d\n", getpid(), getppid());
sleep(3);
}
exit(0);
}
//father
while (1){
int status = 0;
pid_t ret = waitpid(id, &status, WNOHANG);
if (ret > 0){
printf("wait child success...\n");
printf("exit code:%d\n", WEXITSTATUS(status));
break;
}
else if (ret == 0){
printf("father do other things...\n");
sleep(1);
}
else{
printf("waitpid error...\n");
break;
}
}
return 0;
}
运行结果就是,父进程每隔一段时间就去查看子进程是否退出,若未退出,则父进程先去忙自己的事情,过一段时间再来查看,直到子进程退出后读取子进程的退出信息。

总结
进程等待是Linux系统中回收子进程资源、获取退出状态的核心机制,wait通过阻塞等待实现基础的子进程回收,而waitpid作为增强版,支持指定子进程、阻塞/非阻塞两种等待模式,既解决了wait的功能局限,也通过非阻塞轮询让父进程在等待期间可执行自身任务;从底层来看,父进程必须通过wait/waitpid系统调用才能获取子进程退出状态,本质是进程隔离机制和内核资源管理规则的要求------系统调用作为内核中介,既打破了父子进程的地址空间隔离,又能完成僵尸进程的资源回收,避免了资源泄漏和状态访问异常的问题。
✨ 坚持用 清晰易懂的图解 + 代码语言, 让每个知识点都 简单直观 !
🚀 个人主页 :不呆头 · CSDN
🌱 代码仓库 :不呆头 · Gitee
📌 专栏系列 :
💬 座右铭 : "不患无位,患所以立。"

