【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

📌 专栏系列

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

相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质3 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式