《深入理解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內核中的任務隊列)。它也具有一些可延遲函數的特點(需要被激活和延后執行),但是能夠能夠在不同的進程間切換,以完成不同的工作。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM