kill -9 杀不掉的进程:一次僵尸进程与父进程收尸的复盘

监控报警一台服务器进程数从两百多涨到三千多还在涨,ps aux 满屏 defunct,kill -9 一个个砸过去纹丝不动,Linux 里最狠的强制信号集体失效。排查梳理:ps 的 STAT 那列是 Z 即 Zombie 僵尸,数了一下 3100 个,看僵尸的父进程 PPID 全是 8000 一个不停 fork worker 的调度服务且它活得好好的;僵尸进程是已经退出已经死了的进程代码不跑了内存释放了,内核只给它留个壳记着 PID 和退出状态码等父进程来认领,僵尸几乎不占 CPU 和内存不是性能问题它唯一危害是占着 PID 攒多了耗尽 PID 让整个系统 fork 不出新进程;子进程一生是父进程 fork 生出它干完活 exit 死掉内核把它降格成僵尸存着退出状态父进程调用 wait 或 waitpid 收尸读走退出状态并通知内核清除僵尸才消失,僵尸本是极短暂过渡态父进程不 wait 它就永远卡在过渡态;kill 本质是向一个活进程发信号 kill -9 发的 SIGKILL 是无法捕获的强制终止信号但僵尸已经死了没有活进程在那头接收信号,所以 kill -9 对僵尸完全无效不是不够狠是拿错工具清除僵尸是父进程 wait 的职责;收尸责任被内核指定给父进程僵尸堆积根因 100 在父进程没写 wait 没处理 SIGCHLD;清僵尸不能杀僵尸本身要杀或重启那个不收尸的父进程父进程一死它名下所有僵尸过继给 1 号进程 init systemd 自动全部回收,根治是改父进程代码显式 wait 或 signal SIGCHLD SIG_IGN 让内核直接回收不产生僵尸;僵尸和孤儿是两回事孤儿是父进程先死子进程还活着会被 init 收养孤儿无害。正确做法是看到 defunct 别杀僵尸去查父进程,以及一套僵尸进程排查纪律。

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,不肯走?一个"死了的进程",还能叫进程吗?它如果已经死了,我又该去"杀"谁?这件事逼着我把进程的一生、forkwait、僵尸进程到底是什么、为什么 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 来接管收尸

避坑清单

  1. ps 里 STAT 是 Z 或 COMMAND 带 defunct 的是僵尸进程,它已经退出死掉了不是活进程
  2. kill -9 杀不掉僵尸是必然的,它已经死了没有活进程在接收信号,不是 kill -9 不够狠
  3. 僵尸进程几乎不占 CPU 和内存,它唯一的危害是占着一个 PID 不释放
  4. 僵尸攒太多会耗尽系统 PID,导致任何程序都 fork 不出新进程报 Resource temporarily unavailable
  5. 子进程退出后变僵尸是必经的过渡态,正常情况下父进程立刻 wait 收尸僵尸瞬间消失
  6. 收尸的责任被内核指定给父进程,僵尸堆积根因 100 在父进程没调用 wait 没处理 SIGCHLD
  7. 清僵尸不能杀僵尸本身,要杀或重启那个不收尸的父进程,僵尸会过继给 init 自动被收掉
  8. 只杀父进程不改代码僵尸会复发,根治要改父进程显式 wait 或设 SIGCHLD 为 SIG_IGN
  9. 僵尸进程和孤儿进程是两回事,孤儿是父进程先死子进程还活着会被 init 收养孤儿无害
  10. 系统里有少量几个僵尸是正常现象不用处理,只有大量堆积逼近 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
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
Linux教程

free -h 显示内存快满了是内存泄漏吗:一次 Linux 内存认知的复盘

2026-5-21 1:21:43

Linux教程

chmod 777 了还是 Permission denied:一次目录 x 权限与路径解析的复盘

2026-5-21 1:30:39

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索