【问题标题】:printk interrupt disabling and lockingprintk 中断禁用和锁定
【发布时间】:2014-11-10 11:55:53
【问题描述】:

我有一个关于在 3.10 内核中实现 printk() 的问题。我在开头看到它callslocal_irq_save。然后我看到了callsraw_spin_lock(&logbuf_lock)。如果在此之前已经禁用了中断,那么 logbuf_lock 的目的是什么?是不是因为即使在当前 CPU 上禁用了中断,其他 CPU 仍然可以调用 printk,所以需要停止它们写入日志缓冲区?

基本上我有三个问题:

  1. 我看到 printk 获取 logbuf_lock 并写入日志缓冲区,然后尝试获取控制台信号量并释放 logbuf_lock。然后在console_unlock 的循环内部,它获取 logbuf_lock 并禁用中断,然后释放 logbuf_lock 并调用控制台驱动程序,然后恢复中断。这个锁定/禁用中断序列的目的是什么?

  2. 我在 printk() 中看到有关日志缓冲区可能再次被填满的 cmets,因此缓冲区可能不得不再次刷新到控制台。考虑到我在上面 #1 中询问的所有锁定,这种情况会如何发生?

  3. 如果在任何给定时刻只有 1 个 CPU 上的代码调用 printk(),在 SMP 系统中的其他内核上仍然可以处理中断吗?我也在尝试了解 printk 对中断延迟的影响。

谢谢。

一些后续行动:

你能澄清一下吗:

local_irq_save()防止本地 CPU 上的中断(并且还避免在使用 cpu 变量访问每个 CPU 数据时在另一个 CPU 上重新调度)

您的意思是调用local_irq_save() 将阻止当前线程仅在访问每个CPU 数据时才在另一个CPU 上重新调度,或者它会阻止当前线程在另一个CPU 周期上重新调度?在这里 printk() 的情况下 local_irq_save() 的目的是什么?我记得在 LMKL 上读过一个线程,它说禁用中断是为了确保日志缓冲区中的条目顺序反映 printk() 调用发生的实际顺序。

【问题讨论】:

  • 请考虑在 linux 论坛中提问,因为您的问题实际上不是可以在这里解决的编程问题。
  • @fast:这怎么不是编程问题?它可能与 Linux 内核编程有关。

标签: linux linux-kernel scheduler


【解决方案1】:

如果在此之前已经禁用了中断,那么 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。

【讨论】:

  • 感谢您提供非常有帮助和彻底的回复。我在原始问题中添加了一些后续内容。
  • Here's 提到在 printk 中禁用中断的原因的线程是为了允许确定性行为。
  • 我对你的最后 2 段感到困惑。在 for 循环末尾的 3.10 kernel 中,我看到了对 raw_spin_unlock(&logbuf_lock); 的调用,对控制台驱动程序的调用,然后是对 local_irq_restore(flags); 的调用。从我阅读代码来看,当调用console_unlock() 时,中断仍然被禁用,我的理解是local_irq_restore() 将使它们保持禁用状态。我也不确定为什么释放 logbuf_lock 比 local_irq_restore 早。
  • 而且 -rt 开发人员似乎同意该评估。也许一些主线开发者更担心中断风暴会无限期地延迟控制台输出(甚至可能溢出环形缓冲区)?
  • @AlexHoppus:如果您查看log_store(),谁调用了log_make_free_space(),可能还有truncate_msg(),现在似乎是新消息,那些可能丢失或截断的消息,以及printk返回实际记录的字节数。这与 emit_log_char() 历史上发生的情况不同。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-09-20
  • 2011-11-23
  • 2015-01-24
  • 1970-01-01
  • 2020-10-02
  • 1970-01-01
相关资源
最近更新 更多