非子进程退出通知支持
Daniel Colascione 提交了一些代码,以支持进程了解其他进程何时终止。通常,进程可以知道其自己的子进程何时结束,但对于不相关的进程,或者至少不容易知道。Daniel 的补丁在每个进程的 /proc 目录条目中创建了一个新文件,名为 "exithand",任何其他进程都可以读取该文件。如果目标进程仍在运行,则尝试 read()
其 exithand 文件将只是阻塞,迫使查询进程等待。当目标进程结束时,read()
操作将完成,查询进程因此将知道目标进程已结束。
这种事情的用处可能不明显。毕竟,非子进程按定义是不相关的。为什么内核要支持它们互相监视呢?Daniel 给出了一个具体的例子,说:
Android 的 lmkd 为了释放内存以响应各种内存压力信号而杀死进程。在继续杀死下一个进程(如果需要)之前,最好等待被杀死的进程实际退出。由于 lmkd 杀死的进程不是 lmkd 的子进程,因此 lmkd 目前无法等待进程在发送 SIGKILL 后实际死亡。
Daniel 解释说,在 Android 上,lmkd 进程目前只会不断检查 proc 目录中每个它试图杀死的进程是否存在。通过实现这个新的接口,lmkd 可以简单地等待 read()
操作完成,而不是持续轮询进程,从而节省持续轮询所需的 CPU 周期。
更普遍地说,Daniel 在后来的电子邮件中说:
我想从系统中消除轮询循环。轮询循环不利于唤醒归因、不利于功耗、不利于优先级继承,并且不利于延迟。“我应该等待多久再检查 $CONDITION ?”这个问题没有正确的答案。如果我们能够为某些东西提供显式的等待队列接口,我们就应该这样做。此外,PID 轮询容易受到 PID 重用的影响,而这种机制(就像任何基于 struct pid 的机制一样)对此免疫。
Joel Fernandes 建议,作为一种替代方案,使用 ptrace() 来获取进程退出通知,而不是在 /proc 下创建一个全新的文件。Daniel 解释说:
一次只能有一个进程 ptrace 给定的进程,所以我不喜欢 ptrace 作为任何东西的机制,除了调试。依赖 ptrace 进行退出通知会干扰调试器和崩溃转储收集系统等。此外,ptrace 可以做太多事情(例如读取和写入进程内存),因此需要非常强的特权,这对于这种机制是不必要的。此外:ptrace 的接口很复杂,并且依赖于重复调用各种 wait 函数,而此补丁中的接口足够简单,可以从 shell 中使用。
PID(进程 ID)重用的问题再次出现,因为并非所有人都清楚 /proc 目录中的一个全新文件是解决问题的最佳方法。正如 David Laight 所说,Linux 在所有 PID 上都使用了引用计数器,因此任何重用都可以被看到。他认为 /proc 目录应该包含某种方式来暴露该引用计数。
其他操作系统内核有其他方法来尝试避免 PIT 重用,或至少减轻其缺点。正如 Joel 解释的那样:
如果您查看 NetBSD pid 分配器,您会看到它使用低位 pid 来索引数组,高位作为序列号。数组槽也以 LIFO 方式重用,因此在数字被重用之前,您总是需要大量 pid 分配/释放。非顺序分配也使得预测 pid 何时被重用变得更加困难。当表格几乎满时,表格大小会加倍。
但对此,Daniel 回复说:
NetBSD 仍然只是粉饰问题。真正的问题是,整个基于 PID 的进程 API 模型是不安全的,而巧妙的 PID 分配器并不能解决根本的竞争条件。只要 PID 重用是可能的,就存在潜在的竞争条件,而正确性取决于希望。在不更改 Unix 进程 API 的情况下解决 PID 竞争问题的唯一方法是使 pid_t 变得非常宽,以至于它永远不会回绕。
在其他地方,Aleksa Sarai 仍然不相信在 /proc 目录中创建一个全新的文件会是一件好事,如果可以避免的话。Aleksa 理解 Daniel 想要避免持续轮询,但感觉仍然有可行的替代方案。例如,Aleksa 说:“当您打开 /proc/$pid 时,您已经拥有了底层进程的句柄,并且您已经可以轮询以检查进程是否已死亡(例如,fstatat 失败)。如果我们只是使用 inotify 事件来告诉用户空间进程已死亡,以避免用户空间进行轮询循环,会怎么样呢?”
Daniel 回复说,Aleksa 的解决方案比 Daniel 的解决方案复杂得多。他说 inotify 和相关的 API 是:
...旨在广泛监控系统活动,而不是等待某些特定事件。它们需要大量的设置代码,并且由于两者都是事件流 API,具有可能溢出的缓冲区,因此两者都需要一些逻辑供用户空间检测缓冲区溢出,并在发生这种情况时回退到显式扫描。它们也是内核的可选部分。
Daniel 继续说道:
鉴于我们 *可以* 廉价地为用户空间提供干净且一致的 API,为什么我们反而要将一些奇异且难以使用的接口强加给用户空间呢?要求用户空间轮询目录文件描述符,并在轮询返回时,通过查找 fstatat 中的某些错误(我们必须指定哪些错误)来检查,这很笨拙。/proc/pid 是一个目录。在其他什么情况下,内核会要求用户空间以这种方式使用目录呢?
辩论继续进行,邮件列表上没有达成决议。Daniel 继续坚持认为他的方法比任何提议的替代方案都更简单,并且他还认为这符合 UNIX 本身的精神。在某个时候,他解释说:
基本的 unix 数据访问模型是,用户空间应用程序想要信息(例如,文件中的下一批字节、套接字中的下一个数据包、信号 FD 中的下一个信号等),并通过在文件描述符上进行系统调用来告诉内核。通常,内核在可用时将请求的信息返回给用户空间,可能会阻塞直到信息可用。有时用户空间不想阻塞,因此它将 O_NONBLOCK 添加到打开的文件模式,在这种模式下,内核可以告诉用户空间请求者“稍后重试”,但真理的来源仍然是通常阻塞的系统调用。用户空间如何知道在“稍后重试”的情况下何时重试?通过使用 select/poll/epoll/whatever,这表明了“稍后重试”重试的好时机,但并非决定性的,因为该通常阻塞的系统调用仍然是唯一的真理来源,并且允许 poll 报告虚假的就绪性。此模型工作正常,并且围绕它构建了大量的心理和技术基础设施。系统几乎对每个对应用程序有用的信息位都使用它。
反对 Daniel 补丁的意见似乎源于避免向 /proc 添加新文件的愿望。/proc 和其他内核接口确实存在随着时间的推移变得臃肿、过于复杂和难以维护的风险。Linus Torvalds 和其他顶级贡献者希望避免这种情况,尤其是在一旦实现接口就很难删除它们的情况下。一旦用户软件开始依赖给定的接口,Linux 就非常不愿意破坏该软件。其中一个原因是并非所有软件都是开源的,并且较旧的闭源工具可能没有得到维护,因此可能无法适应任何新接口。他们依赖的某些东西的更改可能意味着该软件根本无法与较新的内核一起使用。内核开发人员希望尽可能避免这种情况。
鉴于反对意见,目前尚不清楚 Daniel 的补丁是否会以当前形式进入树中。可能用户代码(在本例中为 Android 操作系统)目前将不得不继续使用其他更复杂的方式来了解进程何时死亡。
Daniel Colascione 的附言(2019 年 1 月 10 日)感谢您撰写关于这项工作的文章!我只想补充一点,该项目正在进行中,我计划在 Christian Brauner 的 pidfd_kill 补丁落地后,刷新我的非子等待工作。我目前的想法是,返回退出句柄的系统调用可能是 readdir 可见的 proc 文件的一个可行的替代方案。
一个尚未解决的难题是弄清楚谁应该能够读取进程的退出状态,如 这里 的线程中所述。只有父进程有权访问吗?像 FreeBSD 中那样所有进程都可以访问吗?只有同一用户?Root 用户?
尽管如此,总体思路是您应该能够以某种方式获取文件描述符,您可以从中 read(2) 包含退出状态的 siginfo_t(类似于 waitid(2)),并且我期待以某种方式将此功能添加到 Linux 中。
注意:如果您在上面被提及并希望在评论区上方发布回复,请将包含您的回复文本的消息发送至 ljeditor@linuxjournal.com。