【Linux】 进程控制(二):进程等待(wait/waitpid) 与 子进程获取(status)

引言

在Linux进程管理中,子进程退出后若父进程未及时回收其资源,会产生僵尸进程并引发内存泄漏问题,而waitwaitpid作为核心的进程等待系统调用,不仅能解决僵尸进程问题,还能让父进程获取子进程的退出状态;其中wait仅支持阻塞等待任意子进程,waitpid则扩展了指定子进程等待、非阻塞等待等能力,本文将详细讲解进程等待的必要性、两种等待方法的使用及核心原理,帮助理解Linux进程资源回收的底层逻辑。


目录

一、进程等待的必要性

  1. 子进程退出,父进程如果不管不顾,就可能造成'僵尸进程'的问题,进而造成内存泄漏。
  2. 另外,进程一旦变成僵尸状态,那就刀枪不入,"杀人不眨眼"的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  3. 最后,父进程派给子进程的任务完成的如何,我们需要知道。

如,⼦进程运⾏完成,结果对还是不对,或者是否正常退出。

  1. ⽗进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

二、进程等待的方法

当子进程执行完毕后,如果父进程没有及时回收(例如父进程处于死循环中),子进程会进入僵尸状态(Z状态),等待父进程读取其退出状态。僵尸进程无法被杀死,其占用的内核资源(如进程描述符)会一直保留,导致资源泄露。因此,我们需要通过进程等待(如 wait() 或 waitpid())来回收僵尸进程。

  • 如果父进程先于子进程结束,子进程会被 init 进程(PID=1)接管并自动回收,不会永久僵尸。
  • 僵尸进程的清理必须由父进程完成,这是 Unix/Linux 进程管理的设计特点。

接下来讲解两种进程等待的方法!


2.1 wait

2.1.1 认识wati
  1. 调用的时候需要包含头文件#include <sys/types.h>#include <sys/wait.h>
  2. 使用 wait()waitpid() 系统调用。如果不需要知道子进程的死亡原因,直接 wait(NULL) 即可完成回收;如果需要处理退出状态,则使用 wait(&status) 获取详细信息。
  3. wait 的返回值是判断等待子进程是否成功的核心依据:若等待成功,返回子进程的 PID (大于 0);单进程场景下,可直接对比该返回值与 fork 给父进程的子进程 PID 来验证匹配性(因为fork给父进程返回的是子进程的pid )。若当前进程无任何子进程,wait 调用直接失败,返回 -1

记住:好父进程,从不留下僵尸孩子

2.1.2 wait的使用

代码演示

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    pid_t pid = fork();
    if (pid < 0) 
    {
        perror("fork failed");  // fork失败时打印错误信息
        return 1;
    }

    // 子进程逻辑
    if (pid == 0) 
    {
        int cnt;  // 把循环变量声明移到循环外,兼容C89标准
        for (cnt = 2; cnt > 0; cnt--) 
        {
            // 打印子进程PID、父进程PID、循环计数
            printf("子进程 pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt);
            sleep(1);  // 子进程每次循环休眠1秒
        }
        exit(0);  // 子进程正常退出
    }

    // 父进程逻辑
    sleep(4);  // 父进程休眠4秒,确保子进程先运行完毕
    pid_t ret = wait(NULL);  // 回收子进程资源(不关心退出状态)
    // 打印回收结果:成功则输出子进程PID,失败则提示错误
    if (ret == pid) 
    {
        printf("父进程成功回收子进程:%d\n", ret);
    } 
    else 
    {
        printf("回收子进程失败,wait返回值:%d\n", ret);
    }
    sleep(2);  // 父进程最后休眠2秒,观察输出

    return 0;
}

我们可以观察到这样的进程状态变化过程:

  • 前 2 秒内,子进程处于运行状态(循环打印信息并休眠),父进程则进入 4 秒的休眠阶段,二者均处于活跃状态;
  • 2 秒后子进程执行完循环逻辑并退出,但此时父进程仍在休眠中,无法立即调用wait()系统调用回收子进程资源,因此子进程会转变为僵尸进程(Zombie Process)(仅保留 PID 等内核级信息,用户级资源已释放);
  • 又过 2 秒后,父进程的休眠周期结束,随即执行wait()操作并成功回收该子进程 ------ 此时子进程的所有内核资源被彻底释放,僵尸进程状态消失;
  • 父进程在完成子进程回收后,会继续执行 2 秒的休眠逻辑,因此这一阶段能观察到系统中仅有父进程处于运行状态的场景。

思考:

当我们让子进程进入死循环无法退出时,父进程调用wait()后会一直停在该调用处,持续等待子进程退出事件的发生;在此期间父进程完全无法执行后续任何任务,这种现象在操作系统中被称为什么?

wait()的本质不是"轮询检测状态",而是阻塞等待子进程退出的内核事件(无轮询、不占用CPU)。

这种现象称之为阻塞状态,接下来让我们来看看如何解决这种状态!


2.2 waitpid

2.2.1 认识waitpid

waitpid 是 Linux 下用于回收子进程资源 的系统调用,是 wait 函数的增强版。它解决了 wait 的两大痛点:

  1. wait 只能阻塞等待任意一个子进程退出,无法指定子进程;
  2. wait 只能阻塞等待,无法实现非阻塞式查询。

waitpid 既兼容 wait 的所有功能,又支持"指定子进程等待""非阻塞等待"等扩展能力,是进程资源回收的更灵活选择。


2.2.2 使用前提
  1. 头文件(必须包含)
c 复制代码
#include <sys/types.h>   // 提供pid_t等类型定义
#include <sys/wait.h>    // 提供waitpid函数声明和宏定义(如WNOHANG)
  1. 函数原型
c 复制代码
pid_t waitpid(pid_t pid, int *wstatus, int options);

返回值、三个参数的含义和用法是核心,下面逐一拆解:

  • 第一个参数
pid 值 含义
pid > 0 等待PID等于该值的指定子进程(精准回收某个子进程)
pid = -1 等待当前进程的任意一个子进程退出(完全等价于 wait 的行为)
pid = 0 等待和当前进程同进程组的任意子进程(多进程组场景用)
pid < -1 等待进程组ID等于该值绝对值的任意子进程(小众场景,一般不用)
  • 第二个参数
  • 本质是 int* 类型的指针,用于接收子进程的退出状态/终止原因
  • 若传入 NULL:表示不关心子进程的退出信息(和 wait(NULL) 一样);
  • 若传入有效指针:需配合 <sys/wait.h> 中的宏解析状态(后续会讲)。
  • 第三个参数

控制 waitpid 的等待行为,最常用的两个值:

options 值 含义
0 阻塞等待(等价于 wait 的行为):父进程暂停执行,直到指定子进程退出
WNOHANG 非阻塞等待:父进程调用后立即返回,不管子进程是否退出(核心扩展)
  • 返回值
返回值 含义
> 0 成功回收子进程,返回值是被回收子进程的PID
0 仅当 options=WNOHANG 时出现:子进程还在运行,未退出(非阻塞特征)
-1 等待失败(如指定的 pid 不是当前进程的子进程、被信号中断等),同时设置 errno 标识错误原因

2.2.3 waitpid的使用

代码如下:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

// 子进程执行逻辑
void child_task()
{
    for (int cnt = 2; cnt > 0; cnt--)
    {
        printf("子进程 pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt);
        sleep(1);
    }
}

int main()
{
    pid_t pid = fork();
    if (pid < 0) 
    {
        perror("fork失败");
        return 1;
    }

    // 子进程逻辑
    if (pid == 0) 
    {
        child_task();
        exit(0);
    }

    // 父进程逻辑
    sleep(4);  // 等待子进程退出成僵尸进程
    // waitpid(-1, NULL, 0) 等价于 wait(NULL),阻塞等待任意子进程
    pid_t ret = waitpid(-1, NULL, 0);
    
    // 结果判断与打印
    printf(ret == pid ? "父进程成功回收子进程:%d\n" : "回收子进程失败\n", ret);
    sleep(2);

    return 0;
}

1. 前2秒:子进程运行、父进程休眠(二者均存活)

  • 代码执行后,fork()创建子进程:
    • 子进程进入child_task(),循环2次(cnt=2→1),每秒打印一次进程信息,全程占用CPU执行逻辑;
    • 父进程跳过子进程分支后,直接执行sleep(4):此时父进程进入可中断阻塞状态(S状态),放弃CPU使用权,仅等待4秒计时结束,期间不执行任何逻辑。
  • 这2秒内,子进程处于"运行/短暂休眠"状态,父进程处于"4秒休眠的前2秒",系统中能看到父子两个进程均为活跃状态(无僵尸进程)。
  1. 第2~4秒:子进程退出→变为僵尸进程
  • 子进程完成2次循环后调用exit(0)
    • 子进程的用户级资源(代码、数据、文件描述符等)会立即释放;
    • 但Linux为了让父进程获取子进程的退出状态,会保留子进程的内核级资源 (PID、退出码、进程状态等),此时子进程变为僵尸进程(Z状态) (用ps命令可看到状态为Z+)。
  • 此时父进程仍在sleep(4)的后2秒休眠中,无法执行waitpid回收子进程,因此僵尸进程会持续存在。
  1. 第4秒后:父进程回收子进程→仅父进程运行
  • 父进程4秒休眠结束,执行waitpid(-1, NULL, 0)
    • waitpid阻塞式系统调用,但此时子进程已退出,因此会立即回收子进程的内核级资源(PID被释放、僵尸状态消失);
    • 父进程打印"回收成功"的提示,证明子进程已被彻底清理。
  • 最后父进程执行sleep(2):此时子进程已完全消失,系统中仅父进程处于阻塞休眠状态,因此能观察到"只有父进程运行"的场景;2秒后父进程休眠结束,代码执行完毕退出。
2.2.4获取退出信息
  • 我们首先先来研究一下status,在这里我们只研究低16比特位,高16比特位目前不关心。

status是一个整型变量,但status不能简单的当作整型来看待,status的不同比特位所代表的信息不同

在status的低16比特位当中,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,则低7位表示终止信号,而第8位比特位是core dump标志(也不用关心)。

复制代码
exitSignal = status & 0x7F;      //退出信号
  • 0x7F 换算成十进制是 127,转化为二进制为低 7 个比特位上都为 1(高位均为 0),可以通过按位与操作将 status 的低 7 位(0~6 位) 提取出来,得到子进程的退出信号。

    exitCode = (status >> 8) & 0xFF; //退出码

  • 将 status右移 8 位,把原本存储在 8 ~ 15 位的退出码移动到 0~7 位(低 8 位),再与 0xFF 执行按位与操作(0xFF 二进制是低 8 位全为 1),过滤高位后即可提取出子进程的退出码。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

void RunChild()
{
  int cnt = 2;
  while(cnt)
  {
    printf("子进程 pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
    cnt--;
    sleep(1);
  }
}

int main()
{
  pid_t id = fork();
  if(id < 0)
  {
    perror("fork失败");  // 补充文案,明确错误类型
    return 1;
  }
  else if(id == 0)
  {
    RunChild();
    exit(2);
  }
  else 
  {
    sleep(4);
    int status = 0;
    pid_t ret = waitpid(-1, &status, 0);
    
    // 优化:先判断waitpid是否失败(-1),覆盖更多错误场景
    if(ret == -1)
    {
      perror("waitpid失败");  // 精准打印错误原因
      return 1;
    }
    else if(ret == id)
    {
      // 提取退出信号/退出码,用临时变量简化打印行,提升可读性
      int exit_signal = status & 0x7F;
      int exit_code = (status >> 8) & 0xFF;
      printf("父进程成功回收子进程: %d, 退出信号: %d, 退出码: %d\n", ret, exit_signal, exit_code);
    }
    else
    {
      printf("等待子进程失败,回收的PID: %d\n", ret);  // 补充错误PID,便于排查
    }
    sleep(2);
  }
  return 0;
}

对于此,系统当中提供了两个宏来获取退出码和退出信号。

WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号。
WEXITSTATUS(status):用于获取进程的退出码。

思考:思考:子进程退出后,父进程为何无法直接读取其退出状态、资源使用等数据,必须通过wait/waitpid等系统调用才能获取?

  1. 进程隔离:父子进程拥有独立地址空间,子进程的退出状态(退出码、终止信号)存在于内核维护的子进程专属数据结构中,父进程作为用户态程序,无权直接访问内核态的这些私有数据;
  2. 内核资源管理 :子进程退出后会变为僵尸进程,其退出状态仅暂存于内核进程表中,需通过wait/waitpid触发内核操作,才能将状态从内核态拷贝到父进程的用户态内存,同时完成僵尸进程的资源回收。

简单来说,wait/waitpid是内核提供的"中介接口",既解决了进程隔离导致的访问限制,又能让内核统一管理子进程的退出资源。


三、非阻塞轮询

  1. 上述所给例子中,当子进程未退出时,父进程都在一直等待子进程退出,在等待期间,父进程不能做任何事情,这种等待叫做阻塞等待。

  2. 实际上我们可以让父进程不要一直等待子进程退出,而是当子进程未退出时父进程可以做一些自己的事情,当子进程退出时再读取子进程的退出信息,即非阻塞等待。

  3. 做法很简单,向waitpid函数的第三个参数potions传入WNOHANG,这样一来,等待的子进程若是没有结束,那么waitpid函数将直接返回0,不予以等待。而等待的子进程若是正常结束,则返回该子进程的pid。

  4. 例如,父进程可以隔一段时间调用一次waitpid函数,若是等待的子进程尚未退出,则父进程可以先去做一些其他事,过一段时间再调用waitpid函数读取子进程的退出信息。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
	pid_t id = fork();
	if (id == 0){
		//child
		int count = 3;
		while (count--){
			printf("child do something...PID:%d, PPID:%d\n", getpid(), getppid());
			sleep(3);
		}
		exit(0);
	}
	//father
	while (1){
		int status = 0;
		pid_t ret = waitpid(id, &status, WNOHANG);
		if (ret > 0){
			printf("wait child success...\n");
			printf("exit code:%d\n", WEXITSTATUS(status));
			break;
		}
		else if (ret == 0){
			printf("father do other things...\n");
			sleep(1);
		}
		else{
			printf("waitpid error...\n");
			break;
		}
	}
	return 0;
}

运行结果就是,父进程每隔一段时间就去查看子进程是否退出,若未退出,则父进程先去忙自己的事情,过一段时间再来查看,直到子进程退出后读取子进程的退出信息。


总结

进程等待是Linux系统中回收子进程资源、获取退出状态的核心机制,wait通过阻塞等待实现基础的子进程回收,而waitpid作为增强版,支持指定子进程、阻塞/非阻塞两种等待模式,既解决了wait的功能局限,也通过非阻塞轮询让父进程在等待期间可执行自身任务;从底层来看,父进程必须通过wait/waitpid系统调用才能获取子进程退出状态,本质是进程隔离机制和内核资源管理规则的要求------系统调用作为内核中介,既打破了父子进程的地址空间隔离,又能完成僵尸进程的资源回收,避免了资源泄漏和状态访问异常的问题。


✨ 坚持用 清晰易懂的图解 + 代码语言, 让每个知识点都 简单直观 !

🚀 个人主页不呆头 · CSDN

🌱 代码仓库不呆头 · Gitee

📌 专栏系列

💬 座右铭 : "不患无位,患所以立。"

相关推荐
秦少游在淮海8 小时前
网络缓冲区 · 通过读写偏移量维护数据区间的高效“零拷贝” Buffer 设计
linux·开发语言·网络·tcp协议·muduo·网络缓冲区
炮院李教员8 小时前
Ubuntu 24.04 安装common-extensions
linux·运维·ubuntu
YJlio8 小时前
ZoomIt 学习笔记(11.9):绘图模式——演示时“手写板”:标注、圈画、临时白板
服务器·笔记·学习
满天星83035778 小时前
【Linux】信号(下)
android·linux·运维·服务器·开发语言·性能优化
拾贰_C8 小时前
【Ubuntu】怎么查询Nvidia显卡信息
linux·运维·ubuntu
濊繵8 小时前
Linux网络--IP 分片和组装的具体过程
linux·网络·tcp/ip
牛老师讲GIS8 小时前
2025年前端开发的未来:服务器优先、人工智能驱动、更贴近底层
运维·服务器·人工智能
百锦再8 小时前
【无标题】
服务器·ai·k8s·京东云·core·net·云鼎
苹果醋38 小时前
JAVA设计模式之观察者模式
java·运维·spring boot·mysql·nginx