子进程退出时,内核会保留其退出状态等相关信息,以便父进程可以获取,因此子进程会先进入僵尸状态。如果父进程一直没有回收子进程的残留信息,子进程的 PCB 就会一直驻留在内存中,造成资源泄漏。想要释放这部分资源,就需要父进程通过进程等待来实现。
父进程通过进程等待,可以完成两件事:回收子进程的资源 和 获取子进程的信息。
进程等待的核心是回收资源:这是必须执行的操作,目的是清除僵尸进程、释放 PCB 和 PID,避免资源泄漏。
获取子进程信息是附加功能:父进程可根据自身需求选择是否读取,不影响资源回收的核心目标。
结论:哪怕父进程不需要子进程的任何信息,也必须调用 wait()/waitpid(),这是保证系统稳定的基本要求。
上面提到了wait() 和 waitpid()
先来了解wait()

wait()是等待任意一个退出的子进程。
wait() 是回收僵尸进程的核心手段,调用后必然会完成一次回收(要么立即,要么阻塞等待);
核心逻辑:wait() 以 "阻塞" 为代价,确保父进程能可靠回收僵尸子进程,不会遗漏;
关键细节:一次 wait() 仅回收一个僵尸进程,多僵尸需循环调用。
它的参数是一个输出型参数,把子进程的相关信息通过一个整形指针放入到该整形指针指向的变量。
当wait成功回收子进程时,返回的是这个子进程的pid,失败是返回-1。
当父进程调用wait()时,能够回收僵尸进程。父进程需要获得子进程退出时的相关信息就可以选择给该函数传递参数。不需要获得时就传NULL。下面用代码来演示,这里只演示父进程回收僵尸进程。不演示父进程获取子进程退出时的信息。所以给该函数传入NULL。

上面的代码子进程会每1秒进行打印,打印5次之后子进程退出
父进程则是一直没有进行回收子进程。
我们可以观察到的内容就是,前5秒子进程是正常运行。子进程要退出时,子进程会先编程僵尸进程,此时处于僵尸状态。


上面的代码当创建子进程时,进程就开始分流。
子进程会执行上面 if 语句里面的内容,父进程执行图中25行及后面的内容。
当父进程执行到wait时,父进程就会被阻塞,什么也不能做,直到子进程退出。
因为父进程调用了 wait () 主动等待并回收子进程,所以子进程退出后会被立即清理,不会进入僵尸状态(Z 状态),因此无法通过 ps 查看到僵尸进程。
所以这里当父进程成功打印时,就代表着回收成功。并且成功回收子进程的资源,所以rid的值为子进程的pid

现在来讲waitpid()

当 waitpid() 的第一个参数为-1,第三个参数为0时。等价wait()
现在来讲 waitpid 中的第二个参数
在上一篇文章中讲过了进程终止时会返回一个退出码。
父进程就可以通过 waitpid 函数的第二个参数来获得子进程的退出码和其它的信息。

运行上面的代码来看运行成功时status的值时多少

可以发现这里status的值为256.
上面说过父进程就可以通过 status 获得子进程的退出码和其它的信息
但是我们子进程的退出码是1,status的值是256。两者看起来没有关系。
接下来就告诉大家status该如何看。
首先status是一个整形变量,在内存中就占4个字节,也就是32个bit位。

我们可以把这32位分成2个部分。16到31位我们先不考虑,默认为0.
0到15位可以再划分为2个部分

这里的8到15为就是子进程的退出码。

0到7位可以再划分2个部分

第7为是core dump标志位,默认为0.
0到6为是终止信号
在上一篇文章中,我们说过进程退出有三种场景

当进程退出为1和2这两种情况时,终止信号都为0
我们上面的代码都是正常运行。所以这里的终止信号就是0

最终就可以得到status

想要获得子进程的退出码只需要先让status右移8位,再按位与上0xFF就可以获得退出码。

运行程序并观察最终结果:

试着把子进程的退出码修改位20,然后再运行程序观察结果。

结果如下:

成功的获得到了子进程的退出码。

在上面我们说过status中的0到6位是终止信号。
当终止信号位0时,代表着1、2两种情况。
当代码异常终止时,就会出现非0信号。
我们先使用kill -l来查看所有信号。

之前我们用过kill -9 进程的pid来强制杀死进程过。
这里的kill -9就是用9号信号量来杀死进程。
我们直到终止信号是0到6位,想要获取终止信号的值就只需要status&0x7F即可

运行程序,结果如下:

再举几个例子:
例子1:

此时子进程会一直循环下去。

用kill -9 进程pid杀死子进程

这里就显示了子进程发生了异常,被信号量9给杀死。
例子2:

运行结果:

例子3:

运行结果:

这里做个小总结:
终止信号为 0 → 进程正常退出(代码完整运行完毕,无外部信号终止):
1、退出码为 0 → 代码运行完毕,结果正确;
2、退出码非 0 → 代码运行完毕,结果不正确(如逻辑错误、参数错误等)。
终止信号非 0 → 进程异常终止(被外部信号强制杀死,代码未完整运行):
此时退出码无意义,核心关注终止信号编号(如 9=SIGKILL、11=SIGSEGV),排查异常原因
现在有一个宏:
WEXITSTATUS(),可以把status传入这个宏中,可以获取进程的退出码。可以代替我们上面的位操作。

运行结果:

现在还有一个宏:
WIFEXITED()把status传入这个宏中
c
WIFEXITED(status)
这个表达式为真时(为0),就代表着子进程正常退出。为假时(非0)就代表子进程异常退出。

运行结果:

修改一下代码,让代码出现异常。

运行结果:

这里要做个区别
WIFEXITED(status)和status&0x7F
前者不能获得终止信号,只是用来判断进程是否出现异常。
后者能够获得终止信号。

现在来讲waitpid中的最后一个参数。
前面的等待都是阻塞等待。
当options为WNOHANG时,那么就是非阻塞等待。
先解释HANG
当使用计算机时,计算机突然卡死,一点反应都没有。我们称这个计算机被 夯(HANG)住了,也就是卡死了。
W :等待
NO: 不要
HANG:夯
WNOHANG:非阻塞等待
前面使用waitpid的时候都是阻塞等待。也就是父进程执行到waitpid时,就必须停下来。什么也不能做(被阻塞,可以理解为"卡死")。
当options为WNOHANG时,就是非阻塞等待。父进程执行到waitpid时,可以一边等待一边去执行自己剩下的内容。
举个例子:
一、阻塞等待 (默认 options=0)
生活场景:老明点了外卖,在送达前必须站在门口一动不动等 ------ 不能玩手机、不能炒菜、不能做任何事,只能傻等,直到外卖员敲门(子进程退出),才能继续做其他事。
二、非阻塞等待 (options=WNOHANG)
生活场景:老明点了外卖,但正在炒菜(父进程有自己的任务要处理),不能一直等:
1、先到门口看一眼(执行 waitpid),发现外卖没到(子进程没退出);
2、立刻回屋继续炒菜(父进程执行自己的代码);
3、过 1 分钟再到门口看一眼(再次执行 waitpid),循环往复;
4、直到某次检查发现外卖到了(子进程退出),才停止循环,处理外卖(回收子进程资源)。
这个 "每隔一会儿检查一次" 的循环,就是非阻塞轮询。
当waitpid为非阻塞调用时,waitpid的返回值就有了变化:
返回值大于0时,代表等待结束(子进程退出)
返回值小于0时,代表等待失败
返回值等于0时,代表此次调用结束但是子进程还没有退出(也就是老明去门口检查,发现外卖没到)
易错提醒:返回值 0 不是失败,只是 "当前没可回收的子进程",这是区分阻塞 / 非阻塞的核心特征!
把waitpid为阻塞调用时的返回值写出来给大家对比:
返回值大于0时,代表等待结束(子进程退出)
返回值小于0时,代表等待失败
上面在说非阻塞调用时,提到了非阻塞轮询。也就是老明不断循环的过程。所以我们就需要用一个循环来包含waitpid来实现多次调用
下面写一个代码给大家展示一下:

运行程序:

可以发现父进程在等待过程中还可以做其它的事,这就是非阻塞等待。
最后把子进程杀掉:
