关注了就能看到更多这么棒的文章哦~
A discussion on printk()
By Jake Edge
October 4, 2022
LPC
DeepL assisted translation
https://lwn.net/Articles/909980/
出于各种原因,内核的打印函数 printk() 多年来一直有许多与之相关的改进工作。printk() 的一个顽固问题是它带来的 latency 对于实时 Linux 内核来说是不可接受的;在这一点上,printk()是 RT_PREEMPT patch 完全合并之前需要完成修改的最后一部分工作。因此,人们一直在努力重新实现 printk(),以解决 latency 以及其他一些问题,但这些都没有进入 mainline;最近在 2022 年 Linux Plumbers 大会(LPC)上的讨论似乎为新的解决方案铺平了道路,不久之后 mainline 中应该就有相关的内容了。
[John Ogness]
为 Linux 6.0 提出的 printk() pull request 被 Linus Torvalds 拒绝后,RT_PREEMPT 的维护者 Thomas Gleixner 重新开始在白板前引导大家来讨论出一种更可接受的方式来了结这些工作。这引出了 9 月 11 日发布的一组 patch set,正在 LPC 开始之前。Torvalds 说,根据相当粗略的 review,"看起来这个方案似乎模型比较合理"。John Ogness 也一直在研究 printk() 的问题,他安排在 9 月 14 日举行一次小讨论(birds-of-a-feature meeting),讨论这个提案。
Ogness 在会议开始时介绍了(并演示了)新的代码。Ogness 说,当他和 Gleixner 提到 "console lock"时,他们指的不是现有的、适用于所有 console 的全局锁,而是他们会新加入的一个 per-console lock。每个 console 还会新增一个 state structure,用来跟踪它们的状态。其中包含 console 在使用的 CPU、console priority(normal、emergency、panic),还有一些 flag。CPU 也会跟踪它们当前正在打印的优先级信息,这样拥有最高优先级信息的 CPU 就能真正开始访问做 print 的内核线程。还有一些嵌套优先级的跟踪记录,这样一来,如果一个 CPU 正在打印 panic 级别的信息的时候导致了一个 warning,那么 warning 打印不应该开始,而应该只是被添加到 ring buffer 里面。
希望进行打印时,可以根据指定它要打印的上下文来获取相应的 CPU 的 per-console lock。这个上下文中包括 CPU ID,消息的优先级,以及一些描述了 acquisition type 的属性,比如是 friendly 的还是 hostile 的 takeover,或者 CPU 是否愿意 spin-wait 并带有一个超时值等等。内部实现上来说,会有一个简单的规则来管理哪个 CPU 获得 console:哪个 CPU 有最高优先级的消息要输出。例如,如果正在打印一个 warning,那么另一个 warning 不可以夺取控制台的控制权,只能把自己的 warning 放入 ring buffer,此外还有一个 panic priority 可以影响这里的行为。
Takeovers
Ogness 描述了一个 friendly takeover 的过程。在这个例子中,CPU 0 当前正在以优先级 1(正常优先级)打印,而 CPU 1 想以优先级 2(紧急优先级)打印一个 warning;每一个 printing console 都必须在每次输出一个字节之前(或以硬件能处理的最小粒度上)进行检查,看另一个 CPU 是否需要 print。console_can_proceed() 的检查是看另一个 print 请求的优先级是否更高;如果确实是的话,它就把打印任务交给另一个 CPU,完成后再把控制权还给 CPU 0。
[Thomas Gleixner]
如果 CPU 0 被抢占或者不能友好协商来放弃 console,那么就需要一个机制来让拥有更高优先级打印任务的 CPU 进行 hostile takeover 来强行接管。所有的接管动作都是通过 atomic compare-and-exchange 操作来完成的。值得注意的是,虽然在任何时候都只有一个 CPU 在真正向 console 打印,但其他 CPU 仍然可以使用 printk();数据会进入 ring buffer,并最终传到 console,至少希望是这样。
Mathieu Desnoyers 问道,在发生 hostile takeover 的时候,printer thread 会发生什么情况。Ogness 说,console_can_proceed() 函数将识别出 console 状态发生了变化,这表明是 hostile takeover,那么这个函数会报错返回。Gleixner 补充说,可以添加 policy 来决定要怎么做,因为这个决定是取决于上下文和驱动程序的。
控制台驱动程序可以将自己标记为 unsafe 状态,或者平时就是在 unsafe 状态下,这样可以让系统策略来决定何时以及如何使用它们。例如,在 panic 情况下,如果有一个 safe console 可用,系统可能不想在 unsafe console 上 print;也许应该在尝试其他驱动程序之前先使用 pstore 驱动程序,但这个决定应该是在系统中的更高层面来做出,而不是在当前在讨论的 console-handling 代码核心中实现。
如今,如果有两个安全的 console 和一个不安全的 console,那么会被依次写入,者可能导致两个安全的控制台出现了这一行信息,但是第三个控制台被 lockup 了,因此没有进一步的数据打印了。Ogness 说,这个想法是为了向控制台驱动程序提供足够的信息,使他们能够更好地决定该怎么做。这些驱动程序将知道当前状态,而不是仅仅得到要打印的字符串和其长度信息,这些状态包括:信息的优先级,控制台是否被 hostile takeover 等等。"我们有工具和信息来做出明智的决定",也就是确定如何能尽力确保输出信息能到达用户。
printk()的维护者 Petr Mladek 问,在发生 takeover 时,那些积压在 ring buffer 中的消息会怎么处理;旧的信息是否会在新的更高优先级的消息之前被打印出来?Gleixner 说,目前旧信息会先输出。这样做的部分原因是,这已经是目前的工作方式了,但是确实有人认为优先级较高的信息应该优先于已经在 ring buffer 排队的信息。目前还只能按当前方式实现,因为会需要混用新旧驱动程序;等所有的驱动都被转换了之后,就可以选其他方式了。
Incremental
Ogness 说,跟早期的那些提案不同,当前的方案是一种渐进式的实现方法。新增了一个 console flag 来表示这个驱动程序实现了这些机制;这意味着它们提供了 write_thread() 以及 write_atomic() callback 函数,并遵循新的方案里的工作方式。write_thread() 是在线程上下文中调用的,因此它可以 sleep,而 write_atomic() 是在 atomic 环境下调用的,因此不会阻塞。console 驱动程序将被更新,而那些没有更新或者尚未更新的驱动程序仍将继续完全按照他们当前的功能来继续运行。为了测试,Gleixner 和 Ogness 使用了一个 "早期 uart8250 console 的 hack 版本",这个版本也被贴到了讨论主题上。8250_early console 在重入和非屏蔽中断(NMI)场景下都是安全的,这使得它成为一个合理的测试环境:
[……]我们想集中精力验证 friendly takeover 和 hostile takeover 的核心机制,不希望同时还要处理 port lock 或其他可怕的问题。
Ogness 说,希望这些拥有线程和 atomic 环境支持的优势能给控制台开发人员带来一些动力,促使他们更新现有代码。最终,系统需要确保所有控制台都在新方案下工作,才能真正从这个方案中受益。他和 Gleixner 一直在与 Daniel Vetter 和 graphics 开发人员合作,试图确保 graphics console driver 能够改成新机制来工作。
在回答一个关于部分写入(partial write)的问题时,Ogness 指出,在新的 state 结构中,有记录这个 console 的 sequence counter。该序列计数器只有在一条记录被完全打印出来时才会被更新;如果这个控制台被一个接管者打断了,它将在序列计数器被增加之前停止打印;接管的 CPU 将在记录的开始处重新开始。他说,会有一个 demo 来演示这种行为。
Steven Rostedt 询问了同时拥有新旧 console 的系统,他们既然无法从这项工作中受益;是否有办法关闭那些旧式的 console?Gleixner 说,会增加一个内核命令行选项来 "忽略所有旧式 console"。他说,实时内核将需要强制打开这个选项。
Demo
Ogness 在 QEMU 中运行了一个演示,显示了系统在修改过的串行控制台的输出,该控制台以 9600bps 的速度运行。这样做是为了突出展示一下串行控制台的缓慢程度,因为它在内核实际完成启动后很长时间还在继续打印启动信息。他把控制台改为 14400bps 后重启了系统;一旦到了线程打印很活跃的时候(当时运行在 CPU 0 上,这是启动所用的 CPU),他在 CPU 1 上触发了一个 warning。随即,CPU 1 上的打印就切换到 atomic 模式,把积压的启动信息继续打印了出来。
从输出结果中可以看到,它显示了信息实际发生时的时间戳,而不是打印时的时间戳,这是新机制的一个优点。在系统启动大约 7 秒后,他触发了 warning,等积压的启动信息被打印出来之后,这个 warning 就会最终显示在输出中。它显示 CPU 1 请求以优先级 2 打印,CPU 0 做了 friendly handover。然后在 CPU 1 开始打印 warning 之前有一个 12 秒的时间窗,因为这个阶段都是用来把积压信息打印出来的。
他翻回去查看 CPU 1 接管的地方,其中显示了 thread 模式下 CPU 0 的 partial line,然后马上是 atomic 模式下 CPU 1 打印出来的同一行的完整内容。他说,这看起来也许有点难看,但由于是同一个驱动程序,而且状态会跟踪正在发生的事情,所以可以做得更干净一些。例如,可以在这个 partial line 之后打印一个换行符,Gleixner 说。还有人建议说让 atomic 模式的写法在开始输出当前行(后面是积压行)之前预先打印一些信息,以表明控制台的接管动作就发生在那个时间点。
Gleixner 还澄清说,这里显示的时间戳是 printk()信息被添加到 ring buffer 的时间,而不是实际输出信息到控制台的时间点。在把 warning 添加到 ring buffer 之前,积压的信息已经被处理了,但这个行为可能会被改掉。也许把 warning 信息提供出来要比花时间打印大量的积压信息更重要。Gleixner 说,所有的机制都已经 ready,"但我们还没有使用这些机制";也许当所有的 console 驱动程序都转换完毕后,这样做才有意义。
Ogness 接下来进行另一个演示。在进入线程打印模式,他就在 boot CPU(CPU 0)上引起 warning;这导致 CPU 进行了 hostile takeover,进入 atomic 模式。然后,他发送了一个 NMI,由于内核在编译时指定如果发生了未知的 NMI 就 panic,所以这里就产生了 kernel panic。这就开始了更多的关于信息打印顺序以及 panic 信息是否应该优先处理的讨论。
但 Torvalds 指出,把积压的信息打印出来是很重要的,因为它可能包含了导致后续问题的背景信息。他说,demo 的输出看起来不太好,主要是因为这是一个伪造的例子,但在现实世界中,积压信息提供了重要的线索,没有它们的话,panic 信息就没有意义了。重要的是要确保 stack trace 以及其他信息都进入 ring buffer,在这个动作之后,积压的信息就会被打印出来。
Gleixner 表示同意,并说有很多细节需要逐步解决。会议的目的是确定这个方法是否合理,是否应该继续下去,或者他们是否应该 "直接放弃,然后宣布 printk() 永远会是个雷区"。
Console changes
Greg Kroah-Hartman 说,如果不进一步了解控制台驱动程序中需要进行的具体改动,他就无法真正回答这个问题。Gleixner 说,驱动程序需要提供这两个回调函数;在相当简单的串行驱动程序中,比如他们用于测试的这个驱动程序里,这两个回调是相当类似的。更复杂的控制台,如 graphics 或者 network console 就需要更多的工作来决定在原子环境下这个 write_atomic()回调实际上可以做些什么动作。
Kroah-Hartman 指出,USB serial console 需要启用中断,这在 atomic 上下文中是行不通的;对这些可以做什么?Gleixner 同意,这些机制在某些情况下会无法正常工作,但驱动程序将有足够的信息来做出明智的决定,并只是返回一个 error 或将自己标记为无法在 atomic 上下文中使用。现在,对于这些类型的控制台,"我们事实上在尝试、祈祷和放弃",但在新方案下,它们可以简单地被跳过(或推后处理)。
Rostedt 问道,在可以从任何上下文中写入的 console 驱动记录好 message 之后,再尝试这些被跳过的控制台。Gleixner 说,这需要管理员来进行选择,因为尝试其他控制台有可能会让系统挂死。kexec 的开发者担心,如果控制台打印让系统挂死了,那么 kexec 子系统就无法使用了。比如说管理员应该可以配置让 log 只记录到 pstore,然后尝试确保在 crash 之后来通过 kexec 运行一个新 kernel。
在一些系统(如台式机和嵌入式系统)中,有一个令人担忧的问题:USB 串口控制台是唯一可用的方案。如果 console 不能使用中断,它可能无法工作好(甚至根本无法工作),但它可能会有一些信息对查清问题是至关重要的。会议同意,应该有某种方式来表明,一旦其他更安全的通路都尝试完了就需要尝试最后的方案。
会议还同意,一旦 console 都被修改了,那么之前的 "early console" 这个概念就应该取消掉了。write_atomic() 回调将用来完成 early console 的职责。用于测试的这个魔改版的驱动正是这样做的:它注册为 early console,然后等系统达到可以使用 thread 的阶段了,就继续作为普通线程控制台来运行了。
在 David Woodhouse 向 Gleixner 表白了他的爱意之后(而且不是第一次了,他在笑声中补充说),Gleixner 想说清楚一些事情:
printk() 是我在 Linux 的遗留混乱中最后想要清理的东西。然后,我将把工作交给年轻人,他们可以开始清理我 20 年前放入内核的东西。
就这样,会议基本上结束了,尽管 Ogness 还进行了另一个演示。看来大家对提出的方向没有什么大的反对意见。Ogness 发布了一份会议报告以及一组准备好的 patch,现在正在讨论中;Mladek 已经接受了 6.1 的五个清理类型的 patch。剩下的可能还需要一段时间,但我们有理由希望这个长期存在的需要改善的领域能得到解决,这将最终为 RT_PREEMPT patch set 的最后几项的 merge 来铺平道路。在这最终发生时,应该值得庆祝一下了。
[感谢 LWN 的用户支持编者去都柏林参加 Linux Plumbers 大会]。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~