如果在此之前已经禁用了中断,那么 logbuf_lock 的目的是什么?是不是因为即使在当前 CPU 上禁用了中断,其他 CPU 仍然可以调用 printk,所以需要停止它们写入日志缓冲区?
是的。 local_irq_save() 防止本地 CPU 上的中断(并且还避免在使用 cpu 变量访问每个 CPU 的数据时在另一个 CPU 上重新调度),而自旋锁可以防止其他 CPU。
如果在任何给定时刻只有 1 个 CPU 上的代码调用 printk(),是否仍可以在 SMP 系统中的其他内核上处理中断?
是的。
我看到 printk 获取 logbuf_lock 并写入日志缓冲区,然后尝试获取控制台信号量并释放 logbuf_lock。然后在循环内的console_unlock 中,它获取logbuf_lock 并禁用中断,然后释放logbuf_lock 并调用控制台驱动程序,然后恢复中断。这个锁定/禁用中断序列的目的是什么?
有两件事需要保护:日志缓冲区和控制台驱动程序。 logbuf_lock 保护日志缓冲区,而console_sem 保护对控制台驱动程序列表和实际控制台本身的访问。
打印内核消息是一个两步过程。首先将消息放入日志缓冲区,然后将日志缓冲区发送到console_unlock() 中的控制台。这两个步骤不需要在对printk() 的同一次调用中发生。此外,在注册/启动/恢复/...控制台时可能会发生将日志缓冲区刷新到控制台。
将消息放入日志缓冲区后,printk() 尝试获取console_sem。它甚至可以在中断上下文中执行此操作,因为down_trylock() 不会休眠。如果它获取信号量,它可以继续将日志缓冲区内容发送到控制台。如果它没有获取信号量,则由控制台信号量的持有者负责将日志缓冲区内容发送到console_unlock() 上的控制台。
当console_unlock() 发送到控制台时,可能有其他CPU 调用printk()。所以console_unlock() 循环,直到没有更多内容可以发送到控制台。在每个循环中,它获取指向日志缓冲区部分的指针以发送到控制台,在logbuf_lock 下,然后,不再在logbuf_lock 下,将输出发送到控制台。在向控制台发送内容时,由于 logbuf_lock 没有被占用,其他 CPU 可以继续向日志缓冲区添加内容。
我在 printk() 中看到有关日志缓冲区可能再次被填满的 cmets,因此缓冲区可能不得不再次刷新到控制台。考虑到我在上面 #1 中询问的所有锁定,这种情况会如何发生?
缓冲区可能在释放logbuf_lock 之后但在up()ing console_sem 之前被填满。并且logbuf_lock 在up()ing console_sem 之前释放,因为up() 可能会导致唤醒,这需要获取运行队列锁,这可能会对使用运行队列锁调用的printk() 产生优先级反转问题(commit 0b5e1c5255 )。
有提议的补丁来改变这个锁定方案:Jan Kara [PATCH 0/8 v4] printk: Cleanups and softlockup avoidance,其中包括尝试避免 CPU 在console_unlock() 上无限循环(它发生在大型系统上,通过慢速串行控制台记录大量启动事件),将该工作移交给其他 CPU;并尽量减少在printk() 上禁用中断的时间。
您的意思是调用 local_irq_save() 会阻止当前线程仅在访问每个 CPU 数据时被重新调度到另一个 CPU 上,或者它会阻止当前线程被重新调度到另一个 CPU 周期?
后者。 local_irq_save() 防止在本地 CPU 上处理中断,这可能最终在从 ISR 返回时调用 schedule()。 schedule() 也可以从其他地方调用,但由于printk() 应该能够从中断上下文中使用,所以它调用的任何内容都不应最终调用schedule()(例如:down_trylock() 用于代替down()这就是原因)。
在 printk() 的情况下 local_irq_save() 的目的是什么?
Jan Kara 的[PATCH 3/8] printk: Enable interrupts before calling console_trylock_for_printk() 试图尽量减少中断被禁用的时间。在那个补丁之前,中断被禁用至少有以下原因:
- 保护受
logbuf_lock 保护的数据免受中断。这种情况通常可以通过自旋锁的 IRQ 变体解决(例如:raw_spin_lock_irqsave()),但这里有更多的东西可以在禁用中断的情况下运行。
- 防止当前 CPU 突然变化。
can_use_console(),从console_trylock_for_printk() 调用,询问:我们此时可以在这个 cpu 上实际使用控制台吗?,并指出:控制台驱动程序可能假设每个 cpu 资源已经已分配。 转移到另一个 CPU 可能会令人困惑。
- 虽然
console_sem 与down_trylock() 一起使用,因此如果中断也尝试printk() 应该不是问题,但在持有console_sem() 时被抢占会阻止其他人打印到控制台。
所以上面的补丁不再为后两种情况禁用中断,而是将它们包装在preempt_disable()/preempt_enable()中,允许中断,但不允许抢占。
我记得在 LMKL 上读过一个线程,它说禁用中断是为了确保日志缓冲区中的条目顺序反映 printk() 调用发生的实际顺序。
您可以分享对该线程的引用吗?您可能会注意到在 3.10 中有一个 cont 缓冲区,它保存了最后一个换行符中的所有字符。当换行符到达或不同的任务printk()s 时,它会被刷新到“真实”日志缓冲区。
在该线程上,除了以下摘录之外,我没有发现有关日志条目顺序的任何有效问题:
嗯,我相信有人得到了
DDetetccctted 113223 HHzz CPUCPU
AFAIK 完全是假的。日志缓冲区的顺序由logbuf_lock 保证,
写入vprintk_emit() 中的日志缓冲区时已禁用中断,并使用锁定变体在console_unlock() 上读取时禁用中断(raw_spin_lock_irqsave())。因此,对日志缓冲区的访问是安全的,不会受到其他 CPU 或中断的干扰。
在较新的内核中,仍然存在将日志行拆分为多个 printk() 调用的情况,该情况由 cont 缓冲区覆盖,该缓冲区保存部分行,并在另一个 CPU/中断干扰时刷新它们,所以一个日志行可以分成几行,并且它们之间有不相关的日志行,但没有日志行应该有混合输出。
另一个可能导致损坏的原因是,由于日志缓冲区是一个环形缓冲区,理论上它可能会溢出,这意味着覆盖以前的消息。
该线程中的一个有效关注点是日志缓冲区的及时输出。这是通过在每个vprintk_emit() 调用中尝试调用console_unlock()(调用控制台驱动程序)来实现的。如果无法获取控制台信号量,则该消息已经在日志缓冲区中,当前信号量所有者将其输出到控制台。
该线程中提到的一件有趣的事情是,在commit a0f1ccfd8d: "lockdep: do not recurse in printk" 之前,(printk.cbefore 和after)在调用release_console_sem()(这是console_unlock() 的前身)之前重新启用了中断。显然,当 lockdep(一个锁验证器,能够检测可能的死锁和其他锁定问题,以及 print 诊断)被启用时,它可能会在尝试从printk() 打印k 时导致锁定。因此,对spin_{,un}lock_irq{save,restore}() 的调用被拆分为禁用/启用中断和获取/释放锁,在两者之间添加了lockdep_on/off() 调用,并且扩展了lockdep 和中断的禁用以覆盖整个函数。
回到:
我看到 printk 获取 logbuf_lock 并写入日志缓冲区,然后尝试获取控制台信号量并释放 logbuf_lock。然后在循环内的console_unlock 中,它获取logbuf_lock 并禁用中断,然后释放logbuf_lock 并调用控制台驱动程序,然后恢复中断。这个锁定/禁用中断序列的目的是什么?
console_unlock() 不仅从vprintk_emit() 调用,它还会在注册新控制台、恢复控制台、将带有未决输出的 CPU 热插拔到控制台时调用,...这些地方通常启用中断。所以,console_unlock() 必须考虑到这一点。
您似乎已经注意到,虽然在 console_unlock()(它调用 raw_spin_lock_irqsave())上使用 logbuf_lock 时禁用了中断,但在释放锁 (raw_spin_unlock()) 时它们不会(可能)重新启用,它们是只有在call_console_drivers() 之后才可能重新启用 (local_irq_console())。我看到在禁用中断的情况下调用 call_console_drivers() 的唯一原因是为了避免 CPU 在我们的控制下发生变化(控制台驱动程序可以访问每个 CPU 的变量)。
一个有趣的数据点是 -rt(实时)补丁集在调用 console_unlock() 之前以及在 @987654403 中的 call_console_drivers()(在 -rt 中受 migrate_disable()/migrate_enable() 保护,不允许 CPU 迁移)之前重新启用中断@。这样做是为了最大限度地减少printk() 期间的中断延迟。 PREEMPT_RT_FULL 支持低中断延迟。可以在printk.c in the linux-stable-rt tree at git.kernel.org看到,相关补丁是printk-rt-aware。