关注了就能看到更多这么棒的文章哦~
GDB and io_uring
By Jake Edge
March 31, 2021
DeepL assisted translation
https://lwn.net/Articles/851076/
在用 GDB 来调试那些使用了 io_uring 的程序时,出现了一个问题,从而引出了后续的一系列可能的解决方案,并且最终有一个方案被合并到 Linux 5.12-rc5 中。这个问题来源于 5.12 合并窗口中改变了 io_uring 线程的创建方式,它们跟使用 io_uring 的进程关联了起来。这些 "I/O 线程" 在内核中得到特殊的处理,这就导致了 GDB 这边出现问题(很可能其他使用 ptrace()的程序也会出错)。解决方法就是按照跟其他线程一样的方式来对待它们,因为事实已经证明,对它们进行特殊化处理所引起的问题要比所解决的问题更加多。
Stefan Metzmacher 在 3 月 20 日向 io-uring 邮件列表报告了这个问题。他希望将 GDB 连到(attach to)一个使用了 io_uring 的程序进程,但 GDB 调试器 "进入了一个死循环,因为它无法 attach 到 io_threads"。PF_IO_WORKER 线程是 io_uring 中用在可能会发生阻塞操作的情况下。他在 bug 报告之后提出了两个 patch set,来通过不同的方式隐藏这些线程。之所以要隐藏掉它们,是因为 GDB 只要看不到这些线程,就不会去试图 attach 到这些线程上去。在 5.12 之前,这些线程也是存在的,但并没有不与 io_uring-using 进程关联起来,所以 GDB 就看不到。
当然,对于开发者来说,如果不能在使用了 io_uring 的代码上运行 GDB 调试器,这可不是一件好事,尤其是,如果他们应用程序中的 io_uring 支持代码很可能是比较新的,因此它更可能需要更多调试方式。io_uring 子系统的维护者 Jens Axboe 很快就出面帮助 Metzmacher 解决了这个问题。Axboe 发布了一个 patch set,其中实现了对隐藏 PF_IO_WORKER 线程的效果,以及对这些线程相关的 signal 处理能力的一些调整。其实,他就是完全移除了这些线程接收 sginal 的能力。
这让 Eric W. Biederman 有些不满意。他想知道为什么 io_uring 线程就不能接收信号,尤其是 SIGSTOP。ptrace()使用 SIGSTOP 来 attach 到正在运行的进程,但 I/O thread 缺乏一些用户空间的正常上下文,无法处理信号。Linus Torvalds 解释说,signal 处理是在线程返回用户空间时进行的,但对于内核线程来说,这个动作永远不会发生。他在另一封邮件中进一步描述了这一点:
SIGSTOP 处理本质上是在 signal handling 的时候进行的,而 signal handling 本质上是在 "返回用户空间" 时完成的。
于是:你根本无法向内核线程发送任何信号,除非它会明确地对这些 signal 进行手动处理。在这方面,SIGSTOP 就跟 user space "实际上" 发出的 signal 的处理程序没有什么不同。
实际上,内核线程唯一能处理的信号是 SIGKILL (在此情况下结束线程)。
[……]我确实也认为 IO thread 不需要进行 signal 处理,因为它们根本无法用正常的方式来处理 signal。
几天后,Axboe 发布了另一个版本的 patch set,并详细介绍了这个问题以及提出的解决方案:
Stefan 报告说,在 attach 到一个使用了 io_uring 的任务上的时候,gdb 会非常困惑,反复尝试 attach 到 IO thread 上,尽管每次它都会收到一个 -EPERM 返回值。这个 patch set 会忽略 same_thread_group() 中的 PF_IO_WORKER 类型的线程,除非是在做我们特地进行的统计工作。
同时我们还避免在/proc/<pid>/task/ 中列出 IO thread,这样 gdb 就不会认为它应该 stop 并 attach 这些线程。这样一来就跟之前的内核行为保持一致了。之前的内核中,这些异步线程与拥有 io_uring 的进程无关,因此 gdb(和其他工具)就不会去理会这些进程的。
但似乎那些补丁改得有点过了。Biederman 指出,这些线程完全不会再显示在/proc 中,这样以来 top 等诊断工具也就看不到了。Torvalds 指出,就连 ps 也都看不到这些进程了,因此他 "认为把它们隐藏起来不是一个正确的做法"。有一些讨论在说是不是把这类线程列在在 /proc 其他子目录下,但 Axboe 认为这可能会让那些常用工具处理简单,但很可能会 "搞乱一些东西"。Biederman 说,需要有一些机制来告诉 GDB(和其他调试器)这些线程是特殊的:"我在想,在尝试 attach 时得到-EPERM(或者可能其他不同的错误代码)是不是就已经能判断出这个线程不可以被调试了。"
Axboe 在 patch cover letter 中提到,这里的底层问题可能真的是 GDB 的 bug,Biederman 似乎表示赞同了,但 Oleg Nesterov 表示反对。"内核改变了规则,这导致了 GDB 出错"。但 Biederman 认为,这并不是严格意义上的 regression,"这其实是 gdb 没有支持好新的功能"。不过,哪怕真的算是 gdb 的 bug,Axboe 也认为要等 gdb 的更新版能到用户手里的话需要等待的时间还是太长了,所以才需要用这种方式来解决;除此之外,"我觉得可能 gdb 不是唯一一个会出问题的工具,还会有其他工具没想到这里有些线程是无法 attach 的。"
所以,人们希望有某种解决方案,能让所有的一切都能 "正常工作"——而这似乎正是 Torvalds 想出的办法所能实现的效果:
实际上,也许正确的处理方法是干脆让所有的 io 线程都接受 signal,从而将所有特殊情况都处理掉。
当然,这些信号永远不会传递到用户空间,但如果我们:
在有待处理的 signal 的时候,就让这个 thread 主体执行 get_signal()
允许对他们进行 ptrace_attach
这样它们看起来就跟普通线程差不多了,只是从来不做 user-space 的 signal 处理。
之前我们让 IO thread 采用特殊方式来处理 signal,这已经引起了很多问题,也许解决的办法就是干脆不让它们这么特殊?
Axboe 也同意这种 "支持 signal,这样就能让所有一切都可以默认正常工作"。为此,他尝试按照这个思路进行修改后再使用 GDB attach,并获得了成功。于是在 3 月 25 日提出了第一版 "允许 IO thread 使用 signal" 的 patch set,经过一些修正后,在 3 月 26 日提出了第 2 版。后者在 3 月 28 日迅速被 5.12-rc5 所采纳。此外 kernel 也 revert 了一周前 5.12-rc4 所合入的一些临时的 fix。
总之这个“I/O 线程直接像其他线程和进程一样接受信号,而不是成为一种特殊情况”的想法,大大地简化了解决方案。就像 Axboe 在第一个版本的 cover letter 中所说的那样,事后来看,这个方案是显而易见的:
就像其他大多数的好想法一样,只要一听到,你就明白了这是一个好想法。事实上,我们最终用这个方案的时候,不需要处理任何特殊情况了,这就清楚地表明,这个方案确实是正确的解决方案。事实上,这组 patch 中大多数是 revert 代码,这进一步证明了这一点。
最后终于出现了这个更好的解决方案。至少在一定程度上得益于 Torvalds 重新思考了自己的决策,重新考虑了一些假设条件。虽然 PF_IO_WORKER 线程对发送给它们的 signal 不能做任何事情,但也并不需要去拒绝这些信号。一旦认识清楚这一点,patch set 就相当简单了。而与此同时,对于使用 io_uring 的代码的开发者来说,这个令人不快的问题也被快速消灭了。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~