Linux的中断上下文中不能睡眠

发布时间 2023-11-03 13:59:24作者: watsondd

  Understanding the Linux Kernel, 3rd Edition在4.3. Nested Execution of Exception and Interrupt Handlers提到中断处理中不能阻塞,原文如下,

The price to pay for allowing nested kernel control paths is that an interrupt handler must never block, that is, no process switch can take place until an interrupt handler is running.

Linux Kernel Development Third Edition在Chapter 7 Interrupts and Interrupt Handlers也提到了类似的要求,

Interrupt context, on the other hand, is not associated with a process.The current macro is not relevant (although it points to the interrupted process).Without a backing process, interrupt context cannot sleep—how would it ever reschedule? Therefore, you cannot call certain functions from interrupt context. If a function sleeps, you cannot use it from your interrupt handler—this limits the functions that one can call from an interrupt handler.

然而,对于为什么不能睡眠,并没有深入解释。而且当时内核代码还是v2.6,现在内核代码已经有比较大的变化,现在是否还有这样的限制?

  对于这个问题,个人认为这一篇来自Kernelnewbies网站的讨论存档,Sleeping in the interrupt handler解释得最为清晰。其中提到了两点,第一,中断发生时没有明确对应的进程上下文。第二,中断发生时,进程上下文的状态是不确定的。下面对这两点进行一个深入的解析。

  中断是异步于进程发生的,所以任何进程都有可能被中断,这也就是说中断发生时无法获取到准确的进程上下文。虽然上面的链接中也提到了,理论上是可以获取到一个进程上下文的(也就是被打断执行的线程),但是,出于中断处理的原因将这个进程调度出去是没有道理的(原文用词是“unfair”),因为中断本身和这个进程没有任何关联,并不能代表这个进程。从另外一个角度来讲,中断上下文不是一个进程,也就无法作为调度的一个单位。作一个不恰当的类比的话,在代数方程中,使用字母xyz表示未知数,理论上可以是任何数字,但是又不是数字。自然数集合只会包括具体的数字,而不会包括可以表示未知数的字母。类似地,调度也只能发生在进程集合中,而中断上下文不属于这个集合,也就不适合进行调度。

  根据上面的解释,如果强行进行调度的话,似乎也没有什么问题,最多是中断处理的时间变得更长,响应不是很及时。但是,上面的链接又提到了在中断处理时进行调度会引起另外一个更大问题,也就是死锁。
例如,在一个单CPU系统上,首先进程A在运行,获得了一个自旋锁; 中断发生,并且进行了调度;进程B开始运行,尝试也获得这个自旋锁;这时就进入了死锁。根本原因是被打断的进程执行状态是不确定的,而如果允许调度的话,调度后进程执行状态也是不确定的,很容易出现不可预知的问题。当然,如果要求中断发生时,被打断执行的进程不允许获得自旋锁,就不会出现这个问题。实际上,Unreliable Guide To Locking — The Linux Kernel documentation就提到在硬件IRQ和其它上下文之间进行临界区保护时,必须要使用spin_lock_irqsave/spin_unlock_irqrestore或者spin_lock_irq/spin_unlock_irq这两对接口。但是,如果将这个限制扩展到所有的临界区域或者同步原语,显然是不现实的。

  接着上面的分析,如果被中断执行的进程没有获得锁,也就是不处于临界区,那么是不是就可以在中断处理时进行调度了呢?这个问题的答案,就涉及到内核抢占这一特性了。对于可抢占内核(配置了CONFIG_PREEMPT),在从中断返回到被打断的进程前,会检查是否有抢占发生。如果有的话,就会调用__schedule切换到另外一个进程。从这个角度讲,中断处理流程中并不是严格禁止进程切换的。这里需要注意的是,只有允许抢占发生,才会有这个动作。如果在被打断时,进程已经获得了自旋锁,那么就是不会发生抢占的。而且,除了获得自旋锁,还有其它操作也会禁止抢占。

  在[Sleeping in the handler]这个链接中还提到了另外一点,为什么缺页异常的处理中可以睡眠,而中断处理不能。按照Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3A: System Programming Guide, Part 1的术语定义,中断是interrupt,而缺页异常是exception。但是,interrupt和excetpion的处理流程是在同一章节讲述的。对于ARMv7-A架构来说,中断是interrupts,而缺页异常是aborts。DDI0406C_C_arm_architecture_reference_manual的Exception handling这一章节将interrupts、aborts、reset和诸如svc指令产生的事件都叫做exception。也就是说,对于处理器来说,这两种事件上的处理细节上会有些差异,执行的流程大致是类似的。
那么为什么Linux内核有不一样的要求呢?这个问题在原文的讨论中,已经给出了答案。

The reason the page fault handler can sleep is that it is invoked only by code that is running in process context. Because the kernel's own memory is not pagable, only user-space memory accesses can result in a page fault. Thus, only a few certain places (such as calls to copy_{to,from}_user()) can cause a page fault within the kernel. Those places must all be made by code that can sleep (i.e., process context, no locks, et cetera).

缺页异常是由代码执行产生的,其时机和位置是确定的。而中断是随机产生的。通过代码可以保证缺页异常时睡眠不会发生睡眠。对于中断,却无法达到这种效果。

  综上所述,严格意义上,中断处理时是能够进行调度,但是场景也是严格受限的。如果在中断处理过程中的任意时刻,都允许进行调度的话,要么会引起死锁问题,要么需要对整个操作系统加上不合理的限制。