【Linux我做主】细说进程等待

发布于:2025-09-05 ⋅ 阅读:(20) ⋅ 点赞:(0)

Linux进程等待

github地址

有梦想的电信狗

0. 前言

学习进程等待之前,请先移步到进程状态和进程退出的学习:

在 Linux 编程中,进程退出后并不会立即完全消失,如果父进程不及时回收,就会留下僵尸进程,造成资源泄漏,甚至影响系统稳定。
因此,进程等待不仅是清理子进程的必要操作,也是父进程获取子进程退出状态的重要途径。本文将结合示例,介绍进程等待的意义、waitwaitpid 的用法,以及阻塞和非阻塞等待的差别与应用。


1. 进程等待的必要性

​ 当父进程创建子进程后,如果对子进程“放任不管”,很快就会遇到一个严重的问题——僵尸进程。僵尸进程不仅影响系统资源的使用,还可能成为系统不稳定的隐患。因此,进程等待是进程管理中不可或缺的一环。

1.1 避免僵尸进程与资源泄漏

  • 子进程退出后,其代码和数据会被释放,但 内核依然会保留一部分信息(如退出状态、统计信息、PID 等),供父进程读取。
  • 如果父进程不调用等待机制来回收子进程,那么这些信息会一直滞留在系统中,使子进程长期处于 僵尸状态
  • 僵尸进程不会再占用 CPU,但它们占用的内核资源不可忽视,随着数量增多,将导致系统资源泄漏,甚至阻塞新的进程创建

1.2 僵尸进程不可被直接清除

  • 一旦进程进入僵尸状态,它已经“死去”,无法被 kill -9 等信号杀死。换句话说,僵尸进程是 刀枪不入 的,唯一的清除方式就是让父进程通过 进程等待 回收它们的资源。

1.3 获取子进程的运行结果

除了资源回收,父进程往往还关心子进程是否正确完成了任务:

  • 子进程是否正常退出
  • 子进程的退出码是多少?
  • 子进程是否因某个信号异常终止

这些信息对于任务调度、错误处理和日志记录都非常重要。通过进程等待,父进程能够获取到这些状态,从而 确认子任务的执行结果


综上所述,进程等待的必要性主要体现在两个方面

  1. 必不可少的资源回收 —— 防止僵尸进程堆积,避免系统资源泄漏。
  2. 可选择性的结果获取 —— 让父进程能够获知子进程的执行情况,辅助系统的正确运行与维护。

2. 进程等待的三个问题

1. 为什么要有进程等待

当一个子进程结束时,它会进入 僵尸进程(Zombie Process) 状态。僵尸进程本身并不会继续占用 CPU,但它依然在内核中保留着一定的资源(如 PCB 等),直到父进程通过 进程等待 的方式去读取它的退出状态并回收这些资源。
如果父进程不做等待,这些僵尸进程将长期滞留在系统中,最终造成 资源泄漏,严重时甚至会导致系统无法再创建新的进程。

因此,进程等待至少有两个核心目的:

  1. 必须解决的问题
    • 回收子进程,避免僵尸进程堆积,防止系统内存资源泄漏
  2. 可选关心的问题
    • 获取子进程的退出状态,了解其任务是否顺利完成。
    • 父进程可以根据这些信息决定后续操作(如重新调度任务、打印日志、做错误处理等)。

换句话说,进程等待既是 资源回收的必需操作,也是 父进程获取任务结果的一种手段


2. 进程等待是什么

  • 进程等待 是父进程通过系统调用 waitwaitpid 来检测和获取子进程的退出状态,并同时完成资源回收的过程(完成对僵尸进程的回收)。

  • 常用的系统调用有:

    • wait阻塞等待任意一个子进程退出,并返回其退出状态

    • waitpid:可以选择性地等待某个特定子进程,支持阻塞或非阻塞模式


3. 怎么实现进程等待

父进程调用系统调用 waitwaitpid,可实现对子进程的状态检测资源回收,从而避免僵尸进程的产生。逻辑流程大致如下:

  1. 子进程执行完任务并退出 → 内核保留其状态 → 子进程进入 僵尸状态
  2. 父进程调用 wait/waitpid
    • 如果有已退出的子进程,立即回收并获取状态;
    • 如果没有子进程退出,wait 会阻塞等待,而 waitpid 可选择等待方式,阻塞或立即返回
  3. 内核在回收完成后,释放子进程的 PCB 等资源。

3. 僵尸进程演示

  • 以下代码进行了僵尸进程的演示:子进程循环5次后退出,父进程为死循环,父进程中没有对子进程进行回收,子进程成为僵尸进程
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork failed\n");
        return 1;
    } else if (pid == 0) {
        // child
        int cnt = 5;
        while (cnt) {
            printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        exit(0);
    } else {
        // father
        while (1) {
            printf("I am father, pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }

        // 此时父进程没有针对子进程干任何事情,子进程退出后会变成僵尸进程
    }
    return 0;
}

在这里插入图片描述

在这里插入图片描述

5秒过后,子进程退出,父进程仍在运行。父进程的代码中没有对子进程的资源进行回收,因此子进程变成僵尸进程

  • 子进程一般退出的时候,如果父进程没有主动回收子进程信息,子进程会一直让自己处于Z状态。进程的相关资源尤其是task_struct结构体不能被释放。<defunct>的意思即为死的,意为僵尸进程
    • 未来父进程将子进程回收后,操作系统才能将子进程的资源进行释放。
    • 如果一个进程一直处于僵尸进程,自身的内存资源会被一直占用,从而引发内存泄露。之后可以在父进程中调用waitpid();函数解决该问题

在这里插入图片描述

父进程终止后,原来的子进程直接被操作系统领养,变成孤儿进程。操作系统直接将该进程领养并回收了。

父进程终止后,僵尸子进程会被 init(或 systemd)接管,并由其回收资源,不再是僵尸进程

4. wait

wait的手册声明

man 2 wait

在这里插入图片描述

在这里插入图片描述

在 Linux 中,wait 用于等待任意一个子进程的状态发生变化(通常是退出)。

  • 成功情况

    • 返回值:子进程的 PID(> 0)
  • 失败情况

    • 返回值:-1

    • 说明:

      • 失败时,errno 会被设置为合适的错误码,例如:
        • ECHILD:调用进程没有未等待的子进程。
  • 参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL


  • 总结逻辑

    • > 0:成功,返回子进程 PID

    • -1:失败,没有子进程可等待或者所有子进程都已经被回收过了(或出错)

wait的使用

wait单个子进程

代码如下

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

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork failed\n");
        return 1;
    } else if (pid == 0) {
        // child
        int cnt = 5;
        while (cnt) {
            printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        exit(0);
    } else {
        // father
        int cnt = 10;
        while (cnt) {
            printf("I am father, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        // 10 秒后 父进程对子进程进行wait回收
        pid_t ret = wait(NULL);  // 暂时传入 NULL 指针,不关心子进程的状态
        if (ret == pid) {
            printf("wait success %d\n", ret);
        }
        sleep(5);
    }
    return 0;
}

在这里插入图片描述

代码和运行现象分析

  • 父进程创建子进程,父子进程分别运行,父进程循环10秒,子进程循环5秒
  • 父进程循环10秒后对子进程进行wait回收,回收后等待5秒后退出。子进程循环5秒后退出
  • 因此:
    • 前5秒,父子进程同时运行
    • 第二个五秒子进程退出,父进程循环,子进程成为僵尸状态
    • 第三个五秒,回收后,父进程运行等待五秒,子进程被回收,因此只有父进程在运行
    • 最后,父进程退出

wait多个子进程

  • wait() 调用一次只能等待任意一个子进程,如何等待多个进程呢,需要我们循环等待子进程
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define N 10

void runChild() {
    int cnt = 5;
    while (cnt) {
        printf("I am child Process, pid: %d, ppid: %d\n", getpid(), getppid());
        cnt--;
        sleep(1);
    }
}
// 2. 如果有多个子进程,wait如何正确等待
int main() {
    for (int i = 0; i < N; ++i) {
        pid_t id = fork();
        if (id == 0) {
            runChild();
            exit(0);
        }
        // 父进程只会在循环内不断地创建子进程
        printf("creat child process: %d success\n", id);  // 这行代码只有父进程会执行
    }
    sleep(10);

    // wait 一次只能等待任意一个子进程,如何等待多个进程
    for (int i = 0; i < N; ++i) {
        pid_t id = wait(NULL);	// 等待任意一个子进程
        if (id > 0) {
            printf("wait %d success\n", id);
        }
    }
    sleep(5);
    return 0;
}

在这里插入图片描述

现象阐述

  • ./myproc,进程启动的一瞬间,我们看到10个进程在运行
  • 第一个5秒过后,子进程全部退出,全部变为僵尸进程,持续五秒
  • 第二个5秒过后,父进程对子进程进行回收,回收后父进程继续运行
  • 回收过后。父进程独自运行第三个5秒后结束

循环等待子进程的的逻辑

  • wait一次只能等待任意一个子进程,等待多个进程需要确保wait执行多次
for (int i = 0; i < N; ++i) {
    pid_t id = wait(NULL);
    if (id > 0) {
        printf("wait %d success\n", id);
    }
}

至此,进程等待是必须的wait完成了回收子进程,避免僵尸进程堆积,防止系统资源泄漏的工作。

wait时的阻塞等待

  • 以上是父进程等待子进程退出后再进行wait回收的场景,如果父进程不进行等待,且子进程一直不退出,父进程调用wait会怎么样呢
  • 我们对上述wait多个子进程的代码进行修改,让子进程永远不退出,父进程也不再sleep(5)等待子进程退出
// wait 等待多个子进程,但任意一个子进程永不退出的场景
void runChild() {
    int cnt = 5;
    while (1) {	// 永不退出
        printf("I am child Process, pid: %d, ppid: %d\n", getpid(), getppid());
        cnt--;
        sleep(1);
    }
}
int main() {
    for (int i = 0; i < N; ++i) {
        pid_t id = fork();
        if (id == 0) {
            runChild();
            exit(0);
        }
        // 父进程只会在循环内不断地创建子进程
        printf("creat child process: %d success\n", id);  // 这行代码只有父进程会执行
    }
    // sleep(10);

    // wait 一次只能等待任意一个子进程,如何等待多个进程
    // 等待多个进程时,任意一个子进程都不退出
    for (int i = 0; i < N; ++i) {
        pid_t id = wait(NULL);
        if (id > 0) {
            printf("wait %d success\n", id);
        }
    }
    sleep(5);
    return 0;
}

在这里插入图片描述

结论

  • 如果子进程不退出,默认父进程调用的系统调用wait函数就不会返回,该行为称为阻塞等待

    • 因此父进程调用的wait函数的return条件是子进程退出
  • 因此进程在等待时,既可以等待硬件资源,也可以等待软件资源(比如等待子进程退出)

wait的简单总结

  • 父进程的wait调用会回收僵尸进程,解决内存泄露问题
  • 如果子进程不退出,那么父进程调用的wait就不会返回,父进程阻塞等待,直到子进程退出

5. waitpid

手册声明

进程等待至少有两个核心目的

  1. 必须解决的问题
    • 回收子进程,避免僵尸进程堆积,防止系统内存资源泄漏
  2. 可选关心的问题
    • 获取子进程的退出状态,了解其任务是否顺利完成。
    • 父进程可以根据这些信息决定后续操作(如重新调度任务、打印日志、做错误处理等)。

通过wait(NULL)(阻塞等待),我们已经解决了回收子进程,避免僵尸进程堆积,防止系统资源泄漏的问题,那么我们如何获取子进程的退出状态,得知子进程是否完成相应的任务呢

这时waitpid就要登场了

在这里插入图片描述

返回值与参数详解

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

  • 返回值

    • 正常返回时,waitpid返回收集到的子进程的进程ID
    • 如果设置参数optionsWNOHANG为非阻塞等待,调用时waitpid发现子进程没有退出,直接返回0;
    • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
  • 参数

    • pid
      • pid == -1等待任一个子进程,与wait等效。
      • pid > 0:等待其进程ID与pid相等的子进程
    • status输出型参数,传入外部变量的地址,函数内将子进程的退出状态设置给外部的status
    • options:
      • WNOHANG:若指定pid的子进程没有结束,则waitpid()函数返回0,不予等待。若正常结束,则返回该子进
        程的ID
      • 传入0:表示设置阻塞等待,与wait等效。
  • wait的功能是waitpid的子集,以下两种写法完全等效

// 都传入 NULL 指针,标识 不关心子进程的状态
pid_t ret = wait(NULL); 
pid_t ret = waitpid(-1, NULL, 0);

status参数获取进程的退出信息

引入

  • 输出型参数 status 的基本用法:传入status的地址,函数内对外部的 status 进行修改
int status = 0;
pid_t ret = waitpid(pid, &status, 0);	// 传入status的地址,函数内对外部的 status 进行修改
if (ret == pid) {
    printf("wait success %d, status: %d\n", ret, status);
}
  • 获取子进程退出的status的演示,这里设置子进程的退出码为1exit(1)
// 5. 获取子进程的退出状态
int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork failed\n");
        return 1;
    } else if (pid == 0) {
        // child
        int cnt = 5;
        while (cnt) {
            printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        exit(1);
    } else {
        // father
        int cnt = 10;
        while (cnt) {
            printf("I am father, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        // 10 秒后 父进程对子进程进行wait回收
        int status = 0;
        pid_t ret = waitpid(pid, &status, 0);
        if (ret == pid) {
            printf("wait success %d, status: %d\n", ret, status);
        }
        sleep(3);
    }
    return 0;
}
  • 运行结果如下

在这里插入图片描述

在这里插入图片描述

  • 我们子进程的退出码为1 ( exit(1) ),为什么这里获取到的退出码不是1,而是256呢?
  • 接下来我们详细解释

status的二进制编码及其应用

回顾进程终止的原因/情景

  • 代码运行完毕,结果正确

  • 代码运行完毕,结果不正确

  • 代码异常终止

父进程等待,期望获得子进程退出的哪些信息呢

  1. 子进程代码是否异常
  2. 没有异常,结果对吗?不对是因为什么呢?
    1. 子进程的exit()中的退出码,不同的退出码,表示不同的出错原因
    2. 父进程通过子进程的退出码,可以获取到子进程退出的原因

上文中**退出码显示为256与进程退出状态的编码规则有关**

1. 进程退出状态的编码规则

在父进程调用 wait/waitpid 时,会得到一个整数 status,该值的低 16 位用于表示子进程的退出情况。我们暂时只考虑status的低16位

其结构如下:

 ┌─────────────── 16位 ────────────────┐
 15            8 7                   0
 ┌──────────────┬───────────┬─────────┐
 │   退出状态    │ core dump │ 终止信号 │
 └──────────────┴───────────┴─────────┘
  • 低 7 位(0~6 位):表示子进程被哪个信号终止进程出现异常本质就是被信号终止了)。
  • 第 8 位:表示是否产生了 core dump 文件。
  • 次低 8 位(8~15 位):表示子进程正常退出时的退出码exit()return 返回的值)。

👉 总结

  • 若子进程因信号终止:则 低 7 位 ≠ 0
  • 若子进程正常退出:则 低 7 位 == 0,此时退出码保存在次低 8 位
  • 这样通过子进程的status的低16位就可以判断子进程的退出情况了

在这里插入图片描述

  • kill -l 命令查看Linux中的所有信号,Linux中的信号编号不是从0开始的也从侧面证明了,未出现异常时 status 的低八位为0
    在这里插入图片描述

2. exit(1) 的情况

如果子进程调用 exit(1)无异常,则:

  • 退出码 == 1(写入到 status 的第 9 位)。
  • 低 7 位 == 0(表示没有信号导致异常退出)。

因此 status 的二进制结果为:

0000 0001 0000 0000

换算成十进制即 256

在这里插入图片描述


3. 如何判断子进程的退出情况
  1. 先判断是否异常退出:进程出现异常本质是收到了信号
    • 检查 status 的低 7 位。
    • 若 ≠ 0,说明子进程是被某个信号终止。
  2. 再判断退出码
    • 若低 7 位 == 0,说明子进程正常退出,无异常。
    • 此时读取 status 的次低 8 位,即退出码。
4. 是否可以通过全局变量获取退出状态?

不能及其原因

  • 尽管是定义全局变量,但父子进程拥有独立的地址空间,父子进程之间具有独立性
  • 即使子进程修改了某个全局变量(如 status),由于写时拷贝(COW)机制,父进程并不会感知到子进程的修改,无法获取到子进程修改后的全局变量
  • 只有通过 wait/waitpid 系统调用,父进程才能得到内核提供的子进程退出信息。

👉 结论:父进程必须通过 wait/waitpid 获取子进程的退出状态。

如何获取正确的status

  • 通过位运算的方式分别获取到是否出现异常和进程的退出码

    • status & 0x7Fstatus & 0111 1111,可以提取出status低七位
    • (status >> 8) & 0xFF:status右移八位后,再(status >> 8) & 1111 1111可以提取出status次低八位
  • 以下代码子进程exit(1),可以得到正确的退出状态

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork failed\n");
        return 1;
    } else if (pid == 0) {
        // child 进程
        int cnt = 5;
        while (cnt) {
            printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        exit(1);
    } else {
        // father 进程
        int cnt = 10;
        while (cnt) {
            printf("I am father, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        // 10 秒后 父进程对子进程进行wait回收
        int status = 0;
        pid_t ret = waitpid(pid, &status, 0);
        if (ret == pid) {
            // 通过位运算直接获取到正确的异常信号和退出码
            printf("wait success %d, exit sig: %d, exit code: %d\n", ret, status & 0x7F, (status >> 8) & 0xFF);
        }
        sleep(3);
    }
    return 0;
}

在这里插入图片描述

  • 如果发生了异常,我们也能通过该计算方式得到不同的错误码

  • 在子进程中添加访问野指针异常

  •   else if (pid == 0) {
          int* p = NULL;
          // child
          int cnt = 5;
          while (cnt) {
              printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
              cnt--;
              sleep(1);
              *p = 100;	// 访问野指针,会出现异常
          }
    
  • 在这里插入图片描述

9号信号杀进程异常

在这里插入图片描述

用操作系统提供的宏获取进程退出的status

我们可以对status经过位运算别获取子进程的退出码和判断子进程是否收到异常退出的信号,但这么做似乎有些繁琐,操作系统为我们提供了宏函数,用于判断进程是否正常退出和查看进程的退出码

  • WIFEXITED(status):若status为进程正常终止返回的状态,则WIFEXITED(status)为真。(查看进程是否是正常退出
  • WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码。(查看进程的退出码
  • WTERMSIG(status)获取导致进程终止的信号编号
  • 以上宏函数,底层是根据位运算实现的

代码示例

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork failed\n");
        return 1;
    } else if (pid == 0) {
        // child
        int cnt = 3;
        while (cnt) {
            printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        exit(11);
    } else {
        // father
        int cnt = 5;
        while (cnt) {
            printf("I am father, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        // 5 秒后 父进程对子进程进行wait回收
        int status = 0;
        pid_t ret = waitpid(pid, &status, 0);
        if (ret == pid) {
            if (WIFEXITED(status))
                printf("进程正常执行完毕, 退出码: %d\n", WEXITSTATUS(status));
            else {
                printf("进程出异常了\n");
            }
        } else {
            printf("wait fail\n");
        }
        sleep(3);
    }
    return 0;
}

在这里插入图片描述

waitpid失败的场景

  • waitpid失败的重要场景:等待的子进程不是当前进程的子进程
  • pid_t ret = waitpid(pid + 4, &status, 0),这里将等待的进程编号改为pid + 4
  • 等待的子进程不是当前父进程的子进程时,会触发waitpid函数的等待失败
int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork failed\n");
        return 1;
    } else if (pid == 0) {
        // child
        int cnt = 5;
        while (cnt) {
            printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        exit(1);
    } else {
        // father
        int cnt = 10;
        while (cnt) {
            printf("I am father, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        // 10 秒后 父进程对子进程进行wait回收
        int status = 0;
        pid_t ret = waitpid(pid + 4, &status, 0);
        if (ret == pid) {
            printf("wait success %d, exit sig: %d, exit code: %d\n", ret, status & 0x7F, (status >> 8) & 0xFF);
        } else {
            printf("wait fail\n");
        }
        sleep(3);
    }
    return 0;
}

在这里插入图片描述

waitpid等待多个子进程

  • 将第一个参数设为-1,则等待任意一个子进程。waitpid(-1, &status, 0);
// waitpid 等待多个子进程
void runChild() {
    int cnt = 5;
    while (cnt) {
        printf("I am child Process, pid: %d, ppid: %d\n", getpid(), getppid());
        cnt--;
        sleep(1);
    }
}
int main() {
    for (int i = 0; i < N; ++i) {
        pid_t id = fork();
        if (id == 0) {
            runChild();
            exit(i);
        }
        // 父进程只会在循环内不断地创建子进程
        printf("creat child process: %d success\n", id);  // 这行代码只有父进程会执行
    }

    for (int i = 0; i < N; ++i) {
        int status = 0;
        // 等待任意一个子进程
        pid_t id = waitpid(-1, &status, 0);
        if (id > 0) {
            printf("wait %d success, exit code: %d\n", id, WEXITSTATUS(status));
        }
    }
    sleep(3);
    return 0;
}

在这里插入图片描述

  • 可以看到,先创建的进程PID较小,退出码也较小

在这里插入图片描述

// 循环创建多个子进程
for (int i = 0; i < N; ++i) {
    pid_t id = fork();
    if (id == 0) {
        runChild();
        exit(i);
    }
    // 父进程只会在循环内不断地创建子进程
    printf("creat child process: %d success\n", id);  // 这行代码只有父进程会执行
}
// 循环等待多个子进程
for (int i = 0; i < N; ++i) {
    int status = 0;
    // 等待任意一个子进程
    pid_t id = waitpid(-1, &status, 0);
    if (id > 0) {
        printf("wait %d success, exit code: %d\n", id, WEXITSTATUS(status));
    }
}

简单分析

  • waitpid(-1, &status, 0):这里pid设为-1,表示等待任意一个子进程,options设为0,表示阻塞等待
  • 返回值id > 0时,标识waitpid函数等待成功,返回了所等待进程的pid

6. 进程等待的原理示意

内核源码

// Linux 内核源源码
struct task_struct {
	int 			exit_state;
	int				exit_code;
	int				exit_signal;
}
  • 可以看到,内核源码task_struct中存放了进程退出的三个变量,分别表示进程的退出状态退出码退出信号

  • 而在底层实现上,子进程退出时,代码和数据可以释放,但task_strcut必须先保留。exit_status的值会根据exit_codeexit_signal,经过位运算最终组合而得到,最终再将exit_status的值传给status

  • waitpid的本质:获取内核数据结构task_struct中的进程状态将进程状态由Z状态改为X状态

7. 非阻塞轮询

在这里插入图片描述

  • 返回值
    • 正常返回时,waitpid返回收集到的子进程的进程ID
    • 如果设置了选项WNOHANG,而调用中waitpid发现子进程没有退出,则返回0;
    • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

接下来我们来介绍**waitpid**的第三个参数options(阻塞方式)

思考:如果父进程在进行等待时,子进程运行了很久都不退出,这期间就会造成父进程状态为阻塞状态

  • 父进程一旦进入阻塞,会被链入到子进程task_struct的等待队列中,父进程就不会被CPU运行了,这不是我们所希望的,有没有什么方法不让父进程在等待时阻塞呢?
  • 参数 options 为我们提供了相应的控制方法
    • options传入0:可以实现像wait函数一样的阻塞等待
    • options传入WNOHANG:控制waitpid非阻塞等待
  • 那么**什么是阻塞等待(blocking wait)和非阻塞等待(non-blocking wait)**呢?

阻塞等待

定义
当父进程调用 waitpid(不加 WNOHANG),如果子进程还没有退出,父进程就会 停下来进入阻塞状态,直到子进程结束或收到信号为止

特点

  • 父进程“什么都不做”,一直等子进程。
  • 父进程此时 无法继续执行其它代码

阻塞等待是最简单的,也是最常应用的等待方式


非阻塞等待

定义
父进程调用 waitpid 时加上 WNOHANG 参数,如果子进程还没有退出,waitpid立刻返回 0,不会阻塞父进程。这样父进程可以去做别的事情,稍后再回来轮询一次子进程状态。

特点

  • 父进程不会停下来,可以一边做其他工作,一边不时检查子进程状态
  • 需要通过 轮询(循环调用 waitpid)来发现子进程是否结束。

非阻塞轮询的演示

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

#define N 10

// 非阻塞等待
int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork failed\n");
        return 1;
    } else if (pid == 0) {
        // child 进程
        int cnt = 5;
        while (cnt) {
            printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        exit(11);
    } else {
         // 父进程
        int status = 0;
        while (1) {	 // while(1) 不断轮询
            pid_t ret = waitpid(pid, &status, WNOHANG);
            // 等待成功或失败,break退出轮询
            if (ret > 0) {
                // 等待成功,获取子进程退出信息
                if (WIFEXITED(status))
                    printf("子进程正常运行完毕,退出码 %d\n", WEXITSTATUS(status));
                else {
                    printf("进程退出异常\n");
                }
                break;
            } 
             // 等待失败
            else if (ret < 0) {   
                printf("wait failed\n");
                break;
            } 
            // ret == 0 代表子进程还没有退出,可进行询问,或做其他任务
            else {	 
                printf("请问你运行完毕了吗? 子进程还没有退出,再等等\n");
                sleep(1);
            }
        }
    }
    return 22;
}

在这里插入图片描述

在这里插入图片描述

非阻塞轮询父进程运行其他任务

父进程应该做什么

  1. 父进程调用 waitpid 的主要目的
    父进程调用 waitpid,核心任务是等待并回收子进程,防止出现僵尸进程。
    在等待期间,父进程也可以“顺带”做一些轻量的任务,但这些任务不能过于复杂,否则可能影响对子进程状态的及时处理。
  2. 子进程退出与回收的时机
    • 不是必须立刻回收:子进程一旦退出,内核会将其状态信息保存下来,此时子进程会进入 僵尸状态
    • 父进程稍后再回收也可以:只要父进程在合适的时机调用 wait / waitpid,就能拿到子进程的退出信息并完成回收。
  3. 合理的等待策略
    • 如果父进程需要一直等子进程,可以使用阻塞等待
    • 如果父进程还有其他逻辑要执行,可以选择非阻塞轮询,并定期检查子进程是否退出,然后再回收。

怎么做

非阻塞等待,设计父进程做一些自己的工作

创建任务数组
#define TASK_NUM 10		// 定义任务数组中任务的总数

typedef void (*task_t)();  // 定义任务的函数指针
task_t tasks[TASK_NUM];    // 定义函数指针数组
设计任务函数
  • 以下函数仅为模拟子任务的过程
// 设计任务
void task1() {
    printf("这是一个执行打印日志的任务, pid: %d\n", getpid());
}
void task2() {
    printf("这是一个执行检测网络健康状态的一个任务, pid: %d\n", getpid());
}
void task3() {
    printf("这是一个进行绘制图形界面的任务, pid: %d\n", getpid());
}
初始化和添加任务
int AddTask(task_t task);

void InitTask() {
    // 初始化函数指针数组
    for (int pos = 0; pos < TASK_NUM; ++pos)
        tasks[pos] = NULL;
    // 添加任务
    AddTask(task1);
    AddTask(task2);
    AddTask(task3);
    // 还可以接着添加别的任务 ...
}
int AddTask(task_t task) {
    int pos = 0;
    // 找可以添加任务的位置
    for (; pos < TASK_NUM; ++pos) {
        // 第一个为NULL的位置可以添加任务
        if (!tasks[pos])
            break;
    }
    // 循环结束后,pos 可能 == TASK_NUM
    if (pos == TASK_NUM)
        return -1;
    tasks[pos] = task;	// 添加任务函数的指针
    return 0;
}
删除、检查和执行任务
  • 这里只实现执行任务,检查、执行、更新任务读者可以自行补充完成
void DelTask() {
}
void CheckTask() {
}
void UpdateTask() {
}
void ExecuteTask() {
    for (int i = 0; i < TASK_NUM; ++i) {
        if (!tasks[i])
            continue;
        tasks[i]();
    }
}
父进程轮询调用子任务
  • 父进程轮询等待单个子进程的场景
int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork failed\n");
        return 1;
    } else if (pid == 0) {
        // child
        int cnt = 5;
        while (cnt) {
            printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        exit(11);
    }
    // 父进程
    else {
        int status = 0;
        InitTask();
        while (1) {  // 轮询
            pid_t ret = waitpid(pid, &status, WNOHANG);	// 设置非阻塞等待
            // 等待成功或失败,break退出轮询
            if (ret > 0) {
                // 等待成功,获取子进程退出信息
                if (WIFEXITED(status))
                    printf("子进程正常运行完毕,退出码 %d\n", WEXITSTATUS(status));
                else {
                    printf("进程退出异常\n");
                }
                break;
            } else if (ret < 0) {
                // 等待失败
                printf("wait failed\n");
                break;
            } else {
                // ret == 0 说明 子进程未退出
                // 父进程的工作 放在这个块中执行
                ExecuteTask();
                usleep(500000);
            }
        }
    }
    return 22;
}

需要注意的是

  • 以上代码仅为创建了单个子进程的场景,如果有有多个子进程需要回收,需要将waitpid(pid, &status, WNOHANG)中的pid改为-1,一次等待任意一个子进程
  • 等待成功或失败时,不应该直接break,而是设计一个计数器,记录需要等待的进程的个数,并不断调整

总结

  • 父进程的核心责任:对子进程进行回收,避免僵尸进程。
  • 等待方式的选择
    • 阻塞等待 —— 父进程只关心子进程,适合简单场景。
    • 非阻塞等待 —— 父进程一边等待,一边做其他轻量任务,适合并行场景。
  • 最终的进程退出顺序
    • 最后终止的一定是父进程。
    • 通过正确的进程等待机制,可以保证:
      1. 父进程始终是最后退出的进程;
      2. 父进程能正确释放所有曾经创建过的子进程

8. 结语

进程等待的核心作用有两点:

  1. 回收子进程,避免僵尸进程
  2. 获取子进程状态,辅助任务管理

无论是使用阻塞等待还是非阻塞等待,合理的等待机制都是保证程序稳定与高效运行的关键。掌握这些方法,才能在实践中写出更健壮的 Linux 程序。


以上就是本文的所有内容了,如果觉得文章对你有帮助,欢迎 点赞⭐收藏 支持!如有疑问或建议,请在评论区留言交流,我们一起进步

分享到此结束啦
一键三连,好运连连!

你的每一次互动,都是对作者最大的鼓励!


征程尚未结束,让我们在广阔的世界里继续前行! 🚀


网站公告

今日签到

点亮在社区的每一天
去签到