开发者发现kill命令存在延续十年的系列bug,曾致Endcrawl短暂宕机
来源标题:A Decade of Kill Bugs
Endcrawl在2026年9月24日因kill(1)命令的bug短暂宕机,所有服务随一条kill命令瞬间停止。作者追查后发现该bug影响了Ubuntu 14、16、18、22和24 LTS中的一系列kill问题。文章记录了这十年间的kill bug并给出建议。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
2026年9月24日早晨,Endcrawl发生了一次短暂的服务中断。我当时刚输入了:
sudo kill -TERM -1490947
紧接着,所有服务瞬间全部宕机。
我立刻开始自责。也许是我把那个数字打错了!也许是我的代码里有信号处理的bug。也许我对Unix的理解没有自己以为的那么透彻。也许我就是个白痴!
结果呢,这次宕机是kill(1)的一个bug导致的。
这真的很令人意外。kill程序已经50岁了。它的规范简单且稳定。它每天大概要被调用十亿次。
kill里出现bug实在太让人意外了,以至于一旦发现了它,我就没法就此打住。我需要弄清楚这个bug是怎么来的,是什么让程序员们偏离正轨如此之远。当我最终找到根本原因时,还有一个更大的惊喜。它不只导致了一个bug,而是一整系列kill bug,随Ubuntu 14、16、18、22和24 LTS一同发布。
这篇文章记录了这个十年间的kill bug系列,然后给出一些建议。
回到那次宕机。我手忙脚乱地灭火。之后又来了意想不到的第二场火。这一切结束后,我相当沮丧。我准备接受自己犯了个愚蠢的错误,准备继续当天的工作。但心里总有一种挥之不去的感觉……
还记得《阅后即焚》的最后一场戏吗?
“我他妈也不知道。我猜我们学到了别再这么干了。”
“不过我他妈也不知道我们到底干了什么……”
刚才到底发生了什么?我是不是又要遭遇第三次宕机?我怎么才能确定?
未被理解的宕机,注定会再次发生。那种挥之不去的感觉,是一种要求我去理解的呼唤。我叹了口气,把其他所有工作搁置一旁,开始认真做事故复盘。
让我们仔细看看导致宕机的那条命令:
sudo kill -TERM -1490947
这里的负数是一个进程组标识符,简称pgid。pgid就像pid,但标识的是一组相关进程。按照惯例,kill一个正数会向单个进程发送信号,而kill一个负数则会向该进程组中的所有进程发送信号。
我当时是想取消一个大型的Endcrawl 7k测试渲染。一般人可能会杀掉其中一个进程,让失败级联到其他进程。但我觉得同时杀掉整个进程组更干净利落。
进程组信号并非什么稀奇的操作。你一直在用它。每次你按下^C,tty就会向前台进程组发送SIGINT。交互式shell会把除了最简单命令之外的所有命令都放在新的进程组中运行,这其中一个好处就是让作业控制成为可能。
进程组信号实际上是Unix一项了不起的创新。我太喜欢它了,甚至把它加进了daemontools,现在svc -+h在多进程服务重启时可以避免产生孤儿进程。
总之,想象一下我有多惊讶:这条命令不只杀掉了那些渲染进程,而是杀掉了机器上除一个进程之外的所有进程!
下面是procps 4.0.4的相关代码片段,它是Ubuntu 24.04 LTS中/bin/kill的提供者。你能看出bug在哪吗?
opterr=0; /* suppress errors on -123 / while ((i = getopt_long(argc, argv, "l::Ls:hVq:", longopts, &optindex)) != -1) switch (i) { / ... / case '?': if (!isdigit(optopt)) { xwarnx(_("invalid argument %c"), optopt); print_usage(stderr); } else { / Special case for signal digit negative
- PIDs / pid = (long)('0' - optopt); if (!execute_kill((pid_t) pid, signo, use_sigqueue, sigval)) exitvalue = EXIT_FAILURE; exit(exitvalue); / ... */
我最初的猜测是错的。-1490947并不存在下溢或溢出bug。
注意我们身处的是未识别选项的switch分支。下面是getopt(3)对未识别选项的说明:
默认情况下,getopt()会在标准错误上打印错误信息,把出错的选项字符放入optopt,并返回'?'作为函数结果。
啊哈。optopt变量只保存了破折号后面的第一个字符,而不是整个负pid。当用字符0减去字符1时,我们得到的pid是-1。kill pid -1会发生什么?糟糕的事情:
如果pid等于-1,那么sig会被发送给调用进程有权限发送信号的每一个进程,但进程1(init)除外。
这条命令是以root身份运行的,所以把-1490947误解析为-1,就杀掉了除init之外的所有进程。Bug找到了,谜团解开了。
但对我来说,这反而引出了更多问题。为什么代码会假设未识别的选项是“signal [原文如此] digit negative PID”?为什么单个数字的负pid要特殊处理,而多数字负pid的一般情况又由谁处理?
更何况,这样的bug怎么会存在于2026年,而且还是在一个Ubuntu LTS版本里?它是一直都在吗?
不,不可能。我过去杀过进程组。而且kill -TERM -12345这种写法是古老的Unix咒语。这以前明明是好用的!
回想起来,我隐约记得有些负pid的kill出过岔子。从来不是“哎呀这杀掉了眼前一切”那种。更像是“奇怪,什么都没发生”那种,或者“噗,我的shell没了”那种。都是些小问题,容易当作误操作耸耸肩就算了。
事实证明,我并不是在凭空想象。过去十年对负pid杀手来说是一段艰难岁月。围绕负pid的bug不少于四个。而且它们全都随Ubuntu LTS版本发布了。
在我讲述这四个bug的故事之前,让我们追溯到更远的Unix历史。
起初Unix有进程。到1973年它有了kill(1)程序。但演化出我们今天所知的进程组,花了十年时间。直到1986年的Unix V9,你才能通过负pid杀掉一个进程组。据我所知,这些年来负pid杀进程在BSD家族里一直工作正常。
90年代初,一个叫Linux的东西出现了。虽然Linux最初为其用户态借用了大量BSD代码,但到90年代末,这种做法陷入了法律风险。一切都要重写,每个轮子都要重新发明。而且要快。对Unix程序员来说,那是个激动人心的时代。
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。