軟中斷、tasklet和工作隊列並不是Linux內核中一直存在的機制,而是由更早版本的內核中的“下半部”(bottom half)演變而來。下半部的機制實際上包括五種,但2.6版本的內核中,下半部和任務隊列的函數都消失了,只剩下了前三者。本文重點在於介紹這三者之間的關係。(函數細節將不會在本文中出現,可以參考文獻,點這裏)
(1)上半部和下半部的區別
上半部指的是中斷處理程序,下半部則指的是一些雖然與中斷有相關性但是可以延後執行的任務。舉個例子:在網絡傳輸中,網卡接收到數據包這個事件不一定需要馬上被處理,適合用下半部去實現;但是用戶敲擊鍵盤這樣的事件就必須馬上被響應,應該用中斷實現。
兩者的主要區別在於:中斷不能被相同類型的中斷打斷,而下半部依然可以被中斷打斷;中斷對於時間非常敏感,而下半部基本上都是一些可以延遲的工作。由於二者的這種區別,所以對於一個工作是放在上半部還是放在下半部去執行,可以參考下面四條:
a)如果一個任務對時間非常敏感,將其放在中斷處理程序中執行。
b)如果一個任務和硬件相關,將其放在中斷處理程序中執行。
c)如果一個任務要保證不被其他中斷(特別是相同的中斷)打斷,將其放在中斷處理程序中執行。
d)其他所有任務,考慮放在下半部去執行。
(2)爲什麼要使用軟中斷?
軟中斷作爲下半部機制的代表,是隨着SMP(share memory processor)的出現應運而生的,它也是tasklet實現的基礎(tasklet實際上只是在軟中斷的基礎上添加了一定的機制)。軟中斷一般是“可延遲函數”的總稱,有時候也包括了tasklet(請讀者在遇到的時候根據上下文推斷是否包含tasklet)。它的出現就是因爲要滿足上面所提出的上半部和下半部的區別,使得對時間不敏感的任務延後執行,而且可以在多個CPU上並行執行,使得總的系統效率可以更高。它的特性包括:
a)產生後並不是馬上可以執行,必須要等待內核的調度才能執行。軟中斷不能被自己打斷,只能被硬件中斷打斷(上半部)。
b)可以併發運行在多個CPU上(即使同一類型的也可以)。所以軟中斷必須設計爲可重入的函數(允許多個CPU同時操作),因此也需要使用自旋鎖來保護其數據結構。
(3)爲什麼要使用tasklet?(tasklet和軟中斷的區別)
由於軟中斷必須使用可重入函數,這就導致設計上的複雜度變高,作爲設備驅動程序的開發者來說,增加了負擔。而如果某種應用並不需要在多個CPU上並行執行,那麼軟中斷其實是沒有必要的。因此誕生了彌補以上兩個要求的tasklet。它具有以下特性:
a)一種特定類型的tasklet只能運行在一個CPU上,不能並行,只能串行執行。
b)多個不同類型的tasklet可以並行在多個CPU上。
c)軟中斷是靜態分配的,在內核編譯好之後,就不能改變。但tasklet就靈活許多,可以在運行時改變(比如添加模塊時)。
tasklet是在兩種軟中斷類型的基礎上實現的,因此如果不需要軟中斷的並行特性,tasklet就是最好的選擇。
(4)爲什麼要使用工作隊列work queue?(work queue和軟中斷的區別)
上面我們介紹的可延遲函數運行在中斷上下文中(軟中斷的一個檢查點就是do_IRQ退出的時候),於是導致了一些問題:軟中斷不能睡眠、不能阻塞。由於中斷上下文出於內核態,沒有進程切換,所以如果軟中斷一旦睡眠或者阻塞,將無法退出這種狀態,導致內核會整個僵死。但可阻塞函數不能用在中斷上下文中實現,必須要運行在進程上下文中,例如訪問磁盤數據塊的函數。因此,可阻塞函數不能用軟中斷來實現。但是它們往往又具有可延遲的特性。
因此在2.6版的內核中出現了在內核態運行的工作隊列(替代了2.4內核中的任務隊列)。它也具有一些可延遲函數的特點(需要被激活和延後執行),但是能夠能夠在不同的進程間切換,以完成不同的工作。
對於softirq,linux kernel中是在中斷處理程序執行的,具體的路徑爲:
- do_IRQ() --> irq_exit() --> invoke_softirq() --> do_softirq() --> __do_softirq()
在__do_softirq()中有這麼一段代碼:
- do {
- if (pending & 1) {
- h->action(h);
- rcu_bh_qsctr_inc(cpu);
- }
- h++;
- pending >>= 1;
- } while (pending);
你看,這裏就是對softirq進行處理了,因爲pengding是一個__u32的類型,所以每一位都對應了一種softirq,正好是32種(linux kernel中實際上只使用了前6種 ).
h->action(h),就是運行softirq的處理函數。
對於tasklet,前面已經說了,是一種特殊的softirq,具體就是第0和第5種softirq,所以說tasklet是基於softirq來實現的。
tasklet既然對應第0和第5種softirq,那麼就應該有對應的處理函數,以便h->action()會運行tasklet的處理函數。
我們看代碼:
- softirq.c
- void __init softirq_init(void)
- {
- open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
- open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
- }
這裏註冊了兩種tasklet所在的softirq的處理函數,分別對應高優先級的tasklet和低優先級的tasklet。
我們看低優先級的吧(高優先級的也一樣)。
- static void tasklet_action(struct softirq_action *a)
- {
- struct tasklet_struct *list;
- local_irq_disable();
- list = __get_cpu_var(tasklet_vec).list;
- __get_cpu_var(tasklet_vec).list = NULL;
- local_irq_enable();
- while (list) {
- struct tasklet_struct *t = list;
- list = list->next;
- if (tasklet_trylock(t)) {
- if (!atomic_read(&t->count)) {
- if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
- BUG();
- t->func(t->data);
- tasklet_unlock(t);
- continue;
- }
- tasklet_unlock(t);
- }
- local_irq_disable();
- t->next = __get_cpu_var(tasklet_vec).list;
- __get_cpu_var(tasklet_vec).list = t;
- __raise_softirq_irqoff(TASKLET_SOFTIRQ);
- local_irq_enable();
- }
- }
你看,在運行softirq的處理時(__do_softirq),對於
- do {
- if (pending & 1) {
- h->action(h);
- rcu_bh_qsctr_inc(cpu);
- }
- h++;
- pending >>= 1;
- } while (pending);
如果tasklet有任務需要處理,會運行到h->action(),這個函數指針就會指向tasklet_action(),然後在tasklet_action()裏再去執行tasklet對應的各個任務,這些任務都是掛在一個全局鏈表裏面的,具體的代碼這裏就不分析了。
另外, softirq在smp中是可能被同時運行的,所以softirq的處理函數必須被編寫成可重入的函數。
但tasklet是不會在多個cpu之中同時運行的,所以tasklet的處理函數可以編寫成不可重入的函數,這樣就減輕了編程人員的負擔。