一、进程等待的必要性与核心概念

1.1 为什么需要进程等待?

之前讲过,子进程退出,父进程如果不管不顾,就可能造成"僵尸进程"的问题,进而造成内存泄漏。

另外,进程一旦变成僵尸状态,那就刀枪不入,kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。

最后,父进程派给子进程的任务完成得如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出(看父进程是否关心)。

1.2 进程等待的解决方案

基于上述必要性,系统提供了特定的系统调用来供父进程回收子进程,获取子进程退出信息。其中最核心的两个函数是 wait 和 waitpid。

在具体执行时,父进程通过进程等待可以达成两个主要目的:

  • 回收子进程资源(最关键的硬性需求);
  • 获取子进程退出信息(可选的控制需求)。

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

2.1 进程等待函数

要使用进程等待功能,必须引入以下两个系统调用头文件:

#include <sys/types.h>
#include <sys/wait.h>

系统提供了两个主要的等待接口:

pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);

2.2 wait 函数详解

wait 函数是较简易的等待接口,它的参数和行为非常直接:

  • 参数:int *status 是一个输出型参数,用于获取子进程的退出状态(不关心则可传入 NULL)。注意,NULL 本身是一个宏,它的值是 0,传进去就等于告诉内核"我不关心退出状态"。

在这里插入图片描述

  • 作用:等待任意一个退出的子进程。只要有任意一个子进程退出,wait 就会立刻回收它并返回;
  • 返回值:
    1. 回收成功:返回目标僵尸进程的 pid;
    2. 回收失败:返回 -1。

如果父进程在调用 wait 接口时,其关注的子进程尚未退出,父进程默认会阻塞在调用处(其行为类似于 scanf 的等待输入),直到子进程退出才继续向下执行。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int Func01()
{
    pid_t id = fork();

    if(id == 0)
    {
        // 子进程
        int cnt = 5;
        while(cnt)
        {
            pid_t _pid = getpid();
            printf("我是一个子进程 我的PID是:%d\n", _pid);
            sleep(1);
            cnt--;
        }
        exit(0);
    }

    // 父进程
    sleep(7);

    pid_t rid = wait(NULL);

    if(rid > 0)
    {
        printf("%d\n", rid);
    }

    sleep(7);

    return 0;
}

int main()
{
    Func01();
    return 0;
}
while true; do ps -axj | head -n 1 && ps -axj | grep process | grep -v grep; sleep 1; echo "---------------------------------------"; done

在这里插入图片描述

2.3 waitpid 函数详解

相比 wait,waitpid 提供了更加精准和灵活的控制。

pid_t waitpid(pid_t pid, int *status, int options);

1. 返回值

waitpid 的返回值情况:

  • 大于 0:等待结束,返回子进程 pid;
  • 等于 0:调用结束,但是子进程没有退出;
  • 小于 0:等待失败。

2. pid

参数 pid 用于指定父进程想要等待的目标子进程,其取值具有不同的控制情况:

  • 补充说明:

pgid 就是一个父进程 fork 出来的所有子孙进程。

  • < -1:等待其进程组 ID 等于 pgid 的任意子进程;
  • -1:等待任意子进程,此时的功能与 wait 类似;
  • 0:等待与调用进程属于同一个进程组的任意子进程;
  • > 0:等待特定的那个子进程。

3. options

options 参数用来控制等待的方式(阻塞或非阻塞),常见的核心选项为:

  • 0:默认行为。如果子进程未退出,父进程将在调用处保持阻塞等待;
  • WNOHANG:若指定的子进程没有结束,则 waitpid() 函数不会阻塞,而是立即返回 0,允许父进程去执行其他任务(非阻塞轮询)。

2.4 阻塞等待与非阻塞等待

在这里插入图片描述

张三想要约李四下来喝酒。李四说他需要上楼拿点东西准备一下。

  • 非阻塞等待(轮询检测):

张三在楼下等待。他等了一会儿,打电话给李四问:"你好了吗?"李四回复:"快了快了。"然后挂断了电话。张三又等了半天,期间拿出手机刷了一会儿短视频,接着再次打电话过去问:"好了吗?"李四回答:"马上好。"然后又挂断了电话。张三如此反复,一共给李四打了五通电话。最终,李四终于下楼了。

这就是非阻塞等待。张三在等待期间可以做自己的事情(比如刷视频),每隔一段时间主动打电话确认状态,这种重复检测的过程就是非阻塞轮询。

  • 阻塞等待:

今天张三又来约李四喝酒。这一次,张三给李四打电话时说道:“在你想好、收拾好并走下来之前,千万不要挂断电话,我就一直在等着你。”

这就是阻塞等待。张三挂起当前的其他活动,不执行任何其他操作,电话一直保持接通状态,直到满足条件(李四下楼)为止。

2.5 非阻塞等待的应用

那么是不是得给父进程找点事情干干?怎么干?我们用 C 的函数指针,我们来设计一个。

typedef void (*task)();

void DownloadTask()
{
    printf("【父进程并行任务】正在下载网络数据...\n");
}

void LogTask()
{
    printf("【父进程并行任务】正在向日志文件写入状态...\n");
}

void CheckTask()
{
    printf("【父进程并行任务】正在检测系统内存占用...\n");
}

void Func02()
{
    pid_t id = fork();

    // 导入任务
    task tasks[] = {DownloadTask, LogTask, CheckTask};
    int task_cnt = sizeof(tasks) / sizeof(tasks[0]);

    if(id == 0)
    {
        // 子进程
        int cnt = 5;
        while(cnt)
        {
            pid_t _pid = getpid();
            printf("我是一个子进程 我的PID是:%d\n", _pid);
            sleep(1);
            cnt--;
        }
        exit(0);
    }
    else if(id > 0)
    {
        // 父进程
        // 一直循环直到获取到退出情况
        while(1)
        {
            // 获取退出情况后面讲
            int status = 0;
            // 使用 WNOHANG 参数进行非阻塞等待
            // id: 要等待的子进程PID
            // &status: 获取子进程退出状态的输出型参数
            // WNOHANG: 若子进程未结束,函数不阻塞,立即返回0
            pid_t w_id = waitpid(id, &status, WNOHANG);

            if(w_id == id)
            {
                printf("等待成功pid:%d\n", id);
                // 等待成功了你也退出吧
                exit(0);
            }
            else if(w_id == 0)
            {
                // 上面进程还在干自己的事,你可以去做一些别的事情
                int i = 0;
                for(i; i < task_cnt; i++)
                {
                    tasks[i]();
                    sleep(1);
                }
            }
            else
            {
                printf("等待失败\n");
            }
        }
    }
}

在这里插入图片描述

三、深入理解 status 状态参数

3.0 进程的退出信息到底是怎么被父进程获取的

先问你一个问题:父进程能不能直接获取子进程的退出信息?答案是不可以,因为进程之间相互独立。谁可以获取进程的退出信息?操作系统。所以父进程要获取退出信息找谁要?找操作系统要。怎么要?waitpid / wait 系统调用。

在 Linux 内核中,子进程即使退出了,代码和数据可以释放,但是 task_struct 不能释放,task_struct 依然被保留在操作系统的进程表里。在子进程的 task_struct 内部,维护着类似以下的字段:

long exit_state;
int exit_code, exit_signal;
  1. 当子进程退出时,操作系统会将其退出的错误码和信号写入到它自身的 exit_code 和 exit_signal 中;
  2. 父进程调用 waitpid(&status) 时,会通过系统调用陷入内核;
  3. 操作系统切换到父进程的上下文,读取子进程 task_struct 中的 exit_code 和 exit_signal;
  4. 操作系统将这两个值按照位图规则打包,写入父进程传入的 status 变量的内存空间中;
  5. 提取完成后,操作系统才真正地将子进程的 task_struct 从内存中清理、销毁。

这也就完美解释了"为什么要存在僵尸进程"——为了等待父进程来读取这些保存在内核结构里的退出状态。

在这里插入图片描述

3.1 status 的位图结构

wait 和 waitpid 的 status 参数是一个整型指针。它不能简单地当作普通的整数来看待,在系统内核中,它被当作一个位图结构来处理。

在这里插入图片描述

1. 正常终止(Normal Termination)

当代码运行完毕,进程正常退出时(例如 main 函数返回或调用 exit()),status 的低 16 位结构如下:

在这里插入图片描述

  • 次低 8 位(第 8 到 15 比特位):保存子进程的退出码。8 个比特位正好对应 256 个 Shell 进程退出码;
  • 低 7 位(第 0 到 6 比特位):其值全部为 0;
  • 第 7 比特位:core dump 标志位(默认为 0)。

core dump 标志位是什么?不讲,得等待信号章节才能讲。

3.2 status 代码演示

1. 正常退出

任务函数 DownloadTask / LogTask / CheckTask 的定义见 2.5 节。

void Func02()
{
    pid_t id = fork();

    // 导入任务
    task tasks[] = {DownloadTask, LogTask, CheckTask};
    int task_cnt = sizeof(tasks) / sizeof(tasks[0]);

    if(id == 0)
    {
        // 子进程
        int cnt = 5;
        while(cnt)
        {
            pid_t _pid = getpid();
            printf("我是一个子进程 我的PID是:%d\n", _pid);
            sleep(1);
            cnt--;
        }
        exit(1);
    }
    else if(id > 0)
    {
        // 父进程
        // 一直循环直到获取到退出情况
        while(1)
        {
            // 获取退出情况后面讲
            int status = 0;
            // 使用 WNOHANG 参数进行非阻塞等待
            // id: 要等待的子进程PID
            // &status: 获取子进程退出状态的输出型参数
            // WNOHANG: 若子进程未结束,函数不阻塞,立即返回0
            pid_t w_id = waitpid(id, &status, WNOHANG);

            if(w_id == id)
            {
                printf("等待成功pid:%d\n", id);
                // 等待成功了你也退出吧
                // 退出前打印一下 status 的值
                printf("status的值:%d\n", status);
                exit(0);
            }
            else if(w_id == 0)
            {
                // 上面进程还在干自己的事,你可以去做一些别的事情
                int i = 0;
                for(i; i < task_cnt; i++)
                {
                    tasks[i]();
                    sleep(1);
                }
            }
            else
            {
                printf("等待失败\n");
            }
        }
    }
}

在这里插入图片描述

打印结果是什么?256。哎?怎么是 256 呢?再仔细想一想,第八位是 1,0~7 位是 0,是不是确实是 256?是的。

2. 被信号所杀

当进程由于遭遇异常(如除 0 错误、野指针访问)而被系统生成的信号强制终止时,退出码便失去了意义:

  • 低 7 位(第 0 到 6 比特位):保存导致子进程终止的终止信号;
  • 次低 8 位:未启用(无意义);
  • 第 7 比特位:core dump 标志位。

无异常检测的标准(如果进程没有发生异常):

  • 低 7 个比特位必定为 0;
  • 一旦发现低 7 个比特位不为 0,则说明进程是异常退出的,此时提取出的退出码将毫无意义。

3.3 通过 status 获取退出信息与信号

1. 传统方法:位运算提取

我们可以直接通过位操作从 status 中截取对应的比特位。

void Func02()
{
    pid_t id = fork();

    // 导入任务
    task tasks[] = {DownloadTask, LogTask, CheckTask};
    int task_cnt = sizeof(tasks) / sizeof(tasks[0]);

    if(id == 0)
    {
        // 子进程
        int cnt = 5;
        while(cnt)
        {
            pid_t _pid = getpid();
            printf("我是一个子进程 我的PID是:%d\n", _pid);
            sleep(1);
            cnt--;
        }
        exit(1);
    }
    else if(id > 0)
    {
        // 父进程
        // 一直循环直到获取到退出情况
        while(1)
        {
            // 获取退出情况后面讲
            int status = 0;
            // 使用 WNOHANG 参数进行非阻塞等待
            // id: 要等待的子进程PID
            // &status: 获取子进程退出状态的输出型参数
            // WNOHANG: 若子进程未结束,函数不阻塞,立即返回0
            pid_t w_id = waitpid(id, &status, WNOHANG);

            if(w_id == id)
            {
                printf("等待成功pid:%d\n", id);
                // 等待成功了你也退出吧
                // 退出前打印一下 status 的值
                printf("status的值:%d\n", status);
                // 因为初始的 status 没有收到信号之前都是 0
                // (status >> 8) & 0xFF: 右移 8 位并按位与 0xFF,提取次低 8 位的退出码
                // status & 0x7F: 按位与 0x7F,提取最低 7 位的终止信号
                // (status >> 7) & 0x1: 右移 7 位并按位与 0x1,提取第 7 位看是否 core dump
                printf("是否收到异常信号:%d\n", ((status >> 7) & 1));
                printf("信号是:%d\n", status & 0x7F);
                printf("退出码:%d\n", (status >> 8) & 0xFF);
                exit(0);
            }
            else if(w_id == 0)
            {
                // 上面进程还在干自己的事,你可以去做一些别的事情
                int i = 0;
                // 验证初始的 status 没有收到信号之前都是 0
                // printf("status的值:%d\n", status);
                for(i; i < task_cnt; i++)
                {
                    tasks[i]();
                    sleep(1);
                }
            }
            else
            {
                printf("等待失败\n");
            }
        }
    }
}

在这里插入图片描述

在这里插入图片描述

2. 系统标准方法:宏函数提取

相比手动进行位运算,Linux 系统提供了标准宏函数,能更安全、直观地解析 status:

  • WIFEXITED(status):若子进程正常终止,返回真(True);
  • WEXITSTATUS(status):在 WIFEXITED 为真的前提下,用于提取子进程的退出码;
  • WIFSIGNALED(status):若子进程因信号异常终止,返回真(True);
  • WTERMSIG(status):在 WIFSIGNALED 为真的前提下,用于提取终止信号。
void Func02()
{
    pid_t id = fork();

    // 导入任务
    task tasks[] = {DownloadTask, LogTask, CheckTask};
    int task_cnt = sizeof(tasks) / sizeof(tasks[0]);

    if(id == 0)
    {
        // 子进程
        int cnt = 5;
        while(cnt)
        {
            pid_t _pid = getpid();
            printf("我是一个子进程 我的PID是:%d\n", _pid);
            sleep(1);
            cnt--;
        }
        exit(1);
    }
    else if(id > 0)
    {
        // 父进程
        // 一直循环直到获取到退出情况
        while(1)
        {
            // 获取退出情况后面讲
            int status = 0;
            // 使用 WNOHANG 参数进行非阻塞等待
            // id: 要等待的子进程PID
            // &status: 获取子进程退出状态的输出型参数
            // WNOHANG: 若子进程未结束,函数不阻塞,立即返回0
            pid_t w_id = waitpid(id, &status, WNOHANG);

            if(w_id == id)
            {
                printf("等待成功pid:%d\n", id);
                // 等待成功了你也退出吧
                // 退出前打印一下 status 的值
                printf("status的值:%d\n", status);
                // 因为初始的 status 没有收到信号之前都是 0
                // (status >> 8) & 0xFF: 右移 8 位并按位与 0xFF,提取次低 8 位的退出码
                // status & 0x7F: 按位与 0x7F,提取最低 7 位的终止信号
                // (status >> 7) & 0x1: 右移 7 位并按位与 0x1,提取第 7 位看是否 core dump
                // printf("是否收到异常信号:%d\n", ((status >> 7) & 1));
                // printf("信号是:%d\n", status & 0x7F);
                // printf("退出码:%d\n", (status >> 8) & 0xFF);
                printf("是否收到异常信号:%d\n", WIFSIGNALED(status));
                printf("信号是:%d\n", WTERMSIG(status));
                printf("退出码:%d\n", WEXITSTATUS(status));
                printf("是否正常终止:%d\n", WIFEXITED(status));
                exit(0);
            }
            else if(w_id == 0)
            {
                // 上面进程还在干自己的事,你可以去做一些别的事情
                int i = 0;
                // 验证初始的 status 没有收到信号之前都是 0
                // printf("status的值:%d\n", status);
                for(i; i < task_cnt; i++)
                {
                    tasks[i]();
                    sleep(1);
                }
            }
            else
            {
                printf("等待失败\n");
            }
        }
    }
}

在这里插入图片描述

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐