[Linux系统编程——Lesson11.进程控制:等待]
目录
前言
进程等待
1-1🧐什么是进程等待❓
1-1-2🔥 进程等待必要性🔥
1-2 进程等待的⽅法🔎
1-2-1 wait⽅法📖
功能说明
返回值
1️⃣父进程能够等待回收子进程僵尸状态,子进程的状态Z->X
2️⃣如果子进程没有退出的情况下,父进程调用了wait函数,那么父进程就必须进行阻塞等待,直到子进程变为僵尸进程后,wait函数自动对子进程进行回收。
🕵️♀️特点
1-2-2 waitpid方法📖
🕵️♀️参数说明
🕵️♀️返回值
总结
1-2-3 获取⼦进程status📖
根据上面讲述的内容提出三个问题🧐:
1-2-4 参数options🕵️♀️
1️⃣核心定义与本质差异
2️⃣关键技术细节对比
1.函数调用方式
2.返回值与结果判断
3. 适用场景
3️⃣典型问题与注意事项
4️⃣总结
🕵️♀️细节剖析
1️⃣操作系统层面:父进程如何获取子进程的退出信息
2️⃣父进程在子进程的等待队列中等待的机制
总结
🔎父进程等待多个子进程
1️⃣核心原理铺垫
2️⃣完整代码实现(C 语言)
结果关键结论🔎
3️⃣进程监控验证
1. 首次查询结果解析: 僵尸进程
2. 二次查询结果为空:僵尸进程被回收
3. 为什么会出现僵尸进程?
前言
经过上节内容的学习[Linux系统编程——Lesson10.进程控制:创建与终止],我们已经掌握了进程控制的创建与终止。
本节内容我们将继续深入理解操作系统的运行机制,学习进程的等待。📖
进程等待
1-1🧐什么是进程等待❓
进程等待是操作系统中父进程用于管理和同步子进程的重要机。它是通过系统调用
wait
或waitpid
,来对子进程进行状态检测与资源回收的功能。
- 目的:
- 回收子进程资源:子进程退出后,如果父进程不进行处理,子进程会变成僵尸进程,占用系统资源,导致内存泄漏。进程等待可使父进程回收子进程资源,避免此类问题发生。
- 获取子进程退出信息:父进程派给子进程任务后,需要了解任务完成情况,如子进程是否正常退出、退出码是多少、是否因异常信号终止等,通过进程等待,父进程能获取这些信息,以便进行后续处理。
- 等待方式:
wait
函数:会阻塞父进程,直到任意一个子进程退出。它的返回值为退出的子进程的 PID,若返回 - 1 则表示等待失败。通过其参数可以获取子进程的退出状态,若不关心则可设置为NULL
。waitpid
函数:可以更灵活地控制等待过程,它能指定等待特定 PID 的子进程。若将第三个参数设置为WNOHANG
,则以非阻塞方式等待,若指定的子进程没有结束,函数立即返回 0,若子进程正常结束,则返回该子进程的 ID。
1-1-2🔥 进程等待必要性🔥
- 之前讲过,⼦进程退出,⽗进程如果不管不顾,就可能造成‘僵⼫进程’的问题,进⽽造成内存 泄漏。
- 另外,进程⼀旦变成僵⼫状态,那就⼑枪不⼊,“杀⼈不眨眼”的kill -9 也⽆能为⼒,因为谁也 没有办法杀死⼀个已经死去的进程。
- 最后,⽗进程派给⼦进程的任务完成的如何,我们需要知道。如,⼦进程运⾏完成,结果对还是 不对,或者是否正常退出。
- ⽗进程通过进程等待的⽅式,回收⼦进程资源,获取⼦进程退出信息
所以,核心可以概括为三个方面🔑:
1️⃣防止僵尸进程与资源泄漏:
子进程退出后若未被父进程处理,会成为僵尸进程(Zombie Process),保留 PCB 等关键信息占用系统资源。这些资源无法被内核自动回收,长期积累会导致系统资源耗尽,这是进程等待最核心的作用。
2️⃣彻底清理已结束进程:
僵尸进程已经终止运行,但由于父进程未回收其资源,系统无法完全释放它的标识符等信息。即使使用kill -9
也无法删除僵尸进程,只有通过父进程的等待操作才能彻底清理。
3️⃣获取子进程执行结果:
父进程通过等待机制可以获取子进程的退出状态,包括:
- 正常退出时的返回值(如程序执行结果是否符合预期)
- 异常终止时的信号信息(如是否因段错误、中断等信号退出)这让父进程能根据子进程的执行情况进行后续决策,实现进程间的同步与协作。
简言之,进程等待是父进程履行 "资源回收" 和 "结果确认" 职责的必要机制,是多进程编程中保证系统稳定性和流程可控性的基础。
1-2 进程等待的⽅法🔎
1-2-1 wait⽅法📖
在 Unix/Linux 系统中,
wait
是用于进程等待的系统调用,主要功能是让父进程阻塞等待子进程退出,并回收子进程资源、获取退出状态。
功能说明
阻塞等待:调用
wait
后,父进程会进入阻塞状态,直到有一个子进程退出(或收到终止信号)才会返回。
回收资源:子进程退出后,
wait
会回收其占用的内核资源(如 PCB 结构),避免产生僵尸进程。
获取退出状态:通过参数
status
可以获取子进程的退出信息(需配合宏解析),若不关心可设为NULL
。
返回值
- 成功:返回退出的子进程的 PID(进程 ID)。
- 失败:返回
-1
(例如没有子进程时调用)。
通过对wait函数的使用可以得到以下结论📖:
1️⃣父进程能够等待回收子进程僵尸状态,子进程的状态Z->X
- 通过对下面代码的运行和使用脚本观察进程可以得到下图,我们发现脚本中子进程先变为了僵尸进程,再消失了。
#include <stdio.h> #include <sys/types.h> // 定义基本系统数据类型(如pid_t) #include <sys/wait.h> // 提供等待子进程的函数(如wait) #include <unistd.h> // 提供POSIX操作系统API(如fork、sleep、getpid等) #include <stdlib.h> // 提供exit等函数// 子进程执行的函数 void Worker() {int cnt = 3;while(cnt){// 打印子进程信息:进程ID、父进程ID和计数器// getpid():获取当前进程ID// getppid():获取父进程IDprintf("I am child process , pid: %d , ppid : %d , cnt : %d\n" ,getpid(),getppid(),cnt--);sleep(1); } } int main() { // 创建子进程,fork()会返回两次// 子进程中返回0,父进程中返回子进程ID,失败返回-1pid_t id = fork();if(id == 0){ // 子进程执行路径Worker(); // 调用子进程工作函数exit(0); // 子进程完成工作后退出} else { // 父进程执行路径(id为子进程的PID)// 休眠5秒,此时子进程已经执行完毕并退出// 这段时间内子进程会处于僵尸状态(Z状态)sleep(5); // 等待子进程退出,回收其资源// wait(NULL):阻塞等待任意一个子进程退出,NULL表示不关心子进程退出状态pid_t rid = wait(NULL);// 验证等待的是否是我们创建的子进程if(rid == id)printf("Wait success , pid : %d\n",getpid()); // 打印父进程IDsleep(3); }return 0; }
代码的核心逻辑说明:
- 使用
fork()
创建了一个子进程,实现了进程的复制- 子进程通过
Worker()
函数循环打印信息 3 次后退出- 父进程先休眠 5 秒,这段时间子进程会成为僵尸进程(已退出但未被回收)
- 父进程通过
wait(NULL)
回收子进程资源,避免僵尸进程长期存在- 主要展示了进程创建、进程标识(PID/PPID)和进程回收的基本概念
2️⃣如果子进程没有退出的情况下,父进程调用了wait函数,那么父进程就必须进行阻塞等待,直到子进程变为僵尸进程后,wait函数自动对子进程进行回收。
- 我们之前讲阻塞状态是讲到过scanf函数,当进程调用到scanf函数时,我们不进行输入,键盘资源就没有就绪,进程就会变为阻塞状态。进程不仅仅可以等待硬件资源,还可以等待软件资源,等待进程也是等待资源,所以这时候父进程就会变为阻塞状态,当子进程运行完毕后,父进程的软件资源也就准备就绪了。
#include <stdio.h> #include <sys/types.h> // 定义基本系统数据类型(如pid_t) #include <sys/wait.h> // 提供进程等待相关函数(如wait) #include <unistd.h> // 提供Unix标准函数(如fork、sleep、getpid等) #include <stdlib.h> // 提供进程退出函数(如exit)// 子进程要执行的工作函数 void Worker() { int cnt = 3; // 循环计数器,控制子进程执行次数while(cnt) // 循环3次{ // 打印子进程信息:进程ID、父进程ID和剩余计数// getpid():获取当前进程(子进程)的ID// getppid():获取当前进程的父进程IDprintf("I am child process , pid: %d , ppid : %d , cnt : %d\n" ,getpid(),getppid(),cnt--); sleep(1); } } int main() { // 创建子进程,fork()会产生两个返回值// 父进程中返回子进程的PID(正数)// 子进程中返回0// 失败时返回-1pid_t id = fork(); if(id == 0) // 子进程执行分支(id为0){ // 子进程执行路径Worker(); // 调用工作函数,执行子进程任务exit(0); // 子进程完成任务后退出} else // 父进程执行分支(id为子进程PID){ // 父进程执行路径printf("Wait before\n"); // 标记等待开始前的状态// 等待子进程结束并回收资源// wait(NULL)会阻塞父进程,直到有子进程退出// NULL表示不关心子进程的退出状态pid_t rid = wait(NULL);printf("Wait after\n"); // 子进程退出后,父进程从wait()返回并继续执行// 验证回收的是否是我们创建的子进程if(rid == id)// 打印父进程ID,确认等待成功printf("Wait success , pid : %d\n",getpid());sleep(3); }return 0; }
代码核心逻辑说明:
- 使用
fork()
创建子进程,实现了进程的复制- 子进程通过
Worker()
函数循环 3 次打印自身信息,每次间隔 1 秒- 父进程会在
wait(NULL)
处阻塞,直到子进程执行完毕并退出- 子进程退出后,父进程回收其资源并继续执行后续代码
- 整个程序展示了进程创建、父子进程协作及资源回收的基本机制
通过上面两个代码的运行我们可以得到一个结论:
🧐一般而言,父子进程谁先运行我们不知道,但是最后一般都是父进程最后退出。
🕵️♀️特点
wait
只能等待任意一个子进程退出,无法指定特定子进程。- 若父进程没有子进程,
wait
会立即返回-1
并设置错误码。- 若调用时已有子进程退出(成为僵尸进程),
wait
会立即处理该子进程并返回。⚠️如果需要更灵活的等待方式(如指定子进程、非阻塞等待),可以使用
waitpid
函数。
1-2-2 waitpid方法📖
waitpid
是 Unix/Linux 系统中更灵活的进程等待系统调用,相比wait
它支持指定等待的子进程、控制阻塞行为等。(在功能上可以完全替代wait函数)
①子进程已退出(僵尸状态):
调用
wait
/waitpid
会立即处理该子进程,回收其资源(清除僵尸状态),并通过status
参数返回退出信息,函数返回子进程 PID。②子进程正在运行:
- 若使用默认模式(
wait
或waitpid
不带WNOHANG
):父进程会阻塞,直到子进程退出或收到终止信号。- 若使用
waitpid
的WNOHANG
选项:函数立即返回 0,不阻塞,父进程可继续执行其他任务。③无符合条件的子进程:
(如父进程没有子进程,或
waitpid
指定的 PID 不存在)函数立即返回 -1,同时设置errno
表示错误(如ECHILD
)。
- 这一行为机制保证了父进程既能及时回收资源,又能灵活控制等待方式,是多进程协作的基础。例如,循环调用带
WNOHANG
的waitpid
可实现父进程在等待子进程的同时处理其他任务,避免长时间阻塞。
🕵️♀️参数说明
pid
:指定等待的子进程 PID,取值有三种情况:
pid > 0
:等待 PID 等于该值的子进程。pid = 0
:等待与父进程同组的任意子进程(同进程组内的子进程)。pid = -1
:等待任意子进程(与wait
功能相同)。pid < -1
:等待进程组 ID 等于该值绝对值的任意子进程。
status
:用于存储子进程的退出状态,与wait
的status
参数用法一致(通过宏解析)。
options
:控制等待行为的选项,常用取值:
0
:默认值,阻塞等待(与wait
相同)。WNOHANG
:非阻塞模式。若指定的子进程未退出,函数立即返回0
而非阻塞。WUNTRACED
:除了等待退出的子进程,还等待被暂停(stopped)的子进程。
🕵️♀️返回值
- 成功:
- 若子进程退出,返回该子进程的 PID。
- 若使用
WNOHANG
且子进程未退出,返回0
。- 失败:返回
-1
(如无符合条件的子进程)。
wait🆚
waitpid
对比维度 wait 函数 waitpid 函数 函数原型 pid_t wait(int *status) pid_t waitpid(pid_t pid, int *status, int options) 等待目标 只能等待任意一个子进程 可通过 pid 参数指定目标:
- pid > 0:特定 PID 子进程
- pid = 0:同组任意子进程
- pid = -1:任意子进程(同 wait)
- pid < -1:指定进程组的任意子进程
阻塞行为 始终阻塞等待,直到有子进程退出 可通过
options
控制:-
0
:阻塞等待(同 wait)-
WNOHANG
:非阻塞,子进程未退出时返回 0返回值 - 成功:退出子进程的 PID - 失败:-1 复杂场景:
等待特定子进程- 非阻塞等待(父进程需并发处理任务)
- 监控子进程暂停 / 继续状态
适用场景 简单场景,仅需等待任意子进程(如单子女进程) 复杂场景:
- 等待特定子进程
- 非阻塞等待(父进程需并发处理任务)
- 监控子进程暂停 / 继续状态
功能覆盖 功能单一,仅支持基础等待 功能更全面,可完全替代 wait
(当pid = -1
且options = 0
时)总结
waitpid 是
wait
的增强版,功能更全面灵活,可完全替代wait
(当pid = -1
且options = 0
时,两者行为一致)。实际开发中,waitpid
因可控性更强而更常用,尤其是在多进程编程场景中。
1-2-3 获取⼦进程status📖
我们发现进程等待所使用的两个函数wait和waitpid,都有一个status参数,那么它到底是什么呢❓
- 在进程等待中,
wait
和waitpid
函数的status
参数是一个输出型参数,由操作系统填充,用于存储子进程的退出状态信息,- 如果对status参数传入NULL,表示不关心子进程的退出状态信息。
- 否则,操作系统会通过该参数,将子进程的退出信息反馈给父进程。
数据类型与存储方式:
status
是int
型,占 4 个字节,但通常只用到后面 2 个字节(16 个比特位)来存储有效信息。
信息存储内容:
- 正常终止情况:低 8 位(0-7 位)存储的是进程的退出状态,这个状态是由
exit
系统调用或 C 语言中的exit
函数传递给操作系统的。第 8 位存储是否生成了 core dump 文件,若生成则该位被置为 1。高 8 位(16-31 位)通常被设置为 0,用于标识进程的终止状态。- 被信号终止情况:低 8 位存储的是导致进程终止的信号编号,如
SIGSEGV
表示段错误,SIGKILL
表示进程被强制终止等。第 8 位同样用于标识是否生成了 core dump 文件,高 8 位通常为 0。status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16 ⽐特位):
在status的低16比特位当中,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,则低7位表示终止信号,而第8位比特位是core dump标志。
exitCode = (status >> 8) & 0xFF; //退出码 exitSignal = status & 0x7F; //退出信号
- 做个形象的比喻,如果你正在考试,如果你以正确的方式参加了考试,那么你的成绩必定有好有坏,就看你如何看待自己考试的结果了;但是,如果你考试作弊了,考试还未结束,你就被监考老师叉出去了,提前结束了考试,此时,你的卷子尽管可能有得分,这个得分有没有参考价值呢?当然没有。
- 所以,如果一个进程正常终止,我们可以拿到它的退出状态即进程退出码;如果一个进程被信号(监考老师)终止,此时退出状态是没有意义的,但是我们可以查看终止信号,(至少看看是什么原因导致的考试异常结束)。
- 相关宏定义:可以通过一些宏来获取
status
中的具体信息,常用的宏如下:
WIFEXITED(status)
:判断子进程是否正常退出,若正常退出则返回真。WEXITSTATUS(status)
:当WIFEXITED(status)
为真时,用于获取子进程的退出码。WIFSIGNALED(status)
:判断子进程是否因为信号终止,若是则返回真。WTERMSIG(status)
:当WIFSIGNALED(status)
为真时,获取导致子进程终止的信号编号。代码示例:
#include <stdio.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h> // 子进程执行的函数 void Worker() { int cnt = 3; while(cnt) { // 打印子进程ID、父进程ID及计数printf("I am child process , pid: %d , ppid : %d , cnt : %d\n" ,getpid(),getppid(),cnt--); sleep(1); // 休眠1秒,模拟工作过程} } int main() { // 创建子进程,返回值id在父进程中为子进程PID,在子进程中为0pid_t id = fork(); if(id == 0) { // 子进程执行区域Worker(); // 调用工作函数,循环3次后退出exit(10); // 子进程退出,传入退出码10 } else { // 父进程执行区域printf("Wait before\n"); // 等待前的提示int status = 0; // 用于存储子进程退出状态的变量// 等待任意子进程(-1)退出,阻塞式等待(0),将状态存入status// rid接收实际退出的子进程PIDpid_t rid = waitpid(-1,&status,0);printf("Wait after\n"); // 等待结束的提示// 验证等待的是否是我们创建的子进程if(rid == id)// 打印父进程ID、原始状态值、提取的退出信号和退出码// status>>8:右移8位获取高8位(退出码相关)// status&(0xFF):与运算获取低8位(信号相关)printf("Wait success , pid : %d , status : %d , exit sig : %d , exit code : %d\n",getpid(), status, status&0xFF, (status>>8)&0xFF);sleep(3); // 父进程等待3秒后结束}return 0; }
关键部分说明:
fork()
调用后创建子进程,通过返回值id
区分父子进程- 子进程执行
Worker()
函数后通过exit(10)
退出,10 是自定义退出码- 父进程使用
waitpid(-1, &status, 0)
进行阻塞等待:
- 第一个参数
-1
表示等待任意子进程- 第二个参数
&status
用于接收子进程退出状态- 第三个参数
0
表示阻塞等待(直到子进程退出才返回)- 状态解析方式:
status&0xFF
获取低 8 位,通常表示终止信号(正常退出时为 0)(status>>8)&0xFF
获取高 8 位,通常表示退出码(信号终止时无效)
- 当我们在进程中故意写一个错误,观察信号编号和退出码,此时我们发现无论子进程的退出码是多少,只要子进程出现异常,那么子进程的退出码就是0,并且父进程可以得到子进程的信号编号
- 上面是代码中出现错误导致子进程发现异常,若是子进程正常运行,我们给子进程发送信号会发生什么呢?我们发现同样可以使子进程终止,父进程也可以获得信号编号。
根据上面讲述的内容提出三个问题🧐:
当一个进程异常(收到信号),那么这个进程的退出码还有意义吗❓
- 答:异常终止时退出码无意义当进程因收到信号而异常终止时,其退出码不再具备实际意义。因为退出码是进程正常结束时通过
exit()
主动设置的返回值,而信号终止属于被动强制结束,此时操作系统会用status
的低 8 位存储终止信号编号,而非退出码,因此读取退出码没有实际价值。
我们怎么判断一个子进程有没有收到信号❓
- 可通过解析
wait/waitpid
获取的status
参数判断:信号编号始终大于 0,若通过WTERMSIG(status)
或status&0xFF
获取的值为 0,说明子进程正常退出(未收到信号);若值大于 0,则表示子进程被对应编号的信号终止。
我们为什么不定义一个全局变量status去获取子进程的退出信息,而使用系统调用去获得❓
- 进程具有独立性,父子进程的地址空间相互隔离。即使定义全局变量
status
,子进程对其的修改会触发操作系统的 “写时拷贝” 机制,仅修改子进程私有地址空间中的变量副本,父进程无法感知。因此必须通过wait/waitpid
等系统调用,由操作系统将子进程的退出状态主动传递给父进程。
🔎三者共同体现了操作系统对进程管理的核心原则:进程隔离保证安全性,而系统调用则提供了受控的进程间交互渠道(如状态传递)。
上面编写的代码中想获取子进程的信号编号和退出码还需要进行位操作,为了给不熟悉编程人员提供遍历,操作系统提供了下面两个函数
- WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
exitNormal = WIFEXITED(status); //是否正常退出 exitCode = WEXITSTATUS(status); //获取退出码
1-2-4 参数options🕵️♀️
在
waitpid
函数中,option
参数用于控制父进程的等待行为,最常用的两个选项如下:
1️⃣0
(阻塞等待)
当option
设为0
时,父进程会进入阻塞状态,暂停执行,直到有子进程退出后才会被唤醒,继续处理子进程的退出状态。这是最常用的等待方式,适用于父进程明确需要等待子进程完成后再继续执行的场景。
2️⃣WNOHANG
(非阻塞等待)
当option
设为WNOHANG
时,父进程不会阻塞,而是立即返回:
- 若有子进程退出,
waitpid
返回该子进程的 PID,并通过status
传递退出状态;- 若没有子进程退出,
waitpid
立即返回0
,父进程可以继续执行其他任务,无需等待。这种非阻塞方式适用于父进程需要同时处理自身任务和子进程状态的场景(例如循环检查多个子进程是否退出)。
示例代码片段(展示两种选项的区别):
1-2-5 🔥阻塞等待vs非阻塞等待🔥
1️⃣核心定义与本质差异
阻塞等待(Block Waiting) 非阻塞等待(Non-Block Waiting) 核心逻辑 父进程调用wait()或waitpid(pid, &status, 0)后,立即进入阻塞状态,暂停执行自身代码。 父进程调用waitpid(pid, &status, WNOHANG)后,不暂停执行,立即返回结果,不阻塞自身流程。 等待条件 必须等待子进程完全退出(变为僵尸态后被回收),wait/waitpid才会返回,父进程才继续执行。 无论子进程是否退出,waitpid都会立即返回;若子进程未退出,需通过轮询重复调用等待。 父进程状态 阻塞期间,父进程无法执行任何自身任务(如处理其他请求、打印日志等),资源被 “闲置”。 等待期间,父进程可正常执行其他耗时短的任务(如轮询检查多个子进程、处理客户端请求等)。 2️⃣关键技术细节对比
1.函数调用方式
阻塞等待:依赖
wait()
函数(默认阻塞)或waitpid()
函数的options
参数设为0
(默认值),示例:#include <stdio.h> #include <unistd.h> #include <stdlib.h>int main() { pid_t id = fork();int cnt = 5;if(id == 0){// 子进程while(cnt){printf("我是子进程,我还在运行,我还有%d...\n",cnt--);sleep(1);}exit(111);}// 父进程int status = 0;pid_t ret_id = wait(&status);printf("child exit code:%d,child exit singl:%d\n",(status >> 8) & 0xFF,status & 0x7F); }
- 仔细观察上文中示例中的结果,父进程的输出总在子进程结束之后,如图:
- 在子进程运行期间,父进程一直在等待,并没有做其他事情,直到等待子进程成功。我们把这种情况称为父进程在进行
阻塞等待
。
- 那如果父进程不想干干地等待子进程结束,而是想在等待的期间做点其他有意义的事情该如何处理呢?
②非阻塞等待:仅依赖
waitpid()
函数,且options
参数必须设为WNOHANG
(“无悬挂等待”,即不阻塞),非阻塞等待时,父进程可以做自己占据时间不多的事情,示例:
- 下面设计一个非阻塞轮询等待的代码来实现这个功能
#include <stdio.h> #include <sys/types.h> #include <sys/wait.h> // 提供waitpid等进程等待函数 #include <unistd.h> // 提供fork、sleep等系统调用 #include <stdlib.h> // 提供exit函数#define TASK_NUM 5 // 任务表最大容量 typedef void(*task_t)(); // 定义函数指针类型,用于表示任务////////////////////////////////////////////////////// // 定义三个示例任务函数(模拟父进程可并行处理的工作) void download() {printf("this is a download task is running!\n"); } void printLog() { printf("this is a write log task is running!\n"); } void show() { printf("this is a show task is running!\n"); } //////////////////////////////////////////////////////// 初始化任务表(将所有任务指针置空) void InitTasks(task_t tasks[],int num) { int i = 0; for(; i < num ; i++) { tasks[i] = NULL; // 空指针表示该位置无任务} } // 向任务表中添加任务(找到第一个空位置插入) int AddTask(task_t tasks[], int num ,task_t task) {int i = 0; for( ; i < num ; i++){if(tasks[i] == NULL) {tasks[i] = task; // 存储任务函数的地址return 1; // 添加成功返回1}}return 0; // 任务表满,添加失败返回0 }// 执行任务表中所有非空任务(回调执行) void executeTask(task_t tasks[] , int num) {int i = 0;for(; i < num ; i++){if(tasks[i]) // 若任务存在(非空指针),则调用该函数tasks[i]();} }// 子进程执行的函数(模拟子进程的工作) void Worker() {int cnt = 3;while(cnt){// 打印子进程ID、父进程ID和剩余计数printf("I am child process , pid: %d , ppid : %d , cnt : %d\n" ,getpid(),getppid(),cnt--);sleep(1); // 休眠1秒,模拟任务执行耗时} }int main() {// 定义一个任务表(存储待执行的任务函数)task_t tasks[TASK_NUM];// 初始化并添加任务到任务表中InitTasks(tasks,TASK_NUM);AddTask(tasks,TASK_NUM,download); // 添加下载任务AddTask(tasks,TASK_NUM,printLog); // 添加日志任务AddTask(tasks,TASK_NUM,show); // 添加显示任务// 创建子进程pid_t id = fork();if(id == 0) // 子进程执行分支{Worker(); // 子进程执行自己的任务(循环3次后退出)exit(0); // 子进程完成任务后退出}else // 父进程执行分支(非阻塞等待逻辑核心){// 非阻塞轮询等待:通过循环不断检查子进程状态while(1){int status = 0; // 用于存储子进程退出状态信息// 非阻塞等待子进程:WNOHANG标志表示不阻塞// 若子进程未退出,立即返回0;若已退出,返回子进程PID;失败返回-1pid_t rid = waitpid(id, &status, WNOHANG);if(rid > 0) // 情况1:子进程已退出(等待成功){// 解析子进程退出状态:// status>>8:获取退出码(正常退出时有效)// status&0x7F:获取终止信号(被信号终止时有效)printf("child quit success , exit code : %d , exit sig : %d\n", status>>8, status&0x7F);break; // 退出等待循环}else if(rid == 0) // 情况2:子进程未退出(等待成功但未就绪){printf("------------------------------------------------------\n");printf("child is alive , wait again , father do other thing...\n");executeTask(tasks, TASK_NUM); // 父进程执行自己的任务(任务表中的任务)printf("------------------------------------------------------\n");}else // 情况3:等待失败(rid < 0,通常是参数错误){printf("wait failed!\n");break; // 退出等待循环}sleep(1); // 休眠1秒后再次轮询,避免CPU空转}}return 0; }
核心逻辑说明:
①非阻塞等待机制:父进程通过
waitpid(id, &status, WNOHANG)
实现非阻塞等待,WNOHANG
标志确保父进程不会被阻塞。每次调用后根据返回值判断子进程状态:
- 子进程已退出(
rid > 0
):处理退出信息并结束等待- 子进程未退出(
rid == 0
):父进程执行自己的任务(任务表中的函数)- 等待失败(
rid < 0
):退出循环
②任务表设计:采用函数指针数组实现任务的动态管理(
InitTasks
初始化、AddTask
添加、executeTask
执行),父进程在等待子进程期间可通过executeTask
批量执行预定义任务,体现了非阻塞等待中 "父进程可并行处理其他工作" 的特点。
③父子进程协作:子进程通过
Worker
函数执行自身任务(循环 3 次后退出),父进程通过轮询方式等待,同时利用等待间隙处理任务表中的工作,实现了 "并行" 执行的效果(实际是时间片轮转)。
2.返回值与结果判断
等待类型 子进程已退出时的返回值 子进程未退出时的返回值 等待失败时的返回值(如参数错误) 阻塞等待 返回退出子进程的 PID(正数) 不会出现此情况(阻塞至子进程退出) 返回-1,并设置errno 非阻塞等待 返回退出子进程的 PID(正数) 返回0(表示等待成功但子进程未退出) 返回 -1
,并设置errno
3. 适用场景
阻塞等待:适合父进程仅需等待单个子进程,且自身无其他紧急任务的场景。
非阻塞等待:适合父进程需要管理多个子进程,或需同时处理其他任务的场景。
3️⃣典型问题与注意事项
①非阻塞等待需避免 “孤儿进程”
若非阻塞等待仅调用一次且父进程提前退出,未退出的子进程会成为孤儿进程(被init
进程或systemd
接管)。因此,非阻塞等待必须配合循环轮询,确保子进程退出前父进程不退出,示例:
② 阻塞等待的 “资源闲置” 问题
若子进程执行时间较长(如耗时 10 秒),父进程在阻塞期间无法处理任何任务,会导致资源浪费。此时需结合非阻塞等待或多线程优化。
③状态信息获取的一致性
无论阻塞还是非阻塞等待,子进程的退出信息(退出码、终止信号)均通过status
参数(位图结构)获取,需使用系统提供的宏(如WIFEXITED(status)
判断正常退出、WEXITSTATUS(status)
获取退出码)解析,避免直接位操作导致错误。
4️⃣总结
特性 阻塞等待 非阻塞等待 父进程灵活性 低(阻塞期间无法执行其他任务) 高(等待期间可处理其他任务) 实现复杂度 简单(单次调用即可) 较高(需循环轮询,处理返回值分支) 资源利用率 低(父进程资源闲置) 高(父进程资源充分利用) 适用场景 单任务等待、无其他并发需求 多子进程管理、需并发处理其他任务 本质上,两种等待方式是父进程 “等待子进程” 与 “自身任务执行” 的权衡 —— 阻塞等待牺牲灵活性换取实现简单,非阻塞等待牺牲少量代码复杂度换取资源高效利用。
🕵️♀️细节剖析
1️⃣操作系统层面:父进程如何获取子进程的退出信息
子进程退出时,操作系统会通过一系列机制保存其退出状态,并提供接口让父进程安全获取,核心流程如下:
①子进程退出的状态保存
当子进程调用exit()
或被信号终止时,操作系统会:
- 将子进程标记为僵尸进程(Zombie),保留其 PCB(进程控制块)不释放;
- 在 PCB 中记录退出信息:包括退出码(正常退出时)、终止信号(被信号杀死时)、资源使用情况(如 CPU 时间、内存占用)等;
- 发送
SIGCHLD
信号给父进程,通知子进程状态变化(父进程可捕获该信号触发处理逻辑)。
②父进程获取信息的接口
父进程通过wait()
或waitpid()
系统调用主动获取子进程退出信息:
- 系统调用触发后,操作系统会查找目标子进程的 PCB;
- 若子进程已退出(僵尸态),则将 PCB 中的退出信息复制到父进程指定的
status
参数(用户空间变量);- 释放子进程的 PCB 资源(彻底清理僵尸进程),并返回子进程的 PID。
③status
参数的解析
退出信息以位图结构存储在status
中,父进程需通过系统提供的宏解析:
WIFEXITED(status)
:判断子进程是否正常退出(非信号终止);WEXITSTATUS(status)
:提取正常退出时的退出码(需配合WIFEXITED
使用);WIFSIGNALED(status)
:判断子进程是否被信号终止;WTERMSIG(status)
:提取终止子进程的信号编号(需配合WIFSIGNALED
使用)。
2️⃣父进程在子进程的等待队列中等待的机制
操作系统通过等待队列(Wait Queue) 管理进程的阻塞与唤醒,父进程等待子进程的过程本质是对等待队列的操作:
等待队列的结构
每个子进程的 PCB 中维护一个等待队列,队列成员是等待该子进程退出的父进程(或其他进程)。队列元素包含:
- 等待进程的 PCB 指针;
- 唤醒后需执行的回调函数(如将进程状态从阻塞改为就绪)。
阻塞等待的入队过程
当父进程调用
wait()
或waitpid(..., 0)
(阻塞模式)时:
- 操作系统检查子进程是否已退出:
- 若已退出(僵尸态),直接返回退出信息;
- 若未退出,将父进程的状态从运行态改为阻塞态,并将其 PCB 加入子进程的等待队列;
- 父进程让出 CPU,调度器选择其他就绪进程执行。
子进程退出时的唤醒过程
子进程退出后,操作系统会:
- 遍历该子进程等待队列中的所有父进程;
- 将这些父进程的状态从阻塞态改为就绪态,移出等待队列,放入系统就绪队列;
- 当调度器下次调度时,这些父进程有机会获得 CPU,继续执行(从
wait/waitpid
调用处返回)。
非阻塞等待与轮询
非阻塞等待(waitpid(..., WNOHANG)
)不会将父进程加入等待队列:
- 无论子进程是否退出,系统调用立即返回(返回 0 表示子进程未退出);
- 父进程需通过循环重复调用(轮询),直到子进程退出,期间可执行其他任务;
- 此模式下父进程始终处于运行态或就绪态,不进入阻塞队列。
总结
机制 核心原理 退出信息获取 子进程退出时,操作系统保存信息至 PCB;父进程通过wait/waitpid从 PCB 读取并清理。 等待队列作用 阻塞等待时,父进程进入子进程的等待队列并阻塞;子进程退出后,父进程被唤醒。 阻塞 vs 非阻塞差异 阻塞等待依赖等待队列进入休眠,非阻塞等待通过轮询避免阻塞,不进入等待队列。 本质上,操作系统通过 PCB 保存状态、等待队列管理阻塞、系统调用提供接口,实现了父进程对子进程的可控等待,既保证了资源的正确回收,又支持灵活的进程协作模式。
🔎父进程等待多个子进程
1️⃣核心原理铺垫
在分析代码前,需先明确 3 个关键特性(这是子进程 “无序执行”“等待顺序可控” 的根本原因):
- 进程调度的随机性:操作系统的 CPU 调度是 “抢占式” 的,父进程创建子进程后,子进程何时获得 CPU 时间片完全由内核调度决定,因此子进程执行顺序不固定(与创建顺序无关)。
- exit 码的传递:子进程通过
exit(编号)
将 “顺序编号” 作为退出状态码传递给父进程,父进程通过waitpid
获取该状态码(需通过WEXITSTATUS(status)
解析,因为exit
的参数仅占低 8 位)。- waitpid 的等待模式:
waitpid(-1, &status, 0)
:等待任意一个已终止的子进程(无序等待);waitpid(pid[i], &status, 0)
:等待指定 PID的子进程(有序等待,需先保存子进程 PID)。
2️⃣完整代码实现(C 语言)
- 以下代码严格遵循需求:父进程按 0~5 顺序创建 6 个子进程,子进程执行
Worker
函数输出编号并传递 exit 码,父进程通过waitpid
等待并输出退出码。#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> #include <sys/types.h>// Worker函数:子进程执行逻辑(参数为子进程顺序编号) void Worker(int child_num) {// 输出子进程编号(含PID,便于区分)printf("子进程:编号=%d,PID=%d,正在执行\n", child_num, getpid());// 将编号传入exit,作为退出状态码(仅低8位有效,0~5合法)exit(child_num); }int main() {int i;pid_t pid[6]; // 存储6个子进程的PID(用于后续指定等待)int status; // 存储waitpid获取的子进程退出状态// 第一步:父进程按0~5顺序创建6个子进程for (i = 0; i < 6; i++) {pid[i] = fork(); // 创建子进程,返回值:父进程得子PID,子进程得0,失败得-1if (pid[i] == -1) { // 错误处理:创建子进程失败perror("fork failed");exit(1);} else if (pid[i] == 0) { // 子进程执行逻辑(父进程不进入此分支)Worker(i); // 调用Worker,传入顺序编号iexit(0); // 冗余(Worker已调用exit)}// 父进程分支:仅记录子PID,继续循环创建下一个子进程}printf("父进程:已完成6个子进程的创建(父PID=%d)\n\n", getpid());// 第二步:父进程等待子进程(两种等待方式可选)// 方式1:无序等待(等待任意子进程,与创建顺序无关)printf("=== 父进程:无序等待子进程(按子进程终止顺序输出) ===\n");for (i = 0; i < 6; i++) {// waitpid(-1, ...):等待任意已终止的子进程,阻塞直到有子进程退出pid_t terminated_pid = waitpid(-1, &status, 0);if (terminated_pid == -1) {perror("waitpid failed");exit(1);}// 解析退出码:WEXITSTATUS(status) 提取exit参数的低8位int exit_code = WEXITSTATUS(status);printf("父进程:捕获子进程(PID=%d),退出码=%d\n", terminated_pid, exit_code);}// // 方式2:有序等待(按子进程创建顺序0~5等待,强制顺序)// printf("=== 父进程:有序等待子进程(按创建顺序0~5输出) ===\n");// for (i = 0; i < 6; i++) {// // waitpid(pid[i], ...):仅等待编号为i的子进程(PID存在pid[i]中)// pid_t terminated_pid = waitpid(pid[i], &status, 0);// if (terminated_pid == -1) {// perror("waitpid failed");// exit(1);// }// int exit_code = WEXITSTATUS(status);// printf("父进程:捕获子进程(编号=%d,PID=%d),退出码=%d\n", i, terminated_pid, exit_code);// }printf("\n父进程:所有子进程已退出,父进程结束\n");return 0; }
输出结果:
结果关键结论🔎
- 子进程创建顺序≠执行顺序:父进程按 0→1→2→3→4→5 创建子进程,但输出中编号 0、2 先执行,编号 1 后执行 —— 这是内核调度随机性导致的。
- 父进程等待顺序≠创建顺序:无序等待(
waitpid(-1)
)时,父进程按 “子进程终止顺序” 捕获,而非创建顺序(如上例中先捕获编号 0,再捕获编号 2)。- 退出码与编号一致:无论子进程执行 / 终止顺序如何,父进程通过
WEXITSTATUS(status)
获取的退出码,始终等于子进程的顺序编号(确保 “身份正确”)。
3️⃣进程监控验证
为直观看到 “父进程一次性创建 6 个子进程”,可在运行代码时,通过
ps
命令监控进程状态:
- 打开两个终端,终端 1 运行代码(为便于监控,先修改代码:在父进程创建完所有子进程后,添加一段休眠时间(如
sleep(30)
),让父进程和子进程不立即退出,留给监控操作的时间。)
- 终端 2 执行监控命令(查看父进程的所有子进程):
ps -ef | grep 1233 | grep -v grep
1. 首次查询结果解析:
<defunct>
僵尸进程输出中
[test1] <defunct>
表示这些子进程(PID: 3555460~3555465)已经终止,但父进程(PID: 3555459)尚未通过wait()
或waitpid()
系统调用回收它们的资源(如进程描述符、退出状态等),因此成为僵尸进程。
2. 二次查询结果为空:僵尸进程被回收
第二次查询时,所有进程(包括父进程和子进程)都消失了,原因是:
- 父进程(3555459)最终执行了
waitpid()
或wait()
调用,回收了所有僵尸子进程的资源,僵尸进程从进程表中移除。- 父进程在完成所有子进程的回收后,自身也正常退出,因此整个进程树(父 + 子)都从系统中消失。
3. 为什么会出现僵尸进程?
僵尸进程是子进程终止后,父进程 “尚未回收” 的中间状态:
- 子进程终止时,内核会保留其退出状态(供父进程查询),并将其标记为僵尸进程。
- 父进程必须通过
waitpid()
或wait()
主动获取子进程的退出状态,内核才会彻底释放该子进程的资源。- 代码中,父进程在创建子进程后,过了一段时间才执行
waitpid()
循环(例如代码中加了sleep
延迟逻辑),因此在延迟期间,子进程先终止并成为僵尸进程,直到父进程执行等待操作后才被回收。
结束语:
以下就是我对【Linux系统编程】进程控制:等待的理解
感谢你的三连支持!!!