【Linux 系统篇(二十一)】进程(九):进程等待 wait/waitpid、status状态参数


大家好,欢迎来到 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 进程等待的核心目的

  父进程进行进程等待,主要达成两件事:

  1. 回收子进程资源:这是硬性需求,不做就会产生僵尸进程。
  2. 获取子进程退出信息:这是可选需求,但很多场景下非常重要。

一句话:

  进程等待 = 父进程回收子进程 + 获取子进程退出状态。


二、进程等待的两个核心系统调用

  要使用进程等待,需要包含以下头文件:

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;
}

  这个例子非常适合观察僵尸进程:

  1. 子进程先运行 5 秒,然后退出。
  2. 父进程还在 sleep(7),没有调用 wait
  3. 这 2 秒内,子进程就是僵尸状态。
  4. 父进程 sleep 结束后调用 wait,立刻回收子进程。
  5. 僵尸进程消失。

  你可以打开另一个终端,用 ps 命令观察:

bash 复制代码
ps aux | grep 程序名

  你会看到子进程状态先是 S+(睡眠),然后变成 Z+(僵尸),最后被回收后消失。


四、waitpid 函数详解

4.1 函数原型

c 复制代码
pid_t waitpid(pid_t pid, int *status, int options);

  waitpidwait 更强大,因为它可以指定等待哪个子进程,还可以控制阻塞方式。

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 父进程能直接拿到子进程退出信息吗?

  不能

  进程之间是相互独立的。子进程退出时,它的退出信息不会直接飞到父进程手里。谁掌握这些信息?操作系统内核。

  所以父进程要获取子进程退出信息,必须通过系统调用找操作系统要。waitwaitpid 就是干这个的。

6.2 task_struct 中的退出信息

  在 Linux 内核中,每个进程都有一个 task_struct。子进程即使退出了,它的 task_struct 也不会立刻销毁。内核会在其中保存退出信息,类似:

c 复制代码
long exit_state;
int exit_code, exit_signal;

  当子进程退出时,操作系统会把退出码和终止信号写入它自己的 task_struct

  父进程调用 waitpid(&status) 时:

  1. 通过系统调用进入内核。
  2. 内核读取子进程 task_struct 中的 exit_codeexit_signal
  3. 内核按位图规则打包,写入父进程传入的 status 变量。
  4. 提取完成后,内核才真正销毁子进程的 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,本质上是:

  1. 进入内核。
  2. 找到已经退出的子进程。
  3. 读取它的 task_struct 中的退出信息。
  4. 把这些信息打包成 status 返回给父进程。
  5. 销毁子进程的 task_struct,释放 PID 等资源。

  所以,父进程回收的"资源",主要就是子进程残留的内核数据结构。

7.2 为什么会有僵尸进程?

  因为父进程一直在运行,却没有调用 wait / waitpid 回收已经退出的子进程。

  子进程虽然退出了,但它的 task_struct 还在,等待父进程读取退出信息。只要父进程不读,它就一直留着,变成僵尸。

7.3 阻塞等待时,父进程在干什么?

  当父进程调用 wait 且子进程还没退出时,父进程会被挂起,进入睡眠状态,不再占用 CPU。等子进程退出后,内核唤醒父进程,父进程继续执行。

  所以阻塞等待并不浪费 CPU。

7.4 非阻塞等待时,父进程在干什么?

  非阻塞等待使用 WNOHANG。父进程调用 waitpid 后,如果子进程没退出,立即返回 0。父进程可以继续执行自己的代码,过一会儿再轮询检查。

  缺点是父进程需要反复检查,可能浪费一些 CPU。通常会在轮询之间加 sleep 降低频率。


结语:

  今天的内容到这里就结束了,希望你能有所收获~

干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _

相关推荐
螺蛳粉 螺蛳粉1 小时前
mysql的备份与恢复
linux·运维·数据库·mysql
ao-weilai1 小时前
Linux网络编程:Socket UDP
linux·服务器·网络·c++
可乐鸡翅yeah_2 小时前
混合内容 Mixed‑Content 安全策略,HLS HTTPS 页面加载 HTTP 资源排错实战
运维·ffmpeg·音视频·媒体·m3u8
爱吃香菜的初学者2 小时前
九.Linux——文件操作
linux
园长的牧歌2 小时前
Ubuntu 打开应用在导航栏没有图标是个齿轮
linux·ubuntu
曦夜日长2 小时前
Linux系统篇,进程概念(四):内核链表的深度理解、进程优先级的底层理解
linux·运维·服务器·php
‎ദ്ദിᵔ.˛.ᵔ₎2 小时前
Linux 库制作与原理
linux
Wang's Blog2 小时前
Java 接入Redis: Redis下载与源码编译安装
java·服务器·redis
liulilittle3 小时前
Linux AI 开发环境搭建实录
linux·ai·llm·agent·hyper-v·dev·opencode