《Linux系统编程之进程控制》【进程等待】

【进程等待】目录

  • 前言:
  • ---------------进程等待---------------
    • [1. 进程为什么要等待?](#1. 进程为什么要等待?)
    • [2. 怎么进行进程的等待?](#2. 怎么进行进程的等待?)
    • [3. 细说waitpid的第二个参数status!!!](#3. 细说waitpid的第二个参数status!!!)
    • [4. status位图具体是怎么划分情况的?](#4. status位图具体是怎么划分情况的?)
    • [5. 不存在的0号信号?](#5. 不存在的0号信号?)
    • [6. 如何从status中读取"退出码"和"退出信号"呢?](#6. 如何从status中读取”退出码“和“退出信号”呢?)
    • [7. 本质上父进程是怎么拿到这些信息的?](#7. 本质上父进程是怎么拿到这些信息的?)
    • [8. 如何理解第三个参数options?](#8. 如何理解第三个参数options?)
    • [9. 为什么非阻塞轮询调用的效率更高?](#9. 为什么非阻塞轮询调用的效率更高?)
    • [10. 怎么使用代码实现非阻塞轮询调用?](#10. 怎么使用代码实现非阻塞轮询调用?)

往期《Linux系统编程》回顾:

/------------ 入门基础 ------------/
【Linux的前世今生】
【Linux的环境搭建】
【Linux基础 理论+命令】(上)
【Linux基础 理论+命令】(下)
【权限管理】

/------------ 开发工具 ------------/
【软件包管理器 + 代码编辑器】
【编译器 + 自动化构建器】
【版本控制器 + 调试器】
【实战:倒计时 + 进度条】

/------------ 系统导论 ------------/
【冯诺依曼体系结构 + 操作系统基本概述】

/------------ 进程基础 ------------/
【进程入门】
【进程状态】
【进程优先级】
【进程切换 + 进程调度】

/------------ 进程环境 ------------/
【环境变量】
【地址空间】

/------------ 进程控制 ------------/
【进程创建 + 进程终止】

前言:

hi~,小伙伴们大家好呀!(ノ≧∀≦)ノ

今天是冬二九哦,没什么可聊的了那鼠鼠就为大家科普一下什么是冬二九吧!(≧ω≦)

冬二九:是中国民间 "数九寒天" 的第二个九天,即冬至后的第 10---18 天,属于深寒前奏,气温持续走低,还未到 "三九、四九" 的最冷峰值。

  • 算法:冬至起算,每 9 天为一 "九",共九九 81 天;二九 = 第 10---18 天;也有 "冬至逢壬数九" 的变体,时段相近
  • 气候:白昼渐长但热量收支仍逆差,地温与气温持续走低,北方多低温冰冻、南方多湿冷或阴雨;民间常以 "一九二九不出手" 形容其寒冷(゚∀゚)
  • 文化:源于古人对寒暖节律的总结,兼具农事与生活指引,各地有九九歌传唱

好了我们接着来学习是进程控制的第二讲 【进程等待】 吧!(´∀`)♡

--------------- 2025 年 12 月 30 日(冬月十一)周二,冬二九

这部分内容是进程控制的 "关键衔接环节",更是解决 "僵尸进程" 这个棘手问题的核心方案,咱们先通俗剧透核心价值和核心内容:

  • 进程等待 :就是父进程主动等待子进程终止,并获取子进程终止状态(正常退出码/异常信号)的过程。 我们将会从基础问题:为什么要等待?怎么去等待?开始思考,并涉及关于退出码的底层原理本质,和阻塞等待和非阻塞轮询的等待的全面解剖╰(°▽°)╯

掌握进程等待,就能彻底打通进程 "创建 - 运行 - 终止 - 回收" 的全生命周期管理闭环,解决进程控制中的核心痛点,为后续学习进程替换打下扎实基础!ヽ(✿゚▽゚)ノ

---------------进程等待---------------

1. 进程为什么要等待?

回忆一下我们之前学习进程状态时提到的 "僵尸状态(Zombie)":

  • 处于该状态的进程,其内核数据结构(PCB,进程控制块)仍保留在系统中,但实际的代码、数据等用户态资源已被释放
  • 这种 "已死亡但未被清理" 的状态,本质上是一种系统资源泄露------PCB 占用的内核内存无法被回收,若大量僵尸进程堆积,会逐渐耗尽系统的进程表资源,最终导致无法创建新进程

那么,如何解决这种僵尸进程带来的资源泄露问题?

答案正是我们接下来要深入学习的:进程等待机制


这里有一个关键知识点需要明确:

  • 进程一旦进入僵尸状态,就变得 "刀枪不入"------ 即便是 "强制终止进程" 的终极命令 kill -9 也对它无能为力
  • 原因很简单 :僵尸进程本身已经 "死亡",它不再执行任何代码,自然也无法响应任何信号
    • 对于一个已经死去的进程,"杀死" 这个动作本身就失去了意义
    • 因此,清理僵尸进程的唯一途径,就是通过父进程的 "等待" 操作来主动回收它的资源

除此之外,进程等待还有一个重要作用:获取子进程的执行结果

父进程派生子进程去完成特定任务后,必然需要知道任务的执行情况:

  • 子进程是正常退出还是异常终止?
  • 如果是正常退出,最终的计算结果是否符合预期?
  • 如果是异常终止,是因哪个信号(如:段错误、浮点异常)导致的?

这些关键信息都存储在子进程的 PCB 中,只有通过进程等待,父进程才能读取到这些退出状态。


总结来说,父进程通过 "进程等待" 机制,主要实现两个核心目标:

  • 强制且必须的:回收子进程资源
    这是进程等待最核心的价值,通过清理僵尸进程的 PCB,避免内核资源泄露,保障系统稳定
  • 可选但常用的:获取子进程退出信息
    这是父进程与子进程 "通信" 的一种间接方式,帮助父进程判断任务执行结果,进而决定后续逻辑(如:重试任务、记录错误日志等)

2. 怎么进行进程的等待?

wait

wait :用于使父进程暂停执行,等待其子进程终止,并获取子进程的退出状态

  • wait 函数是 POSIX 标准定义的函数,属于进程控制类函数,用于处理父子进程之间的同步和资源回收问题

函数原型

c 复制代码
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);
  • 参数status 是一个指向整数的指针,用于存储子进程的退出状态信息
    • 如果不需要获取子进程的退出状态,可以将其设置为 NULL
  • 返回值
    • 成功时,返回终止子进程的进程 ID(pid
    • 如果调用进程没有子进程,或者子进程都已经结束:
      • wait 函数返回 -1
      • 并设置 errno 来指示具体的错误情况,常见的错误码如:ECHILD(没有子进程)

功能作用

  • 等待子进程终止
    • 父进程调用 wait 后会进入阻塞状态,直到它的一个子进程终止
    • 这样可以保证父进程在子进程完成任务后,再进行后续操作,实现父子进程之间的同步
  • 回收子进程资源
    • 当子进程终止后,若不及时回收资源,子进程会进入僵尸状态,占用系统资源
    • wait 函数可以回收子进程的进程控制块等资源,避免僵尸进程的产生
  • 获取子进程退出状态
    • wait 函数能够获取子进程的退出码
    • 从而让父进程得知子进程是正常结束还是异常终止,以及具体的错误信息(如果有)

注意事项

  • 多个子进程情况:如果父进程有多个子进程,wait 只会等待并回收其中一个子进程的资源。
    • 如果需要等待所有子进程结束,可以使用循环调用 wait 或者使用 waitpid 函数来指定等待特定的子进程
  • 默认是阻塞等待:wait 函数默认是阻塞的。
    • 如果不想让父进程一直等待,可以使用 waitpid 并设置 WNOHANG 标志来实现非阻塞等待
    • 即如果没有子进程终止,waitpid 会立即返回而不会阻塞父进程

waitpid

waitpid :用于实现父进程等待子进程结束,并获取子进程退出状态,它能更精准地控制等待哪个子进程,以及等待的方式是阻塞还是非阻塞

  • 它是一个用于进程控制的系统调用,和 wait 功能类似,都用于等待子进程终止,但它提供了更灵活的控制方式
  • waitpidPOSIX 标准定义的函数

函数原型

c 复制代码
#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);
  • pid :指定要等待的子进程的进程 ID,其取值不同含义也不同:
    • pid > 0:等待进程 ID 等于 pid 的子进程
    • pid = -1:等待任何一个子进程,此时功能和 wait 类似
    • pid = 0:等待其组 ID 等于调用进程组 ID 的任何子进程(了解即可)
    • pid < -1:等待其组 ID 等于 pid 绝对值的任何子进程(了解即可)
  • status :和 wait 函数的 status 类似,是一个指向整数的指针,用于存储子进程的退出状态信息。
    • 可以通过 WIFEXITEDWEXITSTATUSWIFSIGNALEDWTERMSIG 等宏来解析该状态
  • options :提供额外的选项来控制waitpid的行为,常用的选项有:
    • WNOHANG:如果指定的子进程没有结束,则 waitpid 不阻塞,立即返回 0
    • WUNTRACED:除了等待子进程正常终止,还会报告因信号而停止的子进程的状态
    • WCONTINUED:如果子进程之前被停止,现在继续运行,则返回其状态信息

返回值

  • 大于 0:如果等待的子进程终止,返回该终止子进程的进程 ID
  • 等于 0:如果设置了 WNOHANG 选项,且指定的子进程没有终止,waitpid 不会阻塞,此时返回 0
  • 等于 -1:表示出错,同时会设置 errno 来指示具体的错误情况,常见的错误码如:ECHILD(没有子进程)、EINTR(等待过程中被信号中断)

功能特点

  • 精准控制等待对象wait 只能等待任意一个子进程结束,而 waitpid 可以根据 pid 参数精准指定等待某个特定子进程,或者特定进程组中的子进程
  • 灵活选择等待方式:通过设置 options 参数,可以实现阻塞或非阻塞等待,在需要非阻塞获取子进程状态,或者在循环中轮询子进程状态时非常有用

注意事项

  • 错误处理:当 waitpid 返回 -1 时,要根据 errno 的值进行合适的错误处理,确保程序的健壮性
  • options 组合使用:多个 options 选项可以通过按位或(|)操作组合使用,如:waitpid(pid, &status, WNOHANG | WUNTRACED),以满足更复杂的等待需求

3. 细说waitpid的第二个参数status!!!

首先我们需要明确:在 C 语言中,函数向调用者传递结果的方式主要有两种:

  • 通过函数返回值传递:为函数定义特定的返回值类型,在函数内部用 return 语句将结果直接返回给调用者(例如:int add(int a, int b) { return a+b; }
  • 通过输出型参数传递:将指针作为函数的形参,在函数内部通过指针修改外部变量的值,从而将结果 "带出去"
    • 这类用于承载输出结果的指针参数,被称为 "输出型参数" (例如:void swap(int* x, int* y) { int tmp=*x; *x=*y; *y=tmp; }

在进程等待中,父进程获取子进程退出信息的方式,正是第二种 ------ 通过 waitpid 函数的第二个参数 status(一个 int* 类型的指针)来实现。

这个 status 就是典型的输出型参数 :父进程调用 waitpid 时传入 status 的地址,当子进程终止后,操作系统会将子进程的退出信息写入 status 指向的内存空间,供父进程读取

cpp 复制代码
#include <unistd.h>     // 包含 sleep 等函数声明
#include <sys/types.h>  // 包含 pid_t 类型定义
#include <sys/wait.h>   // 包含 waitpid 等进程等待相关函数声明
#include <stdio.h>      // 包含 printf 等标准输入输出函数声明
#include <errno.h>      // 包含 errno 错误码定义
#include <string.h>     // 包含 strerror 函数声明(用于将错误码转为错误信息字符串)
#include <stdlib.h>  // 添加这行,包含 exit 函数的声明

int main()
{
    //1.调用 fork 创建子进程
    pid_t id = fork();

    //2.子进程执行的代码段
    if (id == 0)
    {
        
        //2.1:定义计数器 cnt,用于控制子进程循环次数
        int cnt = 3;
        while (cnt)
        {
            //第一步:打印子进程的 PID(进程 ID)和 PPID(父进程 ID)
            printf("我是一个子进程,pid:%d, ppid:%d\n", getpid(), getppid());

            //第二步:子进程休眠 1 秒,控制打印频率
            sleep(1);

            //第三步:计数器减 1
            cnt--;
        }
        //2.2:子进程调用 exit 函数终止,参数 1 作为退出码
        exit(1);
    }

    //3.父进程执行的代码段
    //3.1:定义 status 变量,用于存储子进程的退出状态信息
    int status = 0;

    //3.2:调用 waitpid 等待指定子进程(pid 为 id)退出,0 表示阻塞等待
    pid_t rid = waitpid(id, &status, 0);

    //情况一:waitpid 成功,打印子进程 PID 和获取到的 status 值
    if (rid > 0)
    {
        printf("wait success, rid: %d, status: %d\n", rid, status);
    }
    //情况二:waitpid 失败,打印错误码和对应的错误信息
    else
    {
        printf("wait failed: %d: %s\n", errno, strerror(errno));
    }

    //4.主函数返回 0,进程正常退出
    return 0;
}

提到 "子进程的退出信息",我们很自然会联想到 "进程退出码",但是这里有一个:

关键误区:status 指向的变量并非直接存储退出码,而是一个包含了多种信息的 "复合状态"


为什么不直接用 status 存储退出码呢?

原因很简单

  • 进程的终止场景远不止 "正常执行完毕(结果正确 / 错误)" 这两种
  • 如果只用一个值表示退出码(比如:0 代表成功,非0 代表失败),就无法区分 "进程正常退出但结果错误" 和 "进程因异常(如:段错误、被信号杀死)而终止" 这两种本质不同的情况

为了同时承载 "正常退出信息" 和 "异常终止信息",status 指向的 int 型变量被设计成了一种 "位图结构"------ 它的 32 个比特位被划分成不同的区域,每个区域对应特定的状态信息,而非简单存储一个整数。

4. status位图具体是怎么划分情况的?

status 位图的具体划分(以 Linux 系统为例)

int 类型共 32 个比特位,其结构划分如下(高地址位在前,低地址位在后):

比特位范围 (31~0) 含义 说明
31 ~ 16 未使用 这 16 个高位比特目前无实际意义 默认均为 0
15 ~ 8 正常退出码 (exitcode) 当进程正常终止 (如:main 返回、调用 exit)时 这 8 个比特存储退出码
7 Core Dump 标志 (coredump) 若进程终止时产生了 Core Dump 文件 (用于调试) 此位为 1,否则为 0
6 ~ 0 终止信号 (signal) 当进程异常终止 (如:被 SIGSEGVSIGINT 等信号杀死)时 这 7 个比特存储终止信号的编号

举例:退出码为 1 时的 status 结构

当我们的程序正常退出且退出码设为 1 时,status 的位图状态如下:

  • 高 16 位(31~16):全 0
  • 次高 8 位(15~8):存储退出码 1,即二进制 00000001
  • 低 8 位(7~0):因进程正常退出,Core Dump 标志(位 7)为 0,终止信号(位 6~0)也为 0,即二进制 00000000

因此,整个 status 的二进制值为:00000000 00000000 00000001 00000000,对应的十进制值为 256 (即:1 << 8

5. 不存在的0号信号?

在 Linux 系统中,我们查看信号列表(如:通过 kill -l 命令)时会发现,每个信号都由 "数值" 和 "名称" 两部分组成 ------

  • 2 对应 SIGINT(中断信号,通常由 Ctrl+C 触发)
  • 11 对应 SIGSEGV(段错误信号,如访问野指针时产生)

需要注意的是这些信号的:

  • "数值" 本质上是系统定义的宏常量(而非普通整数)
  • "名称" 则是宏的标识符

这样的设计既方便开发者记忆,也能避免直接使用硬编码数值导致的可读性差、易出错问题。

更关键的一个细节是:所有信号中不存在 "0 号信号"

这一设计与进程的终止机制紧密相关 ------ 我们知道,进程的异常终止几乎都是由收到信号触发的(比如:被强行杀死、触发运行时错误等),而这些终止信号的编号会被存储在 waitpid 函数 status 参数的 "低 7 位"(位图的 6~0 位)中。


那么 "0 号信号不存在" 的意义何在?这其实是一种 "默认无异常" 的约定:

  • 当进程正常终止(如:通过 main、exit 返回)时:不存在任何导致其异常的信号,此时 status 的低 7 位会被置为 0
    • 由于系统中没有 0 号信号,这个 "0 值" 就明确表示 "进程从未收到过终止信号",即进程的终止是主动且正常的
  • 当进程异常终止时:触发终止的信号编号(如:211 等)会被填入这 7 位中,父进程通过解析该值就能明确子进程异常终止的原因

简单来说 :"无 0 号信号" 的设计,为 status 位图的低 7 位赋予了清晰的语义:

  • 0:代表 "无异常信号"
  • 非0:代表 "触发异常的信号编号"

从而实现了对 "正常终止" 和 "异常终止" 的精准区分

6. 如何从status中读取"退出码"和"退出信号"呢?

父进程无法直接从 status 中读取退出码,必须通过系统提供的宏来解析对应区域的信息:

  • 用 WIFEXITED(status) 宏判断进程是否正常退出
  • 若为真,说明低 8 位的终止信号为 0,可进一步用 WEXITSTATUS(status) 宏提取次高 8 位的退出码
  • 用 WIFSIGNALED(status) 宏判断进程是否被信号终止
    • 若为真,说明进程被信号终止了,可通过 WTERMSIG(status) 宏提取低 7 位的终止信号编号

这也解释了为什么 status 不能直接等同于退出码 ------ 它是一个 "状态集合",而退出码只是其中的一部分

7. 本质上父进程是怎么拿到这些信息的?

我们首先要明确一个核心前提:进程具有独立性------

  • 父子进程拥有各自独立的地址空间,子进程的局部变量、全局变量等数据,父进程无法直接访问或读取
  • 哪怕在子进程中定义全局变量存储退出状态,对父进程来说也毫无意义,因为两者的数据空间完全隔离

因此 :要获取子进程的退出状态(如:退出码)、终止信号等关键信息,必须依赖操作系统提供的系统调用(如:waitwaitpid)------ 这是父子进程间间接传递 "终止信息" 的唯一合法途径,也是我们必须使用这类接口的根本原因。


那么,waitwaitpid 是如何突破进程独立性,拿到这些信息的呢?

要解答这个问题,我们需要先搞清楚:子进程的终止信息究竟存储在哪里?

这里就关联到我们之前学过的 "僵尸状态(Zombie)":

  • 当子进程终止后,其用户态的代码、数据等资源会被立即释放,但内核会保留它的进程控制块(PCB)
  • PCB 是操作系统管理进程的核心数据结构,其中记录了进程的所有关键元信息 ------ 而子进程的终止信息,恰恰就存储在 PCB 中

我们可以合理推测,子进程的 PCB 中必然包含类似 int exit_code(存储正常退出码)、int exit_signal(存储异常终止信号编号)这样的字段:

  • 若子进程正常退出(如调用 main 函数返回 0exit(1)),exit_code 会被赋值为对应的退出码,exit_signal 则置为 0(表示无异常信号)
  • 若子进程异常终止(如被 SIGSEGV 信号杀死),exit_signal 会被赋值为触发终止的信号编号,exit_code 则失去实际意义

当父进程调用 waitpid 时,整个信息传递流程是这样的:

  • 内核查找目标子进程:父进程通过 waitpid 的第一个参数(pid)指定要等待的子进程,操作系统内核在进程表中找到该子进程的 PCB(此时子进程通常已处于僵尸状态)
  • 提取 PCB 中的终止信息:内核从该 PCB 中读取 exit_code、exit_signal 等数据,再结合 Core Dump 标志等信息,按照预设的 "位图规则"(即:status 的 32 位结构)进行整合
  • 写入父进程的地址空间:父进程调用 waitpid 时,会传入第二个参数 status(一个指向 int 类型的指针)。内核会将整合后的终止信息,直接写入 status 指向的内存地址 ------ 这个地址属于父进程的地址空间,因此父进程可以直接读取
  • 释放子进程 PCB:信息传递完成后,内核会彻底释放子进程的 PCB 资源,子进程从僵尸状态转为 "彻底消亡",不再占用系统资源

简单来说waitwaitpid 的本质,是通过操作系统内核作为 "中介":

  • 先从子进程残留的 PCB 中提取终止信息,再按照约定格式写入父进程的内存空间,最终实现了 "跨进程信息传递" 与 "子进程资源回收" 的双重目标
  • 这也解释了为何只有系统调用能完成这项工作 ------ 因为只有内核拥有访问所有进程 PCB 的权限,能打破进程独立性的限制

8. 如何理解第三个参数options?

好,现在我们来介绍 waitpid 系统调用的第三个参数 ------options 的作用。

options 参数用于控制 waitpid 的等待行为,其默认值为 0。当options = 0 时,waitpid 会进入阻塞等待状态:

  • 父进程调用 waitpid 后会立即暂停执行,直到目标子进程终止,waitpid 才会返回并继续执行父进程的后续逻辑
  • options 可以通过 "按位或(|)" 组合多个选项,但多数选项在日常开发中使用频率较低,其中最核心且必须掌握的是 WNOHANG 选项(注意:全称 "Without Hanging")

WNOHANG 选项的核心作用是实现非阻塞等待

  • 如果调用 waitpid 时,目标子进程尚未终止,函数不会阻塞父进程,而是会立即返回 0,让父进程可以继续执行其他任务
  • 只有当子进程已经终止时,waitpid 才会返回子进程的 PID(表示成功回收)

举个直观的对比:

  • 默认阻塞等待(options=0 :父进程调用 waitpid 后 "停在原地",什么都不做,直到子进程结束才 "醒过来"
  • 非阻塞等待(options=WNOHANG :父进程调用 waitpid 后 "问一句" 子进程是否结束 ------ 没结束就先去忙别的,过一会儿再回来 "问",直到子进程结束再回收资源

这里需要特别说明:

  • 阻塞调用的逻辑非常直观:

    • "先等任务完成,再做后续处理",这符合大多数程序的执行逻辑(比如:父进程必须等子进程处理完数据,才能继续汇总结果)
    • 它也是计算机领域最基础、最常见的调用方式之一,无需额外的轮询逻辑,代码实现更简洁、稳定
  • 非阻塞调用则多用于需要 "并行处理多个任务" 的场景:

    • 比如父进程既要等待子进程,又要同时处理用户输入、网络请求等,此时非阻塞等待能避免父进程因 "死等子进程" 而卡住其他逻辑

小案例:sleep 7

当我们在终端执行 sleep 7 命令时,sleep 会作为一个独立的进程运行。

此时,有个现象值得关注:

  • sleep 运行期间,如果我们输入 lspwd 等其他命令,终端(由 bash shell 管理)会没有任何响应
  • 这是因为 sleep 进程的父进程是 bash,而 bash 此时处于 "阻塞等待" 状态 ------ 它会一直等待 sleep 进程执行完毕,才会重新回到 "可接收新命令" 的状态

具体来说,bash 执行 sleep 7 时,会创建子进程运行 sleep,然后自身进入阻塞态:

  • 不再监听终端输入、也不处理新的命令请求
  • 只有当 sleep 进程结束(比如:等待 7秒自然结束,或被信号终止),bash 才会从阻塞中恢复,重新接管终端,此时输入 lspwd 才会得到响应

小故事:兄弟你饿了吗

下面我们用一个生活化的小故事,来通俗解释 "阻塞式调用""非阻塞式调用" 的区别:

场景一:非阻塞轮询调用 ------ 为问 C 语言重点反复打电话

你是个爱贪玩的大学生,结课最后一节 C 语言课也翘了,眼看要考试,只能求助学霸张三 ------ 他的笔记里一定有考试重点。

你跑到张三寝室楼下,拨通了他的电话:"张三,你饿不饿?我请你吃午饭,顺便把 C 语言笔记也带着!"

张三在电话那头说:"没问题!但我正在写一道高数题,写完就下楼,你稍等会儿。"

挂了电话,你没耐心站着等,每隔 3 分钟就打一次电话追问:

  • 第一次:"题写完没?" → 张三:"还没,再等下!"(挂电话)
  • 第二次:"出门了吗?" → 张三:"快了,正在找笔记本!"(挂电话)
  • 第三次:"下楼了没?" → 张三:"马上!刚出寝室的门!"(挂电话)

直到第 N 次追问后,张三终于出现在楼下 ------ 你这才停止打电话,跟着他去吃饭、抄笔记。


故事对应系统调用逻辑:

  • :对应「父进程」,需要通过 "询问" 获取 "结果"(张三的笔记 = 子进程的退出状态
  • 张三:对应「子进程」,正在执行 "任务"(写高数题 = 子进程处理业务逻辑),完成后才会 "响应"
  • 打电话询问:对应「系统调用(如:waitpid)」,是父进程获取子进程状态的方式

这种 "打一次电话问一句、没结果就挂掉,过会儿再打" 的行为,就是非阻塞轮询调用

  • 父进程调用 waitpid 时带上 WNOHANG 选项,就像 "打一次电话"------ 如果子进程没结束(张三没写完题),waitpid 会立即返回 0(相当于 "还没好,挂了"),父进程可以去做别的事(比如:刷会儿手机)
  • 之后再循环调用 waitpid 重复询问("再打一次电话"),直到子进程结束(张三下楼),waitpid 才返回子进程 PID(表示 "任务完成,可回收")

场景二:阻塞式调用 ------ 为等高数笔记抱着电话不撒手

你 C 语言侥幸考了 60 分,可两天后又要考高数了,你对高数是一窍不通。这次你又想到了张三,赶紧跑到他楼下打电话:"好哥们,你饿没?带高数笔记下来,我请你去吃 KFC!"

张三说:"OK!但我现在正在打游戏,得把这把打完才能走。" 听到张三正在打游戏,你赶紧说:"我不挂电话了,你那边也别挂了!你打你的游戏,我就在电话这边听着你打游戏。"

于是,你抱着手机一动不动,听着电话那头的游戏音效,既不挂电话,也不做别的事 ------ 直到 15 分钟后张三喊 "游戏结束!我下楼了",你才挂掉电话,等着他下来。


故事对应系统调用逻辑:

这种 "打一次电话就抱着不撒手,直到对方完成任务才挂" 的行为,就是阻塞式调用

  • 父进程调用 waitpid 时如果 options=0(默认),就像 "不挂电话等张三"------ 父进程会立即暂停所有逻辑(不刷手机、不做别的),进入 "阻塞状态"
  • 直到子进程结束(张三打完游戏),waitpid 才会返回子进程 PID(表示 "任务完成,可回收"),父进程才继续执行后续代码(比如:去吃 KFC)

总结:两种调用的核心区别

类型 核心特点 对应场景举例 系统调用实现
阻塞式调用 调用后暂停等待 直到目标任务完成才返回 等张三打完游戏再走 (必须等结果) waitpid(pid, &status, 0)
非阻塞轮询调用 调用后立即返回 无结果则循环重复询问 隔 3 分钟问一次张三是否写完题 (可忙别的) while(waitpid(..., WNOHANG)==0);

而我们之前学的 wait 系统调用,本质就是 "默认阻塞的 waitpid"------ 它会一直等子进程结束,属于典型的阻塞式调用。

9. 为什么非阻塞轮询调用的效率更高?

在第一次找张三复习 C 语言时,你采用的 "非阻塞轮询" 方式中,每次挂断电话到下一次拨打电话的间隙,你处于完全空闲的状态 ------ 那么这段时间里,你其实可以做很多事情。

  • 关键在于,非阻塞调用的核心优势是不 "绑架" 调用方的时间
  • 你给张三打完电话后,不需要站在原地傻等,可以转身去做自己的事 ------ 比如:去便利店买瓶水、甚至刷几道简单的选择题预热复习
  • 这些事情和张三 "写高数题" 的过程互不干扰,你不需要等张三结束才能行动
  • 从进程调度的角度看,这就相当于父进程与子进程在并发运行
  • 父进程(你)调用非阻塞的waitpid后,没有被 "卡住",可以继续执行自己的逻辑(做其他事)
  • 子进程(张三)则在独立执行任务(写题)。两者的动作在时间上有重叠,而非 "父进程停摆,只等子进程"

这正是非阻塞调用 "效率更高" 的本质原因:

  • 它的高效并非能让子进程的任务(张三写题)变快,而是充分利用了原本会被 "空等" 浪费的时间。原本阻塞等待时,父进程只能闲置
  • 而非阻塞轮询时,父进程可以在间隙中处理其他任务 ------ 单位时间内完成的事情变多了,整体效率自然就提升了

10. 怎么使用代码实现非阻塞轮询调用?

非阻塞轮询调用1.0

cpp 复制代码
#include <stdio.h>      // 包含printf等输入输出函数
#include <stdlib.h>     // 包含exit函数
#include <unistd.h>     // 包含sleep、getpid等函数
#include <sys/types.h>  // 包含pid_t类型定义
#include <sys/wait.h>   // 包含waitpid函数

int main()
{
    //1.创建子进程
    pid_t id = fork();

    //2.处理fork失败的情况
    if (id < 0)
    {
        perror("fork failed");
        return 1;
    }

    //3.子进程执行逻辑
    else if (id == 0)
    {
        //3.1:控制子进程运行次数
        int cnt = 3;  
        while (cnt > 0)
        {
            //第一步:打印子进程身份信息
            printf("我是子进程,pid: %d, ppid: %d, 剩余运行次数: %d\n",getpid(), getppid(), cnt);

            //第二步:子进程休眠1秒
            sleep(1); 

            //第三步:计数器减 1
            cnt--;
        }

        //3.2:子进程正常退出,退出码为10
        printf("子进程即将退出\n");
        exit(10);
    }

    //4.父进程执行逻辑:采用非阻塞轮询方式等待子进程状态变化
    else
    {
        // 无限循环实现轮询:持续检查子进程状态直到回收完成或出错
        while (1)
        {
            //4.1:定义status变量,用于存储子进程的退出状态信息(输出型参数)
            int status = 0;

            //4.2:调用waitpid进行非阻塞等待
            pid_t rid = waitpid(id, &status, WNOHANG);
            /* 参数详解:
            *      1. id:指定要等待的子进程PID(精准等待目标子进程)
            *      2. &status:指向存储状态信息的变量地址,内核会将子进程退出信息写入此处
            *      3. WNOHANG:非阻塞选项(Without Hanging)
            *               - 若子进程未退出,waitpid立即返回0,不阻塞父进程
            *               - 若子进程已退出,返回子进程PID;若出错,返回-1
            */

            /*--------------------------------------- 情况一 ----------------------------------------*/
            //情况1:rid > 0 表示成功回收已终止的子进程(返回值为子进程PID)
            if (rid > 0)
            {
                //1.1:解析子进程的退出码:
                int exit_code = (status >> 8) & 0xFF;

                //1.2:解析子进程的终止信号:
                int exit_signal = status & 0x7F;

                //1.3:打印回收成功的基本信息
                printf("父进程:成功回收子进程,子进程pid: %d\n", rid);
                printf("父进程:子进程退出码: %d,终止信号: %d\n",exit_code, exit_signal);

                //1.4:根据终止信号判断子进程退出方式
                if (exit_signal == 0)
                {
                    // 终止信号为0:表示子进程正常退出(无异常信号)
                    printf("父进程:子进程正常退出\n");
                }
                else
                {
                    // 终止信号非0:表示子进程因收到该信号而异常终止
                    printf("父进程:子进程因信号%d异常终止\n", exit_signal);
                }

                break;  // 子进程已回收,跳出轮询循环
            }

            /*--------------------------------------- 情况二 ----------------------------------------*/
            //情况2:rid == 0 表示子进程尚未终止,非阻塞调用立即返回
            else if (rid == 0)
            {
                //2.1:提示子进程仍在运行,父进程可利用间隙处理其他任务
                printf("父进程:子进程尚未退出,继续等待...(当前可处理其他任务)\n");

                //2.2:休眠1秒降低轮询频率,避免频繁调用waitpid占用过多CPU资源
                sleep(1); 
            }

            /*--------------------------------------- 情况三 ----------------------------------------*/
            //情况3:rid == -1 表示waitpid调用失败(如:子进程不存在、被信号中断等)
            else
            {
                perror("父进程:等待子进程失败");
                break; 
            }
        }
    }

    //5.父进程完成所有工作后退出
    return 0;
}

非阻塞轮询调用2.0

cpp 复制代码
#include <stdio.h>      // 标准输入输出函数,如 printf
#include <errno.h>      // 错误码相关定义,如 errno
#include <string.h>     // 字符串处理函数,如 strerror
#include <stdlib.h>     // 通用函数,如 exit
#include <unistd.h>     // 系统调用函数,如 fork、sleep、getpid、getppid
#include <sys/types.h>  // 数据类型定义,如 pid_t
#include <sys/wait.h>   // 进程等待相关函数,如 waitpid

//1.定义函数指针类型 func_t,指向无参数、无返回值的函数
typedef void (*func_t)();

//2.函数指针数组,用于存储注册的任务函数
#define NUM 5   // 定义任务数量为 5
func_t handlers[NUM + 1];

//3.实现任务函数
//3.1:下载任务函数
void DownLoad()
{
    printf("我是一个下载的任务...\n");
}

//3.2:刷新任务函数
void Flush()
{
    printf("我是一个刷新的任务...\n");
}

//3.3:日志记录任务函数
void Log()
{
    printf("我是一个记录日志的任务...\n");
}


//4.注册任务函数到 handlers 数组
void registerHandler(func_t h[], func_t f)
{
    int i = 0;

    //4.1:查找 handlers 数组中第一个空位置
    for (; i < NUM; i++)
    {
        if (h[i] == NULL) break;
    }

    //4.2:如果数组已满,直接返回
    if (i == NUM) return;
    
    //4.3:将函数 f 注册到找到的空位置
    h[i] = f;
    
    //4.4:标记下一个位置为空(方便后续查找)
    h[i + 1] = NULL;
}

int main()
{
    //1.注册三个任务函数到 handlers 数组
    registerHandler(handlers, DownLoad);
    registerHandler(handlers, Flush);
    registerHandler(handlers, Log);

    //2.创建子进程
    pid_t id = fork();

    //3.子进程执行区域
    if (id == 0)
    {
        int cnt = 3;
        while (1)
        {
            printf("我是一个子进程,pid:%d, ppid:%d\n", getpid(), getppid());

            sleep(1);  

            cnt--;
        }
        exit(10);  // 子进程正常退出,退出码为 10
    }

    //4.父进程执行区域
    while (1)
    {
        //4.1:定义status变量,用于存储子进程的退出状态信息(输出型参数)
        int status = 0;
        //4.2:非阻塞等待子进程退出,WNOHANG 表示子进程未退出时立即返回 0
        pid_t rid = waitpid(id, &status, WNOHANG);

        //情况一:waitpid 成功,子进程已退出,打印子进程 PID、退出码、终止信号
        if (rid > 0)
        {
            printf("wait success, rid: %d, exit code: %d, exit signal: %d\n",rid, (status >> 8) & 0xFF, status & 0x7F);
            break;  
        }

        //情况二:waitpid 返回 0,子进程未退出,执行注册的任务函数
        else if (rid == 0)
        {
            //1)遍历函数指针数组 handlers,执行所有已注册的任务函数)
            for (int i = 0; handlers[i]; i++) //循环条件 handlers[i]:当数组元素为 NULL 时停止遍历(NULL 是数组结束的标志)
            {
                //注意:通过函数指针调用任务函数(回调机制)
                handlers[i]();
            }

            //2)打印提示信息,说明本轮非阻塞等待的结果:子进程仍在运行
            printf("本轮调用结束,子进程没有退出\n");

            //3)父进程休眠 1 秒,降低轮询频率
            sleep(1);
        }

        //情况三:waitpid 调用失败,打印提示信息
        else
        {
            printf("等待失败\n");
            break; 
        }
    }

    return 0;
}
相关推荐
zfj3212 小时前
top 命令中的 wa (IO wait) 指标,理论上几乎完全是由磁盘IO(包括swap)引起的,而不是网络IO
linux·网络·top·iowait
Xの哲學2 小时前
Linux网卡注册流程深度解析: 从硬件探测到网络栈
linux·服务器·网络·算法·边缘计算
用户6135411460162 小时前
libicu-62.1-6.ky10.x86_64.rpm 安装步骤详解(麒麟V10系统)
linux
l木本I2 小时前
Reinforcement Learning for VLA(强化学习+VLA)
c++·人工智能·python·机器学习·机器人
strive programming3 小时前
Effective C++_异常(解剖挖掘)
c++
wregjru3 小时前
【读书笔记】Effective C++ 条款1~2 核心编程准则
java·开发语言·c++
lingran__3 小时前
C语言自定义类型详解 (1.1w字版)
c语言·开发语言
秋4274 小时前
防火墙基本介绍与使用
linux·网络协议·安全·网络安全·架构·系统安全
取加若则_4 小时前
深入解析Linux进程优先级机制
linux·服务器