大家好,我是老吳。
今天是周一,大家工作順利嗎?
這篇文章給大家分享一點小知識:為什么中斷里不能睡眠?
網上很多文章嘗試解釋這個問題,看后我覺得頭皮發麻。
下面,我試著總結一下原因。
首先,讓我們明確一下問題。
對于這個問題,稍微準確一點的問法是:為什么在 Linux 的中斷里,不能 sleep?
但是這個問法仍然不準確。
中斷 (interrupt) 和中斷服務程序 (interrupt service routine, ISR,或者是 interrupt handler),是 2 個不同的概念。
前者是硬件相關的概念,后者是軟件相關的概念。
所以,對于這個問題,最準確的問法是:為什么在 Linux 的 ISR 里,不能 sleep?
由于 sleep 意味著 call scheduler,所以更直白一點的問法是:
為什么在 Linux 的 ISR 里,不能 call scheduler?
最后,再加點限制條件會更準確:為什么在 Linux 的 ISR 里,即便 ISR 沒有 hold 住任何 lock 的時候,都不能 call scheduler?
不能在 ISR 里睡眠的原因是:ISR 與任何 process context (進程上下文) 無關。
process context 是進程的狀態信息,包括:
對于每一個進程,在內核都會有一個 pcb (process control, block,即 Linux 里的 task_struct 結構體) 來管理這些信息。
scheduler 可以訪問所有這些信息,以搶占一個進程并運行另一個進程。
與此相反,取決于內核和迎接架構的版本,ISR 使用單獨的中斷棧或被中斷的進程的內核棧,并且在中斷中會有自己的 hardware context.
因此,由于在 ISR 里沒有 process context,所以不能進行調度。
但是,這個說法描述的其實是當下設計的狀況,而不是當初這樣設計的原因。
在 Linux 的早期版本中,ISR 總是借用當前進程的棧。
所以如果內核想設計成允許在 ISR 里睡眠,是可以很自然地實現進程上下文切換的。
但是,Linux 采用的設計是:在 ISR 里禁止睡眠。
現在,我們的問題變成了:
為什么在 Linux 里,ISR 被設計成不能睡眠?
sleep 會導致 call scheduler 以選擇另一個進程來運行。
內核代碼里有大量的 critical section (臨界區)。
critical section 本質上是一段會訪問或操作共享資源的代碼,例如:
static?int?copy_fs(unsigned?long?clone_flags,?struct?task_struct?*tsk)
{
?struct?fs_struct?*fs?=?current->fs;
?if?(clone_flags?&?CLONE_FS)?{
??/*?tsk->fs?is?already?what?we?want?*/
??spin_lock(&fs->lock);
??if?(fs->in_exec)?{
???spin_unlock(&fs->lock);
???return?-EAGAIN;
??}
??fs->users++;
??spin_unlock(&fs->lock);
??return?0;
?}
?tsk->fs?=?copy_fs_struct(fs);
?if?(!tsk->fs)
??return?-ENOMEM;
?return?0;
}
在 critical section 里,是不能 call scheduler 的。
因為已經有一個進程持有鎖了,如果這時切換到另一個進程,最好的情況下是等待一段無法預測的時間后前一個進程會將鎖釋放出來,最壞的情況是死鎖。
硬件中斷是隨時可能發生的,即便內核執行的路徑正處于 critical section 中。
如果想在 ISR 里支持 sleep,也就是支持 call scheduler 的話,那么所有的 critical section 都必須得禁用中斷,否則硬件中斷一旦來臨系統就會出現 race condition,接下來大概率是死鎖。
Sleep 和 ISR:

我查閱了一下 Linux 4.9 的代碼,當你在一個不能調度的地方 call scheduler (例如 ISR 里 sleep) 的話,內核可以提示你寫的代碼有 BUG:
static?inline?void?schedule_debug(struct?task_struct?*prev)
{
#ifdef?CONFIG_SCHED_STACK_END_CHECK
?if?(task_stack_end_corrupted(prev))
??panic("corrupted?stack?end?detected?inside?scheduler\n");
#endif
?//?錯誤的時機?call?sheduler??
?if?(unlikely(in_atomic_preempt_off()))?{
??__schedule_bug(prev);
??preempt_count_set(PREEMPT_DISABLED);
?}
?[...]
}
我在某個設備驅動的中斷處理函數 XXX_ISR() 里加了 msleep(10) 之后:
[???27.221560]?BUG:?scheduling?while?atomic:?swapper/0/0x00010002
[???27.221609]?Modules?linked?in:?8021q?garp?stp?mrp?llc?usb_f_eem?g_ether?usb_f_rndis?u_ether?exfat(O)
[???27.221712]?CPU:?0?PID:?0?Comm:?swapper?Tainted:?G???????????O????4.9.203?#640
[???27.224736]?Hardware?name:?Samsung?Device
[???27.230575]?[]?(unwind_backtrace)?from?[]?(show_stack+0x10/0x14)
[???27.238267]?[]?(show_stack)?from?[]?(__schedule_bug+0x64/0x84)
[???27.245802]?[]?(__schedule_bug)?from?[]?(__schedule+0x3fc/0x550)
[???27.253512]?[]?(__schedule)?from?[]?(schedule+0x50/0xb4)
[???27.260533]?[]?(schedule)?from?[]?(schedule_timeout+0x114/0x1e8)
[???27.268246]?[]?(schedule_timeout)?from?[]?(msleep+0x2c/0x38)
[???27.275612]?[]?(msleep)?from?[]?(XXX_ISR+0x34/0x8c)
[???27.282982]?[]?(XXX_ISR)?from?[]?(__handle_irq_event_percpu+0x88/0x124)
[???27.292075]?[]?(__handle_irq_event_percpu)?from?[]?(handle_irq_event_percpu+0x1c/0x58)
[???27.301693]?[]?(handle_irq_event_percpu)?from?[]?(handle_irq_event+0x38/0x5c)
[???27.310532]?[]?(handle_irq_event)?from?[]?(handle_edge_irq+0xe0/0x1a4)
[???27.318764]?[]?(handle_edge_irq)?from?[]?(generic_handle_irq+0x24/0x34)
[???27.327091]?[]?(generic_handle_irq)?from?[]?(exynos_irq_eint0_15+0x44/0x98)
[???27.335751]?[]?(exynos_irq_eint0_15)?from?[]?(generic_handle_irq+0x24/0x34)
[???27.344415]?[]?(generic_handle_irq)?from?[]?(__handle_domain_irq+0x54/0xa8)
[???27.353080]?[]?(__handle_domain_irq)?from?[]?(vic_handle_irq+0x58/0x94)
[???27.361398]?[]?(vic_handle_irq)?from?[]?(__irq_svc+0x6c/0xa8)
[???27.368847]?Exception?stack(0xc0d01f58?to?0xc0d01fa0)
總結一下:
硬件中斷是超級寶貴的資源,想在中斷里睡眠的話就得在大量的 critical section 中關閉中斷才能避免 race condition,而關閉硬件中斷將會大大地增加中斷響應的延遲,降低系統的反應速度,這是操作系統的用戶所無法接受的, 因此內核開發者采用的設計是在中斷里不允許睡眠,并且 ISR 應盡快執行并返回以便系統里的進程繼續運行。
那么,那些很耗時的工作該怎么處理呢?
由于硬件中斷可能隨時發生,ISR 隨時會執行。因此,它必須快速運行并退出,以便盡快恢復被中斷代碼的執行。在操作系統看來,無論是硬件中斷還是被中斷的代碼,兩者都是很重要的,因此,ISR 應在盡可能短的時間內執行完畢。
但是,現實情況是,許多 ISR 有大量工作要執行。例如網絡設備的 ISR 除了響應硬件之外,還需要 將網絡數據包從硬件復制到內存中,處理它們,并將數據包向下分發到適當的協議棧或應用程序。
Linux 如何解決這種活多錢少的問題?
答:將 ISR 分為 top half 和 bottom half。
top half 在收到中斷后立即運行,僅執行時間緊迫的工作,例如確認收到中斷或重置硬件,執行完 top half 后,如果進入 ISR 前是處于 critical section 且內核搶占是被關閉 ( 例如 spinlock ) 的話,就會返回到 critical section 里繼續運行,不會產生 race condition 的問題。
void?irq_exit(void)
{
#ifndef?__ARCH_IRQ_EXIT_IRQS_DISABLED
?local_irq_disable();
#else
?WARN_ON_ONCE(!irqs_disabled());
#endif
?account_irq_exit_time(current);
?preempt_count_sub(HARDIRQ_OFFSET);
?//?內核搶占沒被關閉、已經沒有其他?hardirq?了、有?softirq?在?pending?等條件都被滿足時,才會處理?softirq
?if?(!in_interrupt()?&&?local_softirq_pending())
??invoke_softirq();
?[...]
}
而晚一點執行也沒問題的工作將推遲到 bottom half。bottom half 將在某個未來更方便的時間運行,并且是在使能所有中斷、使能內核搶占的情況下進行,那時我們想怎么折騰就怎么折騰吧。
Linux 提供了許多 bottom half 的機制,例如 softirqs、tasklets、workqueues。

所以,有了 bottom half 之后,在 ISR 里睡眠這種需求,其實是完全沒有必要的。
到此,這個問題就解釋完畢了,感謝大家的閱讀。

歡迎轉發、留言、點贊、分享給您的朋友,感謝您的支持!