18.Linux进程退出和进程等待机制详解
🔥keyipatience:个人主页
🎬作者简介:C/C++后端开发学习者
⭐️patience is key in life
Linux的写时拷贝(COW)
0.物理内存页
物理内存=整块仓库 ·
物理内存页=仓库里的一个个小格子
每一个小格子都有唯一的编号物理页号PFN
CPU真正访问内存的时候,都是按“一页一页”拿,不是按字节随便拿
实现机制:
1. 页表与物理页映射
- 父进程和子进程最初指向相同的物理内存页
- 内核将共享的物理页标记为只读,并在页表中记录只读的权限。
2. 缺页异常触发
- 当任一进程尝试写入共享页时,CPU触发缺页异常(Page Fault)。
- 内核捕获异常并检查触发原因是否是对只有读权限的页采取了写操作。
3. 物理页复制
- 内核分配新的物理页,复制原页内容到新页。
- 修改故障进程的页表,使其指向新复制的物理页。
- 将新页标记为可读写,原共享页仍保持只读供其他进程使用。

进程控制
进程的创建在前面我们已经讨论过了,现在我们就不再过多阐述
进程终止
进程终⽌的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。
进程退出场景
• 代码运行完毕,结果正确
• 代码运行完毕,结果不正确
• 代码异常终止
1.main函数的返回值和进程退出码
1.直接先说结论main函数的返回值就是进程的退出码
main函数的返回值表明程序的运行情况,如果返回结果为0就表示代码运行完毕,结果正确,如果为非0就表示代码运行完毕,但结果不正确,对于代码异常终止现在暂时不做讨论,不同的返回值代表不同的出错原因,可以使⽤ strerror 函数来将错误代码转换为对应的错误描述字符串.

在C/C++程序中,main函数的返回值通常用于表示程序的执行状态,这个返回值会被操作系统转换为进程的退出码(Exit Code)。退出码是一个整数存储在task_struct内部,用于指示程序是否成功执行或遇到错误。
2.进程退出码的获取
echo $ ?(打印最近一个进程的退出码信息)


所以第一次打印的就是./mypocess进程的退出码,因为该文件不存在程序运行结果不正确对应的退出码为2,第二次打印的就是对应echo $?这个进程的退出码,因为该程序运行成功退出码就是0


这个进程的退出码就不再是89,因为该进程代码异常终止了,退出码就无意义了。
(进程出现了异常一般是进程收到了信号),后面再详细说明)
3.补充一个知识点
函数返回不是返回的是函数的内部变量吗,而内部变量具有临时性,为什么还可以返回到函数外部呢?其实一般函数的返回都是通过寄存器实现的,return语句就是把返回值写到寄存器内部,当要返回的时候,就再把寄存器的内容写到另一个接受返回值的变量里,就是对应的两条move语句。
如果没有return语句即不返回,那么就不存在move eax 1 ret这条语句,而move 0x123[]这条语句还是存在,只不过eax的值就是默认的值了

进程的退出方法
1.return
main函数的return,main函数结束就表示进程结束,其他函数只表示自己函数调用完成
2.exit


发现退出码为40,即在任何地方调用exit直接表示进程结束,终止整个进程,而非仅退出当前函数最后返回一个状态码给操作系统。所以我们看到没有打印fun end甚至是main函数内部要打印的main。
3.exit和_exit
_exit 是系统调用,直接终止进程,不执行任何清理操作,不会刷新标准 I/O 缓冲区。
exit 是 C 标准库函数,会在终止进程前执行一系列清理操作。它会刷新标准 I/O 缓冲区,并最终调用 _exit 完成进程终止

eg:

exit终止进程前会刷新缓冲区,所以睡眠2s后,会显示要打印的main,如果是_exit则因为直接终止进程不会刷新缓冲区,就不会打印
并且我们在前面知道库和系统调用其实是上下层的关系,(有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发),因为只用操作系统才有能力去终止一个进程,所以exit的底层还是会去调用系统调用,即exit底层封装了_exit.
4.我们讨论的刷新缓冲区,那这个缓冲区到底是在哪里呢?
首先它一定不是操作系统内部的缓冲区,因为exit底层也相当于封装了_exit如果是操作系统内部的话,那么它们的在刷新缓冲区这一块功能应该就是一样的了,这明显和前面说的矛盾了,所以这个缓冲区应该是在库里,是库缓冲区,c语言提供的缓冲区
进程等待
进程等待必要性
• 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
• 另外,进程⼀旦变成僵⼫状态,那就刀枪不入,“杀⼈不眨眼”的kill-9也无能为力,因为谁也 没有办法杀死⼀个已经死去的进程。
• 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是 不对,或者是否正常退出。
• 父亲进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
进程等待的方法
wait() 系统调用
wait() 是最基础的进程等待方法,它会阻塞父进程直到任意一个子进程终止
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
- 参数:
status用于存储子进程的退出状态。(不关心就置为NULL) - 返回值:成功时返回终止的子进程PID,失败返回-1。
- 特点:
- 无法指定特定子进程
- 会阻塞调用者直到有子进程终止,即如果没有子进程退出父进程就一直处于阻塞状态

waitpid() 系统调用
waitpid() 提供了更精细的控制,可以等待特定子进程或非阻塞检查。
下面我们先整体大概看一下它的第一个参数pid和返回值,后面会注重讲status和options
pid_t waitpid(pid_t pid, int *status, int options);
pid:指定要等待的子进程PID>0:等待特定PID的子进程-1:等待任意子进程(类似wait())0:等待与调用进程同组ID的子进程(暂时不关心)<-1:等待进程组ID等于pid绝对值的子进程(暂时不关心)
-
返回值:
- 成功时返回子进程PID
- 如果使用
WNOHANG且没有子进程终止,返回0(后面会讲) - 失败返回-1
waitpid的第二个参数status
status 参数用于存储子进程的退出状态信息,通常通过宏来解析其具体含义
我们先看一个代码:


为什么打印的status不是1呢?这就需要我们仔细去理解一下status
理解status
• wait和waitpid,都有⼀个status参数,该参数是⼀个输出型参数,由操作系统填充。
• 如果传递NULL,表示不关心子进程的退出状态信息。
• 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
• status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16 比特位)

正常终止(exit () /return)
- 第 0~7 位:固定为
0 - 第 8~15 位:存放退出状态码(exit code,0~255)
// 最低 7 位为 0 → 说明是正常退出
if ((status & 0x7F) == 0) {
// 退出码在高 8 位,所以要右移 8 位取出来
int exit_code = (status >> 8) & 0xFF;
}
被信号杀死(如 kill -9、段错误)
- 第 0~6 位:存放终止信号编号(比如 9 代表 SIGKILL)
- 第 7 位:core dump 标志位(为 1 说明生成了 core 文件:程序崩溃时内核拍的内存 + 寄存器 + 调用栈快照,用于事后调试,某些信号杀了才会生成)
- 第 8~15 位:未使用(通常为 0)
- 被信号所杀的退出码无意义
// 最低 7 位不为 0 → 说明是被信号杀死
if ((status & 0x7F) != 0) {
// 取出终止信号编号
int sig_num = status & 0x7F;
// 取出 core dump 标志
int core_dump = (status >> 7) & 1;
}
其对应的宏
| 宏 | 做了什么 | 对应图中的位段 |
|---|---|---|
WIFEXITED(status) |
判断是否正常退出 | 检查低 7 位是否为 0(对应(status & 0x7F) == 0) |
WEXITSTATUS(status) |
获取正常退出的退出码 | 取高 8 位的值(对应(status >> 8) & 0xFF) |
WIFSIGNALED(status) |
判断是否被信号杀死 | 检查低 7 位是否非 0(对应(status & 0x7F) != 0) |
WTERMSIG(status) |
获取终止信号 | 取低 7 位的值(对应status & 0x7F) |
WCOREDUMP(status) |
是否生成了 core 文件 | 取第 7 位的值(对应(status >> 7) & 1) |
eg:
int status;
// 阻塞等待子进程退出
waitpid(child_pid, &status, 0);
if (WIFEXITED(status)) {
printf("正常退出,退出码:%d\n", WEXITSTATUS(status));
}
else if (WIFSIGNALED(status)) {
printf("被信号杀死,信号:%d\n", WTERMSIG(status));
}
怎么做到的呢?(进一步理解僵尸状态)
进程具有独立性,那么子进程的退出信息,父进程就不能直接拿到,那wait和waitpid是怎么得到子进程的退出信息包括退出码和退出信号的呢?
前面我们知道一旦子进程变成僵尸状态,它的退出信息就应该放到其task_struct里面了所以task_struct里面就得有相应的变量来存储这些退出信息呀,看一看源码,发现果然存在
![]()
exit_state:记录进程退出状态(如EXIT_ZOMBIE或EXIT_DEAD)。exit_code:存储子进程的退出码- exit_signal :存储子进程的退出信号
所以完整的一个流程大概是,(1)子进程退出时,状态设置为EXIT_ZOMBIE(僵尸状态),(2)将退出码或信号编号存入task_struct的exit_code等字段,(3)等到父进程调用wait()或waitpid()时,内核就会从子进程的task_struct中读取exit_code等等,(4)填充到父进程提供的状态变量中,(5)最后释放子进程的task_struct内存,彻底清除僵尸进程。
确保父进程能可靠获取子进程的退出状态,同时避免资源泄漏。
waitpid的第三个参数options
WNOHANG:非阻塞模式,无子进程退出就立即返回0
WUNTRACED:同时管暂停和终止的子进程(非常少用,可以不管)
0:阻塞状态(子进程不退出,父进程就一直卡着)
下面我们注重说一下WHONHANG非阻塞模式

我们可以通过一个例子来理解一下,当我们要和朋友出去玩去并在他楼下等他时,打电话叫他下楼时,如果第一次打电话他说还有一会,那我们直接就把电话挂了,过了一会再打给他,如果他说还要等一会,那我们就有直接把电话挂了,在打了几次后还没有下来,这时我们就不挂电话了,一直保持通话,时刻关注对方到底还有多久才下来。
在这个例子中,我们打了一次电话但直接挂断就相当于非阻塞状态,打了多次电话就相当于轮询多次调用,所以综合来看就是非阻塞轮询,轮询就是通过循环的方式实现。如果我们打了电话但并没有挂断,而是一直保持通话就相当于阻塞状态。如果处于非阻塞状态的话等待方就可以做一些自己的事,所以非阻塞状态可以支持更高并发量,提高效率
所以这时的waitpid(optios为WNOHANG在非阻塞状态下)就会多一个返回值情况,解释前面waitpid的返回值问题
![]()
(1)大于0(返回子进程的pid):等待成功(人下来了)
(2)等于0:只是调用结束,立即返回0,不阻塞,但子进程没有退出(相当于挂了电话,人还没有下来)
(3)小于0:等待失败(人不去了)
代码实现
阻塞等待指定子进程
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
sleep(2);
return 10; // 子进程退出码 10
}
int st;
waitpid(pid, &st, 0); // 阻塞等待该子进程
if (WIFEXITED(st)) {
printf("子进程退出码:%d\n", WEXITSTATUS(st));
}
return 0;
}
非阻塞轮询(WNOHANG)循环实现
pid_t ret;
int st;
while (1) {
ret = waitpid(-1, &st, WNOHANG);
if (ret > 0) {
printf("子进程 %d 已退出\n", ret);
break;
} else if (ret == 0) {
printf("子进程还在运行...\n");
sleep(1);
}
}
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐





所有评论(0)