Linux內(nèi)核中的軟中斷、tasklet和工作隊列詳解(超詳細(xì)~)
本文基于Linux2.6.32內(nèi)核版本。
引言
軟中斷、tasklet和工作隊列并不是Linux內(nèi)核中一直存在的機(jī)制,而是由更早版本的內(nèi)核中的“下半部”(bottom half)演變而來。下半部的機(jī)制實際上包括五種,但2.6版本的內(nèi)核中,下半部和任務(wù)隊列的函數(shù)都消失了,只剩下了前三者。
介紹這三種下半部實現(xiàn)之前,有必要說一下上半部與下半部的區(qū)別。
上半部指的是中斷處理程序,下半部則指的是一些雖然與中斷有相關(guān)性但是可以延后執(zhí)行的任務(wù)。舉個例子:在網(wǎng)絡(luò)傳輸中,網(wǎng)卡接收到數(shù)據(jù)包這個事件不一定需要馬上被處理,適合用下半部去實現(xiàn);但是用戶敲擊鍵盤這樣的事件就必須馬上被響應(yīng),應(yīng)該用中斷實現(xiàn)。
兩者的主要區(qū)別在于:中斷不能被相同類型的中斷打斷,而下半部依然可以被中斷打斷;中斷對于時間非常敏感,而下半部基本上都是一些可以延遲的工作。由于二者的這種區(qū)別,所以對于一個工作是放在上半部還是放在下半部去執(zhí)行,可以參考下面4條:
如果一個任務(wù)對時間非常敏感,將其放在中斷處理程序中執(zhí)行。
如果一個任務(wù)和硬件相關(guān),將其放在中斷處理程序中執(zhí)行。
如果一個任務(wù)要保證不被其他中斷(特別是相同的中斷)打斷,將其放在中斷處理程序中執(zhí)行。
其他所有任務(wù),考慮放在下半部去執(zhí)行。
有寫內(nèi)核任務(wù)需要延后執(zhí)行,因此才有的下半部,進(jìn)而實現(xiàn)了三種實現(xiàn)下半部的方法。這就是本文要討論的軟中斷、tasklet和工作隊列。
下表可以更直觀的看到它們之間的關(guān)系。

軟中斷
軟中斷作為下半部機(jī)制的代表,是隨著SMP(share memory processor)的出現(xiàn)應(yīng)運(yùn)而生的,它也是tasklet實現(xiàn)的基礎(chǔ)(tasklet實際上只是在軟中斷的基礎(chǔ)上添加了一定的機(jī)制)。軟中斷一般是“可延遲函數(shù)”的總稱,有時候也包括了tasklet(請讀者在遇到的時候根據(jù)上下文推斷是否包含tasklet)。它的出現(xiàn)就是因為要滿足上面所提出的上半部和下半部的區(qū)別,使得對時間不敏感的任務(wù)延后執(zhí)行,而且可以在多個CPU上并行執(zhí)行,使得總的系統(tǒng)效率可以更高。它的特性包括:
產(chǎn)生后并不是馬上可以執(zhí)行,必須要等待內(nèi)核的調(diào)度才能執(zhí)行。軟中斷不能被自己打斷(即單個cpu上軟中斷不能嵌套執(zhí)行),只能被硬件中斷打斷(上半部)。
可以并發(fā)運(yùn)行在多個CPU上(即使同一類型的也可以)。所以軟中斷必須設(shè)計為可重入的函數(shù)(允許多個CPU同時操作),因此也需要使用自旋鎖來保其數(shù)據(jù)結(jié)構(gòu)。
相關(guān)數(shù)據(jù)結(jié)構(gòu)
軟中斷描述符
struct softirq_action{ void (*action)(struct softirq_action *);}; 描述每一種類型的軟中斷,其中void(*action)是軟中斷觸發(fā)時的執(zhí)行函數(shù)。
軟中斷全局?jǐn)?shù)據(jù)和類型
相關(guān)API
注冊軟中斷
即注冊對應(yīng)類型的處理函數(shù)到全局?jǐn)?shù)組softirq_vec中。例如網(wǎng)絡(luò)發(fā)包對應(yīng)類型為NET_TX_SOFTIRQ的處理函數(shù)net_tx_action.
觸發(fā)軟中斷
實際上即以軟中斷類型nr作為偏移量置位每cpu變量irq_stat[cpu_id]的成員變量__softirq_pending,這也是同一類型軟中斷可以在多個cpu上并行運(yùn)行的根本原因。
軟中斷執(zhí)行函數(shù)
執(zhí)行軟中斷處理函數(shù)__do_softirq前首先要滿足兩個條件:
不在中斷中(硬中斷、軟中斷和NMI) 。
有軟中斷處于pending狀態(tài)。
系統(tǒng)這么設(shè)計是為了避免軟件中斷在中斷嵌套中被調(diào)用,并且達(dá)到在單個CPU上軟件中斷不能被重入的目的。對于ARM架構(gòu)的CPU不存在中斷嵌套中調(diào)用軟件中斷的問題,因為ARM架構(gòu)的CPU在處理硬件中斷的過程中是關(guān)閉掉中斷的。只有在進(jìn)入了軟中斷處理過程中之后才會開啟硬件中斷,如果在軟件中斷處理過程中有硬件中斷嵌套,也不會再次調(diào)用軟中斷,because硬件中斷是軟件中斷處理過程中再次進(jìn)入的,此時preempt_count已經(jīng)記錄了軟件中斷!對于其它架構(gòu)的CPU,有可能在觸發(fā)調(diào)用軟件中斷前,也就是還在處理硬件中斷的時候,就已經(jīng)開啟了硬件中斷,可能會發(fā)生中斷嵌套,在中斷嵌套中是不允許調(diào)用軟件中斷處理的。Why?我的理解是,在發(fā)生中斷嵌套的時候,表明這個時候是系統(tǒng)突發(fā)繁忙的時候,內(nèi)核第一要務(wù)就是趕緊把中斷中的事情處理完成,退出中斷嵌套。避免多次嵌套,哪里有時間處理軟件中斷,所以把軟件中斷推遲到了所有中斷處理完成的時候才能觸發(fā)軟件中斷。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書、實戰(zhàn)項目及代碼)? ? ??


實現(xiàn)原理和實例
軟中斷的調(diào)度時機(jī):
do_irq完成I/O中斷時調(diào)用irq_exit。
系統(tǒng)使用I/O APIC,在處理完本地時鐘中斷時。
local_bh_enable,即開啟本地軟中斷時。
SMP系統(tǒng)中,cpu處理完被CALL_FUNCTION_VECTOR處理器間中斷所觸發(fā)的函數(shù)時。
ksoftirqd/n線程被喚醒時。
? ?下面以從中斷處理返回函數(shù)irq_exit中調(diào)用軟中斷為例詳細(xì)說明。
? ? 觸發(fā)和初始化的的流程如圖所示:

軟中斷處理流程
首先調(diào)用local_softirq_pending函數(shù)取得目前有哪些位存在軟件中斷。
調(diào)用__local_bh_disable關(guān)閉軟中斷,其實就是設(shè)置正在處理軟件中斷標(biāo)記,在同一個CPU上使得不能重入__do_softirq函數(shù)。
重新設(shè)置軟中斷標(biāo)記為0,set_softirq_pending重新設(shè)置軟中斷標(biāo)記為0,這樣在之后重新開啟中斷之后硬件中斷中又可以設(shè)置軟件中斷位。
調(diào)用local_irq_enable,開啟硬件中斷。
之后在一個循環(huán)中,遍歷pending標(biāo)志的每一位,如果這一位設(shè)置就會調(diào)用軟件中斷的處理函數(shù)。在這個過程中硬件中斷是開啟的,隨時可以打斷軟件中斷。這樣保證硬件中斷不會丟失。
之后關(guān)閉硬件中斷(local_irq_disable),查看是否又有軟件中斷處于pending狀態(tài),如果是,并且在本次調(diào)用__do_softirq函數(shù)過程中沒有累計重復(fù)進(jìn)入軟件中斷處理的次數(shù)超過max_restart=10次,就可以重新調(diào)用軟件中斷處理。如果超過了10次,就調(diào)用wakeup_softirqd()喚醒內(nèi)核的一個進(jìn)程來處理軟件中斷。設(shè)立10次的限制,也是為了避免影響系統(tǒng)響應(yīng)時間。
調(diào)用_local_bh_enable開啟軟中斷。
軟中斷內(nèi)核線程
之前我們分析的觸發(fā)軟件中斷的位置其實是中斷上下文中,而在軟中斷的內(nèi)核線程中實際已經(jīng)是進(jìn)程的上下文。
這里說的軟中斷上下文指的就是系統(tǒng)為每個CPU建立的ksoftirqd進(jìn)程。
軟中斷的內(nèi)核進(jìn)程中主要有兩個大循環(huán),外層的循環(huán)處理有軟件中斷就處理,沒有軟件中斷就休眠。內(nèi)層的循環(huán)處理軟件中斷,每循環(huán)一次都試探一次是否過長時間占據(jù)了CPU,需要調(diào)度就釋放CPU給其它進(jìn)程。具體的操作在注釋中做了解釋。
tasklet
由于軟中斷必須使用可重入函數(shù),這就導(dǎo)致設(shè)計上的復(fù)雜度變高,作為設(shè)備驅(qū)動程序的開發(fā)者來說,增加了負(fù)擔(dān)。而如果某種應(yīng)用并不需要在多個CPU上并行執(zhí)行,那么軟中斷其實是沒有必要的。因此誕生了彌補(bǔ)以上兩個要求的tasklet。它具有以下特性:
一種特定類型的tasklet只能運(yùn)行在一個CPU上,不能并行,只能串行執(zhí)行。
多個不同類型的tasklet可以并行在多個CPU上。
軟中斷是靜態(tài)分配的,在內(nèi)核編譯好之后,就不能改變。但tasklet就靈活許多,可以在運(yùn)行時改變(比如添加模塊時)。
tasklet是在兩種軟中斷類型的基礎(chǔ)上實現(xiàn)的,因此如果不需要軟中斷的并行特性,tasklet就是最好的選擇。也就是說tasklet是軟中斷的一種特殊用法,即延遲情況下的串行執(zhí)行。
相關(guān)數(shù)據(jù)結(jié)構(gòu)
tasklet描述符
tasklet鏈表
相關(guān)API
定義tasklet
tasklet操作
實現(xiàn)原理
調(diào)度原理
tasklet執(zhí)行過程 TASKLET_SOFTIRQ對應(yīng)執(zhí)行函數(shù)為tasklet_action,HI_SOFTIRQ為tasklet_hi_action,以tasklet_action為例說明,tasklet_hi_action大同小異。

工作隊列
從上面的介紹看以看出,軟中斷運(yùn)行在中斷上下文中,因此不能阻塞和睡眠,而tasklet使用軟中斷實現(xiàn),當(dāng)然也不能阻塞和睡眠。但如果某延遲處理函數(shù)需要睡眠或者阻塞呢?沒關(guān)系工作隊列就可以如您所愿了。
把推后執(zhí)行的任務(wù)叫做工作(work),描述它的數(shù)據(jù)結(jié)構(gòu)為work_struct ,這些工作以隊列結(jié)構(gòu)組織成工作隊列(workqueue),其數(shù)據(jù)結(jié)構(gòu)為workqueue_struct ,而工作線程就是負(fù)責(zé)執(zhí)行工作隊列中的工作。系統(tǒng)默認(rèn)的工作者線程為events。
工作隊列(work queue)是另外一種將工作推后執(zhí)行的形式。工作隊列可以把工作推后,交由一個內(nèi)核線程去執(zhí)行—這個下半部分總是會在進(jìn)程上下文執(zhí)行,但由于是內(nèi)核線程,其不能訪問用戶空間。最重要特點的就是工作隊列允許重新調(diào)度甚至是睡眠。
通常,在工作隊列和軟中斷/tasklet中作出選擇非常容易??墒褂靡韵乱?guī)則:
如果推后執(zhí)行的任務(wù)需要睡眠,那么只能選擇工作隊列。
如果推后執(zhí)行的任務(wù)需要延時指定的時間再觸發(fā),那么使用工作隊列,因為其可以利用timer延時(內(nèi)核定時器實現(xiàn))。
如果推后執(zhí)行的任務(wù)需要在一個tick之內(nèi)處理,則使用軟中斷或tasklet,因為其可以搶占普通進(jìn)程和內(nèi)核線程,同時不可睡眠。
如果推后執(zhí)行的任務(wù)對延遲的時間沒有任何要求,則使用工作隊列,此時通常為無關(guān)緊要的任務(wù)。
實際上,工作隊列的本質(zhì)就是將工作交給內(nèi)核線程處理,因此其可以用內(nèi)核線程替換。但是內(nèi)核線程的創(chuàng)建和銷毀對編程者的要求較高,而工作隊列實現(xiàn)了內(nèi)核線程的封裝,不易出錯,所以我們也推薦使用工作隊列。
相關(guān)數(shù)據(jù)結(jié)構(gòu)
正常工作結(jié)構(gòu)體
延遲工作結(jié)構(gòu)體(延遲的實現(xiàn)是在調(diào)度時延遲插入相應(yīng)的工作隊列)
每cpu工作隊列(每cpu都對應(yīng)一個工作者線程worker_thread)
相關(guān)API
缺省工作隊列
以上均是采用缺省工作者線程來實現(xiàn)工作隊列,其優(yōu)點是簡單易用,缺點是如果缺省工作隊列負(fù)載太重,執(zhí)行效率會很低,這就需要我們創(chuàng)建自己的工作者線程和工作隊列。
自定義工作隊列
實現(xiàn)原理
1.工作隊列的組織結(jié)構(gòu)
即workqueue_struct、cpu_workqueue_struct與work_struct的關(guān)系。
一個工作隊列對應(yīng)一個work_queue_struct,工作隊列中每cpu的工作隊列由cpu_workqueue_struct表示,而work_struct為其上的具體工作。
關(guān)系如下圖所示:

2.工作隊列的工作過程

應(yīng)用實例
linux各個接口的狀態(tài)(up/down)的消息需要通知netdev_chain上感興趣的模塊同時上報用戶空間消息。這里使用的就是工作隊列。
具體流程圖如下所示:

是否處于中斷中在Linux中是通過preempt_count來判斷的,具體如下: 在linux系統(tǒng)的進(jìn)程數(shù)據(jù)結(jié)構(gòu)里,有這么一個數(shù)據(jù)結(jié)構(gòu):
#define preempt_count() (current_thread_info()->preempt_count) 利用preempt_count可以表示是否處于中斷處理或者軟件中斷處理過程中,如下所示: # define hardirq_count() (preempt_count() & HARDIRQ_MASK) #define softirq_count() (preempt_count() & SOFTIRQ_MASK) #define irq_count() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK)) #define in_irq() (hardirq_count()) #define in_softirq() (softirq_count()) #define in_interrupt() (irq_count())

preempt_count的8~23位記錄中斷處理和軟件中斷處理過程的計數(shù)。如果有計數(shù),表示系統(tǒng)在硬件中斷或者軟件中斷處理過程中。
