Linux 进程等待

Linux 进程等待

七月 16, 2025 次阅读

进程等待必要性

  • 子进程退出后,父进程如果不管不顾,就可能造成“僵尸进程”的问题,进而造成内存泄漏。
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的 kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程(当然,你可以将父进程给杀掉,这样子进程就
    会被 init 进程托管然后定时通过 waitpid 函数回收了,不过一般不会这样用)
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。

进程等待方法

wait

函数格式如下:

pid_t wait(int * status);

返回值:成功返回被等待进程的 pid,失败返回 -1

参数:输出型参数,用户与获取子进程的退出状态,若不关心则将其设置为 nullptr

下面重点介绍一下 waitpid 方法,因为 waitpid 方法是完全包括了 wait 的功能的

waitpid

函数原型

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

返回值

  • 成功时:返回收集到的子进程的进程 ID。
  • 设置了 WNOHANG 选项时
    若调用中 waitpid 发现没有已退出的子进程可收集,则返回 0
  • 出错时:返回 -1,并通过 errno 指示具体错误。

参数说明

pid
  • pid = -1:等待任意一个子进程(与 wait 等效)。
  • pid > 0:等待进程 ID 与 pid 相等的子进程。
status(输出型参数)
  • WIFEXITED(status)
    若为正常终止的子进程状态,返回真(用于检查进程是否正常退出)。
  • WEXITSTATUS(status)
    WIFEXITED 非零,提取子进程的退出码。
options
  • 默认值 0:表示阻塞等待。
  • WNOHANG
    若指定的子进程未结束,则 waitpid() 立即返回 0(非阻塞);若子进程正常结束,则返回其进程 ID。

使用方法

接下来我们来试用一下 waitpid 函数:

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    // 子进程
    if(id == 0){
        // 模拟子进程处理任务
        std::cout << "子进程 " << getpid() << " 执行任务中" << std::endl;
        sleep(5);
        std::cout << "子进程 " << getpid() << " 退出" << std::endl;
        exit(0);
    }
    // 父进程
    int ret = waitpid(-1, nullptr, 0);
    if(ret == -1){
        perror("waitpid");
        return 2;
    }
    std::cout << "父进程已回收子进程, pid: " << id << std::endl;
    return 0;
}

运行结果如下:

╭─ljx@VM-16-15-debian ~/linux_review/lesson2
╰─➤  ./pro_wait.o
子进程 2684497 执行任务中
子进程 2684497 退出
父进程已回收子进程, pid: 2684497

等待获取子进程状态 status

通过 wait 函数和 waitpid 函数的 status 这个输出型参数即可获得子进程退出时的状态码

wait 和 waitpid 的 status 参数说明

waitwaitpid 函数都有一个 status 参数,该参数是输出型参数,由操作系统填充:

  1. 参数传递

    • 如果传递 NULL,表示父进程不关心子进程的退出状态信息。
    • 否则,操作系统会通过该参数将子进程的退出信息反馈给父进程。
  2. status 的底层结构
    status 不能简单当作整型处理,而应视为位图(具体细节仅研究低 16 位比特):

下面这张图体现了 status 的底层结构:

status

因此,我们在使用 wait 或 waitpid 函数获取进程的状态码的时候,需要对其进行位运算并通过判断终止信号是否为0来判断进程是正常终止还是被信号所杀

根据 status 的底层结构,我们可以非常方便的获取一个子进程的退出状态码

下面这个示例代码分别对正常终止结果正确、正常终止结果错误和异常终止的进程进行了状态码获取:

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <functional>
#include <vector>

// 正常退出且结果正确
void success()
{
    std::cout << "子进程 " << getpid() << " 将正常退出且结果正确" << std::endl;
    sleep(5);
    exit(0);
}

// 正常退出结果不正确
void uncurrent()
{
    std::cout << "子进程 " << getpid() << " 将正常退出但结果不正确" << std::endl;
    sleep(5);
    exit(2);
}

// 异常退出
void killed()
{
    std::cout << "子进程 " << getpid() << " 将被除零错误信号杀掉" << std::endl;
    sleep(5);
    int a = 10 / 0;
    exit(0);
}

int main()
{
    std::vector<std::function<void()>> exec{success, uncurrent, killed};
    for (int i = 0; i < 3; ++i)
    {
        pid_t id = fork();
        // 子进程
        if (id == 0)
        {
            exec[i]();
        }
    }
    // 父进程
    for (int i = 1; i <= 3; ++i)
    {
        int status_code = 0;
        int ret = waitpid(-1, &status_code, 0);
        if (ret == -1)
        {
            perror("waitpid");
            return 2;
        }
        else
        {
            if (signal_flag == 0)
            {
                // 程序未被异常终止
                int exit_status = (status_code >> 8) & 0xff;
                std::cout << "父进程已回收子进程, pid: " << ret << ",退出码为:" << exit_status <<std::endl;
            }else{
                // 程序被异常终止
                std::cout << "父进程已回收子进程, pid: " << ret << ",程序异常终止,终止信号为:" << signal_flag <<std::endl;
            }
        }
    }
    return 0;
}

测试结果如下:

╭─ljx@VM-16-15-debian ~/linux_review/lesson2
╰─➤  ./pro_wait.o
子进程 2692025 将正常退出且结果正确
子进程 2692027 将被除零错误信号杀掉
子进程 2692026 将正常退出但结果不正确
父进程已回收子进程, pid: 2692025,退出码为:0
父进程已回收子进程, pid: 2692027,程序异常终止,终止信号为:8
父进程已回收子进程, pid: 2692026,退出码为:2

其中 8 号信号正是除零错误,结果完全符合预期

当然,这种位运算获取退出状态和信号的方式其实可以通过系统自带的宏函数来获取,下面是对这几个宏函数的介绍:

  1. WIFEXITED(status):检查进程是否正常退出(通过 exitreturn)。
  2. WEXITSTATUS(status):若进程正常退出,提取其退出码(exitreturn 的值)。
  3. WIFSIGNALED(status):检查进程是否因信号(如 SIGSEGV)终止。
  4. WTERMSIG(status):若进程因信号终止,提取导致终止的信号编号。
  5. WCOREDUMP(status):检查进程是否生成了 core dump 文件(需配合 WIFSIGNALED 使用)。

因此,父进程回收子进程判断状态码的地方可以这样写:

if (WIFEXITED(status_code))
{
                // 程序未被异常终止
    std::cout << "父进程已回收子进程, pid: " << ret << ",退出码为:" << WEXITSTATUS(status_code) <<std::endl;
}else{
                // 程序被异常终止
    std::cout << "父进程已回收子进程, pid: " << ret << ",程序异常终止,终止信号为:" << WTERMSIG(status_code) <<std::endl;
}

利用宏函数来获取所需要的数据,增强了代码的可读性

进程的非阻塞等待方式

通过 WNOHANG 可以让 waitpid 函数实现非阻塞等待,父进程就通过轮询的方式回收子进程,其他时间父进程可以做别的事情,使用示例如下:

测试用例中我将子进程创建数量增加到了6个,检测父进程是否可以在子进程排队等待被回收的情况下马上将子进程批量回收(当检测到有子进程被回收时,立即进入下一个循环检测
是否还有进程需要被回收,而不是让父进程做自己的事情)

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <functional>
#include <vector>

// 子进程创建数目
#define exec_num 6

// 正常退出且结果正确
void success()
{
    std::cout << "子进程 " << getpid() << " 将正常退出且结果正确" << std::endl;
    sleep(2);
    exit(0);
}

// 正常退出结果不正确
void uncurrent()
{
    std::cout << "子进程 " << getpid() << " 将正常退出但结果不正确" << std::endl;
    sleep(4);
    exit(2);
}

// 异常退出
void killed()
{
    std::cout << "子进程 " << getpid() << " 将被除零错误信号杀掉" << std::endl;
    sleep(6);
    int a = 10 / 0;
    exit(0);
}

int main()
{
    std::vector<std::function<void()>> exec{success, uncurrent, killed, success, success, success};
    for (int i = 0; i < exec_num; ++i)
    {
        pid_t id = fork();
        // 子进程
        if (id == 0)
        {
            exec[i]();
        }
    }
    // 父进程
    // 计数器,计算还有多少个进程没有被回收
    int cnt = exec_num;
    while(cnt)
    {
        int status_code = 0;
        int ret = waitpid(-1, &status_code, WNOHANG);
        if (ret == -1)
        {
            perror("waitpid");
            return 2;
        }
        else if(ret != 0)
        {
            --cnt;
            if (WIFEXITED(status_code))
            {
                // 程序未被异常终止
                std::cout << "父进程已回收子进程, pid: " << ret << ",退出码为:" << WEXITSTATUS(status_code) <<std::endl;
            }else{
                // 程序被异常终止
                std::cout << "父进程已回收子进程, pid: " << ret << ",程序异常终止,终止信号为:" << WTERMSIG(status_code) <<std::endl;
            }
            // 马上进入下一次子进程回收检测
            continue;
        }
        // 模拟父进程做别的工作
        sleep(1);
        std::cout << "父进程在做别的事情" << std::endl;
    }
    return 0;
}

测试结果如下:

╭─ljx@VM-16-15-debian ~/linux_review/lesson2
╰─➤  ./pro_wait.o
子进程 2696573 将正常退出且结果正确
子进程 2696574 将正常退出但结果不正确
子进程 2696575 将被除零错误信号杀掉
子进程 2696576 将正常退出且结果正确
子进程 2696577 将正常退出且结果正确
子进程 2696578 将正常退出且结果正确
父进程在做别的事情
父进程在做别的事情
父进程已回收子进程, pid: 2696573,退出码为:0
父进程在做别的事情
父进程已回收子进程, pid: 2696576,退出码为:0
父进程已回收子进程, pid: 2696577,退出码为:0
父进程已回收子进程, pid: 2696578,退出码为:0
父进程在做别的事情
父进程已回收子进程, pid: 2696574,退出码为:2
父进程在做别的事情
父进程在做别的事情
父进程已回收子进程, pid: 2696575,程序异常终止,终止信号为:8

结果表明,父进程可以在子进程排队等待被回收的情况下快速回收子进程