
大家好,欢迎来到 huangjin007_ 的博客
⭐ 个人主页:huangjin007_
🔥 文章收录专栏:Linux 内功修炼手册(系统篇)
总会有一些坚持
能从冰封的土地里
培育出十万朵怒放的蔷薇
Linux 系统篇(二十一) ------ 进程等待 wait/waitpid、status状态参数
文章目录
- [Linux 系统篇(二十一) ------ 进程等待 wait/waitpid、status状态参数](#Linux 系统篇(二十一) —— 进程等待 wait/waitpid、status状态参数)
-
- 一、为什么需要进程等待?
-
- [1.1 子进程退出,父进程不能不管](#1.1 子进程退出,父进程不能不管)
- [1.2 僵尸进程为什么连 kill -9 都杀不死?](#1.2 僵尸进程为什么连 kill -9 都杀不死?)
- [1.3 父进程还需要知道子进程干得怎么样](#1.3 父进程还需要知道子进程干得怎么样)
- [1.4 进程等待的核心目的](#1.4 进程等待的核心目的)
- 二、进程等待的两个核心系统调用
-
- [2.1 wait 和 waitpid 的基本区别](#2.1 wait 和 waitpid 的基本区别)
- [三、wait 函数详解](#三、wait 函数详解)
-
- [3.1 函数原型](#3.1 函数原型)
- [3.2 参数说明](#3.2 参数说明)
- [3.3 返回值](#3.3 返回值)
- [3.4 行为特点](#3.4 行为特点)
- [3.5 代码示例:wait 阻塞回收](#3.5 代码示例:wait 阻塞回收)
- [四、waitpid 函数详解](#四、waitpid 函数详解)
-
- [4.1 函数原型](#4.1 函数原型)
- [4.2 参数 pid 的取值与含义](#4.2 参数 pid 的取值与含义)
- [4.3 参数 options 的控制](#4.3 参数 options 的控制)
- [4.4 返回值](#4.4 返回值)
- [4.5 调用时机总结](#4.5 调用时机总结)
- 五、阻塞等待与非阻塞等待
-
- [5.1 什么是阻塞?什么是非阻塞?](#5.1 什么是阻塞?什么是非阻塞?)
- [5.2 一个生活类比:张三约李四一起去学习](#5.2 一个生活类比:张三约李四一起去学习)
- [5.3 阻塞等待代码示例](#5.3 阻塞等待代码示例)
- [5.4 非阻塞等待代码示例](#5.4 非阻塞等待代码示例)
- [六、深入理解 status 状态参数](#六、深入理解 status 状态参数)
-
- [6.1 父进程能直接拿到子进程退出信息吗?](#6.1 父进程能直接拿到子进程退出信息吗?)
- [6.2 task_struct 中的退出信息](#6.2 task_struct 中的退出信息)
- [6.3 status 不是普通整数,而是位图](#6.3 status 不是普通整数,而是位图)
- [6.4 系统标准方法:宏函数提取](#6.4 系统标准方法:宏函数提取)
- [6.5 为什么 status 打印出来是 256?](#6.5 为什么 status 打印出来是 256?)
- [6.6 退出码只有 8 位](#6.6 退出码只有 8 位)
- [6.7 异常退出示例](#6.7 异常退出示例)
- 七、进程等待的底层原理
-
- [7.1 父进程回收的到底是什么?](#7.1 父进程回收的到底是什么?)
- [7.2 为什么会有僵尸进程?](#7.2 为什么会有僵尸进程?)
- [7.3 阻塞等待时,父进程在干什么?](#7.3 阻塞等待时,父进程在干什么?)
- [7.4 非阻塞等待时,父进程在干什么?](#7.4 非阻塞等待时,父进程在干什么?)
- 结语:
一、为什么需要进程等待?
1.1 子进程退出,父进程不能不管
在 Linux 中,子进程是通过 fork() 创建的。子进程运行结束后会退出,但它的退出并不是"彻底消失"。如果父进程对子进程的退出不管不顾,就可能产生一个非常经典的问题:僵尸进程。
僵尸进程是什么?简单说,子进程已经死了,但它的"尸体"还留在操作系统的进程表里。这个"尸体"就是内核中的 task_struct 结构体。它占着进程表项、PID 等资源。如果大量僵尸进程堆积,就可能造成资源泄漏,严重时甚至耗尽系统 PID。
所以,父进程必须通过某种方式"回收"子进程。这个回收动作,就是进程等待。
1.2 僵尸进程为什么连 kill -9 都杀不死?
很多人第一次听到僵尸进程会想:既然它还在进程表里,那我用 kill -9 杀掉它不就行了?
答案是不行。
kill -9 的本质是向进程发送 SIGKILL 信号。信号需要进程"活着"才能处理。可僵尸进程已经死了,它没有执行流,不会响应任何信号。它只是内核里保留的一段退出信息。所以 kill -9 对僵尸进程无能为力。
谁也没有办法杀死一个已经死去的进程。
1.3 父进程还需要知道子进程干得怎么样
除了回收资源,父进程通常还关心子进程的执行结果。比如:
- 子进程正常退出了吗?
- 退出码是多少?
- 是被信号杀死的吗?
- 如果是被信号杀死的,是哪个信号?
这些信息不能靠猜,必须由父进程通过进程等待来获取。
1.4 进程等待的核心目的
父进程进行进程等待,主要达成两件事:
- 回收子进程资源:这是硬性需求,不做就会产生僵尸进程。
- 获取子进程退出信息:这是可选需求,但很多场景下非常重要。
一句话:
进程等待 = 父进程回收子进程 + 获取子进程退出状态。
二、进程等待的两个核心系统调用
要使用进程等待,需要包含以下头文件:
c
#include <sys/types.h>
#include <sys/wait.h>
系统提供了两个最主要的接口:
c
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
它们都是系统调用,由操作系统内核提供支持。
2.1 wait 和 waitpid 的基本区别
| 特性 | wait | waitpid |
|---|---|---|
| 等待目标 | 任意一个子进程 | 可指定子进程、进程组或任意子进程 |
| 是否阻塞 | 默认阻塞 | 可通过 options 控制阻塞/非阻塞 |
| 灵活性 | 较低 | 很高 |
| 常用场景 | 简单回收 | 精细控制、非阻塞轮询 |
三、wait 函数详解
3.1 函数原型
c
pid_t wait(int *status);
3.2 参数说明
status:输出型参数,用来获取子进程的退出状态。- 如果你不关心子进程怎么退出的,可以传
NULL。 - 如果传的是有效地址,操作系统会把子进程的退出信息填充进去。
- 如果你不关心子进程怎么退出的,可以传
3.3 返回值
- 成功:返回被等待进程的 PID。
- 失败:返回
-1。
3.4 行为特点
wait 会等待任意一个子进程退出。
- 如果调用
wait时,已经有子进程退出了,它会立即返回,并回收该子进程。- 如果调用
wait时,子进程还在运行,父进程会默认阻塞 在wait调用处,直到有子进程退出。- 如果根本没有子进程可等待,
wait会出错返回-1。
这种阻塞很像 scanf 等待输入:你不输入,它就不往下走。
3.5 代码示例:wait 阻塞回收
cpp
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int Func01()
{
// 创建子进程
pid_t id = fork();
if(id == 0)
{
//子进程执行分支
int cnt = 5;
while(cnt)
{
pid_t _pid = getpid();
cout << "我是子进程,我的PID是:" << _pid << endl;
sleep(1);
cnt--;
}
// 子进程运行 5 秒后退出,此时父进程还在 sleep(7)
// 子进程会进入僵尸状态(Zombie)
exit(0);
}
// 父进程执行分支
// 父进程先休眠 7 秒
// 前 5 秒子进程正常运行,后 2 秒子进程已退出但父进程未回收
// 因此子进程处于僵尸状态
sleep(7);
// 父进程调用 wait 阻塞式回收任意子进程
// 此时子进程已经退出,wait 会立刻成功回收
// 传入 NULL 表示不关心子进程退出状态
pid_t rid = wait(NULL);
if(rid > 0)
{
cout << "回收成功,子进程PID: " << rid << endl;
}
// 子进程被回收后,父进程再休眠 7 秒
// 此时用 ps命令 观察,僵尸进程已经消失
sleep(7);
return 0;
}
int main()
{
Func01();
return 0;
}
这个例子非常适合观察僵尸进程:
- 子进程先运行 5 秒,然后退出。
- 父进程还在
sleep(7),没有调用wait。- 这 2 秒内,子进程就是僵尸状态。
- 父进程
sleep结束后调用wait,立刻回收子进程。- 僵尸进程消失。
你可以打开另一个终端,用 ps 命令观察:
bash
ps aux | grep 程序名
你会看到子进程状态先是 S+(睡眠),然后变成 Z+(僵尸),最后被回收后消失。

四、waitpid 函数详解
4.1 函数原型
c
pid_t waitpid(pid_t pid, int *status, int options);
waitpid 比 wait 更强大,因为它可以指定等待哪个子进程,还可以控制阻塞方式。
4.2 参数 pid 的取值与含义
pid 参数决定父进程要等待哪些子进程:
| pid 取值 | 含义 |
|---|---|
pid > 0 |
等待进程 ID 等于 pid 的那个特定子进程 |
pid == -1 |
等待任意一个子进程,与 wait 等效 |
pid == 0 |
等待与调用进程属于同一个进程组的任意子进程 |
pid < -1 |
等待进程组 ID 等于 pid 的任意子进程 |
常用的是:
pid > 0:等待指定子进程。pid == -1:等待任意子进程,类似wait。
4.3 参数 options 的控制
options 用来控制等待方式:
0:默认行为,阻塞等待。WNOHANG:非阻塞等待。如果指定子进程没有结束,waitpid立即返回0,不阻塞父进程。
WNOHANG 会详细讲。
4.4 返回值
waitpid 的返回值有三种重要情况:
> 0:成功回收子进程,返回收集到的子进程 PID。== 0:只有在使用WNOHANG时才会出现,表示子进程还没有退出。< 0:调用出错,返回-1,同时errno会被设置。
4.5 调用时机总结
- 如果子进程已经退出,调用
wait/waitpid会立即返回,释放资源,获得退出信息。 - 如果子进程还在运行,父进程可能阻塞。
- 如果不存在目标子进程,立即出错返回。
五、阻塞等待与非阻塞等待
5.1 什么是阻塞?什么是非阻塞?
阻塞: 父进程调用 wait / waitpid 后,如果条件不满足,就挂起自己,不继续执行,直到条件满足。
非阻塞: 父进程调用 waitpid 并设置 WNOHANG,如果子进程还没退出,函数立即返回 0,父进程可以继续做别的事,过一会儿再来检查。
5.2 一个生活类比:张三约李四一起去学习
非阻塞等待(轮询):
张三约李四一起去图书馆学习。李四说:"我上楼拿点东西,你等我一下。"
张三在楼下等。他等一会儿,打电话问:"好了吗?"李四说:"快了快了。"然后挂断。张三很无聊,就刷了会儿短视频,又打电话问:"好了吗?"李四说:"马上好。"又挂断。张三反复打了五通电话,最后李四终于下楼了。
这就是非阻塞轮询:张三在等待期间可以做自己的事,每隔一段时间主动确认状态。
阻塞等待:
张三又约李四。这次张三打电话说:"在你收拾好东西、走下来之前,千万别挂电话,我一直在线上等你。"
张三什么都不干,电话一直保持接通,直到李四下楼。
这就是阻塞等待:父进程被挂起,不执行其他操作,直到子进程退出。

5.3 阻塞等待代码示例
cpp
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid = fork();
if(pid < 0)
{
printf("%s fork error\n", __FUNCTION__);
return 1;
}
else if(pid == 0)
{
// 子进程
printf("child is run, pid is : %d\n", getpid());
sleep(5);
exit(257);
}
else
{
int status = 0;
// 阻塞式等待,等待 5 秒
pid_t ret = waitpid(-1, &status, 0);
printf("this is test for wait\n");
if(WIFEXITED(status) && ret == pid)
{
printf("wait child 5s success, child return code is :%d.\n",
WEXITSTATUS(status));
}
else
{
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}
运行结果:
text
child is run, pid is : 45110
this is test for wait
wait child 5s success, child return code is :1.
注意:子进程 exit(257),但打印出来的退出码是 1。为什么?因为退出码只有 8 位,257 & 0xFF = 1。后面 status 部分会详细解释。
5.4 非阻塞等待代码示例
cpp
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdlib>
using namespace std;
int Func()
{
pid_t id = fork();
if(id == 0)
{
// 子进程执行流
int cnt = 5;
while(cnt)
{
pid_t _pid = getpid();
cout << "我是一个子进程,我的PID是:" << _pid << endl;
sleep(1);
cnt--;
}
exit(0);
}
// 父进程执行流
sleep(1);
while(1)
{
int status = 0;
// 使用 WNOHANG 进行非阻塞等待
pid_t rid = waitpid(id, &status, WNOHANG);
if(rid > 0)
{
// 子进程已经退出,成功回收
cout << "等待成功,子进程的退出码为"
<< WEXITSTATUS(status) << endl;
break;
}
else if(rid == 0)
{
// 子进程还在运行,本次未捕获到退出
cout << "子进程未退出,父进程继续做其他事" << endl;
sleep(1);
}
else
{
// 等待出错
cout << "等待失败" << endl;
break;
}
}
return 0;
}
int main()
{
Func();
return 0;
}
非阻塞轮询的返回值判断:
> 0:等待成功,子进程已退出。== 0:调用结束,但子进程还没退出。< 0:等待失败。
六、深入理解 status 状态参数
6.1 父进程能直接拿到子进程退出信息吗?
不能。
进程之间是相互独立的。子进程退出时,它的退出信息不会直接飞到父进程手里。谁掌握这些信息?操作系统内核。
所以父进程要获取子进程退出信息,必须通过系统调用找操作系统要。wait 和 waitpid 就是干这个的。
6.2 task_struct 中的退出信息
在 Linux 内核中,每个进程都有一个 task_struct。子进程即使退出了,它的 task_struct 也不会立刻销毁。内核会在其中保存退出信息,类似:
c
long exit_state;
int exit_code, exit_signal;
当子进程退出时,操作系统会把退出码和终止信号写入它自己的 task_struct。
父进程调用 waitpid(&status) 时:
- 通过系统调用进入内核。
- 内核读取子进程
task_struct中的exit_code和exit_signal。- 内核按位图规则打包,写入父进程传入的
status变量。- 提取完成后,内核才真正销毁子进程的
task_struct。
这也解释了僵尸进程存在的原因:
子进程的 task_struct 要保留到父进程读取退出信息之后,才能被销毁。
6.3 status 不是普通整数,而是位图
status 虽然类型是 int,但不能简单当成普通整数看。内核把它当作位图使用。我们主要研究它的低 16 位。
text
bits: 15 14 13 12 11 10 9 8 | 7 | 6 5 4 3 2 1 0
次低 8 位 |core| 低 7 位

正常终止
当子进程正常退出,比如 main 函数返回或调用 exit():
- 次低 8 位(第 8 到 15 位):保存退出码。
- 低 7 位(第 0 到 6 位):全部为 0。
- 第 7 位:core dump 标志位,默认为 0。
被信号所杀
当子进程因为异常被信号终止,比如除零、野指针、被 kill:
- 低 7 位(第 0 到 6 位):保存导致终止的信号。
- 次低 8 位:无意义,不表示退出码。
- 第 7 位:core dump 标志位。
重要结论:
- 如果低 7 位为 0,说明进程正常终止。
- 如果低 7 位不为 0,说明进程异常终止,此时退出码无意义。
6.4 系统标准方法:宏函数提取
WIFEXITED(status):若子进程正常终止,返回真。WEXITSTATUS(status):若WIFEXITED为真,提取退出码。WIFSIGNALED(status):若子进程因信号终止,返回真。WTERMSIG(status):若WIFSIGNALED为真,提取终止信号。
cpp
int Func()
{
pid_t id = fork();
if(id == 0)
{
int cnt = 5;
while(cnt)
{
cout << "我是一个子进程,我的PID是: " << getpid() << endl;
sleep(1);
cnt--;
}
exit(0);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
if(WIFEXITED(status))
{
cout << "wait success, exit code: "
<< WEXITSTATUS(status) << endl;
}
else if(WIFSIGNALED(status))
{
cout << "child process killed by signal: "
<< WTERMSIG(status) << endl;
}
}
else
{
perror("waitpid failed");
}
return 0;
}
6.5 为什么 status 打印出来是 256?
看一个例子:子进程 exit(1),父进程 waitpid 后打印 status,结果是 256。
为什么?
因为退出码 1 被放在次低 8 位,也就是第 8 位。第 8 位的权值是 2^8 = 256。低 8 位全为 0。所以 status 的值就是 256。
二进制:
text
0000 0001 0000 0000
↑
第 8 位为 1
这正好说明:status 不是普通整数,而是位图。
6.6 退出码只有 8 位
子进程退出码并不是无限的。它只占 8 位,范围是 0 ~ 255。
所以:
c
exit(257);
实际保存的退出码是:
text
257 & 0xFF = 1
这就是为什么前面的代码 exit(257),父进程提取出来是 1。
6.7 异常退出示例
如果子进程被信号杀死,比如在另一个终端执行:
bash
kill -9 子进程PID
父进程通过 wait 获取到的 status 低 7 位就是 9。这时:
c
WIFEXITED(status) // 为假
WIFSIGNALED(status) // 为真
WTERMSIG(status) // 返回 9
七、进程等待的底层原理
7.1 父进程回收的到底是什么?
父进程调用 wait / waitpid,本质上是:
- 进入内核。
- 找到已经退出的子进程。
- 读取它的
task_struct中的退出信息。- 把这些信息打包成
status返回给父进程。- 销毁子进程的
task_struct,释放 PID 等资源。
所以,父进程回收的"资源",主要就是子进程残留的内核数据结构。
7.2 为什么会有僵尸进程?
因为父进程一直在运行,却没有调用 wait / waitpid 回收已经退出的子进程。
子进程虽然退出了,但它的 task_struct 还在,等待父进程读取退出信息。只要父进程不读,它就一直留着,变成僵尸。
7.3 阻塞等待时,父进程在干什么?
当父进程调用 wait 且子进程还没退出时,父进程会被挂起,进入睡眠状态,不再占用 CPU。等子进程退出后,内核唤醒父进程,父进程继续执行。
所以阻塞等待并不浪费 CPU。
7.4 非阻塞等待时,父进程在干什么?
非阻塞等待使用 WNOHANG。父进程调用 waitpid 后,如果子进程没退出,立即返回 0。父进程可以继续执行自己的代码,过一会儿再轮询检查。
缺点是父进程需要反复检查,可能浪费一些 CPU。通常会在轮询之间加 sleep 降低频率。
结语:
今天的内容到这里就结束了,希望你能有所收获~
干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _