Linux 进程退出 进程等待

发布时间:2026/10/16 12:21:53
Linux 进程退出 进程等待
引言进程是操作系统的核心概念而进程的退出与回收则是理解进程生命周期不可或缺的一环。本文围绕「进程退出」与「进程等待」两大主题展开梳理了进程退出的三种场景、缺页异常与段错误的区别、exit()与_exit()的差异以及父进程通过wait()/waitpid()回收子进程资源、解决僵尸进程的完整机制。内容以板书笔记的形式呈现重点标注了考试高频考点适合作为操作系统课程复习与面试准备的速查手册。一、进程退出的 3 种场景板书核心正常退出 1代码跑完结果正确main 函数执行到末尾return 0;或者调用exit(0)✅ 退出码 0代表程序顺利跑完没有出错。正常退出 2代码跑完结果不正确程序代码完整跑完了没有崩溃但是业务逻辑结果不对。比如你写程序计算 10/0 做了捕获代码没有崩溃但是识别到运算失败return 非0return 1 /return 2✅ 退出码非 0代表程序正常跑完但是执行结果失败。上面两种都属于正常终止代码流程全部执行完毕。异常终止进程收到信号被干掉代码没跑完半路被操作系统信号杀死。例子段错误、CtrlC、kill命令。这时候退出码无意义我们不看退出码要看是哪个信号让进程崩溃。段错误 (SIGSEGV) 产生的两种常见情况①权限不匹配页表权限拦截比如去写 .rodata 只读区域页表 w 位关闭硬件拦截发 SIGSEGV程序崩溃。②访问非法虚拟地址访问没有页表映射的虚拟地址野指针页表项不存在触发段错误。区分缺页异常 ≠ 段错误缺页虚拟地址合法、有映射只是页面不在物理内存OS 把磁盘页面加载进来程序可以继续跑段错误地址非法 / 权限禁止无法修复直接杀死进程缺页异常别名就叫缺页中断。名字带 “中断”但是它属于 CPU 异常 (Exception)不是硬盘那种外部硬件中断这是课件最容易踩坑的名词陷阱1. 缺页异常缺页中断是什么进程访问一个合法虚拟地址 虚拟地址在进程地址空间范围内页表有这条映射记录但是对应的物理页面不在内存里被放到磁盘 swap 了。 CPU 拿到页表发现有效位是 0 → CPU 内部触发缺页异常缺页中断。完整流程用户态代码访问合法虚拟地址MMU 内存管理单元查页表发现页面不在物理内存MMU 全称Memory Management Unit内存管理单元一句话MMU 是 CPU 里面的硬件小模块专门负责【虚拟地址 → 物理地址】的翻译同时检查页表权限CPU 触发异常进入内核态内核执行缺页处理函数从磁盘 swap 把页面加载进物理内存更新页表返回用户态重新执行刚才那条出错指令程序正常继续跑 可以修复程序不会崩溃。举两个代码例子缺页例子int *p malloc(1024*1024*1024); //申请很大一块虚拟内存 *p 100; //第一次访问还没有物理内存触发缺页异常malloc 只分配虚拟地址第一次读写才分配物理内存触发缺页程序正常跑。段错误例子int *p NULL; *p 100; //访问空指针虚拟地址没有页表映射触发异常内核发SIGSEGV程序崩溃二、谁关心子进程退出状态为什么父进程关心父进程创建子进程干活需要知道两件事子进程任务有没有做完做成功了吗看退出码子进程是不是中途异常崩溃了看收到什么信号子进程退出之后它的 PCB (task_struct) 不会立刻释放会保留退出信息退出码、终止信号变成僵尸进程等着父进程来读取这个结果。父进程调用wait()/waitpid()读取退出信息读完之后内核才会彻底释放子进程 PCB。查看退出码的 Linux 命令bash运行echo $?$?保存上一个命令 / 进程的退出码。$? 0上一个程序正常成功$? !0正常跑完但是失败或者异常崩溃考点一句话背诵进程退出分三类正常成功退出、正常跑完结果错误、收到信号异常终止。退出码只有正常退出才有意义异常终止看信号退出码无效。子进程退出保留退出信息等待父进程 wait 读取不读取就会变成僵尸进程。returnmain 里和exit()用来设置进程退出码0 代表成功非 0 代表失败。图上两条路线路线 1调用exit()C 库函数用户区plaintext正常运行程序 → 执行用户注册的清理函数(atexit) → 冲刷缓冲区、关闭文件流 → 调用内核 _exit() → 内核回收进程大白话你准备下班调用 exit先跑你提前登记好的打扫任务atexit 函数把缓存里堆着没输出的文字全部打印出来关掉打开的文件这个 printf 存放文字的缓冲区在你的程序用户空间里面不在操作系统内核是 C 库帮你维护的。打扫全部做完才通知内核正式结束这个进程。路线 2调用_exit()系统调用直接进内核plaintext正常运行程序 → 直接调用内核 _exit() → 内核回收进程大白话直接拔电源跳过上面所有打扫步骤。不执行 atexit 清理函数、不刷 printf 缓冲区、不关闭文件直接交给内核把进程干掉。缓冲区没输出的内容直接丢掉。1. atexit 是干嘛的atexit()是 C 库函数提前注册一个函数等到进程正常退出的时候自动执行这个注册好的清理函数。int main() { atexit(clean); //注册clean函数进程正常退出自动调用 printf(main运行中\n); return 0; //main return → 触发atexit执行clean }运行顺序printf(main运行中)return 0main 结束执行 atexit 注册的clean()刷新缓冲区最后调用_exit()进入内核终止进程3. 什么时候用用来做收尾清理释放自己申请的资源、关闭自定义文件、打印日志。注意atexit 注册的函数是在用户态执行在内核终止进程之前执行。核心考点一句话return在 main 里面才会触发进程退出普通函数 return 只是函数返回。main里return n等价于exit(n)。exit 可以在任意函数直接结束整个进程。会做全套善后✅ 执行 atexit 注册函数✅ 刷新 C 库 IO 缓冲区、关闭文件流✅ 最后调用内核_exit()终止进程5.2 ——进程等待一、什么是进程等待进程等待父进程调用wait()/waitpid()等待子进程退出回收子进程资源解决僵尸进程并且拿到子进程退出信息。为什么必须做进程等待僵尸进程问题最重要考点子进程退出之后资源不会自动全部释放PCB 进程控制块还留在操作系统里变成僵尸进程。僵尸进程杀不掉kill 命令无效大量僵尸会内存泄漏。✅ 只有父进程调用 wait/waitpid内核才会把这个子进程 PCB 彻底释放。获取子进程退出结果父进程想知道子进程任务跑完了吗是正常跑完崩掉了还是被信号杀死这个是可选需求阻塞概念默认情况下调用wait如果子进程还在运行没有退出父进程就卡在 wait 这里不动不往下执行代码这个状态就叫阻塞等待。等子进程退出wait 才返回。二、两个函数c运行#include sys/types.h #include sys/wait.h pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options);1. wait参数int *status输出型参数用来带回子进程退出信息传NULL代表不关心退出状态。返回值成功返回退出子进程的 pid失败返回 - 1。特点阻塞等待任意一个子进程退出wait 就返回不能指定等哪一个子进程。2. waitpid更强考试重点pid_t waitpid(pid_t pid, int *status, int options);pid指定等待哪个子进程pid 0等待 pid 等于这个值的指定子进程pid -1等待任意子进程等价于 waitstatus和 wait 一样输出型参数存退出信息options选项最常用WNOHANG——非阻塞等待WNOHANG如果子进程还没退出waitpid 不会卡住直接返回 0父进程可以继续干别的事。三、status 这个整数重点int 被拆分好多 bit只用了低 16 位来存子进程退出信息高 16 位不用闲置放着。status 是一个 int 整数不是直接存 exit 的返回值这个 int 里面的二进制位被切分成好几段正常终止子进程 return /exit/_exit低 7 位0第 8~15 位存放退出码 exitcode就是 exit (5) 里面的 5被信号杀死异常终止段错误、kill 命令低 7 位终止信号编号比如 SIGSEGV 段错误是 11 号信号第 7 位core dump 标志1 代表程序崩溃生成 core 转储文件0 不生成内核给 status 打包不能直接拿 status 判断要用宏WIFEXITED(status)判断子进程是否正常退出正常返回真WEXITSTATUS(status)取出子进程的退出码只有正常退出才用WIFSIGNALED(status)判断子进程是否被信号杀死WTERMSIG(status)拿到杀死子进程的信号编号core dump核心转储文件core 文件单词core内核、核心内存dump倾倒、转存core dump 把程序崩溃一瞬间进程在内存里全部数据打包保存成一个文件core 文件core 文件里面存什么程序崩溃那一刻进程的内存数据寄存器的值CPU 上下文函数调用栈函数一层层调用关系 作用事后调试定位崩溃在哪一行代码默认 Linux 云服务器core 文件是关闭的不会自动生成。需要用ulimit -c unlimited打开。四、子进程退出三种场景✅ 代码跑完正常正确退出exit (0)退出码 0❌ 代码跑完正常结束但是结果错误exit (非 0 数字exit (1)/exit (5))程序正常跑完但任务失败异常终止代码没跑完被操作系统信号干掉段错误、除 0 错误、kill -9顶部问题父进程想拿子进程状态为什么不能直接读必须调用 wait/waitpid 系统调用核心一句话进程之间有独立性用户态不能直接访问内核里 PCBtask_struct子进程退出之后它的退出码、退出信号全部保存在内核里的子进程 PCBtask_struct 结构体。PCB 是内核空间的数据用户写的 C 代码跑在用户态用户态不能随便读取内核里面的数据这是操作系统的保护机制防止程序乱改内核系统崩溃。waitpid是系统调用调用之后会切换到内核态由内核帮父进程读取子进程 PCB 里面的 exit_code、exit_signal打包放到 status 变量再把数据拷贝回父进程用户空间。并且调用 wait/waitpid 还有第二个作用回收 PCB。内核拿到父进程这次读取动作之后才会把子进程 PCB 释放掉消除僵尸进程。如果父进程不调用 wait子进程退出PCB 就一直留在内核变成僵尸进程。上半张图流程拆解子进程执行完毕代码跑完 / 崩溃进入退出逻辑。内核不会立刻删掉子进程 PCB保留 PCB把退出信息exitcode、sigcode 终止信号写进 PCB 里面。此时子进程变成僵尸进程。父进程调用waitpid(...)系统调用触发系统调用进入内核内核找到目标子进程的 PCB取出 PCB 里面exit_code退出码、exit_signal终止信号把这两个信息打包塞进 status 整数传回父进程用户空间内核销毁这个子进程 PCB释放内核资源僵尸消失没有 waitpid父进程根本碰不到 PCB 里的退出信息。下半部分waitpid 的两种模式阻塞 VS 非阻塞 WNOHANG函数pid_t waitpid(pid_t pid, int *status, int options);① 不加 WNOHANG默认阻塞模式子进程还活着没有退出 →父进程直接卡住休眠不再跑代码。等到子进程退出内核唤醒父进程waitpid 返回拿到子进程信息。缺点父进程卡在原地没法做别的工作。② 加 WNOHANG非阻塞 轮询options填WNOHANG代表不要卡住。执行 waitpid分 3 种返回值ret_pid 0找到了退出的子进程ret_pid 就是子进程 pidstatus 已经拿到退出信息。✅ret_pid 0子进程还活着没退出。waitpid不会卡住直接返回 0父进程继续跑自己别的代码。过一会可以再来检测一次轮询。ret_pid 0出错比如没有这个子进程。return 0;给谁注释提问为什么 main 总是 return 0return 的值给谁大白话解释main的return 数字等价于exit(数字)。这个返回值交给操作系统内核存在当前进程 PCB 的退出码exit_code。如果这个进程有父进程父进程调用wait/waitpid就可以拿到这个退出码。return 0代表程序正常跑完任务成功return 非0代表程序结束但是执行出错比如return 1举例子main 里面return 5等价于exit(5)父进程 wait 拿到 statusWEXITSTATUS(status)得到 5.