
◆ 博主名称: 小此方-CSDN博客 大家好,欢迎来到小此方的博客。
⭐️Linux系列个人专栏: 【主题曲】Linux
⭐️此方的GitHub: github_此方
⭐️ Re系列专栏:我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)
文章目录
- 概要&序論
- [一、 进程等待的必要性与核心概念](#一、 进程等待的必要性与核心概念)
-
- [1.1 为什么需要进程等待?](#1.1 为什么需要进程等待?)
- [1.2 进程等待的解决方案](#1.2 进程等待的解决方案)
- [二、 进程等待的核心系统调用](#二、 进程等待的核心系统调用)
-
- [2.1 函数原型与基础引入](#2.1 函数原型与基础引入)
- [2.2 wait 函数详解](#2.2 wait 函数详解)
- [2.3 waitpid 函数详解](#2.3 waitpid 函数详解)
-
- [2.3.1 参数 pid 的取值与含义](#2.3.1 参数 pid 的取值与含义)
- [2.3.2 参数 options 的控制](#2.3.2 参数 options 的控制)
- 2.4详细讲解options参数
-
- 2.4.1怎么记忆这个选项------题外话
- [2.4.2 阻塞等待与非阻塞等待](#2.4.2 阻塞等待与非阻塞等待)
- 2.4.3非阻塞等待的代码
- [三、 深入理解 status 状态参数](#三、 深入理解 status 状态参数)
-
- 3.0进程的退出信息到底是怎么被父进程获取的
- [3.1 status 的位图结构](#3.1 status 的位图结构)
-
- [3.1.1 正常终止(Normal Termination)](#3.1.1 正常终止(Normal Termination))
- [3.1.2 被信号所杀(Signaled Termination)](#3.1.2 被信号所杀(Signaled Termination))
- [3.2 通过 status 获取退出信息与信号](#3.2 通过 status 获取退出信息与信号)
-
- [3.2.1 传统方法:位运算提取](#3.2.1 传统方法:位运算提取)
- [3.2.2 系统标准方法:宏函数提取](#3.2.2 系统标准方法:宏函数提取)
概要&序論
Hello大家好,我是此方。本文深刻探讨 Linux 进程等待机制。
- 阐述解决僵尸进程与回收资源的必要性;
- 详解
wait与waitpid系统调用的参数及阻塞行为;- 解构
status状态参数的 16 位位图布局与宏函数解析;- 揭示内核
task_struct的交互原理;- 对比阻塞与非阻塞轮询结合
std::function的应用。好的,我们直接开始。
一、 进程等待的必要性与核心概念
1.1 为什么需要进程等待?
- 之前讲过,子进程退出,父进程如果不管不顾,就可能造成"僵尸进程"的问题,进而造成内存泄漏。
- 另外,进程一旦变成僵尸状态,那就刀枪不入,"杀人不眨眼"的
kill -9也无能为力,因为谁也没有办法杀死一个已经死去的进程。 - 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
1.2 进程等待的解决方案
基于上述必要性,系统提供了特定的系统调用来 供父进程回收子进程,获取子进程退出信息。 其中最核心的两个函数是 wait 和 waitpid。
在具体执行时,父进程通过进程等待可以达成两个主要目的:
- 回收子进程资源(最关键的硬性需求)
- 获取子进程退出信息(可选的控制需求)
二、 进程等待的核心系统调用
2.1 函数原型与基础引入
要使用进程等待功能,必须引入以下两个系统调用头文件:
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.2 wait 函数详解
wait 函数是较简易的等待接口,它的参数和行为非常直接:
- 参数 :
int *status是一个输出型参数,用于获取子进程的退出状态(不关心则可传入NULL)。

- 作用 :等待任意一个 退出的子进程。只要有任意一个子进程退出,
wait就会立刻回收它并返回。 - 返回值 :
- 回收成功:返回目标僵尸进程的pid。
- 回收失败:返回-1。
如果父进程在调用wait接口 时,其关注的子进程尚未退出,父进程默认会 阻塞 在调用处(其行为类似于 scanf 的等待输入),直到子进程退出才继续向下执行。
cpp
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int Func01()
{
// 创建子进程
// fork() 给子进程返回 0,给父进程返回子进程的 PID
pid_t id = fork();
if(id == 0)
{
// === 子进程执行分支 ===
int cnt = 5;
while(cnt)
{
pid_t _pid = getpid();
cout << "我是一个子进程" << "我的PID是:" << _pid << endl;
sleep(1); // 每隔 1 秒打印一次
cnt--;
}
// 子进程运行 5 秒后退出,此时父进程还在 sleep(7),子进程将进入僵尸状态(Zombie)
exit(0);
}
// === 父进程执行分支 ===
// 关键点 1:父进程先休眠 7 秒。
// 此时子进程在前 5 秒正常运行,后 2 秒由于父进程未回收它,子进程处于僵尸状态。
sleep(7);
// 关键点 2:父进程调用 wait 阻塞式回收任意子进程。
// 因为此时子进程已经退出,wait 会立刻成功回收,消除僵尸进程,并返回被回收子进程的 PID。
// 传入 NULL 表示父进程不关心子进程的退出状态(退出码/信号)。
pid_t rid = wait(NULL);
if(rid > 0)
{
// 回收成功,打印被回收的子进程 PID
cout << rid << endl;
}
// 关键点 3:子进程被成功回收后,父进程再次休眠 7 秒。
// 此时通过 ps 命令观察,可以发现原本处于僵尸状态(Z)的子进程已经被彻底清除。
sleep(7);
return 0;
}
int main()
{
Func01();
//Func02();
//Func03();
return 0;
}
2.3 waitpid 函数详解
相比 wait,waitpid 提供了更加精准和灵活的控制。
c
pid_t waitpid(pid_t pid, int *status, int options);
2.3.1 参数 pid 的取值与含义
参数 pid 用于指定父进程想要等待的目标子进程, 其取值具有不同的控制粒度:
< -1:等待其进程组ID等于pid绝对值的任意子进程。-1:等待任意 子进程,此时的功能与wait类似。0:等待与调用进程属于同一个进程组的任意子进程。- ****
> 0:等待特定子进程。
2.3.2 参数 options 的控制
options 参数用来控制等待的方式(阻塞或非阻塞),常见的核心选项为:
0:默认行为。如果子进程未退出,父进程将在调用处保持阻塞等待。WNOHANG:若指定的子进程没有结束,则waitpid()函数不会阻塞,而是立即返回 0,允许父进程去执行其他任务(非阻塞轮询)。
2.4详细讲解options参数
2.4.1怎么记忆这个选项------题外话
你怎么记这个选项:"WNOHANG " 你仔细去读它的音,H-ANG-夯,夯住了,我们说电脑卡住了(阻塞),就是说它夯住了(江浙一带/或者北京的方言中有这么说的,此方也是浙江人),W是等待wait,N是no,这个选项直接翻译过来就是"等待的时候不要夯住 "。
那么有人想要问:什么是阻塞?什么是非阻塞?
2.4.2 阻塞等待与非阻塞等待

张三想要约李四下来喝酒。李四说他需要上楼拿点东西准备一下。
-
非阻塞等待(轮询检测):
张三在楼下等待。他等了一会儿,打电话给李四问:"你好了吗?"李四回复:"快了快了。"然后挂断了电话。张三又等了半天,期间拿出手机刷了一会儿短视频,接着再次打电话过去问:"好了吗?"李四回答:"马上好。"然后又挂断了电话。张三如此反复,一共给李四打了五通电话。最终,李四终于下楼了。
这就是非阻塞等待。 张三在等待期间可以做自己的事情(比如刷视频),每隔一段时间主动打电话确认状态,这种重复检测的过程就是非阻塞轮询。
-
阻塞等待:
今天张三又来约李四喝酒。这一次,张三给李四打电话时说道:"在你想好、收拾好并走下来之前,千万不要挂断电话,我就一直在线上等着你。"
这就是阻塞等待。 张三挂起当前的其他活动,不执行任何其他操作,电话一直保持接通状态,直到满足条件(李四下楼)为止。

2.4.3非阻塞等待的代码
补充一下:非阻塞轮询的时候,这个waitpid的返回值情况:
- 大于0:等待结束,子进程pid。
- 等于0:调用结束,但是子没有退出。
- 小于0:等待失败。
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); // 每隔1秒打印一次
cnt--;
}
// 子进程运行5秒后退出,退出码设为 0
exit(0);
}
// ================= 父进程执行流 =================
// 父进程先睡眠1秒,拉开与子进程执行的步调
sleep(1);
// 开始非阻塞轮询等待(像张三反复打电话给李四一样)
while(1){
int statue = 0;
// 使用 WNOHANG 参数进行非阻塞等待
// id: 要等待的子进程PID
// &statue: 获取子进程退出状态的输出型参数
// WNOHANG: 若子进程未结束,函数不阻塞,立即返回0
pid_t rid = waitpid(id, &statue, WNOHANG);
if(rid > 0)
{
// 情况1:返回值大于0,说明等待成功,子进程已经退出
// WEXITSTATUS(statue) 用于提取子进程的退出码(即exit里的值)
cout << "等待成功,子进程的退出码为" << WEXITSTATUS(statue) << endl;
break; // 成功回收子进程,退出轮询
}
else if(rid == 0){
// 情况2:返回值为0,说明子进程还在运行,本次检测未捕获到其退出
// 此时父进程不会被挂起,可以继续执行后续代码(这里选择打印并休眠后再次轮询)
cout << "执行第2次等待" << "子进程未退出" << endl;
sleep(1); // 等待1秒后,进入下一次轮询检测
}
else {
// 情况3:返回值小于0,说明等待出错(例如传入了不存在的进程PID)
cout << "等待失败" << endl;
break;
}
}
return 0;
}
int main(){
Func();
return 0;
}

那么。是不是得给父进程找点事情干干?怎么干?我们有两种方法:C++11的function<>或者是C的函数指针。我们设计一个.
cpp
#include <iostream>
#include <vector>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdlib>
using namespace std;
// 定义任务类型
using task_t = function<void()>;
// 模拟父进程在轮询期间需要处理的各种轻量级任务
void DownloadTask() { cout << "【父进程并行任务】正在下载网络数据..." << endl; }
void LogTask() { cout << "【父进程并行任务】正在向日志文件写入状态..." << endl; }
void CheckTask() { cout << "【父进程并行任务】正在检测系统内存占用..." << endl; }
int Func() {
// 1. 初始化父进程的任务列表
vector<task_t> tasks;
tasks.push_back(DownloadTask);
tasks.push_back(LogTask);
tasks.push_back(CheckTask);
pid_t id = fork();
if (id == 0) {
// ================= 子进程执行流 =================
int cnt = 5;
while (cnt) {
cout << "我是子进程,PID: " << getpid() << ", 正在运行..." << endl;
sleep(1);
cnt--;
}
exit(0);
}
// ================= 父进程执行流 =================
while (1) {
int status = 0;
// 使用 WNOHANG 进行非阻塞轮询
pid_t rid = waitpid(id, &status, WNOHANG);
if (rid > 0) {
// 情况1:子进程退出,成功回收
if (WIFEXITED(status)) {
cout << "等待成功,子进程退出码: " << WEXITSTATUS(status) << endl;
}
break;
}
else if (rid == 0) {
// 情况2:子进程还未退出,父进程利用这个空档期执行自己的任务
cout << "---------------------------------------------" << endl;
cout << "子进程暂未退出,父进程开始处理轮询任务..." << endl;
// 遍历并执行任务列表中的轻量级任务
for (const auto& task : tasks) {
task();
}
cout << "---------------------------------------------" << endl;
sleep(1); // 减轻轮询频率,每隔1秒检测一次
}
else {
// 情况3:等待出错
perror("waitpid error");
break;
}
}
return 0;
}
int main() {
Func();
return 0;
}
三、 深入理解 status 状态参数
3.0进程的退出信息到底是怎么被父进程获取的
先问你一个问题:父进程能不能直接获取子进程的退出信息?答案是不可以。因为进程之间相互独立 。谁可以获取进程的退出信息?操作系统 ,所以父进程要获取退出信息找谁要!?找操作系统要。怎么要?waitpid/wait系统调用。
在 Linux 内核中,子进程即使退出了,其 task_struct 依然被保留在操作系统的进程表里。在子进程的 task_struct 内部,维护着类似以下的字段:
c
long exit_state;
int exit_code, exit_signal;
- 当子进程退出时,操作系统会将其退出的错误码和信号写入到它自身的
exit_code和exit_signal中。 - 父进程调用
waitpid(&status)时,会通过系统调用陷入内核。 - 操作系统切换到父进程的上下文,读取子进程
task_struct中的exit_code和exit_signal。 - 操作系统将这两个值按照位图规则打包,写入父进程传入的
status变量的内存空间中。 - 提取完成后,操作系统才真正地将子进程的
task_struct从内存中清理、销毁。
这也就完美解释了"为什么要存在僵尸进程"------为了等待父进程来读取这些保存在内核结构里的退出状态。

getpid()这些接口也是差不多原理。
3.1 status 的位图结构
wait 和 waitpid 的 status 参数是一个整型指针。它不能简单地当作普通的整数来看待,在系统内核中,它被当作一个位图结构来处理。

3.1.1 正常终止(Normal Termination)
当代码运行完毕,进程正常退出时(例如 main 函数返回或调用 exit()),status 的低 16 位结构如下:
- 次低 8 位(第 8 到 15 比特位) :保存子进程的退出码。
- 低 7 位(第 0 到 6 比特位) :其值全部为
0。 - 第 7 比特位 :
core dump标志位(默认为0)。

core dump标志位是什么?不讲。得等待信号章节才能讲。
正常退出的status代码演示
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t id = fork();
if (id < 0) {
perror("fork");
return 1;
}
else if (id == 0) {
int cnt = 3;
while (cnt) {
printf("我是一个子进程,pid : %d, ppid : %d\n", getpid(), getppid());
sleep(1);
cnt--;
}
exit(1); // 退出码为 1
}
else {
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0) {
printf("wait success, rid: %d, status: %d\n", rid, status);
}
}
return 0;
}
打印结果是什么?256。哎?怎么是256呢?再仔细想一想,第八位是1,0~7位是0,是不是确实是256?是的。

3.1.2 被信号所杀(Signaled Termination)
当进程由于遭遇异常(如除 0 错误、野指针访问)而被系统生成的信号强制终止时,退出码便失去了意义:
- 低 7 位(第 0 到 6 比特位) :保存导致子进程终止的终止信号。
- 次低 8 位:未启用(无意义)。
- 第 7 比特位 :
core dump标志位。
无异常检测的标准(如果进程没有发生异常)
- 低 7 个比特位必定为
0。- 一旦发现低 7 个比特位不为
0,则说明进程是异常退出的,此时提取出的退出码将毫无意义。
3.2 通过 status 获取退出信息与信号
3.2.1 传统方法:位运算提取
我们可以直接通过位操作从 status 中截取对应的比特位。
cpp
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <cstdlib>
using namespace std;
int Func02()
{
pid_t id = fork();
if (id == 0)
{
int cnt = 5;
while (cnt)
{
cout << "我是一个子进程,我的PID是: " << getpid() << endl;
sleep(1);
cnt--;
}
exit(105); // 示例:以退出码 105 退出
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
// (status >> 8) & 0xFF: 右移 8 位并按位与 0xFF,提取次低 8 位的退出码
// status & 0x7F: 按位与 0x7F,提取最低 7 位的终止信号
printf("wait success, rid: %d, exit code: %d, exit signal: %d\n",
rid, (status >> 8) & 0xFF, status & 0x7F);
}
else
{
perror("waitpid failed");
}
return 0;
}
3.2.2 系统标准方法:宏函数提取
相比手动进行位运算,Linux 系统提供了标准宏函数,能更安全、直观地解析 status:
WIFEXITED(status):若子进程正常终止,返回真(True)。WEXITSTATUS(status):在WIFEXITED为真的前提下,用于提取子进程的退出码。WIFSIGNALED(status):若子进程因信号异常终止,返回真(True)。WTERMSIG(status):在WIFSIGNALED为真的前提下,用于提取终止信号。
cpp
int Func02_Macro()
{
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;
}
好的本期内容就到这里,如果对你有帮助,还不要忘记点赞三联支持。我是此方,我们下期再见。bye!