当前位置: 首页 > news >正文

进程等待(解决僵尸进程)

 一、进程等待

1.1 进程等待的必要性

其中 , 回收子进程资源是最重要的 , 获取子进程的退出信息是可选的(可能只希望它执行,而不需要它执行后的信息)!!!

通过父进程等待解决僵尸进程!!!

1.2 进程等待的两种方法

Linux系统提供了 wait 和 waitpid 两种函数来实现进程等待 。

在介绍进程等待之前  , 可能会疑惑 , 为什么进程等待能够解决僵尸进程 ?

首先 , 需要明确僵尸进程的本质 , 再分析进程等待的作用机制!!!

1)僵尸进程的本质 :

当子进程执行完毕后,它的代码、数据等资源会被释放,但进程控制块(PCB)会暂时保留(PCB 中记录了进程的退出状态、PID 等关键信息)。此时子进程就变成了 “僵尸进程”—— 它已经 “死亡”,但 PCB 还在系统中占用资源。

如果大量僵尸进程存在,会导致系统进程表项被耗尽,无法创建新进程,引发资源泄漏

2)进程等待的作用机制

父进程通过waitwaitpid函数进行进程等待,主要做两件事:

  1. 回收子进程的 PCB 资源:父进程等待子进程退出后,会从系统中移除子进程的 PCB,彻底释放它占用的进程表项等资源。
  2. 获取子进程的退出信息:父进程可以通过status参数获取子进程的退出码、终止信号等信息,从而判断子进程的执行结果。

3)进程等待如何解决僵尸进程

僵尸进程的核心问题是 PCB 未被回收 。而进程等待的过程,就是父进程主动去回收子进程 PCB 的过程。

当父进程调用wait/waitpid时:

  • 如果子进程已经是僵尸进程,父进程会立即回收它的 PCB,僵尸进程被彻底清除。
  • 如果子进程还在运行,父进程会阻塞等待(或非阻塞轮询),直到子进程退出后再回收 PCB,避免子进程变成僵尸进程。

1.2.1 wait方法

解决僵尸进程:

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{pid_t id = fork();if (id == 0){// 子进程int cnt = 5;while (cnt){printf("我是一个子进程,pid: %d ,ppid: %d\n", getpid(), getppid());sleep(1);cnt--;}exit(0);}// 父进程sleep(10);pid_t rid = wait(NULL);if (rid > 0){printf("wait success,rid: %d\n", rid);}sleep(10);return 0;
}

代码效果解释:

1.  进程创建

  • 程序运行后,通过 fork() 创建一个子进程。fork() 调用成功后,父进程和子进程会同时执行后续代码。
  • 子进程中 fork() 返回 0,父进程中 fork() 返回子进程的 PID。

 

2. 子进程行为

  • 子进程进入 while 循环,循环 5 次。每次循环打印子进程的 PID 和父进程的 PID(通过 getpid() 和 getppid() 获取),然后睡眠 1 秒,cnt 递减。
  • 循环结束后,子进程通过 exit(0) 正常退出。

3. 父进程行为

  • 父进程先执行 sleep(10),睡眠 10 秒。
  • 睡眠结束后,调用 wait(NULL) 等待子进程退出wait(NULL) 会阻塞父进程,直到子进程退出。
  • 子进程退出后,wait(NULL) 返回子进程的 PID,父进程打印 “wait success, rid: 子进程 PID”。

相关函数解释:

1. fork() 函数

  • 用于创建子进程,是 Linux 中进程创建的核心函数。
  • 调用后,父进程和子进程拥有独立的地址空间,代码从 fork() 之后开始各自执行。
  • 子进程会复制父进程的代码、数据等资源(通过写时拷贝技术优化)。

2.  进程 ID 相关函数

1)getpid():获取当前进程的 PID。

2)getppid():获取当前进程的父进程 PID。

3.  进程退出函数 exit()

1)用于使进程正常退出,参数为退出码(0 表示正常退出,非 0 表示异常退出)。

2)进程退出时,内核会回收其大部分资源,但进程控制块(PCB)会暂时保留,直到父进程进行进程等待。

4. 进程等待函数 wait()

1)父进程调用 wait(NULL) 会阻塞自己,直到子进程退出。

2) wait() 返回子进程的 PID,用于确认是哪个子进程退出。

3) 参数 NULL 表示不需要获取子进程的退出状态(若需要,可通过非 NULL 的 int* 指针获取)。

4) 进程等待的核心作用是回收子进程的 PCB 资源,避免僵尸进程的产生。

5. 僵尸进程

1) 子进程退出后,若父进程未及时调用 wait() 或 waitpid() 进行进程等待,子进程的 PCB 会残留于系统中,形成僵尸进程。

2) 僵尸进程会占用系统的进程表项资源,若大量存在会导致系统无法创建新进程。本代码中,父进程通过 wait(NULL) 解决了僵尸进程问题。

6.  阻塞与睡眠

1)sleep(10) 使父进程睡眠 10 秒,此期间父进程不执行任何操作。

2)wait(NULL) 使父进程阻塞,直到子进程退出才继续执行。两者都是使进程暂停执行的方式,但 wait() 是为了等待子进程事件,sleep() 是单纯的时间延迟。

1.2.2 waitpid方法

什么情况下,wait() 或者 waitpid() 会失败?

1)wait() 失败:父进程没有子进程 , wait就会失败

2)  waitpid() 失败 : PID的值传错了

status:

  • wait 和 waitpid ,都有一个status参数 , 该参数是一个输出型参数 , 由操作系统填充。
  • 如果传递NULL , 表示不关心子进程的退出状态信息
  • 否则 , 操作系统会根据该参数,将子进程的退出信息反馈给父进程
  • status不能简单的当作整型来看待 , 可以当作位图来看待,具体细节如下图(只研究status低16比特位):

位区间(低 16 位)含义
第 0~6 位终止信号(若进程是被信号杀死的,这里存信号编号;正常退出则这 7 位全为 0)
第 7 位core dump 标志(进程异常终止时,是否生成核心转储文件)
第 8~15 位退出码(仅当进程 “正常退出” 时有效,即第 0~6 位全为 0 时)

理解正常退出:

当子进程通过 returnexit 或 _exit 正常退出时:

  • 第 0~6 位全为 0(表示没有被信号杀死)。
  • 退出码存在第 8~15 位。

如何拿到退出状态?

将 status 右移 8 位,再与 0xFF(二进制 11111111)按位与,提取第 8~15 位的数值。

理解“异常退出” 

当子进程被信号杀死(比如 kill -9 子进程PID)时:

  • 第 0~6 位存储终止信号的编号(比如9对应SIGKILL信号)。
  • 此时第 8~15 位的退出码 “无意义”,因为进程是被信号强制终止的

如何拿到终止信号?

将 status 与 0X7F(二进制 0000000001111111)按位与,提取第 0~6 位的信号编号。

“退出信息” 为什么不能用全局变量传递?

子进程是通过 fork 创建的,它和父进程之间是 写时拷贝(Copy-On-Write) 的关系 —— 子进程会复制父进程的内存空间,但之后父子进程的内存是独立的(父子进程相互独立)

如果定义一个全局变量 int exicode = 0; 子进程修改它时只会修改自己的副本,父进程无法感知。因此,必须通过操作系统提供的 “进程等待” 机制(wait/waitpid)来获取子进程的退出信息—— 操作系统会统一管理进程的退出状态,父进程通过系统调用从内核中读取这些信息。

1.2.3 如何做到的

  1. 退出信息存储在哪里?

在Linux系统中 , 每个进程都有对应的 "task_struct"结构体 , 里面记录了进程的所有关键信息 , 包括 : 进程(PID) 、 父进程(PPID);退出码(exit_code)、终止信号(exit_signal)等退出信息。

子进程退出后 , 它的task_struct 不会被立即销毁,而是暂存这些退出信息,直到父进程来取走   ​

  1. 父进程怎么拿到这些信息?

因为父进程和子进程是相互独立的进程,父进程不能直接访问子进程的内存或数据。所以必须通过系统调用(比如waitpid)来向操作系统 “请求” 子进程的退出信息 —— 操作系统会从子进程的task_struct中提取这些信息,再返回给父进程。

这和我们调用getpid()getppid()的原理是一样的:这些函数也是通过系统调用,从task_struct中读取进程 ID 信息后返回给我们。

我们通过一段代码来演示这个过程(获取退出信息)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>int main() {pid_t pid = fork();if (pid == 0) {// 子进程:睡眠3秒后正常退出,退出码设为10sleep(3);exit(10);} else {// 父进程:等待子进程退出,并获取退出信息int status;waitpid(pid, &status, 0);// 判断是否正常退出if (WIFEXITED(status)) {// 提取正常退出码int exit_code = WEXITSTATUS(status);printf("子进程正常退出,退出码:%d\n", exit_code);}}return 0;
}

流程拆解:

  1. 子进程执行:子进程调用exit(10)后,退出码10被存入它的task_struct中。
  2. 父进程调用waitpid:父进程通过waitpid(pid, &status, 0)发起系统调用,向操作系统请求子进程的退出信息。
  3. 操作系统提取并返回信息:操作系统从子进程的task_struct中取出退出码10,按照 “位图规则” 填充到status变量中。
  4. 父进程解析status:通过WIFEXITED(status)判断子进程是 “正常退出”,再通过WEXITSTATUS(status)提取出退出码10,最终打印结果。

1.3 关于WIFEXITEDWEXITSTATUS的作用

这两个是系统提供的宏函数,用于简化status的解析逻辑:

  • WIFEXITED(status):判断子进程是否 “正常退出”(即通过returnexit_exit退出,而非被信号杀死)。如果返回true,说明可以安全地提取退出码。
  • WEXITSTATUS(status):在WIFEXITEDtrue时,提取子进程的退出码。

1.4 阻塞与非阻塞等待(option)

1.4.1 生活例子

我们用 “等外卖” 这个场景,就能轻松区分两者的本质差异:

场景阻塞等待(对应waitpid(..., 0)非阻塞等待(对应waitpid(..., WNOHANG)
行为你下单后,放下手机啥也不做,盯着门口等外卖员敲门,期间不能刷视频、做家务。你下单后,继续刷视频、做家务,每隔 5 分钟去门口看一眼 “外卖到了没”,没到就继续忙自己的事。
核心特点“死等”:等待期间无法做其他事,直到目标事件(外卖到)发生。“忙等 + 轮询”:不死等,期间能处理其他任务,通过 “定期检查” 确认目标事件是否发生。
对应程序中的逻辑父进程调用waitpid后,暂停执行,直到子进程退出才继续往下跑。父进程调用waitpid后,不管子进程是否退出都会立即返回,返回后可以去执行其他代码,之后再循环调用waitpid检查子进程状态。

1.4.2 返回值

waitpid的返回值(板书重点)

板书里说 “返回值> 0:等待结束;=0:子进程没退出;<0:等待失败”,对应代码中的三个分支:

  • >0:子进程已退出,返回子进程 PID(等待成功,回收资源)。
  • =0:子进程还在运行,调用成功但没拿到结果(非阻塞的核心,父进程可以去忙别的)。
  • <0:等待出错(比如子进程已经被回收过,或 ID 不存在)。

例如:

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>typedef void (*Func_t)();#define NUM 5
Func_t handlers[NUM + 1];void Download()
{printf("我是一个下载的任务...\n");
}void Flush()
{printf("我是一个刷新的任务...\n");
}void Log()
{printf("我是一个记录日志的任务...\n");
}void RegisterHandler(Func_t h[], Func_t f)
{int i = 0;for (; i < NUM; i++){if (h[i] == NULL) break;}if (i == NUM) return;h[i] = f;h[i + 1] = NULL;
}int main()
{RegisterHandler(handlers, Download);RegisterHandler(handlers, Flush);RegisterHandler(handlers, Log);pid_t id = fork();if (id == 0){int cnt = 3;while (1){sleep(3);printf("我是一个子进程, PID : %d, PPID : %d\n", getpid(), getppid());sleep(1);cnt--;}exit(10);}while (1){int status = 0;pid_t rid = waitpid(id, &status, WNOHANG);if (rid > 0){printf("WAIT SUCCESS, RID: %d, EXIT CODE: %d, EXIT SIGNAL: %d\n", rid, (status >> 8) & 0xFF, status & 0x7F);break;}else if (rid == 0){int i = 0;for (; handlers[i]; i++){handlers[i]();}printf("本轮调用结束,子进程没有退出\n");sleep(1);}else{printf("等待失败\n");break;}}return 0;
}
  1. 父进程非阻塞等待与任务执行模块

  • pid_t rid = waitpid(id, &status, WNOHANG);waitpidWNOHANG选项表示非阻塞等待 —— 如果子进程未退出,waitpid立即返回0;如果子进程已退出,返回子进程PID;如果等待失败,返回-1
  • if (rid > 0):子进程已退出,解析status获取退出码((status >> 8) & 0xFF)和终止信号(status & 0x7F),打印信息后跳出循环。
  • else if (rid == 0):子进程未退出,此时执行handlers数组中的所有任务函数(遍历handlers,依次调用注册的DownloadFlushLog函数),打印 “本轮调用结束,子进程没有退出” 后睡眠 1 秒,继续下一轮循环。
  • else:等待失败,打印 “等待失败” 后跳出循环。

该代码通过函数指针数组实现任务注册与回调,结合进程创建(fork非阻塞进程等待(waitpid+WNOHANG,实现了父进程在等待子进程的同时,周期性执行 “下载、刷新、记录日志” 等任务的功能。核心是利用非阻塞等待的特性,让父进程在子进程运行期间不 “空等”,而是持续执行注册的任务,提高了 CPU 资源的利用率。

1.4.3 非阻塞轮询意义

非阻塞的核心价值:提高 CPU 利用率

板书里用 “打电话” 举例:“非阻塞调用,像打电话催李四(子进程),挂了电话可以做别的,不用一直占线等”。

  • 如果父进程用阻塞等待waitpid(id, &status, 0)),那 3 秒内父进程什么都干不了,只能空等(CPU 空闲浪费)。
  • 非阻塞等待,父进程在这 3 秒里可以处理日志、读写文件、甚至创建其他子进程 —— 充分利用 CPU 资源,这在服务器开发(比如同时处理多个客户端请求)中非常重要。

1.4.4 对比阻塞与非阻塞

维度阻塞等待(options=0非阻塞等待(options=WNOHANG
核心逻辑父进程调用后暂停,直到子进程退出才继续。父进程调用后立即返回,通过循环 “轮询” 子进程状态。
waitpid返回值成功返回子进程 PID,失败返回 - 1(不会返回 0)。成功返回 PID,子进程未退出返回 0,失败返回 - 1。
CPU 利用率低(等待期间 CPU 空闲)。高(等待期间父进程可处理其他任务)。
适用场景父进程不需要做其他事,只需等子进程(比如简单的命令执行)。父进程需要并发处理多个任务(比如服务器同时处理多个客户端)。
代码复杂度简单(不用循环,调用一次即可)。稍复杂(需要循环轮询,处理 3 种返回值情况)。
http://www.dtcms.com/a/557388.html

相关文章:

  • 划时代的协作:GitHub Agent HQ 如何开启AI原生开发新纪元
  • Jmeter基础知识详解
  • 设计素材网站合集手机电子商务网站建设
  • 2025年江西省职业院校技能大赛高职组“区块链技术应用”任务书(3卷)
  • Day75 RS-485 通信协议设计、串口编程与嵌入式系统部署实践
  • 中文编码、乱码问题解析处理
  • 如何设计一款百兆网络监控器H81220S
  • 2025年ASOC SCI2区TOP,双重防御网络阻断模型下的供给路线优化,深度解析+性能实测
  • seo关键词教程国外seo综合查询
  • 郑州网站建设饣汉狮网络wordpress重置主题
  • 算法——二叉树、dfs、bfs、适配器、队列练习
  • Linux_Socket_浅谈UDP
  • dfs|位运算
  • 网站开发内容商用图片的网站
  • 凡客建站免费的可以用多久win优化大师有用吗
  • DevOps的实现路径与关键实践
  • 开发实战 - ego商城 - 6 购物车模块
  • 虚幻引擎5 GAS开发俯视角RPG游戏 P06-28 构建属性菜单小部件控制器
  • 线程协作——生产者消费者问题:
  • ROS2系列 (14) : 服务通信介绍——双向通信的核心机制
  • C语言入门(十三):操作符详解(1)
  • 化妆品设计网站家用宽带做网站
  • 雄安建设集团 网站湖北做网站教程哪家好
  • 晋城市 制作网站织梦网站文章发布模板下载
  • Microsoft Speech TTS微软语音识别ISpeechRecoGrammar,ISpeechRecoResult
  • 【Java 开发日记】运行时有出现过什么异常?
  • 企业门户网站设计扬州网页设计培训
  • 从大模型中的chat_template了解jinja模板语法
  • Pandas--数据选择与索引
  • Linux下编译WebSocket++