目录
[1. 进程等待是什么?](#1. 进程等待是什么?)
[2. 为什么要有进程等待?](#2. 为什么要有进程等待?)
[3. 进程等待是怎么做的?](#3. 进程等待是怎么做的?)
[waitpid第二个参数int* status](#waitpid第二个参数int* status)
一、进程等待的重要性
- 当子进程退出时,若父进程未及时处理,将导致僵尸进程的产生,进而引发内存泄漏问题。
- 僵尸进程是已经终止但未被回收的进程,它们会持续占用系统资源。即使使用 kill -9命令也无法清除,因为无法终止一个已经结束的进程。
- 要解决僵尸进程问题,父进程必须通过进程等待机制来回收子进程资源,并获取子进程退出的状态信息。父进程需要获取子进程的执行状态,包括:任务是否正常完成、是否出现异常、执行结果是否正确,以及错误产生的原因。
二、进程等待的三个问题
1. 进程等待是什么?
进程等待是指父进程通过调用wait或waitpid系统函数,实现对子进程的状态监控和资源回收。具体功能包括:
- 检测子进程是否已终止(若终止则进入Z状态)
- 回收子进程资源
- 获取子进程退出信息
- 将子进程状态从Z状态(僵尸状态)转变为X状态(死亡状态)
2. 为什么要有进程等待?
进程等待主要解决两个关键问题:
- 处理僵尸进程:僵尸进程无法被直接终止,必须通过进程等待机制来回收资源,避免内存泄漏(这是必须解决的问题)
- 获取执行状态 :通过进程等待可以获取子进程的退出状态,从而了解子进程的任务完成情况。这个功能可根据实际需求选择使用(虽然某些场景下不需要关注子进程状态,但为了满足多样化的使用需求,操作系统必须提供该功能)
3. 进程等待是怎么做的?
父进程通过调用wait()或waitpid()系统调用来回收子进程,从而解决僵尸进程问题。
三、先看僵尸进程现象
我们通过 fork() 创建父子进程:子进程循环3次打印信息后 exit(0) 终止;父进程无限循环打印信息,未回收子进程,最终导致子进程成为僵尸进程。如下所示:
僵尸进程产生的核心原因本质是 "子进程先于父进程退出,但父进程未执行'回收子进程资源':
- 子进程退出时,会保留少量核心信息(如退出码、PID),等待父进程通过 wait() / waitpid() 读取,此时子进程进入僵尸态(Z状态)。
- 代码中,父进程进入无限 while(1) 循环,完全未调用 wait() 或 waitpid() ,无法读取子进程退出信息,导致子进程残留的资源无法释放,永久处于僵尸态(直到父进程被手动终止)。
那么在这个过程中,我们使用脚本去监控进程变为僵尸的状态,脚本代码如下:
bash
//脚本代码
while :; do ps ajx | head -1 && ps ajx | grep mycommand | grep -v grep; sleep 1; echo "-----------------------------"; done
运行结果:
- 当子进程退出,父进程不去等待子进程,那么子进程就会变成僵尸进程,造成内存泄漏,那么我们应该如何解决呢?那么就是父进程使用wait/waitpid去等待子进程,下面我们先进行介绍wait
- 在进行讲解上述wait之前,我们还应该认识到一个事情,当前子进程的pid是21926,其ppid是21925,这个21925就是父进程,父进程的pid就是21925,同时父进程的ppid是17474,其中17474就是父进程的父进程,那么我们grep过滤一下,查看发现它就是bash,即bash是当前父进程的父进程,即在命令行中启动的进程是bash的子进程
四、wait
概念讲解

在Linux系统手册第2节中,可以查阅到关于wait和waitpid系统调用的详细说明。使用这两个系统调用时,需要包含以下头文件:
#include <sys/types.h>
#include <sys/wait.h>
由于wait的参数status与waitpid功能相关,我们将这两者共有的status参数放在waitpid部分统一讲解。wait函数的参数类型是一个int*类型指针 ,实际使用时可以传入NULL值来调用wait等待进程。
函数功能
wait函数的主要功能是:
- 等待当前进程的任意子进程状态改变
- 检测并等待进程进入僵尸(Z)状态
- 获取进程退出信息
- 释放进程资源
- 成功等待子进程退出时,返回子进程的PID(始终大于0)
- 可通过判断返回值是否大于0来确定等待是否成功
- 对于单个子进程的情况,可直接比较wait返回值与fork返回值是否相等(fork在父进程中返回子进程PID)
- 若当前进程未创建任何子进程(即没有子进程可等待),wait将返回-1表示失败
使用方法
那么下面我们使用这个wait让父进程去等待子进程的退出

代码中父进程通过 fork() 创建子进程,子进程执行 RunChild() ,父进程进入 else 分支先休眠4秒。同时子进程循环2次( cnt=2 → 1 ),每秒打印PID/PPID/计数,2秒后 exit(0) 终止,此时父进程仍在休眠(未回收),子进程短暂成为僵尸进程。父进程在休眠4秒后,调用 wait(NULL) 阻塞等待子进程,成功回收后打印"等待成功",再休眠2秒,最终父进程正常退出。
- 0-2秒:子进程每秒打印1次,共2次,之后子进程退出。
- 2-4秒:子进程已退出,父进程仍在 sleep(4) ,子进程处于僵尸态(Z状态),无任何打印。
- 4秒时:父进程 wait(NULL) 成功回收子进程,打印"我是父进程...子进程的pid: xxx"。
- 4-6秒:父进程 sleep(2) ,无打印;6秒后父进程执行 return 0 退出,程序结束。
运行结果:
我们可以观察到前2秒子进程和父进程都在运行,然后过了两秒,子进程退出,由于父进程此时在休眠,无暇等待子进程,所以子进程此时会成为僵尸进程,再过两秒,父进程休眠完成,此时等待子进程成功,那么此时子进程的资源就被释放了,子进程被回收,父进程等待子进程完成后,并且父进程被设置为休眠2秒,所以此时会观察到只有父进程在运行的场景
阻塞状态
当子进程持续运行且父进程调用wait等待时,父进程中的wait函数会持续检测子进程状态 ,直到子进程退出为止。在此期间,父进程将无法执行其他代码,而是持续在系统调用wait中轮询检测以下信息:
- 子进程是否处于Z(僵尸)状态
- 子进程是否已退出
**若子进程始终不退出,wait调用将无法返回,导致父进程进入阻塞状态,无法继续执行后续代码。**但是为解决此问题,我们可以采用waitpid的非阻塞轮询机制来优化处理流程。
那么为了演示父进程的阻塞状态,我们可以将子进程执行的RunChild函数修改一下,让它直接死循环打印,当子进程调用它的时候就会一直死循环打印进程信息,同时父进程就一直使用wait等待子进程退出即可

运行结果:
- 最终结果导致父进程陷入无限等待,无法回收永远不会退出的子进程。此时父进程进入阻塞状态,处于等待队列中。操作系统会将父进程从CPU运行队列中移除,停止对其调度。父进程的PCB会被转移到子进程PCB的等待队列中,等待子进程状态变化(即子进程退出变为Zombie状态)。
- 由此可知,进程阻塞不仅发生在等待硬件资源就绪时(如scanf等待键盘输入),也可能发生在等待软件资源就绪的情况下。这里的父进程就是在等待子进程状态变化(即子进程退出变为Zombie状态)这一软件资源就绪。
五、waitpid
概念讲解

同样的,我们可以通过man 2 waitpid命令查看相关手册,使用时需要包含头文件:
bash
#include <sys/types.h>
#include <sys/wait.h>
函数功能
waitpid是wait的增强版,它有三个参数:
第一个参数指定要等待的子进程PID:
- 传入-1表示等待任意子进程
- 传入特定PID表示等待指定的子进程
第二个参数是指针类型的输出参数,用于获取子进程的退出信息。在演示中我们暂时传入NULL表示不获取退出信息,后续会详细讲解如何获取。
第三个参数控制等待方式:
- 传入
0表示阻塞等待(类似wait)- 传入WNOHANG表示非阻塞等待
- 非阻塞等待结合循环可实现轮询机制
- 最有意义的用法是在轮询时让父进程同时处理其他任务
返回值说明:
- 成功等待子进程时返回子进程PID
- 如果指定的PID不是当前进程的子进程,则返回
-1表示等待失败
使用方法
我们在父进程中使用waitpid等待子进程退出,其中第一个参数传入-1表示等待当前父进程的任意一个子进程退出,第二个参数传入NULL,表示不想获取子进程的退出信息,第三个参数传入0,表示进行和wait进行一样的阻塞等待模式

运行结果:
- 为什么子进程的退出码为啥是512,难道不应该是2吗?
- 实则不然,这是因为waitpid的第二个参数获取的status的原因,其实waitpid的第二个参数status的设计大有用工
waitpid函数的第二个参树status用于获取子进程的完整退出信息,包括退出状态、退出码以及core dump标志(关于core dump标志将在后续信号相关文章中详细讨论)。
子进程的退出场景可分为三种情况:
父进程主要关注两个关键信息:
- 子进程是否异常终止
- 若正常终止,执行结果是否正确(通过退出码判断:0表示成功,非0表示失败,不同数值对应不同错误原因)
由于需要通过一个整型变量status同时存储退出信号和退出码信息,操作系统采用了位运算的方式巧妙利用32位整数的比特位布局。实际应用中,我们主要关注前16个比特位:
-
当进程异常终止时:
- 退出信号为非零值(表示异常类型)
- 此时不关心退出码(对应比特位未被使用)
-
当进程正常终止时:
- 退出信号为0(Linux系统中不存在0号信号,故用0表示无异常)
- 通过退出码判断执行结果是否正确
在linux的信号中,我们使用kill -l查看之后,发现linux的信号中根本没有0号信号,所以使用0来表示进程没有收到退出信号,即程序正常运行
退出码和推出信号
- 产生主体不同
- 退出码:子进程自己通过 exit(n) 或 return n 主动设置,是"自愿终止"时的"结果报告"。
- 退出信号:Linux内核或其他进程发送的信号(如SIGSEGV、SIGKILL),是"被迫终止"时的"死亡原因"。
- 含义与用途不同
- 退出码:仅表示"正常终止后的执行状态" ,0=执行成功,非0=执行失败(如1=参数错、2=文件不存在,自定义含义)。
- 退出信号:仅表示"异常终止的原因" ,每个信号对应一种错误(如11=段错误、9=被强制杀死),此时退出码无意义。
- 与status的关联不同
- 用 WIFEXITED(status) 判断退出信号为1时,才能用 WEXITSTATUS(status) 取退出码。
- 用 WIFEXITED(status) 判断推出信号为0时,才能用 WTERMSIG(status) 取退出信号。
- 异常终止:退出信号非0(对应具体异常原因),退出码无意义(进程被强制终止,没机会设置)。
- 正常终止:退出信号为0(表示无异常、自愿结束),退出码有意义(是子进程主动设置的执行结果)。
只要退出信号非0(进程异常终止),退出码就完全没意义
- 只有进程通过 exit() / return 主动终止(退出信号=0),退出码才是子进程预先设置的"执行结果报告",有实际含义。
- 一旦退出信号非0(比如被SIGSEGV信号打崩),进程是"被动猝死",根本没机会执行 exit(n) 去设置退出码------此时status里"退出码对应的比特位"是随机无效值,读出来也没有任何参考价值。
waitpid第二个参数int* status
status的32个比特位的前16位的存储 ,以及如何进行位运算提取对应的退出码和退出信号如下

那么此时我们正确进行位运算提取status对应的退出信号和退出码即可

运行结果:
此时我们的退出信号和退出码就可以正常提取出来了
其实操作系统提供了对应提取退出信号和退出码的宏函数 来供我们使用,通常来讲操作系统提供的WIFEXITED 是用来提取status的退出信号 的,但是这个提取的退出信号会被转化为bool值,因为如果出现了异常,此时提取出来的退出信号是非0,那么说明代码有极大可能是没有执行完,子进程收到了退出信号,即子进程就出现了异常错误,退出码也就没有了意义,如果没有出现异常,此时提取出来的退出信号就为0,此时退出码有意义,于是就使用WEXITSTATUS 提取status中的退出码,于是我们的代码就可以转化成下面这样去进行使用

运行结果:

让我们思考一个问题:当子进程退出时,其内核数据结构task_struct(PCB)中会保存退出信息(包括退出信号exit_signal和退出码exit_code)。那么为什么父进程必须通过wait等系统调用才能获取子进程的状态数据?父进程能否直接访问内核空间读取子进程的task_struct数据?
答案是否定的,原因如下:
- 进程独立性原则:父子进程虽然共享代码和数据,但当修改数据时会触发写时拷贝机制,使得各自拥有私有副本。因此子进程修改的数据父进程无法直接获取。
- 安全隔离机制:操作系统不信任任何用户进程。父子进程的代码由用户编写,如果允许父进程直接访问内核空间读取子进程数据,就意味着用户可以随意修改内核数据结构,这将导致系统混乱。因此必须通过操作系统提供的wait等系统调用来获取子进程数据。
实际上,父进程可以通过其他方式向子进程传递数据。例如在后续学习中我们会了解到,父进程可以通过进程替换传递自定义环境变量给子进程。这些环境变量可以包含任意字符串内容,子进程就能据此执行相应操作。
waitpid等待指定的子进程
- waitpid的第一个参数传入子进程的pid,就可以让父进程等待指定的子进程,但是需要注意,这个指定的子进程必须是当前进程的子进程,如果是其它的进程那么waitpid将会等待子进程失败,返回-1
- 父进程是可以获取到子进程的pid传入waitpid的,因为父进程fork创建子进程,fork给父进程返回的就是子进程的pid

运行结果:

waitpid等待多个子进程
- 如果当前进程使用for循环去创建多个子进程的时候,我们该如何使用waitpid去进行多个子进程的等待呢?
- 那么我们也使用同样的for循环方式去让父进程进行N次等待即可,这里的等待是阻塞等待当前进程的任意子进程
- 那么在这个过程中,我们将使用脚本去监控进程的状态,脚本代码如下
bash
//脚本代码
while :; do ps ajx | head -1 && ps ajx | grep mycommand | grep -v grep; sleep 1; echo "-----------------------------"; done

运行结果:
- 如上,我们就可以同时创建出来的多个子进程,并且对这多个子进程进行等待回收
- 这里需要注意,由于子进程执行后父进程一直在等待子进程,所以子进程在退出的瞬间就会被父进程等待成功,进行回收,所以这里我们看不到子进程的进入僵尸状态
- 当然如果想观察到子进程的僵尸状态,那么可以在两个for循环之间让父进程休眠比单个子进程的休眠时间长即可,这样子进程打印,休眠结束,此时父进程还在休眠,无暇等待子进程,此时子进程的僵尸状态就可以被我们检测到
六、非阻塞轮询

waitpid第三个参数options
waitpid函数的第三个参数options用于指定阻塞方式。当传入0时,waitpid会像wait一样让父进程执行阻塞等待。此外还可以传入WNOHANG参数实现非阻塞等待。
关于waitpid的返回值:
- 成功等待子进程时会返回子进程的PID(>0)
- 如果等待的不是当前进程的子进程则返回-1(<0)
- 在非阻塞模式下,若子进程状态未改变会返回0
在阻塞模式下(参数为0或使用wait),父进程会一直等待直到子进程状态改变,期间会被操作系统置为阻塞状态。
而在非阻塞模式下(参数为WNOHANG),如果子进程状态未改变,waitpid会立即返回0,允许父进程通过循环实现非阻塞轮询并执行其他任务。
需要注意的是,在等待间隙执行的任务不宜过重(如大文件拷贝),应以轻量级任务为主(如状态检测或日志打印)。我们可以将这些任务封装到函数指针数组中,并通过管理函数进行增删查改操作。

运行结果:
- 于是我们就可以观察到我们的父进程一边检测子进程,一边做自己的事情,即父进程创建子进程实现了非阻塞轮询+自己的事情
- 当子进程退出的时候,此时waitpid返回等待的子进程的pid,id接收waitpid的返回值,id > 0,父进程判断子进程是否出现异常,以及结果是否正确
并且还可以扩展一下,我们在命令行中执行的命令(程序),这个命令(程序)就是bash通过fork创建的子进程,那么bash就会对这个命令进行阻塞等待,并且对于我们执行的其它命令不响应

运行结果:
- 在这个运行过程中,bash会阻塞等待它的子进程,对于用户执行的其它命名将不会响应,小编尝试执行诸如ls,pwd命令都无效,即bash对我们的命令不响应
- 当我们想要退出的时候按下ctrl + c即可终止退出进程
fork了一个子进程,父子谁先运行?
不确定,调度器说了算
一般父子进程,谁先退出?
子进程先退出,然后父进程负责进行资源回收
七、总结
本文详细讲解了Linux系统中进程等待的重要性与实现方式。主要内容包括:
- 进程等待的必要性
- 子进程退出时若未被回收会形成僵尸进程,导致内存泄漏
- 父进程必须通过wait/waitpid获取子进程退出状态和资源回收
- 进程等待的实现机制
- wait函数:阻塞等待任意子进程退出
- waitpid函数:更灵活的等待方式,可指定子进程PID和等待模式
- 通过status参数获取子进程退出状态(正常/异常终止及退出码)
- 实际应用场景
- 单子进程和多子进程的回收处理
- 阻塞与非阻塞等待方式的区别
- 父进程在等待期间执行其他任务的轮询机制
- 核心原理
- 操作系统通过位运算在status中编码退出信息
- 进程独立性原则要求必须通过系统调用获取子进程数据
- 父进程对子进程的资源回收责任
文章通过代码示例展示了僵尸进程的产生与回收过程,以及如何正确使用wait/waitpid系统调用来避免内存泄漏。
感谢大家的观看!








此时我们的退出信号和退出码就可以正常提取出来了


