1. 程式人生 > >《深入理解Linux核心》軟中斷/tasklet/工作佇列

《深入理解Linux核心》軟中斷/tasklet/工作佇列

軟中斷、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是在兩種軟中斷型別的基礎上實現的,但是由於其特殊的實現機制(將在4.3節詳細介紹),所以具有了這樣不同於軟中斷的特性。而由於這種特性,所以降低了裝置驅動程式開發者的負擔,因此如果不需要軟中斷的並行特性,tasklet就是最好的選擇。

(4)可延遲函式(軟中斷及tasklet)的使用
一般而言,在可延遲函式上可以執行四種操作:初始化/啟用/執行/遮蔽。遮蔽我們這裡不再敘述,前三個則比較重要。下面將軟中斷和tasklet的三個步驟分別進行對比介紹。

(4.1)初始化
初始化是指在可延遲函式準備就緒之前所做的所有工作。一般包括兩個大步驟:首先是向核心宣告這個可延遲函式,以備核心在需要的時候呼叫;然後就是呼叫相應的初始化函式,用函式指標等初始化相應的描述符。
如果是軟中斷則在核心初始化時進行,其描述符定義如下:

struct softirq_action
          {
                   
void (*action)(struct softirq_action *);
                   
void*data;
          };

在\kernel\softirq.c檔案中包括了32個描述符的陣列static struct softirq_action softirq_vec[32];但實際上只有前6個已經被核心註冊使用(包括tasklet使用的HI_SOFTIRQ/TASKLET_SOFTIRQ和網路協議棧使用的NET_TX_SOFTIRQ/NET_RX_SOFTIRQ,還有SCSI儲存和系統計時器使用的兩個),剩下的可以由核心開發者使用。需要使用函式:
         void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
初始化陣列中索引為nr的那個元素。需要的引數當然就是action函式指標以及data。例如網路子系統就通過以下兩個函式初始化軟中斷(net_tx_action/net_rx_action是兩個函式):

    open_softirq(NET_TX_SOFTIRQ,net_tx_action);
     open_softirq(NET_RX_SOFTIRQ,net_rx_action);

這樣初始化完成後實際上就完成了一個一一對應的關係:當核心中產生到NET_TX_SOFTIRQ軟中斷之後,就會呼叫net_tx_action這個函式。
tasklet則可以在執行時定義,例如載入模組時。定義方式有兩種:
靜態宣告

DECLARE_TASKET(name, func, data)
DECLARE_TASKLET_DISABLED(name, func, data)

動態宣告

void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data)

其引數分別為描述符,需要呼叫的函式和此函式的引數—必須是unsigned long型別。也需要使用者自己寫一個類似net_tx_action的函式指標func。初始化最終生成的結果就是一個實際的描述符,假設為my_tasklet(將在下面用到)。

(4.2)啟用
啟用標記一個可延遲函式為掛起(pending)狀態,表示核心可以呼叫這個可延遲函式(即使在中斷過程中也可以啟用可延遲函式,只不過函式不會被馬上執行);這種情況可以類比處於TASK_RUNNING狀態的程序,處在這個狀態的程序只是準備好了被CPU排程,但並不一定馬上就會被排程。
軟中斷使用raise_softirq()函式啟用,接收的引數就是上面初始化時用到的陣列索引nr。
tasklet使用tasklet_schedule()啟用,該函式接受tasklet的描述符作為引數,例如上面生成的my_tasklet:

tasklet_schedule(& my_tasklet)

(4.3)執行
執行就是核心執行可延遲函式的過程,但是執行只發生在某些特定的時刻(叫做檢查點,具體有哪些檢查點?詳見《深入》p.177)。
每個CPU上都有一個32位的掩碼__softirq_pending,表明此CPU上有哪些掛起(已被啟用)的軟中斷。此掩碼可以用local_softirq_pending()巨集獲得。所有的掛起的軟中斷需要用do_softirq()函式的一個迴圈來處理。
而對於tasklet,由於軟中斷初始化時,就已經通過下面的語句初始化了當遇到TASKLET_SOFTIRQ/HI_SOFTIRQ這兩個軟中斷所需要執行的函式:

    open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
     open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);

因此,這兩個軟中斷是要被區別對待的。tasklet_action和tasklet_hi_action內部實現就是為什麼軟中斷和tasklet有不同的特性的原因(當然也因為二者的描述符不同,tasklet的描述符要比軟中斷的複雜,也就是說核心設計者自己多做了一部分限制的工作而減少了驅動程式開發者的工作)。

(5)為什麼要使用工作佇列work queue?(work queue和軟中斷的區別)
上面我們介紹的可延遲函式執行在中斷上下文中(軟中斷的一個檢查點就是do_IRQ退出的時候),於是導致了一些問題:軟中斷不能睡眠、不能阻塞。由於中斷上下文出於核心態,沒有程序切換,所以如果軟中斷一旦睡眠或者阻塞,將無法退出這種狀態,導致核心會整個僵死。但可阻塞函式不能用在中斷上下文中實現,必須要執行在程序上下文中,例如訪問磁碟資料塊的函式。因此,可阻塞函式不能用軟中斷來實現。但是它們往往又具有可延遲的特性。工作佇列是使用核心執行緒執行緒實現的。
因此在2.6版的核心中出現了在核心態執行的工作佇列(替代了2.4核心中的任務佇列)。它也具有一些可延遲函式的特點(需要被啟用和延後執行),但是能夠能夠在不同的程序間切換,以完成不同的工作。