我已经讲过一个故事,关于如何通过将 Linux 内核指向自定义核心转储处理程序然后让该处理程序无法运行来对机器进行软管处理。这会堵塞管道(真的),然后其他所有东西都会堆积在它后面。很快,每个崩溃的进程都会被卡住。
我从那个故事中遗漏的是它是如何走到那一步的。
曾几何时,在一个大公司环境中,有人决定他们想为在同一个 Linux 机器上运行的其他事物提供服务。但是,他们没有编写将通过发布过程和所有这些好东西的 Yet Another Service ™,而是完全采用了不同的设计。
是的,这项服务存在于 *client* 程序中。每次使用通常的 corp RPC 传输构建的程序启动时,它都会尝试绑定到某个 TCP 端口。如果它成功了,它会说“好吧,我想我是赢家”,然后继续在那个端口上启动这个服务。否则,想法是它会连接到该端口并与首先到达那里的人交谈。
这有很多问题。你有一个“服务器”的问题,它凭空出现,这取决于谁或什么首先在机器上启动。它占用了 CPU 时间和内存,这些时间和内存被“充电”回“获胜”的进程,而不是成为它自己的东西,它会追溯到最初写这个东西的人。
想象一下,如果每天早上在一定时间后驶上高速公路的第一辆车必须跟着每个人到他们的办公室并为他们煮咖啡。这就是我所说的那种随机、完全荒谬且难以解决的问题。
更好的是,显然他们所做的这件事是有问题的。随着故事的发展,它启动了线程来做这些事情,并且它设法拥有一些循环引用,这样它就永远不会获得 0 的引用计数,因此它永远不会真正消失。更好的是,试图抢占端口的东西有无限超时,所以它会永远尝试。它实际上从未失败过,因此它从未放弃和关闭。
这意味着在树中某个提交之后构建的每个程序都有这个错误功能,并且无法自行干净地关闭。你必须用足够强的信号朝它的头部射击才能让它消失。
现在,考虑到基本上所有东西最终都使用了这个库,想象一下当核心转储帮助程序获得它时会发生什么。它也卡住了,然后无法关闭。这意味着当盒子上的某些东西崩溃时,内核永远无法完成其核心转储序列。然后支持了它背后的一切。
基本上,这个错误一直在破坏其他程序,但是当它到达核心转储帮助程序时,它变得非常糟糕。正是这种可见性让足够多的人来解决这个问题,并最终将其永久关闭。
我自己对这类事情的启发是,除了一个线程之外,每个线程都在 exit() 中,最后一个线程在内核深处的某个 pipe_* 函数中……并且可能在 do_coredump 或过去的几个堆栈帧中相似的。
如果它发生在你身上,希望你能记住这一点。