今天在ChinaUnix論壇內核源碼版上與linuxfellow網友討論到hardirq和softirq的問題,雖 然在《深入Linux設備驅動程序內核機制》(以下簡稱“ILDD”)第5章“中斷處理”對此已有詳細的解讀,但是我覺得還是有必要再花點時間深入探討一 下這兩者的區別。因爲此前關於ARM上的中斷處理我已經在另一篇帖子解密ARM
based Linux內核中斷處理框架 中討論過,所以下面的討論只限於x86 32位系統。
首先給出一個ILDD書中的一個插圖,關於中斷處理的整體框架:
(圖1)
接下來的一個插圖顯示了內核用於標識中斷上下文(in_interrupt())的變量preempt_count的佈局:
(圖2)
按照x86處理器在外部中斷髮生時的硬件邏輯,在do_IRQ被調用時,處理器已經屏蔽了對外部中斷的響應。在圖中我們看 到中斷的處理大體上被分成兩部分HARDIRQ和SOFTIRQ,對應到代碼層面,do_IRQ()中調用irq_enter函數可以看做hardirq 部分的開始,而irq_exit函數的調用則標誌着softirq部分的開始:
<arch/x86/kernel/irq.c>
-
unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
-
{
-
...
-
irq_enter();
-
-
//handle external interrupt (ISR)
-
...
-
irq_exit();
-
-
return 1;
- }
irq_enter()函數的核心調用是__irq_enter(),後者的主要作用是在圖2的 preempt_count變量的HARDIRQ部分+1,即標識一個hardirq的上下文,所以可以認爲do_IRQ()調用irq_enter函數 意味着中斷處理進入hardirq階段。此時處理器響應外部中斷的能力依然是被disable掉的(EFLAG.IF=0),因爲ISR基本上屬於設備驅 動程序涉足的領域,內核無法保證在設備驅動程序的ISR中會否將EFLAG.IF置1(此處涉及可能的中斷嵌套以及中斷棧溢出問題,可以參考ARM中斷處 理的那個帖子),所以我們會在內核代碼中看到內核開發者爲了儘可能避免非受控的ISR部分給系統帶來的損害所做的努力。按照現在內核的設計理念,在 hardirq部分最好不要打開中斷,所以處在hardirq上下文中的設備驅動程序的ISR應該儘可能快地返回(所謂只完成最關鍵的任務),而將耗時的 中斷處理善後工作留到softirq部分,因爲在softirq部分,內核會使EFLAG.IF=1,所以也意味着softirq部分可以隨時被外部中斷 所打斷。
下面我們重點討論一下softirq部分的實現原理,do_IRQ()中調用irq_exit函數標誌softirq部分 的開始。 irq_exit()函數會首先在圖2的preempt_count變量的HARDIRQ部分-1,目的是清除hardirq的上下文標記。然後它有個關鍵的調用invoke_softirq,用於實際的softirq處理工作:
<kernel/softirq.c>
-
void irq_exit(void)
-
{
-
...
-
sub_preempt_count(IRQ_EXIT_OFFSET);
-
if (!in_interrupt() && local_softirq_pending())
-
invoke_softirq();
-
-
rcu_irq_exit();
-
...
- }
第一個條件,主要用來防止softirq部分的重入,因爲一旦有pending的softirq需要處理,那麼invoke_softirq()的調用 (實際的工作發生在__do_softirq函數中)首先會將圖2中SOFTIRQ部分+1,這樣若在當前正在處理softirq過程中發生了外部中 斷,hardirq部分標識了一個pending softirq,那麼在irq_exit函數中將直接返回,而不會調用到invoke_softirq。
softirq部分的核心代碼段如下:
<kernel/softirq.c>
-
asmlinkage void __do_softirq(void)
-
{
-
...
-
//獲得__softirq_pending變量上保存的pending softirq數據
-
pending = local_softirq_pending();
-
//將圖2中SOFTIRQ部分+1,標識softirq上下文,此時in_interrupt()返回true.
-
__local_bh_disable((unsigned long)__builtin_return_address(0), SOFTIRQ_OFFSET);
-
...
-
restart:
-
/* Reset the pending bitmask before enabling irqs */
-
set_softirq_pending(0);
-
//注意此處,內核調用local_irq_enable打開處理器響應外部中斷能力,EFLAG.IF=1,所以接下來的softirq處理時外部中斷可以進入處理器
-
local_irq_enable();
-
-
h = softirq_vec;
-
do {
-
if (pending & 1) {
-
...
-
//調用每個類型的softirq所對應的處理函數,見下圖3
-
h->action(h);
-
...
-
}
-
h++;
-
pending >>= 1;
-
} while (pending);
-
//softirq處理結束前,重新關閉中斷。中斷的再次打開發生在中斷的返回,也就是在圖1中處理器在執行iret指令時的硬件邏輯,POP EFLAG, EFLAG.IF=1
-
local_irq_disable();
-
-
//再次檢測是否有新的pending softirq,因爲softirq執行時可能有新的外部中斷進來,如果有,此處一併處理。
-
pending = local_softirq_pending();
-
if (pending && --max_restart)
-
goto restart;
- ...
- //將圖2中SOFTIRQ部分-1,標識softirq上下文的結束
-
__local_bh_enable(SOFTIRQ_OFFSET);
- }
對每個類型的softirq的處理髮生在上面的do...while...循環中,原理其實非常簡單,爲節省文字,用下面一個草圖來做說明(圖3):
所以do...while...循環實際上是從bit 0遍歷__softirq_pending變量,目前該變量的0~9分別對應10個不同類型的softirq,每個softirq對應不同的處理函數,比如HI_SOFTIRQ對應的action爲tasklet_hi_action,它與TASKLET_SOFTIRQ的action的原理完全一樣,也就是我們平常所說的tasklet。__softirq_pending是個per-CPU型的變量,因爲SMP系統中每個處理器都可以獨立處理各自到來 的外部中斷,也都對應有各自的hardirq棧和softirq棧,詳見Linux內核中的中斷棧與內核棧的補充說明一貼。另外,在前面的帖子中我們一直說hardirq部分標識一個softirq,何謂標識一個softirq?其實就是調用tasklet_schedule()來把TASKLET_SOFTIRQ位置1.
在ILDD一書中實際上將tasklet分成兩部分,分別放在第5章”中斷處理“和第6章”延遲操作“,第5章側重討論softirq的機制,而第6章則重點討論tasklet。