Linux进程等待
- Linux进程等待
- github地址
- [0. 前言](#0. 前言)
- [1. 进程等待的必要性](#1. 进程等待的必要性)
-
- [1.1 避免僵尸进程与资源泄漏](#1.1 避免僵尸进程与资源泄漏)
- [1.2 僵尸进程不可被直接清除](#1.2 僵尸进程不可被直接清除)
- [1.3 获取子进程的运行结果](#1.3 获取子进程的运行结果)
- [2. 进程等待的三个问题](#2. 进程等待的三个问题)
-
- [1. 为什么要有进程等待](#1. 为什么要有进程等待)
- [2. 进程等待是什么](#2. 进程等待是什么)
- [3. 怎么实现进程等待](#3. 怎么实现进程等待)
- [3. 僵尸进程演示](#3. 僵尸进程演示)
- [4. wait](#4. wait)
- [5. waitpid](#5. waitpid)
-
- 手册声明
- 返回值与参数详解
- status参数获取进程的退出信息
-
- 引入
- status的二进制编码及其应用
-
- [1. 进程退出状态的编码规则](#1. 进程退出状态的编码规则)
- [2. exit(1) 的情况](#2. exit(1) 的情况)
- [3. 如何判断子进程的退出情况](#3. 如何判断子进程的退出情况)
- [4. 是否可以通过全局变量获取退出状态?](#4. 是否可以通过全局变量获取退出状态?)
- 如何获取正确的status
- 用操作系统提供的宏获取进程退出的status
- waitpid失败的场景
- waitpid等待多个子进程
- [6. 进程等待的原理示意](#6. 进程等待的原理示意)
- [7. 非阻塞轮询](#7. 非阻塞轮询)
- [8. 结语](#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次后退出,父进程为死循环,父进程中没有对子进程进行回收,子进程成为僵尸进程
cpp
#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) {
// child
int cnt = 5;
while (cnt) {
printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(0);
} else {
// father
while (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的手册声明
shell
man 2 wait


在 Linux 中,wait
用于等待任意一个子进程的状态发生变化(通常是退出)。
-
成功情况
- 返回值:子进程的 PID(> 0)
-
失败情况
-
返回值:
-1
-
说明:
- 失败时,
errno
会被设置为合适的错误码,例如:ECHILD
:调用进程没有未等待的子进程。
- 失败时,
-
-
参数 :输出型参数,获取子进程退出状态,不关心则可以设置成为
NULL
-
总结逻辑
-
> 0
:成功,返回子进程 PID -
-1
:失败,没有子进程可等待或者所有子进程都已经被回收过了(或出错)
-
wait的使用
wait单个子进程
代码如下:
cpp
#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) {
// child
int cnt = 5;
while (cnt) {
printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(0);
} 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回收
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()
调用一次只能等待任意一个子进程,如何等待多个进程呢,需要我们循环等待子进程
cpp
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define N 10
void 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
执行多次
cpp
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)
等待子进程退出
cpp
// 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
的子集,以下两种写法完全等效:
cpp
// 都传入 NULL 指针,标识 不关心子进程的状态
pid_t ret = wait(NULL);
pid_t ret = waitpid(-1, NULL, 0);
status参数获取进程的退出信息
引入
- 输出型参数 status 的基本用法 :传入
status
的地址,函数内对外部的status
进行修改
cpp
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)
cpp
// 5. 获取子进程的退出状态
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, 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
的二进制结果为:
c
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)
,可以得到正确的退出状态
cpp
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;
}

-
如果发生了异常,我们也能通过该计算方式得到不同的错误码
-
在子进程中添加访问野指针异常
cpp
else if (pid == 0) {
int* p = NULL;
// child
int 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)
:获取导致进程终止的信号编号- 以上宏函数,底层是根据位运算实现的
代码示例:
cpp
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork failed\n");
return 1;
} else if (pid == 0) {
// child
int cnt = 3;
while (cnt) {
printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(11);
} else {
// father
int 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
函数的等待失败
cpp
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 + 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);
cpp
// 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较小,退出码也较小

cpp
// 循环创建多个子进程
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. 进程等待的原理示意
内核源码
cpp
// 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
)来发现子进程是否结束。
非阻塞轮询的演示
cpp
#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
,就能拿到子进程的退出信息并完成回收。
- 合理的等待策略
- 如果父进程需要一直等子进程,可以使用阻塞等待;
- 如果父进程还有其他逻辑要执行,可以选择非阻塞轮询,并定期检查子进程是否退出,然后再回收。
怎么做
非阻塞等待,设计父进程做一些自己的工作
创建任务数组
cpp
#define TASK_NUM 10 // 定义任务数组中任务的总数
typedef void (*task_t)(); // 定义任务的函数指针
task_t tasks[TASK_NUM]; // 定义函数指针数组
设计任务函数
- 以下函数仅为模拟子任务的过程
cpp
// 设计任务
void task1() {
printf("这是一个执行打印日志的任务, pid: %d\n", getpid());
}
void task2() {
printf("这是一个执行检测网络健康状态的一个任务, pid: %d\n", getpid());
}
void task3() {
printf("这是一个进行绘制图形界面的任务, pid: %d\n", getpid());
}
初始化和添加任务
cpp
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_NUM
if (pos == TASK_NUM)
return -1;
tasks[pos] = task; // 添加任务函数的指针
return 0;
}
删除、检查和执行任务
- 这里只实现执行任务,检查、执行、更新任务读者可以自行补充完成
cpp
void DelTask() {
}
void CheckTask() {
}
void UpdateTask() {
}
void ExecuteTask() {
for (int i = 0; i < TASK_NUM; ++i) {
if (!tasks[i])
continue;
tasks[i]();
}
}
父进程轮询调用子任务
- 父进程轮询等待单个子进程的场景
cpp
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;
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 程序。
以上就是本文的所有内容了,如果觉得文章对你有帮助,欢迎 点赞⭐收藏 支持!如有疑问或建议,请在评论区留言交流,我们一起进步
分享到此结束啦
一键三连,好运连连!
你的每一次互动,都是对作者最大的鼓励!
征程尚未结束,让我们在广阔的世界里继续前行!
🚀