【问题标题】:Why kernel code/thread executing in interrupt context cannot sleep?为什么在中断上下文中执行的内核代码/线程不能休眠?
【发布时间】:2018-02-27 01:12:13
【问题描述】:

我正在阅读 Robert Love 的以下文章

http://www.linuxjournal.com/article/6916

说的是

"...让我们讨论一下工作队列在进程上下文中运行的事实。这与其他所有在中断上下文中运行的下半部分机制形成对比。在中断上下文中运行的代码无法休眠或阻塞, 因为中断上下文没有可以重新调度的后备进程。因此,由于中断处理程序与进程没有关联,调度程序没有任何东西可以进入睡眠状态,更重要的是,调度程序没有任何东西可以唤醒。 ..”

我不明白。 AFAIK,内核中的调度程序是 O(1),这是通过位图实现的。那么是什么阻止调度器将中断上下文置于睡眠状态并采取下一个可调度进程并将控制权传递给它?

【问题讨论】:

  • 请注意,linux-rt 补丁集实际上确实使中断处理程序线程化,因此它们可以休眠。这可以改善延迟,但性能会受到很大影响。
  • 这个问题值得更多的审阅者,IMO,每个低级程序员在他的职业生涯中都考虑过这个问题。另请参阅此邮件交换hi.baidu.com/rwen2012/item/2040e7cace3c6a0dac092f3c

标签: linux-kernel


【解决方案1】:

那么是什么阻止调度器将中断上下文置于睡眠状态并采取下一个可调度进程并将控制权传递给它?

问题是中断上下文不是一个进程,因此不能进入睡眠状态。

当中断发生时,处理器将寄存器保存到堆栈中并跳转到中断服务程序的开始处。这意味着当中断处理程序运行时,它在中断发生时正在执行的进程的上下文中运行。中断正在该进程的堆栈上执行,当中断处理程序完成时,该进程将继续执行。

如果您尝试在中断处理程序中休眠或阻塞,您不仅会停止中断处理程序,还会停止它中断的进程。这可能很危险,因为中断处理程序无法知道被中断的进程在做什么,或者即使该进程被挂起是安全的。

一个可能出错的简单场景是中断处理程序和它中断的进程之间的死锁。

  1. Process1 进入内核模式。
  2. Process1 获得 LockA
  3. 发生中断。
  4. ISR 开始使用 Process1 的堆栈执行。
  5. ISR 尝试获取 LockA
  6. ISR 调用 sleep 以等待 LockA 被释放。

此时,您遇到了死锁。 Process1 在 ISR 完成其堆栈之前无法恢复执行。但是 ISR 被阻塞,等待 Process1 释放 LockA

【讨论】:

  • 此外,中断通常需要非常快速的服务,否则您很容易陷入各种麻烦。
  • OK..您的声明中有两个论点:1.“如果您尝试休眠或阻塞 .... 或者即使暂停该进程是安全的。”我完全不认同这个论点。首先,内核不关心用户空间进程正在做什么,或者是否可以安全地挂起它。此外,使用抢占式内核,甚至可以阻塞一个内核线程并启动另一个。 2.“在最坏的情况下,中断处理程序的阻塞可能导致死锁”这是一个锁定问题。如果我的 ISR 在调用 sleep 之前释放所有锁怎么办?
  • @Methos - Re 1. 问题是当您在内核模式下中断进程时,而不是在用户模式下的进程。如果您中断一个内核线程并让处理程序阻塞,这与普通线程抢占不同,因为您将同时抢占两个不相关的上下文,即内核线程和 ISR。如果它们之间存在依赖关系,那么你就死定了。因此,我的示例是关于内核线程持有 ISR 所需的资源。在 ISR 完成之前,内核线程将无法继续执行。但是 ISR 正在等待内核线程。死锁。
  • @Methos - Re 2. 在我的示例中,它是一个持有锁的内核模式进程。我将编辑我的答案以提供更清晰的解释。请注意,ISR 不能在调用 sleep 之前释放锁,因为 ISR 一开始就无法获取锁。如果你试图获取一个锁,你可能会阻塞,这和直接调用 sleep 一样糟糕。
  • 基思,我还是不同意你的观点。锁与此问题无关(尽管您给出的示例显示了典型的死锁情况)。我已经在 arsane 的回答中评论了我的想法。
【解决方案2】:

我认为这是一个设计理念。

当然,您可以设计一个可以在中断中休眠的系统,但除了使系统难以理解和复杂(您必须考虑许多情况)之外,这无济于事。所以从设计的角度来看,将中断处理程序声明为不能休眠是非常明确且易于实现的。


来自 Robert Love(内核黑客): http://permalink.gmane.org/gmane.linux.kernel.kernelnewbies/1791

您不能在中断处理程序中休眠,因为中断没有 一个支持进程上下文,因此没有什么可以重新安排回来 进入。换句话说,中断处理程序与任务无关, 所以没有什么可以“睡觉”和(更重要的是)“没有什么可以 醒来”。它们必须以原子方式运行。

这与其他操作系统没有什么不同。在大多数操作系统中, 中断没有线程化。然而,下半部分通常是。

页面错误处理程序可以休眠的原因是它只被调用 通过在进程上下文中运行的代码。因为内核自己的 内存不可分页,只有用户空间内存访问才能导致 页面错误。因此,只有少数几个特定的​​地方(例如调用 copy_{to,from}_user()) 可能会导致内核中的页面错误。那些 位置必须全部由可以休眠的代码(即进程上下文, 没有锁等等)。

【讨论】:

  • 我得出了类似的结论。但我不确定如何支持这一说法。只是想找出是否有任何“数学”理由不这样做。
  • 我不知道你会如何证明,在“数学”意义上,不可能建立一个允许 ISR 休眠的系统。但是我已经在许多操作系统中进行了编程,但没有一个允许这样做。在实践中,我见过的最接近允许中断处理程序执行诸如睡眠之类的事情的方法是有一个显式进程来处理中断的工作。但是我看到的系统(例如,Solaris)仍然有一个最小的 ISR,不允许做像睡眠这样的事情。它所做的只是唤醒中断线程并让它做真正的工作。
  • @Keith,这个问题似乎没有权威的答案,尽管我认为设计一个 ISR 可以休眠的系统是可能的。在这里我附上了罗伯特·洛夫对这个问题的回答,但在我看来,我认为这是一个设计理念。
  • @arsane - 你说“这是一个设计理念”是什么意思?
  • 通过将其称为设计选择,您会错过 ISR 没有支持进程上下文的原因。给它一个对你没有好处。当 O/S 发生中断时,O/S 的状态是不确定的,因为它几乎可以在任何时候发生。未定义状态意味着在 ISR 中,O/S 无法执行诸如调度线程之类的事情。 ISR 和 O/S 共享的资源受自旋锁保护,因此这些资源的状态在 ISR 中将保持一致。有关自旋锁,请参阅 linuxjournal.com/article/5833。如果你使用普通锁,那些没有禁用中断的锁,你会死锁而不是崩溃
【解决方案3】:

因为此时线程切换基础设施不可用。服务中断时,只有更高优先级的东西可以执行 - 请参阅Intel Software Developer's Manual on interrupt, task and processor priority。如果您确实允许另一个线程执行(您在问题中暗示它很容易做到),您将无法让它做任何事情 - 如果它导致页面错误,您将不得不使用服务在处理中断时无法使用的内核中(原因见下文)。

通常,您在中断例程中的唯一目标是让设备停止中断并在较低的中断级别排队(在 unix 中,这通常是非中断级别,但对于 Windows,它是调度、apc 或被动级别)来完成繁重的工作,您可以访问内核/操作系统的更多功能。见-Implementing a handler

这是操作系统必须如何工作的属性,而不是 Linux 固有的属性。中断例程可以在任何时候执行,因此您中断的状态是不一致的。如果你中断了线程调度代码,它的状态是不一致的,所以你不能确定你可以“休眠”并切换线程。即使您保护线程切换代码不被中断,线程切换也是 O/S 的一个非常高级的功能,如果您保护了它所依赖的所有内容,那么中断更像是一种建议,而不是其名称所暗示的命令。

【讨论】:

  • 线程切换基础设施关闭是什么意思?这只是理论知识还是你能给我参考内核中的实际代码来支持你的主张?
  • +1 - 优秀的分数。您说“这是操作系统必须如何工作的属性。”我会更进一步说“这就是硬件的工作方式”。操作系统必须与之共存。
  • “即使你保护了线程……它的名字也暗示了这一点。”不同意。您是说,只要有中断,操作系统就必须先保留其他所有内容并先处理它,屏蔽中断不是一个好主意,因为那时中断不是中断而是建议。 AFAIK 中断始终是某人需要注意的建议/指示。它取决于操作系统,何时处理它。之所以称为中断,是因为这可能发生在处理器时钟周期之间的任何时间。
  • 我没有完全关注你的 cmets。这是一个中断,因为处理器将停止它正在做的事情——甚至回滚当前指令,并切换执行上下文。是切换部分使它成为中断。是的,您可以屏蔽,但这会导致必须进行实时处理(例如,跳过音频)的操作系统很差。我参考的处理中断的文章清楚地表明,使用 CLI 已被弃用(并且由于另一个处理器仍在运行,因此不能很好地与多个处理器一起使用)
  • 你说的很有道理。抱歉,我之前的评论令人困惑。
【解决方案4】:

那么是什么阻止调度器将中断上下文置于睡眠状态并采取下一个可调度进程并将控制权传递给它?

调度发生在定时器中断上。基本规则是一次只能打开一个中断,所以如果你在“从设备 X 获取数据”中断中进入睡眠状态,定时器中断不能运行来调度它。

中断也会发生多次并重叠。如果你让“得到数据”中断休眠,然后得到更多的数据,会发生什么?包罗万象的规则是:不要在中断中休眠。你会做错的。

【讨论】:

  • 我不同意“基本规则只有一个中断”。可以嵌套中断。请参考 bovet cesati,第 4.3 章,“异常和中断处理程序的嵌套执行”
  • 你的观点很好,但请注意下一段(它们可以重叠,你称之为嵌套)。这是一条“基本”规则,因为如果你不这样做,你最好知道发生了什么。
【解决方案5】:

即使您可以让 ISR 进入睡眠状态,您也不会想要这样做。您希望 ISR 尽可能快,以降低错过后续中断的风险。

【讨论】:

    【解决方案6】:

    不允许中断处理程序阻塞是一种设计选择。当设备上有一些数据时,中断处理程序会拦截当前进程,准备数据的传输并启用中断;在处理程序启用当前中断之前,设备必须挂起。我们想让我们的 I/O 保持忙碌并且我们的系统有响应,那么我们最好不要阻塞中断处理程序。

    我不认为“不稳定状态”是根本原因。进程,无论它们处于用户模式还是内核模式,都应该意识到它们可能会被中断中断。如果中断处理程序和当前进程都将访问某些内核模式数据结构,并且存在竞争条件,则当前进程应禁用本地中断,而且对于多处理器架构,应在临界区使用自旋锁.

    我也不认为如果中断处理程序被阻塞,它就不能被唤醒。当我们说“阻塞”时,基本上意味着被阻塞的进程正在等待某个事件/资源,因此它将自己链接到该事件/资源的某个等待队列中。每当资源被释放时,释放进程负责唤醒等待进程。

    然而,真正令人讨厌的是,被阻塞的进程在阻塞时间内什么都做不了;这种惩罚没有错,这是不公平的。并且没有人可以肯定地预测阻塞时间,因此无辜的进程必须等待不明原因和无限时间。

    【讨论】:

    • 这应该是公认的答案。没有什么可以从根本上防止中断处理程序内部的阻塞。然而,正如 OP 所指出的,阻塞对中断发生时正在运行的进程是不公平的,因为它可能与中断的原因无关。至于提到“未定义状态”的人,这没有任何意义。查看下一条评论...
    • 被中断的进程将始终处于非常明确的状态,以后可以恢复。访问也可以在中断上下文中访问的数据的进程应首先禁用中断,然后再访问此类数据以防止损坏。
    • 一般来说,这是一种设计选择,可以避免惩罚被中断的进程,并鼓励在与中断源相关的明确定义的进程上下文中处理大量工作(例如,磁盘IO 线程)。这导致了 Linux 中使用的下半部分和上半部分设计,其中在中断处理程序中完成快速最小中断确认,然后安排在进程上下文中完成其余处理。
    • 此外,由于重入要求,中断处理程序通常在禁用中断的情况下运行,因此在中断处理程序中花费很长时间会损害延迟和系统响应能力,因为丢失中断的可能性更高.因此,最好有非常快速的中断处理程序,以便快速重新启用中断。
    【解决方案7】:

    linux内核有两种分配中断栈的方法。一个在被中断进程的内核堆栈上,另一个是每个 CPU 的专用中断堆栈。如果中断上下文保存在每个 CPU 的专用中断堆栈上,那么实际上中断上下文完全与任何进程无关。 “当前”宏将产生一个指向当前运行进程的无效指针,因为具有某些体系结构的“当前”宏是使用堆栈指针计算的。中断上下文中的堆栈指针可能指向专用的中断堆栈,而不是某个进程的内核堆栈。

    【讨论】:

    • 这是有道理的,我想到了 Linux 内核中的“当前”指针。但在哪些情况下,内核使用每个 CPU 堆栈而不是进程内核堆栈?
    【解决方案8】:

    本质上,问题是在中断处理程序中是否可以获得有效的“当前”(当前进程任务结构的地址),如果是,则可以相应地修改那里的内容以使其进入“睡眠”状态,如果状态以某种方式发生变化,调度程序可以稍后返回。答案可能取决于硬件。

    但在 ARM 中,这是不可能的,因为“当前”与中断模式下的处理无关。请看下面的代码:

    #linux/arch/arm/include/asm/thread_info.h 
    94 static inline struct thread_info *current_thread_info(void)
    95 {
    96  register unsigned long sp asm ("sp");
    97  return (struct thread_info *)(sp & ~(THREAD_SIZE - 1));
    98 }
    

    USER模式下的sp和SVC模式下的sp是“相同的”(这里的“相同”并不是说它们相等,而是用户模式的sp指向用户空间栈,而svc模式的sp r13_svc指向内核栈,其中用户进程的task_structure在上一次任务切换时更新,当系统调用发生时,进程再次进入内核空间,当sp(sp_svc)仍然没有改变时,这2个sp是相互关联的,从这个意义上说,他们' re 'same'),所以在 SVC 模式下,内核代码可以获得有效的 'current'。但在其他特权模式下,比如中断模式,sp 是“不同的”,指向 cpu_init() 中定义的专用地址。在这些模式下计算的“电流”将与被中断的进程无关,访问它会导致意外的行为。这就是为什么总是说系统调用可以休眠而中断处理程序不能,系统调用在进程上下文中起作用而中断不是。

    【讨论】:

      【解决方案9】:

      高级中断处理程序屏蔽所有低优先级中断的操作,包括系统定时器中断的操作。因此,中断处理程序必须避免让自己参与可能导致其休眠的活动。如果处理程序休眠,那么系统可能会挂起,因为定时器被屏蔽并且无法调度休眠线程。 这有意义吗?

      【讨论】:

        【解决方案10】:

        如果更高级别的中断例程在一段时间后到达它必须做的下一件事情必须发生的地步,那么它需要将请求放入定时器队列,要求运行另一个中断例程(在较低的优先级)一段时间后。

        当该中断例程运行时,它会将优先级提升回原始中断例程的级别,并继续执行。这与睡眠具有相同的效果。

        【讨论】:

          【解决方案11】:

          这只是 Linux 操作系统中的设计/实现选择。这种设计的优点是简单,但它可能不适合实时操作系统要求。

          其他操作系统有其他设计/实现。

          例如,在 Solaris 中,中断可以有不同的优先级,这允许大多数设备中断在中断线程中调用。中断线程允许睡眠,因为每个中断线程在线程上下文中都有单独的堆栈。 中断线程设计适用于优先级高于中断的实时线程。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2011-10-27
            • 2016-06-28
            • 1970-01-01
            • 1970-01-01
            • 2019-11-21
            • 2015-10-09
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多