这条我读的时候是反着期待的。外面转述都往“Claude Code 留了十个孤儿 zsh 进程在后台吃 CPU”上带,好像重点是 agent 又闯祸。其实大头在中间:作者第一次清理居然没杀掉,原因不是权限、不是信号不够狠,是 jobs -p 在非交互 shell 里什么都没拿到,父 shell 自己还死了,kill 那行根本没轮上。原作者叫 Sidhant Panda,上个月发在 DEV。
我读到第三段才决定推。具体数字照抄下来就几个——十个进程,每个吃六成左右 CPU,elapsed 一整天二十二小时四十一分钟,全都 reparent 到 PID 1 底下。参数列表拉开看一眼就知道是 integration test 里为了占满所有核故意 spawn 的 busy-loop。这几个数字凑一起就是一句话:会话结束了两天,后台没停过。
这篇我没跑。一个故意要把核占满的集成测试,我不想在自己机器上复现;只读了。
第一次清理失败那部分值得单独说。他用了 jobs -p,指望从里面拿后台进程的 PID 再去杀。非交互 shell 里 jobs 什么都不返回,没有 PID 喂给 kill。这还只是问题的一半。更麻烦的是父 shell 自己在跑到 kill 那行之前就没了。剩下十个 busy-loop 就这么成了孤儿挂到 PID 1。两件事叠一起,不是 kill 没用力,是 kill 压根没执行。
这个死法比后文给的修复方案有用。我见过不止一篇讲“优雅清理”的,都默认父 shell 会老老实实走到清理那一行。真实场景是你根本不知道它会在哪一步先没了。这篇文章把那个死法写清楚了,所以我愿意推。
作者后来给的解法也是老路线:spawn 完立刻用 $! 把子进程 PID 存下来,在 EXIT、INT、TERM 上挂 trap 去杀。文里还贴了个给 Claude Code 的 prompt,让 agent 先对比负载和核数,再列出 top CPU 的 PPID、elapsed 和完整参数列表,先展示、再决定杀不杀。这个思路我认同——先看后杀,别上来就 kill 扫一片。但说实话,这部分比“jobs -p 为什么失败”薄。
评论区有人补的更有用。trap 对 SIGKILL 不触发。还有人说 Cursor 会话也留过 Node 孤儿进程。trap 只接得住正常退出,真被 SIGKILL 或者干脆崩掉,它自己都收不到信号,更别说替你去杀子进程了。所以 $! 加 trap 是降低概率,不是保底。评论区给的替代方案我记一下:进程组、Linux 的 PR_SET_PDEATHSIG、macOS 上让子进程自尽。都没验证,先挂着。
最后一段作者说修完之后 load average 可能还是高,因为负载是滚动平均,衰减需要时间。很多人清完进程看负载没掉,就觉得自己没修好。其实该看的是 ps 输出的进程列表,不是负载数字。PPID=1 而且 elapsed 长到离谱的,才算真有鬼。这句我读着有点被戳到。
不点链接也能带走的一句:agent 会话结束不等于它 spawn 的后台进程停了;要装 trap 之前,先确认你的 shell 能不能活到 kill 那一行。
我夹子里还躺着一篇讲 trap 清理的,一直没打开。这条文章倒是把 shell 的意外死法讲透了。过阵子我找出来一并看看。