2021 年,一次"我对着 ps 里几百个 <defunct> 进程,kill -9 一个个砸过去,它们【纹丝不动】"的事故,把我对"进程"和"杀进程"这两件事的理解,从头到尾翻新了一遍。那天监控报警:某台服务器的进程数,从平时的两百多,一路涨到了三千多,还在涨。我登上去 ps aux 一看,屏幕被刷屏了——满屏的进程,COMMAND 那一列,全是清一色的 [xxx] <defunct>。我以前零星见过 <defunct>,知道这词是"僵尸",但从没见过这么多。我心想:进程出问题了,杀掉就行。我 kill 12345——没反应。我 kill -9 12345——还是没反应。那个进程,稳稳地待在 ps 列表里,带着它的 <defunct>,像在嘲笑我。我换了好几个 PID,kill -9 砸下去,无一例外,一个都杀不掉。我懵了:kill -9 是 Linux 里最"狠"的信号,是"无条件强制终止",是我心里"没有什么进程是 kill -9 杀不掉的"。可现在,几百个进程,集体对 kill -9 免疫。它们到底【是什么】?如果它们是活的进程,kill -9 为什么杀不动?如果它们是死的——那它们为什么还【赖在 ps 的进程列表里】,占着 PID,不肯走?一个"死了的进程",还能叫进程吗?它如果已经死了,我又该去"杀"谁?这件事逼着我把进程的一生、fork 与 wait、僵尸进程到底是什么、为什么 kill -9 对它无效,还有"杀进程"这个动作的真正含义,彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,一个自己写的多进程任务调度服务
事故现象:
- 监控报警:进程数从 200 多涨到 3000 多,还在涨
- ★ ps aux 满屏 [xxx] <defunct>
- ★ kill -9 砸下去,一个都杀不掉
现场排查:
# 1. ★ 看看这些 defunct 进程,处于什么状态
$ ps aux | grep defunct | head
# USER PID ... STAT ... COMMAND
# root 12345 ... Z ... [worker] <defunct> # ★ STAT 是 Z
# root 12346 ... Z ... [worker] <defunct>
# ★★ STAT 那一列是 Z —— Z = Zombie,僵尸
# 2. ★ 数一下到底有多少个僵尸
$ ps aux | awk '$8 ~ /Z/' | wc -l
3100 # ★★ 3100 个僵尸进程
# 3. ★ kill -9 试试 —— 杀不掉
$ kill -9 12345
$ ps -p 12345
# PID ... STAT ... COMMAND
# 12345 ... Z ... [worker] <defunct> # ★ 杀完它还在!
# 4. ★★ 关键:看僵尸进程的父进程是谁
$ ps -eo pid,ppid,state,cmd | awk '$3=="Z"' | head
# 12345 8000 Z [worker] <defunct>
# 12346 8000 Z [worker] <defunct> # ★ 父进程 PPID 全是 8000
$ ps -p 8000 -o pid,cmd
# 8000 /usr/bin/python /app/scheduler.py # ★★ 元凶:这个调度服务
# 5. ★ 这个父进程 8000,是活的
$ ps -p 8000 -o stat
# Sl # ★ 它活得好好的,在 sleep
根因(后来想清楚的):
1. ★ 僵尸(Z 状态)进程,是一个【已经退出、已经死了】
的进程。它的代码早就不跑了,内存早就释放了。
2. ★ 但一个进程退出后,内核【不会立刻】把它从进程表
里彻底抹掉。它会先变成"僵尸",在进程表里【保留
一个壳】——只剩 PID 号 + 退出状态码这一点点信息。
3. ★ 这个壳,要等它的【父进程】来调用 wait() / waitpid()
"认领"它、读走它的退出状态,内核才会把它【彻底
清除】。这个动作,行话叫"回收 / 收尸"。
4. ★ 本文的 scheduler.py,不停地 fork 出子进程
[worker] 干活。worker 干完活就 exit 了 —— 但
scheduler.py 【从来没调用 wait()】去收尸。
5. ★ 于是每个干完活退出的 worker,都变成一具僵尸,
壳一直挂在进程表里。3100 个 worker 跑完,就攒了
3100 具僵尸。
6. ★ kill -9 杀不掉它们,因为【它们已经死了】。
kill 是"给进程发信号",而僵尸进程根本不会再
响应任何信号 —— 你没法"杀死一个已经死了的人"。
真相:僵尸不是"卡住的活进程",是"死了没人收尸
的进程"。该处理的不是僵尸,是那个不收尸的父进程。
修复 1:先看懂 <defunct> / Z 状态,到底是什么
# === ★ 别急着 kill,先看懂这个 Z 是什么 ===
# === ★ 先看进程的 STAT 列有哪些值 ===
$ ps aux
# STAT 列常见的值:
# R 运行 / 可运行(正在用 CPU 或排队等 CPU)
# S 可中断睡眠(在等某件事,如等网络、等输入)—— 最常见
# D 不可中断睡眠(通常在等磁盘 IO,这种 kill 也杀不动)
# T 停止(被 Ctrl+Z 或调试器暂停了)
# ★ Z 僵尸(zombie)—— 本文的主角
# === ★ Z(僵尸)到底是什么状态 ===
# ★ 一个进程,如果 STAT 是 Z,它的真实含义是:
# ★ "这个进程【已经执行完、已经退出了】。它的
# 程序代码不再运行,它占的内存已经全部释放。
# 它现在【不是一个活着的进程】。"
# ★ 那它为什么还在 ps 列表里?因为内核给它【留了
# 一个壳】—— 一条记录,记着它的 PID、它是谁的
# 孩子、它退出时的状态码(退出码 / 被什么信号杀的)。
# ★ <defunct> 这个英文词,意思就是"已失效 / 已不
# 存在"。ps 用它来标记:这一行,是个空壳。
# === ★ 关键认知:僵尸进程几乎不消耗资源 ===
# ★ 很多人一看到几千个僵尸就慌,以为它们在吃 CPU、
# 吃内存 —— ★ 不是。僵尸进程:
# - 不占 CPU(它的代码根本不运行了);
# - 几乎不占内存(它的内存早释放了,只剩一个
# 极小的进程表条目);
# ★ 僵尸唯一占用的,是一个【PID 号】和那一条进程表
# 记录。它本身【不是性能问题】。
# === ★ 那僵尸的危害到底在哪 ===
# ★ 危害只有一个,但很致命:★ 它占着【PID】不放。
# ★ 系统的 PID 是有上限的(见修复 6)。僵尸一具一具
# 攒下去,攒到把 PID 耗尽 —— 那时,系统里【任何
# 程序想再 fork / 启动新进程,都会失败】。
# ★ 你会看到 "fork: retry: Resource temporarily
# unavailable" 或 "Cannot allocate memory" ——
# 整个系统再也起不了新进程,这才是真正的灾难。
# === 认知 ===
# ★ ps 的 STAT 列里,Z = 僵尸。僵尸进程是【已经退出、
# 已经死了】的进程:代码不跑了、内存释放了,内核只
# 给它留一个壳(PID + 退出状态)。<defunct> 就是这个
# 壳的标记。★ 僵尸几乎不占 CPU 和内存,不是性能
# 问题 —— 它唯一的危害是占着 PID,攒多了会耗尽 PID,
# 让整个系统 fork 不出新进程。
修复 2:进程的一生——fork、exit、wait,僵尸卡在哪一步
# === ★ 把一个子进程的"生与死",完整走一遍 ===
# === ★ 第一步:fork —— 父进程"生"出子进程 ===
# ★ 一个进程(父)要让另一段活并行地干,它会调用
# fork(),【复制出一个子进程】。本文的 scheduler.py
# 就是不停 fork 出 worker 子进程去干活。
# ★ 此刻,父子是一对【父子关系】:子进程的 PPID
# (parent pid),就是父进程的 PID。
# === ★ 第二步:子进程干活,然后 exit —— 子进程"死" ===
# ★ 子进程干完它的活,调用 exit() 退出。它产生一个
# 【退出状态码】(成功是 0,失败是非 0,或者是
# "被某信号杀死")。
# ★ ★ 注意:子进程 exit 的这一刻,它就【死了】——
# 代码停了,内存释放了。但它【不会马上消失】。
# === ★ 第三步:内核把死掉的子进程,变成"僵尸" ===
# ★ 内核的逻辑是:子进程死了,但它的【退出状态】
# 还得有人知道 —— 父进程可能正等着这个结果(子
# 进程成功了没?)。
# ★ 所以内核【不立刻清除】死掉的子进程,而是把它
# 降格成一具【僵尸】:进程表里留一条最小记录,
# 把退出状态【存在里面】,等父进程来取。
# ★ ★ 所以"僵尸"是每个子进程死后的【必经状态】——
# 它是个【临时的、过渡的】状态,本不该停留。
# === ★ 第四步:wait —— 父进程"收尸",僵尸才真正消失 ===
# ★ 父进程要调用 wait() 或 waitpid()。这个调用做两件事:
# - 【读走】子进程的退出状态码(父进程借此知道
# 子进程成功还是失败);
# - 【通知内核】:"这孩子的后事我处理完了,你可以
# 把它彻底清掉了。"
# ★ 内核收到 wait,才把那具僵尸的进程表记录【真正
# 删除】,PID 这才被释放、可以回收再用。
# ★ ★ 这个 wait 的动作,就是"收尸 / 回收(reap)"。
# === ★ 所以,僵尸是卡在哪一步 ===
# ★ 正常流程:fork -> 子进程 exit -> (瞬间僵尸) ->
# 父进程 wait 收尸 -> 僵尸消失。整个过程极快,你
# 几乎看不到那个僵尸态。
# ★ 出问题的流程:fork -> 子进程 exit -> 变成僵尸 ->
# ★ 父进程【永远不 wait】-> 僵尸【永远卡在第三步】,
# 一直挂在进程表里。—— 本文就是这种。
# === 认知 ===
# ★ 子进程的一生:父进程 fork 生出它 -> 它干活后
# exit 死掉 -> 内核把它降格成僵尸(进程表留个壳,
# 存着退出状态)-> 父进程调用 wait()/waitpid() 收尸
# (读走退出状态 + 通知内核清除)-> 僵尸消失、PID
# 释放。僵尸本是【极短暂的过渡态】。父进程【不调用
# wait】,僵尸就永远卡在过渡态,赖着不走。
修复 3:为什么 kill -9 杀不掉僵尸进程
# === ★ 解开本文最大的困惑:kill -9 为什么失效 ===
# === ★ 先搞清楚 kill 到底在做什么 ===
# ★ kill 这个命令,名字起得有误导性。它【不是】
# "杀死",它的本质是:★ "向一个进程【发送一个信号】"。
$ kill 12345 # 默认发 SIGTERM(15),礼貌地请它退出
$ kill -9 12345 # 发 SIGKILL(9),强制信号
# ★ kill -9 之所以"最狠",是因为 SIGKILL 这个信号,
# 进程【无法捕获、无法忽略】—— 内核会直接把那个
# 【还活着的】进程干掉。
# === ★ 关键:kill 的前提是"对面是个活进程" ===
# ★ "发信号"这件事,要成立,必须有一个【活着的、
# 能接收信号的进程】在那头。信号是发给【运行中的
# 进程】、让它做出反应(退出 / 中断 / 自定义处理)的。
# ★ ★ 而僵尸进程,【已经死了】。它的代码早就不运行
# 了。你给它发 SIGKILL,等于:★ 对着一个【已经
# 死了的人】喊"我命令你去死"。
# ★ 它已经死了。SIGKILL 想做的事(终止一个运行中的
# 进程),在僵尸身上【已经完成了】—— 它早就被终止
# 过了。信号没有任何作用对象。
# === ★ 所以 kill -9 杀不掉僵尸,不是 kill -9 不够狠 ===
# ★ 而是:★ 僵尸进程已经不是"进程"了,它是一具
# "尸体" —— 一条等待被回收的进程表记录。
# ★ kill 的能力边界,是"终止一个活进程"。它没有
# 能力,也【不负责】"清除一具尸体"。清除尸体是
# wait() 的活,不是 kill 的活。
# ★ 你用 kill -9 去对付僵尸,是【拿错了工具】——
# 就像拿灭火器去拖地,不是灭火器坏了,是它压根
# 不干这事。
# === ★ 验证:僵尸对所有信号都无反应 ===
$ ps -p 12345 -o stat # Z
$ kill -9 12345
$ ps -p 12345 -o stat # 还是 Z,毫无变化
$ kill -CONT 12345 ; kill -TERM 12345
$ ps -p 12345 -o stat # 依然 Z
# ★ 任何信号砸上去,僵尸都岿然不动 —— 因为没有
# "活进程"在那头接收它们。
# === ★ 那僵尸到底要怎么才能消失 ===
# ★ 只有一条路:★ 它的【父进程】调用 wait() 把它
# 收尸。除此之外,没有任何命令能"直接删掉"一个
# 僵尸 —— 因为它本来就不是被"删"的,是被"回收"的。
# ★ 所以下一节,我们去找那个该收尸、却不收尸的父进程。
# === 认知 ===
# ★ kill 的本质是"向一个【活进程】发信号",kill -9
# 发的 SIGKILL 是无法被捕获的强制终止信号。但僵尸
# 进程【已经死了】—— 它的代码不运行,没有任何活的
# 进程在那头接收信号。所以 kill -9 对僵尸完全无效,
# 不是 kill -9 不够狠,是【拿错了工具】:清除僵尸是
# 父进程 wait() 的职责,不是 kill 能做的事。
修复 4:真正的元凶——那个不收尸的父进程
# === ★ 僵尸杀不掉,那就去找该为它负责的父进程 ===
# === ★ 第一步:找出僵尸进程的父进程 PPID ===
$ ps -eo pid,ppid,state,cmd | awk '$3=="Z"'
# 12345 8000 Z [worker] <defunct>
# 12346 8000 Z [worker] <defunct>
# 12347 8000 Z [worker] <defunct>
# ★ 第二列 PPID 全是 8000 —— 所有僵尸的父进程,
# 是同一个:进程 8000。
# === ★ 一条命令直接统计"谁制造了最多僵尸" ===
$ ps -eo ppid,state | awk '$2=="Z"{print $1}' | sort | uniq -c | sort -rn
# 3100 8000 # ★★ 8000 制造了 3100 具僵尸
# ★ uniq -c 数每个 PPID 出现几次,sort -rn 排序。
# 最上面那个 PPID,就是【僵尸工厂】。
# === ★ 第二步:看清这个父进程 8000 是什么 ===
$ ps -p 8000 -o pid,ppid,stat,etime,cmd
# PID PPID STAT ELAPSED CMD
# 8000 1 Sl 10-03:21 /usr/bin/python /app/scheduler.py
# ★ 它是 scheduler.py,活着(STAT 含 S),已经
# 跑了 10 天。它就是那个不停 fork worker、却从不
# wait 收尸的程序。
# === ★ 第三步:理解 —— 为什么是父进程的"错" ===
# ★ 内核的设计:一个子进程死后变成僵尸,【收尸的
# 责任,被指定给它的父进程】,别人无权代劳。
# ★ scheduler.py 这个程序,代码里只管 fork 子进程
# 去干活,干完就不管了 —— 它【没有写 wait() 的
# 逻辑】,也【没有处理 SIGCHLD 信号】。
# ★ (子进程一死,内核会给父进程发一个 SIGCHLD
# 信号,等于通知它"你有个孩子没了,记得收尸"。
# 一个写得好的父进程,会在 SIGCHLD 处理函数里
# 调用 wait。scheduler.py 把这个信号【忽略了】。)
# ★ ★ 所以僵尸堆积,根因【100% 在父进程】——
# 是它生了孩子却不收尸。僵尸只是"结果"。
# === ★ 第四步:确认父进程是不是"卡死"了 ===
# ★ 有时父进程不收尸,是因为它自己【卡住了】(比如
# 死锁、卡在某个 D 状态的 IO 上),没机会执行 wait。
$ cat /proc/8000/stack # 看它内核态卡在哪(需权限)
$ ps -p 8000 -o stat # 是 Sl(正常睡眠)还是 D(卡 IO)
# ★ 本文 8000 是 Sl,它没卡死 —— 它是【代码里压根
# 没写收尸逻辑】,这是个 bug,不是卡顿。
# === 认知 ===
# ★ 僵尸杀不掉,就去找它的父进程:ps -eo pid,ppid,
# state,cmd 筛 state=Z 看 PPID,或用 uniq -c 统计
# 谁制造了最多僵尸。内核规定【收尸责任在父进程】,
# 别人无权代劳。父进程不收尸通常两种原因:① 代码
# 里压根没写 wait()/没处理 SIGCHLD(本文这种,是
# bug);② 父进程自己卡死了没机会执行 wait。僵尸
# 堆积根因 100% 在父进程,僵尸只是结果。
修复 5:怎么清掉僵尸——杀父进程,让 init 接管
# === ★ 既然杀不掉僵尸,那就处理那个父进程 ===
# === ★ 办法一:温和提醒父进程去收尸(成功率低)===
# ★ 给父进程补发一个 SIGCHLD,提醒它"有孩子要收":
$ kill -CHLD 8000
# ★ 如果父进程代码里【有】SIGCHLD 处理逻辑、只是
# 一时没触发,这一下能让它把僵尸收掉。
# ★ ★ 但本文的 scheduler.py 代码里根本没写收尸
# 逻辑 —— 补发 SIGCHLD 它也不理。这办法对它无效。
# === ★ 办法二:★ 杀掉父进程 —— 最有效的清僵尸手段 ===
# ★ 这是关键认知:★ 当一个进程死掉,它名下【所有
# 的子进程(包括僵尸)】,会被【过继给 1 号进程】
# (init / systemd)。
# ★ 而 1 号进程,是个【尽职的"收尸人"】—— 它会
# 自动、持续地 wait() 回收所有过继给它的僵尸。
# ★ 所以:★ 杀掉那个不收尸的父进程 8000 ->
# 它名下 3100 具僵尸全部过继给 1 号进程 ->
# 1 号进程瞬间把它们【全部收尸】-> 僵尸清空。
$ kill 8000 # 先礼貌地 SIGTERM
$ ps aux | awk '$8 ~ /Z/' | wc -l
0 # ★★ 3100 具僵尸,瞬间清零
# ★ 注意:杀的是【父进程】,不是僵尸。父进程一死,
# 僵尸自动有人收。
# === ★ 杀父进程前,务必想清楚 ===
# ★ scheduler.py 是个正在干活的业务服务,直接 kill
# 它,业务会中断。所以正确姿势是:
# - 若它是 systemd 服务:systemctl restart 它
# —— 重启 = 旧进程死(僵尸被 init 收掉)+ 新进程
# 起,业务只断一瞬。
$ systemctl restart scheduler
# - 若它是手工起的:挑业务低峰,kill 后立刻拉起。
# === ★ 办法三:★ 真正的根治 —— 修父进程的代码 ===
# ★ 杀 / 重启父进程,只是把已堆积的僵尸清掉。只要
# scheduler.py 的代码不改,它跑着跑着,又会攒出
# 几千个僵尸。必须改代码:
# ★ 根治 a:父进程显式 wait 子进程。
# - C:在 SIGCHLD 处理函数里循环 waitpid(-1, &s, WNOHANG);
# - Python:os.waitpid() / subprocess 的 .wait();
# - Shell:wait 命令。
# ★ 根治 b:★ 一个简单粗暴但极有效的办法 —— 让父
# 进程【主动忽略 SIGCHLD】。在 C 里
# signal(SIGCHLD, SIG_IGN),或设 SA_NOCLDWAIT ->
# 这样子进程死后【内核直接回收,根本不产生僵尸】。
# ★ 根治 c:用成熟的进程管理框架 / 库来 fork 子进程,
# 别自己裸写 fork —— 框架通常已处理好收尸。
# === 认知 ===
# ★ 清僵尸不能杀僵尸本身,要处理父进程:① kill -CHLD
# 提醒父进程收尸(父进程有收尸逻辑才有效);②【最
# 有效】杀掉/重启父进程 —— 父进程一死,它名下所有
# 僵尸过继给 1 号进程(init/systemd),1 号进程是
# 尽职收尸人会自动全部回收;③ ★ 根治是改父进程代码:
# 显式 wait,或 signal(SIGCHLD, SIG_IGN) 让内核直接
# 回收不产生僵尸。只杀进程不改代码,僵尸会复发。
修复 6:孤儿进程、PID 耗尽与僵尸排查纪律
# === 这次事故暴露的认知盲区,定几条纪律 ===
# === ★ 先分清:僵尸进程 vs 孤儿进程,是两回事 ===
# ★ 僵尸进程:子进程【先死】,父进程还活着但不收尸
# -> 子进程卡在僵尸态。问题在【父进程】。
# ★ 孤儿进程:父进程【先死】,子进程还活着
# -> 子进程成了"孤儿"。★ 孤儿【不是问题】:它会
# 立刻被 1 号进程收养,以后它死了 init 负责收尸。
# ★ ★ 一句话:僵尸是"死了没人收",孤儿是"还活着
# 但没了爹"。孤儿无害,僵尸堆积才要处理。
# === 1. ★ 看到 <defunct> / Z,别 kill 僵尸,去查父进程 ===
$ ps -eo pid,ppid,state,cmd | awk '$3=="Z"'
# === 2. ★ kill -9 杀不掉僵尸是正常的,它已经死了,没有活进程接收信号 ===
# === 3. ★ 统计谁制造了最多僵尸,锁定僵尸工厂 ===
$ ps -eo ppid,state | awk '$2=="Z"{print $1}' | sort | uniq -c | sort -rn
# === 4. ★ 清僵尸:杀 / 重启那个父进程,僵尸过继给 init 自动收掉 ===
# === 5. ★ 根治:改父进程代码,显式 wait 或 signal(SIGCHLD, SIG_IGN) ===
# === 6. ★ 僵尸真正的危害是耗 PID,看 PID 上限和当前用量 ===
$ cat /proc/sys/kernel/pid_max # PID 的最大值(常见 32768)
$ ps -e | wc -l # 当前进程(含僵尸)总数
# ★ 进程数逼近 pid_max,新进程会 fork 失败:
# "fork: retry: Resource temporarily unavailable"
# === 7. 区分 fork 失败的两种原因:PID 耗尽 vs 用户进程数上限 ulimit -u ===
$ ulimit -u # 单用户能开的进程数上限
# === 8. ★ 少量僵尸(几个、十几个)是正常的,不用管,别草木皆兵 ===
# === 9. 僵尸不占 CPU 不占内存,monitoring 报"进程数高"才需关注僵尸 ===
# === 10. 排查僵尸进程的步骤链 ===
$ ps aux | awk '$8 ~ /Z/' | wc -l # ① 有多少僵尸
$ ps -eo ppid,state|awk '$2=="Z"{print $1}'|sort|uniq -c|sort -rn # ② 谁的锅
$ ps -p 父PID -o pid,stat,cmd # ③ 看清父进程是什么
$ systemctl restart 父服务 或 kill 父PID # ④ 处理父进程,僵尸自动清
# 然后改父进程代码根治,否则必复发。
命令速查
需求 命令
=============================================================
看所有僵尸进程 ps aux | awk '$8 ~ /Z/'
数僵尸进程个数 ps aux | awk '$8 ~ /Z/' | wc -l
看僵尸及其父进程 ps -eo pid,ppid,state,cmd | awk '$3=="Z"'
统计谁制造最多僵尸 ps -eo ppid,state|awk '$2=="Z"{print $1}'|sort|uniq -c|sort -rn
看某进程详情 ps -p PID -o pid,ppid,stat,etime,cmd
提醒父进程收尸 kill -CHLD 父PID
清僵尸(杀父进程) kill 父PID 或 systemctl restart 父服务
看 PID 上限 cat /proc/sys/kernel/pid_max
看单用户进程数上限 ulimit -u
看当前进程总数 ps -e | wc -l
口诀:Z 状态是死了没人收尸的进程 kill -9 杀不动它因为它早就死了
要清僵尸不是杀僵尸 是杀那个不收尸的父进程 让 init 来接管收尸
避坑清单
- ps 里 STAT 是 Z 或 COMMAND 带 defunct 的是僵尸进程,它已经退出死掉了不是活进程
- kill -9 杀不掉僵尸是必然的,它已经死了没有活进程在接收信号,不是 kill -9 不够狠
- 僵尸进程几乎不占 CPU 和内存,它唯一的危害是占着一个 PID 不释放
- 僵尸攒太多会耗尽系统 PID,导致任何程序都 fork 不出新进程报 Resource temporarily unavailable
- 子进程退出后变僵尸是必经的过渡态,正常情况下父进程立刻 wait 收尸僵尸瞬间消失
- 收尸的责任被内核指定给父进程,僵尸堆积根因 100 在父进程没调用 wait 没处理 SIGCHLD
- 清僵尸不能杀僵尸本身,要杀或重启那个不收尸的父进程,僵尸会过继给 init 自动被收掉
- 只杀父进程不改代码僵尸会复发,根治要改父进程显式 wait 或设 SIGCHLD 为 SIG_IGN
- 僵尸进程和孤儿进程是两回事,孤儿是父进程先死子进程还活着会被 init 收养孤儿无害
- 系统里有少量几个僵尸是正常现象不用处理,只有大量堆积逼近 PID 上限才需要排查
总结
这次"几百个进程对 kill -9 集体免疫"的事故,纠正了我一个关于"杀"的、藏得极深的错觉。在我过去的脑子里,kill -9 是一种【绝对的权力】。我从没想过它会"失败"——在我的认知里,一个进程,要么活着、可以被 kill -9 杀死,要么已经不在了、从 ps 里消失。kill -9 杀不掉、却又明明白白列在 ps 里的东西,在我那套世界观里【根本不该存在】。所以那天满屏的 <defunct> 对 kill -9 无动于衷时,我的第一反应不是"我理解错了",而是"系统出鬼了"——我宁可怀疑 Linux 坏了,也没怀疑过自己对"进程"这个词的理解。直到我被逼着去读懂那个 Z,我才发现,我错的地方,比我以为的要根本得多。我一直以为,一个东西出现在 ps 的进程列表里,就【证明它是一个进程】、是个活物。可僵尸进程把这个等式劈开了:它在列表里,但它【不是进程】——它是一个进程【死后留下的回执】,一张写着"此人已故,死因如下"的存根。我对着这张存根 kill -9,内核当然毫无反应——我是在命令一个【已经执行完毕的死亡】"再死一次"。kill -9 从来没有失败,是我把它指向了一个它【本就无能为力、也本就不归它管】的对象。复盘到最深处,我才咂摸出这件事真正的味道:我对僵尸的全部困惑,源于我下意识地认定,"清理一个死掉的进程",和"杀死一个活着的进程",是【同一件事】、该用【同一个工具】。可它们根本是两件事。"杀死",是让一个运行中的东西停下来——这是 kill 的事。"收尸",是把一个已经停下来的东西、它留下的最后痕迹登记并抹除——这是 wait 的事,而且内核把这件事,郑重地、排他地,指派给了【那个把它带到世上的父进程】。我那一整天拿着 kill -9 砸僵尸,就像在葬礼上反复给一具遗体做心肺复苏——工具没错,力气没错,错的是我从没分清"急救"和"善后"是两种完全不同的事。而最后那个真正解决问题的动作,更是反直觉到了家:我没有去碰那三千具僵尸里的任何一个,我去 kill 了那个【活得好好的】父进程。要清掉满地的"尸体",我动手的对象,偏偏是那个唯一还"活着"的。因为问题从来不在尸体身上——尸体是结果,是症状;问题在那个生了孩子却从不收尸的"活人"身上。这次最大的收获,是我学会了在动手"处理"任何一个异常现象之前,先逼问自己一句:我眼前这个东西,是【病根】,还是【病根留下的痕迹】?僵尸进程是痕迹,父进程的 bug 才是病根;一地的错误日志是痕迹,那个抛异常的代码才是病根;监控上一根飙红的曲线是痕迹,背后那个失控的逻辑才是病根。我们的本能,总是去扑那个最显眼、最扎眼、数量最多的"痕迹"——就像我本能地去扑那三千个 <defunct>。但痕迹是杀不完的,你删掉一批,病根还在,它转头又生产出新的一批。真正的功夫,是顺着痕迹,冷静地往回走,走到那个【还活着、还在持续制造痕迹】的源头,在那里动手。kill -9 杀不掉一个僵尸——这句话我会记很久。它提醒我的不是某个命令的边界,而是一件更朴素的事:在你举起工具之前,先看清楚,你对面站着的,究竟是元凶,还是只是它留下的一地狼藉。
—— 别看了 · 2026