【Linux我做主】细说进程等待
Linux进程等待
- Linux进程等待
- github地址
- 0. 前言
- 1. 进程等待的必要性
- 1.1 避免僵尸进程与资源泄漏
- 1.2 僵尸进程不可被直接清除
- 1.3 获取子进程的运行结果
- 2. 进程等待的三个问题
- 1. 为什么要有进程等待
- 2. 进程等待是什么
- 3. 怎么实现进程等待
- 3. 僵尸进程演示
- 4. wait
- wait的手册声明
- wait的使用
- wait单个子进程
- wait多个子进程
- wait时的阻塞等待
- wait的简单总结
- 5. waitpid
- 手册声明
- 返回值与参数详解
- status参数获取进程的退出信息
- 引入
- status的二进制编码及其应用
- 1. 进程退出状态的编码规则
- 2. exit(1) 的情况
- 3. 如何判断子进程的退出情况
- 4. 是否可以通过全局变量获取退出状态?
- 如何获取正确的status
- 用操作系统提供的宏获取进程退出的status
- waitpid失败的场景
- waitpid等待多个子进程
- 6. 进程等待的原理示意
- 内核源码
- 7. 非阻塞轮询
- 阻塞等待
- 非阻塞等待
- 非阻塞轮询的演示
- 非阻塞轮询父进程运行其他任务
- 父进程应该做什么
- 怎么做
- 创建任务数组
- 设计任务函数
- 初始化和添加任务
- 删除、检查和执行任务
- 父进程轮询调用子任务
- 总结
- 8. 结语
Linux进程等待
github地址
有梦想的电信狗
0. 前言
学习进程等待之前,请先移步到进程状态和进程退出的学习:
- 进程状态:https://blog.csdn.net/2301_80064645/article/details/147869604?spm=1001.2014.3001.5501
- 进程退出和终止:https://blog.csdn.net/2301_80064645/article/details/150526116?spm=1001.2014.3001.5502
在 Linux 编程中,进程退出后并不会立即完全消失,如果父进程不及时回收,就会留下僵尸进程,造成资源泄漏,甚至影响系统稳定。
因此,进程等待不仅是清理子进程的必要操作,也是父进程获取子进程退出状态的重要途径。本文将结合示例,介绍进程等待的意义、wait
与 waitpid
的用法,以及阻塞和非阻塞等待的差别与应用。
1. 进程等待的必要性
当父进程创建子进程后,如果对子进程“放任不管”,很快就会遇到一个严重的问题——僵尸进程。僵尸进程不仅影响系统资源的使用,还可能成为系统不稳定的隐患。因此,进程等待是进程管理中不可或缺的一环。
1.1 避免僵尸进程与资源泄漏
- 子进程退出后,其代码和数据会被释放,但 内核依然会保留一部分信息(如退出状态、统计信息、PID 等),供父进程读取。
- 如果父进程不调用等待机制来回收子进程,那么这些信息会一直滞留在系统中,使子进程长期处于 僵尸状态。
- 僵尸进程不会再占用 CPU,但它们占用的内核资源不可忽视,随着数量增多,将导致系统资源泄漏,甚至阻塞新的进程创建。
1.2 僵尸进程不可被直接清除
- 一旦进程进入僵尸状态,它已经“死去”,无法被
kill -9
等信号杀死。换句话说,僵尸进程是 刀枪不入 的,唯一的清除方式就是让父进程通过 进程等待 回收它们的资源。
1.3 获取子进程的运行结果
除了资源回收,父进程往往还关心子进程是否正确完成了任务:
- 子进程是否正常退出?
- 子进程的退出码是多少?
- 子进程是否因某个信号异常终止?
这些信息对于任务调度、错误处理和日志记录都非常重要。通过进程等待,父进程能够获取到这些状态,从而 确认子任务的执行结果。
综上所述,进程等待的必要性主要体现在两个方面:
- 必不可少的资源回收 —— 防止僵尸进程堆积,避免系统资源泄漏。
- 可选择性的结果获取 —— 让父进程能够获知子进程的执行情况,辅助系统的正确运行与维护。
2. 进程等待的三个问题
1. 为什么要有进程等待
当一个子进程结束时,它会进入 僵尸进程(Zombie Process) 状态。僵尸进程本身并不会继续占用 CPU,但它依然在内核中保留着一定的资源(如 PCB 等),直到父进程通过 进程等待 的方式去读取它的退出状态并回收这些资源。
如果父进程不做等待,这些僵尸进程将长期滞留在系统中,最终造成 资源泄漏,严重时甚至会导致系统无法再创建新的进程。
因此,进程等待至少有两个核心目的:
- 必须解决的问题:
- 回收子进程,避免僵尸进程堆积,防止系统内存资源泄漏。
- 可选关心的问题:
- 获取子进程的退出状态,了解其任务是否顺利完成。
- 父进程可以根据这些信息决定后续操作(如重新调度任务、打印日志、做错误处理等)。
换句话说,进程等待既是 资源回收的必需操作,也是 父进程获取任务结果的一种手段。
2. 进程等待是什么
-
进程等待 是父进程通过系统调用
wait
或waitpid
来检测和获取子进程的退出状态,并同时完成资源回收的过程(完成对僵尸进程的回收)。 -
常用的系统调用有:
-
wait
:阻塞等待任意一个子进程退出,并返回其退出状态。 -
waitpid
:可以选择性地等待某个特定子进程,支持阻塞或非阻塞模式。
-
3. 怎么实现进程等待
父进程调用系统调用 wait
或 waitpid
,可实现对子进程的状态检测与资源回收,从而避免僵尸进程的产生。逻辑流程大致如下:
- 子进程执行完任务并退出 → 内核保留其状态 → 子进程进入 僵尸状态。
- 父进程调用
wait/waitpid
:- 如果有已退出的子进程,立即回收并获取状态;
- 如果没有子进程退出,
wait
会阻塞等待,而waitpid
可选择等待方式,阻塞或立即返回。
- 内核在回收完成后,释放子进程的 PCB 等资源。
3. 僵尸进程演示
- 以下代码进行了僵尸进程的演示:子进程循环5次后退出,父进程为死循环,父进程中没有对子进程进行回收,子进程成为僵尸进程
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>int main() {pid_t pid = fork();if (pid < 0) {perror("fork failed\n");return 1;} else if (pid == 0) {// childint cnt = 5;while (cnt) {printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}exit(0);} else {// fatherwhile (1) {printf("I am father, pid: %d, ppid: %d\n", getpid(), getppid());sleep(1);}// 此时父进程没有针对子进程干任何事情,子进程退出后会变成僵尸进程}return 0;
}
5秒过后,子进程退出,父进程仍在运行。父进程的代码中没有对子进程的资源进行回收,因此子进程变成僵尸进程
- 子进程一般退出的时候,如果父进程没有主动回收子进程信息,子进程会一直让自己处于Z状态。进程的相关资源尤其是
task_struct
结构体不能被释放。<defunct>
的意思即为死的,意为僵尸进程- 未来父进程将子进程回收后,操作系统才能将子进程的资源进行释放。
- 如果一个进程一直处于僵尸进程,自身的内存资源会被一直占用,从而引发内存泄露。之后可以在父进程中调用
waitpid();
函数解决该问题
父进程终止后,原来的子进程直接被操作系统领养,变成孤儿进程。操作系统直接将该进程领养并回收了。
父进程终止后,僵尸子进程会被 init
(或 systemd
)接管,并由其回收资源,不再是僵尸进程
-
因此一个进程创建子进程后,父进程的代码中要有对子进程的回收操作。
-
关于僵尸进程的详细介绍请阅读往期文章进程状态 https://blog.csdn.net/2301_80064645/article/details/147869604?spm=1001.2014.3001.5501
-
接下来我们探究使用系统调用
wait/waitpid
进行子进程的回收
4. wait
wait的手册声明
man 2 wait
在 Linux 中,wait
用于等待任意一个子进程的状态发生变化(通常是退出)。
-
成功情况
- 返回值:子进程的 PID(> 0)
-
失败情况
-
返回值:
-1
-
说明:
- 失败时,
errno
会被设置为合适的错误码,例如:ECHILD
:调用进程没有未等待的子进程。
- 失败时,
-
-
参数:输出型参数,获取子进程退出状态,不关心则可以设置成为
NULL
-
总结逻辑
-
> 0
:成功,返回子进程 PID -
-1
:失败,没有子进程可等待或者所有子进程都已经被回收过了(或出错)
-
wait的使用
wait单个子进程
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>int main() {pid_t pid = fork();if (pid < 0) {perror("fork failed\n");return 1;} else if (pid == 0) {// childint cnt = 5;while (cnt) {printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}exit(0);} else {// fatherint cnt = 10;while (cnt) {printf("I am father, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}// 10 秒后 父进程对子进程进行wait回收pid_t ret = wait(NULL); // 暂时传入 NULL 指针,不关心子进程的状态if (ret == pid) {printf("wait success %d\n", ret);}sleep(5);}return 0;
}
代码和运行现象分析:
- 父进程创建子进程,父子进程分别运行,父进程循环10秒,子进程循环5秒
- 父进程循环10秒后对子进程进行
wait
回收,回收后等待5秒后退出。子进程循环5秒后退出 - 因此:
- 前5秒,父子进程同时运行
- 第二个五秒,子进程退出,父进程循环,子进程成为僵尸状态
- 第三个五秒,回收后,父进程运行等待五秒,子进程被回收,因此只有父进程在运行
- 最后,父进程退出
wait多个子进程
wait()
调用一次只能等待任意一个子进程,如何等待多个进程呢,需要我们循环等待子进程
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>#define N 10void runChild() {int cnt = 5;while (cnt) {printf("I am child Process, pid: %d, ppid: %d\n", getpid(), getppid());cnt--;sleep(1);}
}
// 2. 如果有多个子进程,wait如何正确等待
int main() {for (int i = 0; i < N; ++i) {pid_t id = fork();if (id == 0) {runChild();exit(0);}// 父进程只会在循环内不断地创建子进程printf("creat child process: %d success\n", id); // 这行代码只有父进程会执行}sleep(10);// wait 一次只能等待任意一个子进程,如何等待多个进程for (int i = 0; i < N; ++i) {pid_t id = wait(NULL); // 等待任意一个子进程if (id > 0) {printf("wait %d success\n", id);}}sleep(5);return 0;
}
现象阐述:
./myproc
,进程启动的一瞬间,我们看到10个进程在运行- 第一个5秒过后,子进程全部退出,全部变为僵尸进程,持续五秒
- 第二个5秒过后,父进程对子进程进行回收,回收后父进程继续运行
- 回收过后。父进程独自运行第三个5秒后结束
循环等待子进程的的逻辑:
wait
一次只能等待任意一个子进程,等待多个进程需要确保wait
执行多次
for (int i = 0; i < N; ++i) {pid_t id = wait(NULL);if (id > 0) {printf("wait %d success\n", id);}
}
至此,进程等待是必须的:wait
完成了回收子进程,避免僵尸进程堆积,防止系统资源泄漏的工作。
wait时的阻塞等待
- 以上是父进程等待子进程退出后再进行
wait
回收的场景,如果父进程不进行等待,且子进程一直不退出,父进程调用wait
会怎么样呢 - 我们对上述
wait
多个子进程的代码进行修改,让子进程永远不退出,父进程也不再sleep(5)
等待子进程退出
// wait 等待多个子进程,但任意一个子进程永不退出的场景
void runChild() {int cnt = 5;while (1) { // 永不退出printf("I am child Process, pid: %d, ppid: %d\n", getpid(), getppid());cnt--;sleep(1);}
}
int main() {for (int i = 0; i < N; ++i) {pid_t id = fork();if (id == 0) {runChild();exit(0);}// 父进程只会在循环内不断地创建子进程printf("creat child process: %d success\n", id); // 这行代码只有父进程会执行}// sleep(10);// wait 一次只能等待任意一个子进程,如何等待多个进程// 等待多个进程时,任意一个子进程都不退出for (int i = 0; i < N; ++i) {pid_t id = wait(NULL);if (id > 0) {printf("wait %d success\n", id);}}sleep(5);return 0;
}
结论:
-
如果子进程不退出,默认父进程调用的系统调用wait函数就不会返回,该行为称为阻塞等待
- 因此父进程调用的
wait
函数的return
条件是子进程退出
- 因此父进程调用的
-
因此进程在等待时,既可以等待硬件资源,也可以等待软件资源(比如等待子进程退出)
wait的简单总结
- 父进程的
wait
调用会回收僵尸进程,解决内存泄露问题 - 如果子进程不退出,那么父进程调用的
wait
就不会返回,父进程阻塞等待,直到子进程退出
5. waitpid
手册声明
进程等待至少有两个核心目的:
- 必须解决的问题:
- 回收子进程,避免僵尸进程堆积,防止系统内存资源泄漏。
- 可选关心的问题:
- 获取子进程的退出状态,了解其任务是否顺利完成。
- 父进程可以根据这些信息决定后续操作(如重新调度任务、打印日志、做错误处理等)。
通过wait(NULL)
(阻塞等待),我们已经解决了回收子进程,避免僵尸进程堆积,防止系统资源泄漏的问题,那么我们如何获取子进程的退出状态,得知子进程是否完成相应的任务呢?
这时waitpid
就要登场了
返回值与参数详解
pid_ t waitpid(pid_t pid, int *status, int options);
-
返回值:
- 正常返回时,
waitpid
返回收集到的子进程的进程ID; - 如果设置参数
options
为WNOHANG
,为非阻塞等待,调用时waitpid
发现子进程没有退出,直接返回0; - 如果调用中出错,则返回-1,这时
errno
会被设置成相应的值以指示错误所在;
- 正常返回时,
-
参数:
- pid:
pid == -1
:等待任一个子进程,与wait
等效。pid > 0
:等待其进程ID与pid相等的子进程。
- status:输出型参数,传入外部变量的地址,函数内将子进程的退出状态设置给外部的
status
- options:
WNOHANG
:若指定pid
的子进程没有结束,则waitpid()
函数返回0,不予等待。若正常结束,则返回该子进
程的ID- 传入0:表示设置阻塞等待,与
wait
等效。
- pid:
-
wait
的功能是waitpid
的子集,以下两种写法完全等效:
// 都传入 NULL 指针,标识 不关心子进程的状态
pid_t ret = wait(NULL);
pid_t ret = waitpid(-1, NULL, 0);
status参数获取进程的退出信息
引入
- 输出型参数 status 的基本用法:传入
status
的地址,函数内对外部的status
进行修改
int status = 0;
pid_t ret = waitpid(pid, &status, 0); // 传入status的地址,函数内对外部的 status 进行修改
if (ret == pid) {printf("wait success %d, status: %d\n", ret, status);
}
- 获取子进程退出的
status
的演示,这里设置子进程的退出码为1,exit(1)
// 5. 获取子进程的退出状态
int main() {pid_t pid = fork();if (pid < 0) {perror("fork failed\n");return 1;} else if (pid == 0) {// childint cnt = 5;while (cnt) {printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}exit(1);} else {// fatherint cnt = 10;while (cnt) {printf("I am father, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}// 10 秒后 父进程对子进程进行wait回收int status = 0;pid_t ret = waitpid(pid, &status, 0);if (ret == pid) {printf("wait success %d, status: %d\n", ret, status);}sleep(3);}return 0;
}
- 运行结果如下:
- 我们子进程的退出码为1 ( exit(1) ),为什么这里获取到的退出码不是1,而是256呢?
- 接下来我们详细解释
status的二进制编码及其应用
回顾进程终止的原因/情景:
-
代码运行完毕,结果正确
-
代码运行完毕,结果不正确
-
代码异常终止
父进程等待,期望获得子进程退出的哪些信息呢?
- 子进程代码是否异常
- 没有异常,结果对吗?不对是因为什么呢?
- 子进程的
exit()
中的退出码,不同的退出码,表示不同的出错原因! - 父进程通过子进程的退出码,可以获取到子进程退出的原因
- 子进程的
上文中**退出码显示为256与进程退出状态的编码规则有关**
1. 进程退出状态的编码规则
在父进程调用 wait/waitpid
时,会得到一个整数 status
,该值的低 16 位用于表示子进程的退出情况。我们暂时只考虑status的低16位
其结构如下:
┌─────────────── 16位 ────────────────┐15 8 7 0┌──────────────┬───────────┬─────────┐│ 退出状态 │ core dump │ 终止信号 │└──────────────┴───────────┴─────────┘
- 低 7 位(0~6 位):表示子进程被哪个信号终止(进程出现异常本质就是被信号终止了)。
- 第 8 位:表示是否产生了
core dump
文件。 - 次低 8 位(8~15 位):表示子进程正常退出时的退出码(
exit()
或return
返回的值)。
👉 总结:
- 若子进程因信号终止:则 低 7 位 ≠ 0
- 若子进程正常退出:则 低 7 位 == 0,此时退出码保存在次低 8 位。
- 这样通过子进程的
status
的低16位就可以判断子进程的退出情况了。
kill -l
命令查看Linux中的所有信号,Linux中的信号编号不是从0开始的也从侧面证明了,未出现异常时 status 的低八位为0
2. exit(1) 的情况
如果子进程调用 exit(1)
,无异常,则:
- 退出码 == 1(写入到
status
的第 9 位)。 - 低 7 位 == 0(表示没有信号导致异常退出)。
因此 status
的二进制结果为:
0000 0001 0000 0000
换算成十进制即 256。
3. 如何判断子进程的退出情况
- 先判断是否异常退出:进程出现异常本质是收到了信号
- 检查
status
的低 7 位。 - 若 ≠ 0,说明子进程是被某个信号终止。
- 检查
- 再判断退出码:
- 若低 7 位 == 0,说明子进程正常退出,无异常。
- 此时读取
status
的次低 8 位,即退出码。
4. 是否可以通过全局变量获取退出状态?
不能及其原因。
- 尽管是定义全局变量,但父子进程拥有独立的地址空间,父子进程之间具有独立性
- 即使子进程修改了某个全局变量(如
status
),由于写时拷贝(COW)机制,父进程并不会感知到子进程的修改,无法获取到子进程修改后的全局变量。 - 只有通过
wait/waitpid
系统调用,父进程才能得到内核提供的子进程退出信息。
👉 结论:父进程必须通过 wait/waitpid
获取子进程的退出状态。
如何获取正确的status
-
通过位运算的方式分别获取到是否出现异常和进程的退出码
status & 0x7F
:status & 0111 1111
,可以提取出status
的低七位(status >> 8) & 0xFF
:status右移八位后,再(status >> 8) & 1111 1111
可以提取出status
的次低八位
-
以下代码子进程
exit(1)
,可以得到正确的退出状态
int main() {pid_t pid = fork();if (pid < 0) {perror("fork failed\n");return 1;} else if (pid == 0) {// child 进程int cnt = 5;while (cnt) {printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}exit(1);} else {// father 进程int cnt = 10;while (cnt) {printf("I am father, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}// 10 秒后 父进程对子进程进行wait回收int status = 0;pid_t ret = waitpid(pid, &status, 0);if (ret == pid) {// 通过位运算直接获取到正确的异常信号和退出码printf("wait success %d, exit sig: %d, exit code: %d\n", ret, status & 0x7F, (status >> 8) & 0xFF);}sleep(3);}return 0;
}
-
如果发生了异常,我们也能通过该计算方式得到不同的错误码
-
在子进程中添加访问野指针异常
-
else if (pid == 0) {int* p = NULL;// childint cnt = 5;while (cnt) {printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);*p = 100; // 访问野指针,会出现异常}
-
9号信号杀进程异常:
用操作系统提供的宏获取进程退出的status
我们可以对status
经过位运算分别获取子进程的退出码和判断子进程是否收到异常退出的信号,但这么做似乎有些繁琐,操作系统为我们提供了宏函数,用于判断进程是否正常退出和查看进程的退出码
WIFEXITED(status)
:若status
为进程正常终止返回的状态,则WIFEXITED(status)
为真。(查看进程是否是正常退出)WEXITSTATUS(status)
:若WIFEXITED
非零,提取子进程退出码。(查看进程的退出码)WTERMSIG(status)
:获取导致进程终止的信号编号- 以上宏函数,底层是根据位运算实现的
代码示例:
int main() {pid_t pid = fork();if (pid < 0) {perror("fork failed\n");return 1;} else if (pid == 0) {// childint cnt = 3;while (cnt) {printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}exit(11);} else {// fatherint cnt = 5;while (cnt) {printf("I am father, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}// 5 秒后 父进程对子进程进行wait回收int status = 0;pid_t ret = waitpid(pid, &status, 0);if (ret == pid) {if (WIFEXITED(status))printf("进程正常执行完毕, 退出码: %d\n", WEXITSTATUS(status));else {printf("进程出异常了\n");}} else {printf("wait fail\n");}sleep(3);}return 0;
}
waitpid失败的场景
waitpid
失败的重要场景:等待的子进程不是当前进程的子进程pid_t ret = waitpid(pid + 4, &status, 0)
,这里将等待的进程编号改为pid + 4
- 当等待的子进程不是当前父进程的子进程时,会触发
waitpid
函数的等待失败
int main() {pid_t pid = fork();if (pid < 0) {perror("fork failed\n");return 1;} else if (pid == 0) {// childint cnt = 5;while (cnt) {printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}exit(1);} else {// fatherint cnt = 10;while (cnt) {printf("I am father, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}// 10 秒后 父进程对子进程进行wait回收int status = 0;pid_t ret = waitpid(pid + 4, &status, 0);if (ret == pid) {printf("wait success %d, exit sig: %d, exit code: %d\n", ret, status & 0x7F, (status >> 8) & 0xFF);} else {printf("wait fail\n");}sleep(3);}return 0;
}
waitpid等待多个子进程
- 将第一个参数设为-1,则等待任意一个子进程。
waitpid(-1, &status, 0);
// waitpid 等待多个子进程
void runChild() {int cnt = 5;while (cnt) {printf("I am child Process, pid: %d, ppid: %d\n", getpid(), getppid());cnt--;sleep(1);}
}
int main() {for (int i = 0; i < N; ++i) {pid_t id = fork();if (id == 0) {runChild();exit(i);}// 父进程只会在循环内不断地创建子进程printf("creat child process: %d success\n", id); // 这行代码只有父进程会执行}for (int i = 0; i < N; ++i) {int status = 0;// 等待任意一个子进程pid_t id = waitpid(-1, &status, 0);if (id > 0) {printf("wait %d success, exit code: %d\n", id, WEXITSTATUS(status));}}sleep(3);return 0;
}
- 可以看到,先创建的进程PID较小,退出码也较小
// 循环创建多个子进程
for (int i = 0; i < N; ++i) {pid_t id = fork();if (id == 0) {runChild();exit(i);}// 父进程只会在循环内不断地创建子进程printf("creat child process: %d success\n", id); // 这行代码只有父进程会执行
}
// 循环等待多个子进程
for (int i = 0; i < N; ++i) {int status = 0;// 等待任意一个子进程pid_t id = waitpid(-1, &status, 0);if (id > 0) {printf("wait %d success, exit code: %d\n", id, WEXITSTATUS(status));}
}
简单分析:
waitpid(-1, &status, 0)
:这里pid
设为-1
,表示等待任意一个子进程,options
设为0,表示阻塞等待- 返回值
id > 0
时,标识waitpid
函数等待成功,返回了所等待进程的pid
6. 进程等待的原理示意
内核源码
// Linux 内核源源码
struct task_struct {int exit_state;int exit_code;int exit_signal;
}
-
可以看到,内核源码
task_struct
中存放了进程退出的三个变量,分别表示进程的退出状态,退出码,退出信号 -
而在底层实现上,子进程退出时,代码和数据可以释放,但
task_strcut
必须先保留。exit_status
的值会根据exit_code
和exit_signal
,经过位运算最终组合而得到,最终再将exit_status
的值传给status
-
waitpid
的本质:获取内核数据结构task_struct
中的进程状态,将进程状态由Z状态改为X状态
7. 非阻塞轮询
- 返回值:
- 正常返回时,
waitpid
返回收集到的子进程的进程ID; - 如果设置了选项
WNOHANG
,而调用中waitpid
发现子进程没有退出,则返回0; - 如果调用中出错,则返回-1,这时
errno
会被设置成相应的值以指示错误所在;
- 正常返回时,
接下来我们来介绍**
waitpid
**的第三个参数options
(阻塞方式)
思考:如果父进程在进行等待时,子进程运行了很久都不退出,这期间就会造成父进程状态为阻塞状态
- 父进程一旦进入阻塞,会被链入到子进程
task_struct
的等待队列中,父进程就不会被CPU运行了,这不是我们所希望的,有没有什么方法不让父进程在等待时阻塞呢? - 参数
options
为我们提供了相应的控制方法- 给
options
传入0:可以实现像wait
函数一样的阻塞等待 - 给
options
传入WNOHANG
:控制waitpid
为非阻塞等待
- 给
- 那么**什么是阻塞等待(blocking wait)和非阻塞等待(non-blocking wait)**呢?
阻塞等待
定义:
当父进程调用 waitpid
(不加 WNOHANG
),如果子进程还没有退出,父进程就会 停下来,进入阻塞状态,直到子进程结束或收到信号为止。
特点:
- 父进程“什么都不做”,一直等子进程。
- 父进程此时 无法继续执行其它代码。
阻塞等待是最简单的,也是最常应用的等待方式
非阻塞等待
定义:
父进程调用 waitpid
时加上 WNOHANG
参数,如果子进程还没有退出,waitpid
会 立刻返回 0,不会阻塞父进程。这样父进程可以去做别的事情,稍后再回来轮询一次子进程状态。
特点:
- 父进程不会停下来,可以一边做其他工作,一边不时检查子进程状态。
- 需要通过 轮询(循环调用
waitpid
)来发现子进程是否结束。
非阻塞轮询的演示
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>#define N 10// 非阻塞等待
int main() {pid_t pid = fork();if (pid < 0) {perror("fork failed\n");return 1;} else if (pid == 0) {// child 进程int cnt = 5;while (cnt) {printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}exit(11);} else {// 父进程int status = 0;while (1) { // while(1) 不断轮询pid_t ret = waitpid(pid, &status, WNOHANG);// 等待成功或失败,break退出轮询if (ret > 0) {// 等待成功,获取子进程退出信息if (WIFEXITED(status))printf("子进程正常运行完毕,退出码 %d\n", WEXITSTATUS(status));else {printf("进程退出异常\n");}break;} // 等待失败else if (ret < 0) { printf("wait failed\n");break;} // ret == 0 代表子进程还没有退出,可进行询问,或做其他任务else { printf("请问你运行完毕了吗? 子进程还没有退出,再等等\n");sleep(1);}}}return 22;
}
非阻塞轮询父进程运行其他任务
父进程应该做什么
- 父进程调用
waitpid
的主要目的
父进程调用waitpid
,核心任务是等待并回收子进程,防止出现僵尸进程。
在等待期间,父进程也可以“顺带”做一些轻量的任务,但这些任务不能过于复杂,否则可能影响对子进程状态的及时处理。 - 子进程退出与回收的时机
- 不是必须立刻回收:子进程一旦退出,内核会将其状态信息保存下来,此时子进程会进入 僵尸状态。
- 父进程稍后再回收也可以:只要父进程在合适的时机调用
wait
/waitpid
,就能拿到子进程的退出信息并完成回收。
- 合理的等待策略
- 如果父进程需要一直等子进程,可以使用阻塞等待;
- 如果父进程还有其他逻辑要执行,可以选择非阻塞轮询,并定期检查子进程是否退出,然后再回收。
怎么做
非阻塞等待,设计父进程做一些自己的工作
创建任务数组
#define TASK_NUM 10 // 定义任务数组中任务的总数typedef void (*task_t)(); // 定义任务的函数指针
task_t tasks[TASK_NUM]; // 定义函数指针数组
设计任务函数
- 以下函数仅为模拟子任务的过程
// 设计任务
void task1() {printf("这是一个执行打印日志的任务, pid: %d\n", getpid());
}
void task2() {printf("这是一个执行检测网络健康状态的一个任务, pid: %d\n", getpid());
}
void task3() {printf("这是一个进行绘制图形界面的任务, pid: %d\n", getpid());
}
初始化和添加任务
int AddTask(task_t task);void InitTask() {// 初始化函数指针数组for (int pos = 0; pos < TASK_NUM; ++pos)tasks[pos] = NULL;// 添加任务AddTask(task1);AddTask(task2);AddTask(task3);// 还可以接着添加别的任务 ...
}
int AddTask(task_t task) {int pos = 0;// 找可以添加任务的位置for (; pos < TASK_NUM; ++pos) {// 第一个为NULL的位置可以添加任务if (!tasks[pos])break;}// 循环结束后,pos 可能 == TASK_NUMif (pos == TASK_NUM)return -1;tasks[pos] = task; // 添加任务函数的指针return 0;
}
删除、检查和执行任务
- 这里只实现执行任务,检查、执行、更新任务读者可以自行补充完成
void DelTask() {
}
void CheckTask() {
}
void UpdateTask() {
}
void ExecuteTask() {for (int i = 0; i < TASK_NUM; ++i) {if (!tasks[i])continue;tasks[i]();}
}
父进程轮询调用子任务
- 父进程轮询等待单个子进程的场景
int main() {pid_t pid = fork();if (pid < 0) {perror("fork failed\n");return 1;} else if (pid == 0) {// childint cnt = 5;while (cnt) {printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}exit(11);}// 父进程else {int status = 0;InitTask();while (1) { // 轮询pid_t ret = waitpid(pid, &status, WNOHANG); // 设置非阻塞等待// 等待成功或失败,break退出轮询if (ret > 0) {// 等待成功,获取子进程退出信息if (WIFEXITED(status))printf("子进程正常运行完毕,退出码 %d\n", WEXITSTATUS(status));else {printf("进程退出异常\n");}break;} else if (ret < 0) {// 等待失败printf("wait failed\n");break;} else {// ret == 0 说明 子进程未退出// 父进程的工作 放在这个块中执行ExecuteTask();usleep(500000);}}}return 22;
}
需要注意的是:
- 以上代码仅为创建了单个子进程的场景,如果有有多个子进程需要回收,需要将
waitpid(pid, &status, WNOHANG)
中的pid改为-1,一次等待任意一个子进程 - 等待成功或失败时,不应该直接
break
,而是设计一个计数器,记录需要等待的进程的个数,并不断调整
总结
- 父进程的核心责任:对子进程进行回收,避免僵尸进程。
- 等待方式的选择:
- 阻塞等待 —— 父进程只关心子进程,适合简单场景。
- 非阻塞等待 —— 父进程一边等待,一边做其他轻量任务,适合并行场景。
- 最终的进程退出顺序:
- 最后终止的一定是父进程。
- 通过正确的进程等待机制,可以保证:
- 父进程始终是最后退出的进程;
- 父进程能正确释放所有曾经创建过的子进程
8. 结语
进程等待的核心作用有两点:
- 回收子进程,避免僵尸进程;
- 获取子进程状态,辅助任务管理。
无论是使用阻塞等待还是非阻塞等待,合理的等待机制都是保证程序稳定与高效运行的关键。掌握这些方法,才能在实践中写出更健壮的 Linux 程序。
以上就是本文的所有内容了,如果觉得文章对你有帮助,欢迎 点赞⭐收藏 支持!如有疑问或建议,请在评论区留言交流,我们一起进步
分享到此结束啦
一键三连,好运连连!
你的每一次互动,都是对作者最大的鼓励!
征程尚未结束,让我们在广阔的世界里继续前行!
🚀