018 进程控制 —— 进程等待

83 阅读13分钟

进程控制 —— 进程等待

1. 进程等待必要性

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

如果不等待会怎样?

  • 子进程退出了,但父进程没有调用 wait() 系列函数。
  • 子进程的“退出状态”会保留在内核中,直到父进程读取它。
  • 此时子进程的 PCB 没有完全释放,占用系统资源。
  • 如果产生大量僵尸进程,系统资源将耗尽,导致无法创建新进程。

所以:父进程需要“等待”子进程终止并获取其退出状态,以释放系统资源。

面试点拨: 如果不调用 wait() 会怎样?

回答:子进程的退出信息留在内核,PCB 未释放,形成僵尸进程,长期不回收会占满系统资源。


2. 常用等待方法(重点掌握)

函数名作用
wait(int *status)阻塞等待任意一个子进程结束,并获取其退出状态
waitpid(pid, &status, options)更灵活:等待指定子进程,或非阻塞等

1. wait() 示例(阻塞等待子进程)

wait()

  • 原型pid_t wait(int *status);
  • 功能:阻塞等待任意一个子进程退出,并回收其资源。
  • 参数status(输出型参数):保存/获取子进程退出状态(需用宏解析,如 WIFEXITED)。不关心可设置为 NULL。
  • 返回值:成功返回子进程 PID,失败返回 -1

实验目的:

  • 学会使用 wait() 函数阻塞等待子进程结束。
  • 理解如何通过 status 获取子进程的退出状态。
  • 掌握如何判断子进程是否正常退出以及获取其退出码。

[!CAUTION]

下面代码会涉及部分知识盲区,在文章后面会讲到!

实验:

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

int main()
{
    pid_t id = fork();
    if (id == 0)                            // 子进程
    {
        int count = 10;
        while (count--)
        {
            printf("我是子进程...PID:%d, PPID:%d\n", getpid(), getppid());   // 子进程逻辑:打印 PID 和 PPID
            sleep(1);
        }
        exit(0);                            // 子进程退出
    }

    int status = 0;                         // 存储子进程退出状态
    pid_t ret = wait(&status);              // 阻塞等待子进程结束
    if (ret > 0)                            // 父进程
    {
        // 父进程等待子进程结束
        printf("等待子进程结束...\n");
        if (WIFEXITED(status))              // 判断子进程是否正常退出
        {
            // 子进程正常结束
            printf("子进程正常结束,退出状态码:%d\n", WEXITSTATUS(status));
        }
    }

    sleep(3);
    return 0;
}

实验示例结果:

我是子进程...PID:1234, PPID:1233
我是子进程...PID:1234, PPID:1233
...
等待子进程结束...
子进程正常结束,退出状态码:0

2. waitpid() 示例(等待指定子进程,更灵活)

waitpid()

  • 原型pid_t waitpid(pid_t pid, int *status, int options);
  • 功能:更灵活地等待指定子进程,支持非阻塞模式。
  • 参数
  • pid:指定子进程 PID,或 -1 表示任意子进程。
  • options:常用的有 WNOHANG 表示非阻塞等待(立即返回,无子进程退出时返回 0)。
  • 返回值:成功返回子进程 PID,WNOHANG 模式下无退出子进程时返回 0,失败返回 -1

实验目的:

  • 学会使用 waitpid() 函数等待指定子进程。
  • 理解非阻塞等待(WNOHANG)的使用场景和优势。
  • 掌握如何在等待子进程的同时处理其他任务。

实验 1:

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

int main()
{
    pid_t pid = fork();

    if (pid == 0)
    {
        exit(10);
    }
    else
    {
        int status;
        pid_t wpid;
        while ((wpid = waitpid(pid, &status, WNOHANG)) == 0)
        {
            printf("父进程忙别的事...\n");
            sleep(1);
        }
        if (WIFEXITED(status))
        {
            printf("子进程退出码 = %d\n", WEXITSTATUS(status));
        }
    }

    return 0;
}

实验示例结果:

父进程忙别的事...
父进程忙别的事...
...
子进程退出码 = 10

WNOHANG 的用途(后面详讲):它用于非阻塞轮询场景,让父进程可以边处理任务边检查子进程状态。

实验 2:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
    pid_t id = fork();          // 创建子进程

    if (id == 0)
    {
        int time = 5;
        int n = 0;
        while (n < time)
        {
            printf("我是子进程,我已经运行了:%d秒 PID:%d   PPID:%d\n", n + 1, getpid(), getppid());
            sleep(1);
            n++;
        }

        exit(244);              // 子进程退出
    }

    int status = 0;             // 状态
    pid_t ret = waitpid(id, &status, 0);        // 参数 3 为 0,为默认选项

    if (ret == -1)
    {
        printf("进程等待失败!进程不存在!\n");
    }
    else if (ret == 0)
    {
        printf("子进程还在运行中!\n");
    }
    else
    {
        printf("进程等待成功,子进程已被回收\n");
    }

    printf("我是父进程, PID:%d   PPID:%d\n", getpid(), getppid());

    //通过 status 判断子进程运行情况
    if ((status & 0x7F))
    {
        printf("子进程异常退出,core dump:%d   退出信号:%d\n", (status >> 7) & 1, (status & 0x7F));
    }
    else
    {
        printf("子进程正常退出,退出码:%d\n", (status >> 8) & 0xFF);
    }

    return 0;
}

实验示例结果:

我是子进程,我已经运行了:1秒 PID:1234   PPID:1233
...
进程等待成功,子进程已被回收
我是父进程, PID:1233   PPID:1232
子进程正常退出,退出码:244

3. status 退出状态详解

1. 什么是 status

当你用 wait()waitpid() 等函数回收子进程时,会通过一个整型变量 status 返回子进程的 终止状态/状态码 status 信息。这个 status 是一个 32 位整数,它的 各个位(bit)存储了子进程退出的不同信息,主要包括:

  • 子进程是否正常退出
  • 退出的返回码
  • 是否是被信号中断
  • 是否是 core dump

当子进程结束时,它就会返回一个 状态码 status,通过宏函数解读它:

宏函数判断或提取内容实现底层逻辑本质
WIFEXITED()是否正常退出(status & 0x7F) == 0判断是否未被信号终止(是否正常退出)
WEXITSTATUS()获取退出码(status >> 8) & 0xFF提取退出码所在的 8 位(获取 exit 返回码)

这些宏的 设计目的 就是为了 屏蔽底层实现细节,让你写代码时更易读。但其实就是对 status 进行的位运算封装。


2. status 的位布局(Linux 下)

通常(glibc 实现下),status 的位布局如下:

image-20250414221815934

31...........16 | 15.....8 | 7......0
   保留位        | 退出码   | 信号位  
  31                            16 15         8 7      0
+-----------------------------+-------------+--------+
|           保留              | 退出码(exit) | 信号码 |
+-----------------------------+-------------+--------+
                                ↑           ↑
                                |           |
                             (status >> 8)  status & 0x7F                                                 

3. WIFEXITED 和 WEXITSTATUS 的底层原理

1. WIFEXITED(status)

判断子进程是否 正常退出(调用了 exit()return

#define WIFEXITED(status)  (((status) & 0x7F) == 0)

🔸 它检测的是 低 7 位(status & 0x7F)是否为 0,即 没有被信号终止

2. WEXITSTATUS(status)

获取子进程的 退出码(exit() 或 return 的值)

#define WEXITSTATUS(status)  (((status) >> 8) & 0xFF)

🔸 它提取的是 第 8~15 位,因为退出码就被编码在这里。

4. 实验测试

实验目的:

  • 学会解析 status 的各个位,了解子进程的退出状态。
  • 掌握如何通过宏函数判断子进程是否正常退出以及获取其退出码。
  • 理解如何手动解析 status 的位信息。
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

int main()
{
    pid_t pid = fork();
    if (pid == 0)
    {
        exit(66); // 子进程退出码设为 66
    }
    else
    {
        int status = 0;
        waitpid(pid, &status, 0);

        printf("原始 status:%d (0x%x)\n", status, status);

        if (WIFEXITED(status))
        {
            printf("正常退出,返回值 = %d\n", WEXITSTATUS(status));
            printf("手动解析返回值 = %d\n", (status >> 8) & 0xFF);
        }
        else
        {
            printf("非正常退出\n");
        }
    }
    return 0;
}

输出示例:

image-20250414223219766

原始 status:16896 (0x4200)
正常退出,返回值 = 66
手动解析返回值 = 66

示例:手动解析 status

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

int main()
{
    pid_t pid = fork();
    if (pid == 0)
    {
        exit(66);                                       // 子进程退出码设为 66
    }
    else
    {
        int status = 0;
        waitpid(pid, &status, 0);

        printf("原始 status: 0x%x\n", status);

        // 手动解析 status
        if ((status & 0x7F) == 0)                       // 判断是否正常退出
        { 
            int exit_code = (status >> 8) & 0xFF;       // 提取退出码
            printf("手动解析:子进程正常退出,退出码: %d\n", exit_code);
        }
        else
        {
            printf("手动解析:子进程异常退出,信号码: %d\n", (status & 0x7F));
        }
    }
    return 0;
}

扩展:

写法模板:

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

int main()
{
   pid_t pid = fork();
   if (pid == 0)           // 子进程逻辑
   {
       exit(0);
   }
   else if (pid > 0)       // 父进程逻辑
   {
       int status;
       pid_t ret = waitpid(pid, &status, 0);
       if (ret == -1)
       {
           perror("waitpid error");
       }
       else if (WIFEXITED(status))
       {
           printf("子进程正常退出,退出码: %d\n", WEXITSTATUS(status));
       }
       else
       {
           printf("子进程异常退出\n");
       }
   }
   else
   {
       perror("fork error");
   }

   return 0;
}

若子进程是被信号杀死的,还可用:

  • WIFSIGNALED(status):是否被信号终止。
  • WTERMSIG(status):哪个信号导致的。

这些也都是对 status 特定位的封装。

面试点拨:

Q:只创建一个子进程也要 wait() 吗?

A:要,不然会产生僵尸进程。

Q: wait(NULL) wait(&status) 有何不同?

A:前者不关心子进程退出码,后者可以判断退出状态。

Q:wait()waitpid() 的区别是什么?(详见下文)

A:wait() 阻塞等待任意一个子进程,而 waitpid() 可以指定子进程,并支持非阻塞模式。

Q:怎么判断子进程是否异常退出?

A:WIFEXITED(status) 为假时即为异常,可结合 WIFSIGNALED 查看是否被信号终止。


4. 非阻塞轮询

1. 什么是非阻塞轮询(Non-blocking Polling)?

非阻塞轮询 是一种在程序中检查某项资源状态(比如文件描述符、输入输出、子进程状态等)时,不会阻塞(挂起)当前线程或进程的技术。非阻塞轮询其实是 进程等待的一种特殊形式,本质上就是使用 waitpid() 函数时,配合选项 WNOHANG,来实现 非阻塞地检查子进程是否退出

非阻塞轮询底层依赖:

  • waitpid(..., WNOHANG):设置为非阻塞检查子进程。
  • read() / write() 配合 O_NONBLOCK 标志。
  • select() / poll() / epoll() 这些高级接口也支持非阻塞 I/O 检测。

联系:

  • 非阻塞轮询 ≈ 进程等待 + WNOHANG 参数。
  • 是进程等待的一种实现方式,可以避免父进程“卡死”在等待中。
  • 适合场景:父进程还有其他任务要处理、需要同时监控多个子进程、构造后台守护程序等。

这样说难以理解,我们用一个示例来帮助理解:假如你是快递员,你今天安排了送货任务,但你同时还在等一个客户签收你的包裹。现在有两种做法:

场景一:阻塞等待(wait)

你在客户门口等着他开门签字,你哪儿也不去,什么都不干,就在那儿等。就是 wait()waitpid(pid, NULL, 0)

  • 优点:等到了就能马上处理。
  • 缺点:你被“卡住”了,浪费了等的这段时间。
场景二:非阻塞轮询(WNOHANG)

你不一直站在门口,而是 每隔 10 分钟回来敲一次门,空闲的时候你还可以去送别的快递。就是 waitpid(pid, &status, WNOHANG) + sleep(1)

  • 优点:你不会被“卡住”,还能干其他事。
  • 缺点:客户签收可能不能第一时间知道(需要“轮询”)。
场景三:阻塞轮询(极端示例)

你不停敲门、再敲门,一直不走,一直问:“你签了没?你签了没?” 程序中表现为没有 sleep 的非阻塞 waitpid(pid, WNOHANG) 死循环。

  • 缺点:会让 CPU 疯狂运转(忙等待)。
术语与现实对应表
系统术语现实中的你
阻塞等待在门口站着等,不做别的事
非阻塞轮询每隔一段时间回来问一次,期间干别的事
阻塞轮询疯狂按门铃,问个不停,CPU 很累
进程等待等子进程结束,获取退出状态

2. 联系总结(术语图谱)

image-20250414230218594

                  wait/waitpid
                  ┌────────────┐
                  │ 进程等待机制│
                  └────┬───────┘
                       │
         ┌─────────────┴────────────┐
         │                          │
   ┌─────▼─────┐             ┌─────▼─────────┐
   │ 阻塞等待   │             │ 非阻塞轮询     │
   │ wait()    │             │ waitpid(pid, WNOHANG) │
   └────────────┘             └──────────────────────┘

3. 我该怎么选?怎么使用?

场景推荐方法原因
父进程只等子进程结束,没别的事干阻塞等待 (wait)简单、直接、不会浪费资源
父进程还有其他重要任务非阻塞轮询 (waitpid + WNOHANG)不中断其他逻辑,更灵活
你同时要监控多个子进程非阻塞轮询可以处理多个子进程,适合服务端/守护程序
写简单的练习题/实验代码阻塞等待即可写起来方便,看得懂

实验目的:

  • 学会使用非阻塞轮询等待子进程结束。
  • 理解如何在等待子进程的同时处理其他任务。
  • 掌握如何通过 WNOHANG 选项实现非阻塞等待。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
    pid_t pid = fork();
    if (pid == 0)                       // 子进程
    {
        printf("子进程开始运行...\n");
        sleep(5);
        printf("子进程即将退出\n");
        exit(0);
    }
    else                                // 父进程:非阻塞方式轮询子进程状态
    {
        int status;
        while (1)
        {
            pid_t result = waitpid(pid, &status, WNOHANG);      // 非阻塞调用

            if (result == 0)
            {
                // 子进程还未退出
                printf("父进程:子进程还在运行...\n");
            }
            else if (result == pid)
            {
                // 子进程已经退出
                if (WIFEXITED(status))
                {
                    printf("父进程:子进程正常退出,退出码为 %d\n", WEXITSTATUS(status));
                }
                break;
            }
            else
            {
                perror("waitpid error");
                break;
            }
            sleep(1);                    // 轮询间隔
        }
    }
    return 0;
}

实验示例结果:

父进程:子进程还在运行...
父进程:子进程还在运行...
...
子进程开始运行...
子进程即将退出
父进程:子进程正常退出,退出码为 0

4. 小结一句话

非阻塞轮询是一种“智能等待”方式,让父进程在等待子进程的同时,还能处理其他任务,是并发编程的常见技巧。


5. 总结记忆点

内容说明
为什么等待防止僵尸进程,释放系统资源,获取子进程退出信息,确保系统稳定性和资源高效利用。
常用函数wait():阻塞等待任意子进程结束;waitpid():灵活等待指定子进程,支持非阻塞模式。
状态解析使用宏函数 WIFEXITED() 判断子进程是否正常退出,WEXITSTATUS() 获取退出码。
非阻塞轮询适用于父进程需要同时处理其他任务或监控多个子进程的场景,通过 waitpid() 配合 WNOHANG 实现。
推荐写法常用 waitpid(pid, &status, 0),安全灵活,适合大多数场景。
注意事项父进程必须回收子进程资源,否则会导致僵尸进程,长期不回收会耗尽系统资源。
适用场景简单程序使用阻塞等待,复杂程序或需要并发处理时使用非阻塞轮询。

实战技巧

  1. 调试技巧:在调试时,若发现僵尸进程,检查父进程是否正确调用了 wait()waitpid()
  2. 性能优化:在高并发场景下,使用非阻塞轮询避免父进程被长时间阻塞,提高系统响应速度。
  3. 代码健壮性:始终检查 wait()waitpid() 的返回值,处理可能的错误情况。