ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Re:Linux 系统篇(十四):进程篇(三):进程退出与特殊进程 —— 僵尸 Z 状态、孤儿进程深度解析

Re:Linux 系统篇(十四):进程篇(三):进程退出与特殊进程 —— 僵尸 Z 状态、孤儿进程深度解析 观众老爷们大家好 这里是邪修KING的独家频道本文属于系列Linux系统篇 ——操作指令一起学Linux的小伙伴可订阅专栏 Linux系统篇上一篇我们讲透了进程的 R/S/D/T/t 五大状态、内核链表设计、PCB 与队列的底层逻辑最后留下了两个特殊状态Z 僵尸进程和孤儿进程。很多人只知道 “僵尸进程不好”“孤儿进程没人管”但很少讲透本质子进程都退出了内核为什么不直接回收资源僵尸进程到底泄漏了什么内存孤儿进程最后会怎么样本篇我们从进程退出的设计初衷讲起结合代码模拟、状态查看、内核 SLAB 机制把这两个特殊进程彻底讲透。前置思考进程退出就是彻底消失吗很多初学者以为进程执行完 return 0就像程序关闭一样所有东西全部释放干干净净。但 Linux 的设计不是这样。子进程退出时用户态的代码、数据、栈会全部释放但内核里的 PCBtask_struct不会立刻删除。内核会保留进程的退出码、退出原因、运行统计等信息等着父进程来读取回收。如果父进程一直不来读这个 PCB 就会一直残留在内核里变成 ——僵尸进程。如果反过来父进程先退出了子进程还在运行子进程就没了 “家长”变成 ——孤儿进程。一、僵尸进程Zombie Process1.1 什么是僵尸进程1.1.1 定义与产生原因僵尸进程子进程已经执行完毕退出但父进程没有调用wait()/waitpid()回收子进程的退出信息导致子进程的 PCBtask_struct一直残留在内核中进程状态标记为Z。 设计初衷为什么不直接删掉 PCB子进程退出不是悄无声息消失。父进程创建了子进程通常需要知道子进程是正常结束还是异常崩溃、退出码是多少、运行了多久。所以内核特意保留 PCB 里的退出信息等父进程主动来取。父进程取走信息内核才会销毁 PCB完成回收。父进程一直不取PCB 就一直留着这就是僵尸进程的来源。1.1.2 退出信息存在哪里全部存在子进程的task_struct里面exit_code进程退出码比如 return 0 就是 0异常退出是对应信号编号exit_signal发给父进程的退出信号进程运行时间、内存使用统计等信息父进程调用wait()时内核就把这些信息返回给父进程然后释放 PCB。1.2 僵尸进程的危害内核内存泄漏1.2.1 为什么会引发内存泄漏很多文章说 “僵尸进程泄漏内存”但说得很模糊。这里明确✅ 泄漏的是内核内存不是用户态内存。❌ 子进程的用户态代码、数据、栈已经全部释放了这部分内存已经还给系统了。泄漏的是什么是内核里的task_struct结构体、内核栈、相关的内核数据结构。这些是内核空间的内存用户碰不到也不会自动释放。单个 PCB 不大也就几 KB但如果父进程不断创建子进程又不回收僵尸越积越多内核内存会被慢慢吃光最终导致系统无法创建新进程。1.2.2 进程退出了内存泄漏还在吗在。只要父进程还活着、不调用 wait僵尸 PCB 就一直占着内核内存。只有两种情况僵尸会消失父进程调用 wait/waitpid主动回收父进程也退出了僵尸进程变成孤儿僵尸由 1 号进程领养回收1.3 观察与模拟验证 Z 状态1.3.1 编写模拟代码思路父进程 fork 出子进程子进程立刻退出父进程死循环不做任何回收操作。// zombie.c#includestdio.h#includeunistd.h#includestdlib.hintmain(){pid_tpidfork();if(pid0){perror(fork fail);return1;}elseif(pid0){// 子进程打印pid后立刻退出printf([子进程] pid%d执行完毕即将退出变成僵尸\n,getpid());exit(0);}else{// 父进程死循环休眠不回收子进程printf([父进程] pid%d运行中不回收子进程\n,getpid());while(1){sleep(1);}}return0;}1.3.2 编译运行与状态识别# 编译 gcc zombie.c -o zombie # 后台运行方便开另一个终端查看 ./zombie 新开终端执行ps aux | grep zombie你会看到两行父进程STAT 为S正常休眠子进程STAT 为Z后面标注defunct已死亡这就是僵尸进程。验证子进程的代码数据已经释放了但 PCB 还在所以 ps 能看到这个进程但它已经不运行了。1.3.3 回收僵尸杀掉父进程僵尸就会被 1 号进程领养回收kill父进程PID再 ps 查看僵尸进程消失。1.4 深度硬核拓展内核结构的申请与 SLAB 技术1.4.1 什么是 SLAB 技术进程是系统里最高频创建销毁的对象之一。如果每次创建进程都向内存申请一块空间存 task_struct销毁就释放会有两个问题频繁申请释放内存碎片越来越多申请释放的系统开销大性能低Linux 内核用SLAB 分配器解决这个问题SLAB 就是内核的「对象缓存池」。针对 task_struct 这种高频使用的内核对象内核提前申请一批连续内存切成一个个和 task_struct 一样大的 “槽位”缓存起来。创建进程直接从 SLAB 池里拿一个空槽位不用重新申请内存销毁进程把 PCB 放回 SLAB 池标记为空不真正释放内存下次再创建进程直接复用槽位。1.4.2 数据结构对象的缓存机制SLAB 的核心思想是对象复用类似线程池、连接池的思路。不止 task_struct内核里的 inode、file 结构体等高频对象都有自己的 SLAB 缓存池。这也是为什么僵尸进程泄漏是个问题僵尸占着 SLAB 里的槽位池里可用槽位越来越少新进程创建就会变慢甚至失败。二、孤儿进程Orphan Process2.1 什么是孤儿进程2.1.1 定义与产生原因孤儿进程父进程先退出、终止了子进程还在运行失去了父进程就叫孤儿进程。2.1.2 谁来托底一号进程的领养机制Linux 不会让子进程没人管。父进程退出后内核会自动给子进程找新的 “养父”——1 号进程systemd /init。所有孤儿进程都会被 1 号进程领养之后孤儿进程退出由 1 号进程负责调用 wait 回收它的 PCB不会变成僵尸。 为什么是 1 号进程1 号进程是系统启动的第一个进程是所有进程的祖宗。它的职责之一就是 “收尸”领养所有孤儿回收它们的退出资源保证系统没有漏网的僵尸。2.1.3 认识 1 号进程systemd现在主流 Linux 发行版Ubuntu、CentOS7的 1 号进程都是systemd。它不仅是领养孤儿的大家长还负责管理系统服务、开机启动、守护进程等是用户空间的总管。# 查看1号进程psaux|head-2PID1COMMAND 为 /sbin/init 或者 /lib/systemd/systemd。2.2 观察与模拟验证孤儿进程2.2.1 编写模拟代码思路父进程 fork 后sleep 1 秒就退出子进程 sleep 5 秒才退出中间观察父进程退出后子进程的 PPID 变化。// orphan.c#includestdio.h#includeunistd.h#includestdlib.hintmain(){pid_tpidfork();if(pid0){perror(fork fail);return1;}elseif(pid0){// 子进程活5秒前后打印父进程IDprintf([子进程] 我是%d初始父进程是%d\n,getpid(),getppid());sleep(5);printf([子进程] 我是%d现在父进程是%d\n,getpid(),getppid());}else{// 父进程1秒后退出printf([父进程] 我是%d1秒后退出\n,getpid());sleep(1);printf([父进程] 退出了\n);exit(0);}return0;}2.2.2 运行与数据分析gcc orphan.c-oorphan ./orphan输出示例[父进程]我是123451秒后退出[子进程]我是12346初始父进程是12345[父进程]退出了# 等待几秒[子进程]我是12346现在父进程是1父进程退出后子进程的 PPID 变成了 1说明已经被 systemd 领养。子进程退出后1 号进程会自动回收它不会变成僵尸。2.3 必须注意的两个硬核细节2.3.1 细节一为什么父进程退出不会产生僵尸很多人混淆父进程退出了它的子进程会不会变僵尸不会。父进程退出时内核会遍历它所有的子进程把还活着的子进程全部交给 1 号领养已经退出变成僵尸的子进程也会交给 1 号回收。所以父进程退出后它的子进程要么变成孤儿被领养要么直接被 1 号收尸不会残留僵尸。反过来子进程退出父进程活着但不回收才会产生僵尸。这是最常见的场景。2.3.2 细节二前台进程与后台进程的转变我们平时用终端跑程序终端的 bash 就是父进程。前台运行程序占着终端CtrlC 终止bash 回收后台运行加程序在后台跑终端还能输入命令当你关闭终端窗口时bash 进程退出它的后台子进程就变成了孤儿被 systemd 领养继续在后台运行。nohup命令的本质就是让程序忽略终端挂断信号终端关闭后进程作为孤儿继续跑被 1 号领养。三、常见问题与避坑总结3.1 怎么清理僵尸进程找到父进程让它调用 wait最规范的方式但一般改代码才做得到杀掉父进程父进程死了僵尸被 1 号领养回收这是线上最常用的临时处理方式重启系统终极手段不推荐❌ 误区kill -9 僵尸进程。没用僵尸进程已经死了只是残留 PCB信号对它无效杀不掉。3.2 怎么避免产生僵尸父进程里调用wait()/waitpid()及时回收子进程父进程忽略 SIGCHLD 信号内核会自动回收子进程不用 wait两次 fork父进程 fork 一次子进程再 fork 孙子进程然后子进程立刻退出孙子进程变成孤儿被 1 号领养没有僵尸3.3 孤儿进程有害吗孤儿进程本身没害它就是正常运行的进程只是父进程变成了 1 号。只要它正常干活、正常退出1 号会回收不会有资源泄漏。但如果孤儿进程失控、疯狂占资源因为父进程没了只能手动 kill。全文总结僵尸进程 Z子进程已退出父进程未回收PCB 残留内核泄漏内核内存kill 无效杀父进程可解决。孤儿进程父进程先退出子进程存活被 1 号 systemd 领养无害退出由 1 号回收。设计本质保留退出信息是为了让父进程知晓子进程运行结果领养机制是为了保证所有进程都有最终归宿。SLAB 机制内核高频对象缓存池task_struct 从 SLAB 申请释放提升性能减少碎片。下篇预告进程在 CPU 上轮流运行切换的时候为什么能精准从上次停下的地方继续下一篇我们深入进程上下文切换拆解 CPU 寄存器、现场保存、切换全流程搞懂多任务运行的底层秘密。
返回列表