十四、Linux驅(qū)動程序開發(fā)(10) -中斷
5.1中斷
中斷本質(zhì)上是一種特殊的電信號,由硬件設(shè)備發(fā)向處理器,處理器接收到中斷后,會馬上向操作系統(tǒng)反映此信號的到來,然后就由OS復(fù)雜處理這些新到來的數(shù)據(jù)。硬件設(shè)備生成中斷的時候并不考慮與處理器的時鐘同步,也就是說中斷可以隨時產(chǎn)生。
定義:
所謂中斷是指CPU在執(zhí)行程序的過程中,出現(xiàn)了某些突發(fā)事件時CPU必須暫停執(zhí)行當(dāng)前的程序,轉(zhuǎn)去處理突發(fā)事件,處理完畢后CPU又返回原程序被中斷的位置并繼續(xù)執(zhí)行。
分類:
根據(jù)中斷的來源:分為內(nèi)部中斷和外部中斷。內(nèi)部中斷的中斷源來自CPU內(nèi)部(軟件中斷指令,溢出等),外部中斷的中斷源來自CPU外部,由外設(shè)提出請求。
根據(jù)是否屏蔽:分為可屏蔽中斷與不屏蔽中斷(NMI),可屏蔽中斷可通過屏蔽字屏蔽,屏蔽后該中斷不再得到響應(yīng)。
根據(jù)中斷入口跳轉(zhuǎn)方式的不同:分為向量中斷和非向量中斷。采用向量中斷的CPU通常為不同的中斷分配不同的中斷號。非向量中斷的多個中斷共享一個入口地址。進入該入口地址后再通過軟件判斷中斷標(biāo)志來識別具體是哪個中斷。就是說,向量中斷是由硬件提供中斷服務(wù)程序入口地址,非向量中斷由軟件提供中斷服務(wù)程序入口地址。
5.2中斷處理程序
中斷處理程序與其他內(nèi)和函數(shù)的真正區(qū)別在于:
中斷處理程序是被內(nèi)核調(diào)用來響應(yīng)中斷的,而它們運行于我們稱之為中斷上下文的特殊上下文中。
前和后半部(上半部和下半部)
中斷處理的一個主要問題是如何在處理中進行長時間的任務(wù). 常常大量的工作必須響應(yīng)一個設(shè)備中斷來完成, 但是中斷處理需要很快完成并且不使中斷阻塞太長. 這 2 個需要(工作和速度)彼此沖突。
Linux (許多其他系統(tǒng)一起)解決這個問題通過將中斷處理分為 2 半. 所謂的前半部是實際響應(yīng)中斷的函數(shù) -- 你使用 request_irq 注冊的那個. 后半部是由前半部調(diào)度來延后執(zhí)行的函數(shù), 在一個更安全的時間. 最大的不同在前半部處理和后半部之間是所有的中斷在后半部執(zhí)行時都使能——這就是為什么它在一個更安全時間運行. 在典型的場景中, 前半部保存設(shè)備數(shù)據(jù)到一個設(shè)備特定的緩存,調(diào)度它的后半部, 并且退出: 這個操作非???/span>. 后半部接著進行任何其他需要的工作, 例如喚醒進程, 啟動另一個 I/O 操作, 等等. 這種設(shè)置允許前半部來服務(wù)一個新中斷而同時后半部仍然在工作。
5.3注冊中斷處理程序
中斷處理程序是管理硬件的驅(qū)動程序的組成部分。每個設(shè)備都有相關(guān)的驅(qū)動程序,如果設(shè)備使用中斷,那么相應(yīng)的驅(qū)動程序就注冊一個中斷處理程序。驅(qū)動程序可以通過下面的函數(shù)注冊并激活一個中斷處理程序,以便處理中斷:
int request_irq(unsigned int irq,
irqreturn_t (*handler)(int, void *, struct pt_regs *),
unsigned long flags,
const char *dev_name,
void *dev_id);
(1)第一個參數(shù)irq表示要分配的中斷號。對某些設(shè)備,這個值是先定的,對大多數(shù)設(shè)備來說,這個值是可以通過探測獲取,也可以動態(tài)確定。
(2)第二個參數(shù)handler是一個指針,指向處理這個中斷的實際中斷處理程序。只要操作系統(tǒng)一接收到中斷,該函數(shù)就被調(diào)用。
(3)第三個參數(shù)irqflags可以為0,也可能是下列一個或多個標(biāo)志的位掩碼:SA_INTERRUPT:此標(biāo)志表明給定的中斷處理程序是一個快速中斷處理程序。
SA_SAMPLE_RANDOM:此標(biāo)志表明這個設(shè)備產(chǎn)生的中斷對內(nèi)核熵池有貢獻。
SA_SHIRQ:此標(biāo)志表明可以在多個中斷處理程序之間共享中斷線。
(4)第四個參數(shù)devname是與中斷相關(guān)的設(shè)備的ASCII文本表示法。這些名字會被/proc/irq和proc/interrupt文件使用,以便與用戶通信。
(5)第五個參數(shù)dev_id主要用于共享中斷線。用于區(qū)分共享中斷線上的各個中斷處理程序。內(nèi)核每次調(diào)用中斷處理程序時,都會把這個指針傳遞給它。實踐中往往會通過它傳遞驅(qū)動程序的設(shè)備結(jié)構(gòu):這個指針是唯一的,而且有可能在中斷處理程序內(nèi)及設(shè)備模式中被用到。
request_irq()成功執(zhí)行會返回0,如果返回非0值,就表示有錯誤發(fā)生,在這種情況下,指定的中斷處理程序不會被注冊,最常見的錯誤是-EBUSY,它表示給定的中斷線已經(jīng)在使用(或者當(dāng)前用戶或者你沒有指定SA_SHIRQ())。
注意:request_irq()函數(shù)可能會睡眠,因此,不能在中斷上下文或其它不允許阻塞的代碼中調(diào)用該函數(shù),在睡眠不安全的上下文中可以安全的調(diào)用request_irq()函數(shù),是一種常見錯誤。
為什么request_irq()會引起睡眠?
在注冊的過程中,內(nèi)核需要在/proc/irq文件創(chuàng)建一個與中斷對應(yīng)的項,函數(shù)proc_mkdir()就是用來創(chuàng)建這個新的procfs項的,proc_makedir()通過調(diào)用函數(shù)proc_create()對這個新的profs項進行設(shè)置,而proc_create()會調(diào)用函數(shù)kmalloc()來請求分配內(nèi)存,而我們知道函數(shù)kmalloc()是可以睡眠的。
釋放中斷處理程序
釋放中斷線,可以調(diào)用:
void free_irq(unsigned int irq, void *dev_id);
如果指定的中斷線不是共享的,那么,該函數(shù)刪除處理程序的同時將禁用這條中斷線,如果中斷線是共享的,則僅刪除dec_id所對應(yīng)的處理程序,而這條中斷線本身只有在刪除了最后一個處理程序才會被禁用,由此可以看出為什么唯一的dev_id如此重要,對于共享的中斷線,需要一個唯一的信息來區(qū)分其上面的多個處理程序,并通過它來保證函數(shù)free_irq()能夠正確的刪除指定的處理程序,不管在哪種情況下(共享或不共享),如果dev_id非空,它都必須與需要的刪除的處理程序相匹配。
注意:必須從進程上下文中調(diào)用free_irq()。
5.4編寫中斷處理程序
一個典型的中斷處理程序聲明:
staticirqreturn_t intr_handle(int irq, void *dev_id, struct pt_regs*regs
注意,它的類型與request_irq()參數(shù)中handler所要求的參數(shù)類型相匹配(handler函數(shù)有一個類型為irqreturn_t的返回值)。第一個參數(shù)irq就是這個處理程序要響應(yīng)的中斷的中斷線號。dev_id是一個通用的指針,它與在中斷注冊時傳遞給request_irq()的參數(shù)dev_id必須一致。dev_id也有可能指向中斷處理程序使用的一個數(shù)據(jù)結(jié)構(gòu),對每個設(shè)備而言,設(shè)備結(jié)構(gòu)都是唯一的,而且可能在中斷處理程序中也用的到,因此,也通常會把設(shè)備結(jié)構(gòu)傳遞給dev_id。
參數(shù)regs是一個指向結(jié)構(gòu)的指針,該結(jié)構(gòu)包含處理中斷之前處理器的寄存器和狀態(tài),除了調(diào)試以外,很少用到。
中斷處理程序的返回值是一個特殊類型:irqreturn_t。它可能返回兩個特殊的值:IRQ_NONE和IRQ_HANDLED。irqreturn_t這個返回類型實際上就是一個int型,之所以使用這些特殊值是為了與早期的內(nèi)核保持兼容。
中斷處理程序通常會標(biāo)記static,因為它從來不會被別的文件中的代碼直接調(diào)用。
共享的處理程序與非共享的處理程序在注冊和運行方式上比較相似,但差異主要有以下幾點:
(1)request_irq()的參數(shù)flags必須設(shè)置SA_SHIRQ標(biāo)志。
(2)對每個注冊的中斷處理程序來說,dev_id參數(shù)必須唯一。而指向任一設(shè)備結(jié)構(gòu)體的指針就可以滿足這一要求;通常會選擇設(shè)備結(jié)構(gòu),因為它是唯一的,而且中斷處理程序可能會用到它。
(3)中斷處理程序必須能夠區(qū)分它的設(shè)備是否真的產(chǎn)生中斷。這既需要硬件支持,也需要處理程序中有相關(guān)的處理邏輯。如果硬件不支持這一功能,它就沒法知道到底是與它對應(yīng)的設(shè)備發(fā)出了這個中斷,還是共享這條中斷線的其它設(shè)備發(fā)出了這個中斷。
指定SA_SHIRQ標(biāo)志以調(diào)用request_irq()時,只有在以下兩種情況下才可能成功,一是中斷線當(dāng)前未被注冊,二是在該線上的所有已注冊處理程序都指定了SA_SHIRQ。
內(nèi)核接收一個中斷后,它將依次調(diào)用在該中斷線上注冊的每一個處理程序,因此,一個處理程序必須知道它是否應(yīng)該為這個中斷負(fù)責(zé),如果與它相關(guān)的設(shè)備并沒有產(chǎn)生中斷,那么,處理程序應(yīng)該立即退出。
當(dāng)執(zhí)行一個中斷處理程序或下半部時,內(nèi)核處于中斷上下文(interrput context)中。讓我們先回憶一下進程上下文,進程上下文是一種內(nèi)核所處的操作模式,此時內(nèi)核代表進程執(zhí)行——例如,執(zhí)行系統(tǒng)調(diào)用或運行內(nèi)核線程,在進程上下文中,可以通過current宏去關(guān)聯(lián)當(dāng)前進程,此外,因為進程是以進程上下文的形式連接到內(nèi)核中的,因此,在進程上下文可以睡眠,也可以調(diào)用調(diào)度程序。
與之相反,中斷上下文和進程并沒有什么瓜葛,與current宏也是不相干的(盡管它會指向被中斷的進程),因為沒有進程的背景,所以中斷上下文不可以睡眠——否則又怎能對它重新調(diào)度呢?因此,不能從中斷上下文中調(diào)用某些函數(shù),如果一個函數(shù)睡眠,就不能在你的中斷處理程序中使用它——這是對什么樣的函數(shù)可以在中斷處理程序中的使用的限制。
有一點非常重要:中斷處理程序打斷了其它代碼(甚至可能是打斷了在其他中斷線上的另一個中斷處理程序)。正是因為這種異步執(zhí)行的特性,所以所有的中斷處理程序必須盡可能的迅速,簡潔,盡量把工作從中斷處理程序中分離出來,放在下半部執(zhí)行,因為下半部可以在更合適的時間運行。
最后,中斷處理程序并不具有自己的棧,相反,它共享被中斷進程的內(nèi)核棧,如果沒有正在運行的進程,它將使用idle進程的棧,因為中斷處理程序共享別人的堆棧所以它們在棧中獲取空間時必須非常節(jié)省,當(dāng)然,內(nèi)核棧本來就很有限(內(nèi)核棧在32位體系結(jié)構(gòu)上是8KB,在64位體系結(jié)構(gòu)上是16KB,執(zhí)行中的進程上下文和產(chǎn)生的所有中斷都共享內(nèi)核棧)。因此所有的內(nèi)核代碼都應(yīng)該謹(jǐn)慎利用它。
5.5中斷處理機制的實現(xiàn)
中斷處理系統(tǒng)在Linux中的實現(xiàn)是非常依賴于體系結(jié)構(gòu)的。實現(xiàn)依賴于處理器、所使用的中斷控制器的類型、體系結(jié)構(gòu)的設(shè)計及機器本身。
對于中斷從硬件到內(nèi)核的路由,設(shè)備產(chǎn)生中斷,通過總線把電信號發(fā)送給中斷控制器。如果中斷是激活的,那么中斷控制器就會把中斷發(fā)往處理器。在大多數(shù)體系結(jié)構(gòu)中,這個工作就是通過電信號給處理器的特定管腳發(fā)送一個信號,處理器會立即停止它正在做的事情,關(guān)閉中斷系統(tǒng),然后跳轉(zhuǎn)到內(nèi)存中預(yù)定義的位置開始執(zhí)行那里的代碼。這個預(yù)定以的位置是內(nèi)核設(shè)置的,是中斷處理程序的入口點。
在內(nèi)核中,中斷的旅程開始于預(yù)定義入口點,這類似于系統(tǒng)調(diào)用通過預(yù)定義的異常句柄進入內(nèi)核。對于每一個中斷線,處理器都會跳到對應(yīng)的一個唯一的位置。至此,內(nèi)核知道了所接受的中斷的IRQ號。初始入口點只是在棧中保存這個號,并存放當(dāng)前寄存器的值(這些值屬于被中斷的任務(wù)),然后,內(nèi)核調(diào)用函數(shù)do_IRQ()。
5.6中斷控制
Linux內(nèi)核提供了一組接口用于操作機器上的中斷狀態(tài)。這些接口為我們提供了能夠禁止當(dāng)前處理器的中斷系統(tǒng),或屏蔽掉整個機器的一條中斷線的能力,這些例程都是與體系結(jié)構(gòu)相關(guān)的,可以在<asm/system.h>和<asm/irq.h>中找到。 下面給出一些中斷控制方法:
local_irq_disable() 禁止本地中斷傳送
local_irq_enable() 激活本地中斷傳送
local_irq_save() 保存本地中斷傳遞的當(dāng)前狀態(tài),然后禁止本地中斷傳遞
local_irq_restore() 恢復(fù)本地中斷傳遞到給定的狀態(tài)
disable_irq() 禁止給定中斷線,并確保該函數(shù)返回之前在該中斷線上沒有處理程序在運行
disable_irq_nosynoc() 禁止給定中斷線
enable_irq() 激活給定中斷線
irqs_disabled() 如果本地中斷傳遞被禁止,則返回非0,否則返回0
in_interrupt() 如果在中斷上下文中,則返回非0,如果在進程上下文中,則返回0
6.1下半部
下半部的任務(wù)就是執(zhí)行與中斷處理密切相關(guān)但中斷處理程序本身不執(zhí)行的工作。對于在上半部和下半部之間劃分工作,盡管不存在某種嚴(yán)格的規(guī)則,但還是有一些提示可供借鑒:(1)如果一個任務(wù)對時間非常敏感,將其放在中斷處理程序中執(zhí)行。(2)如果一個任務(wù)和硬件相關(guān),將其放在中斷處理程序中執(zhí)行。(3)如果一個任務(wù)要保證不被其它中斷打斷,將其放在中斷處理程序中執(zhí)行。(4)其它所有任務(wù),考慮放在下半部執(zhí)行。當(dāng)我們開始嘗試寫自己的驅(qū)動程序的時候,讀一下別人的中斷處理程序和相應(yīng)的下半部會令你受益匪淺?,F(xiàn)在的問題是:下半部具體放到以后的什么時候去做呢?下半部并不需要指明一個確切時間,只要把這些任務(wù)推遲一點,讓他們在系統(tǒng)不太繁忙并且中斷恢復(fù)后執(zhí)行就可以了。通常下半部在中斷處理程序一返回就會馬上執(zhí)行。下半部執(zhí)行的關(guān)鍵在于當(dāng)它們運行的時候,允許響應(yīng)所有中斷。
因為在中斷處理程序運行的時候,當(dāng)前的中斷線會被屏蔽,如果一個處理程序是SA_INTERRUPT類型,它執(zhí)行的時候會禁止所有本地中斷(而且把本地中斷線全局屏蔽掉),再加上中斷處理程序要與其它程序——甚至是其它的中斷處理程序——異步執(zhí)行。
具體放到以后什么時候去做呢?
在這里,“以后”僅僅用來強調(diào)不是“馬上”而已,下半部并不需要指明一個確切時間,只是把這些任務(wù)推遲一點,讓它們在系統(tǒng)不太繁忙并且中斷恢復(fù)后執(zhí)行就可以了,通常下半部在中斷處理程序一返回就會馬上執(zhí)行,下半部執(zhí)行的關(guān)鍵在于當(dāng)它們運行的時候,允許響應(yīng)所有的中斷。
6.2軟中斷
軟中斷是用軟件方式模擬硬件中斷的概念,實現(xiàn)宏觀上的異步執(zhí)行效果,tasklet也是基于軟中斷實現(xiàn)的。
異步通知所基于的信號也類似于中斷。
硬中斷是外部設(shè)備對CPU的中斷
軟中斷通常是硬中斷服務(wù)程序?qū)?nèi)核的中斷。
信號則是由內(nèi)核(或其它進程)對某個進程的中斷。
軟中斷是在編譯期間靜態(tài)分配的。不像tasklet那樣能被動態(tài)的注冊或去除。軟中斷由softirq_action結(jié)構(gòu)表示,它定義在<linux/interrupt.h>中:
structsoftirq_action {
void( *action)(struct softirq_action *);
/*待執(zhí)行的函數(shù)*/
Void*date; /傳遞給函數(shù)的參數(shù)*/
} ;
在kernel/softirq.c中定義了一個包含有32個該結(jié)構(gòu)體的數(shù)組。
staticstrcut softirq_action softirq_vec[32]; 每個注冊的軟中斷都占據(jù)該數(shù)組中的一項。
(1) 軟中斷處理程序:
軟中斷處理程序action的函數(shù)原型如下:
voidsoftirq_handler(struct softirq_action *)
當(dāng)內(nèi)核運行一個軟中斷處理程序的時候,它就會執(zhí)行這個action函數(shù),其唯一的參數(shù)為指向相應(yīng)的softirq_action結(jié)構(gòu)體的指針。
一個軟中斷不會搶占另外一個軟中斷,實際上,唯一可以搶占軟中斷的是中斷處理程序,不過,其它的軟中斷——甚至是相同類型的軟中斷——可以在其它處理器上同時執(zhí)行。
(2) 執(zhí)行軟中斷:
一個注冊的軟中斷必須在被標(biāo)記后才會執(zhí)行。這被稱作觸發(fā)軟中斷(raisingthe softirq)。通常,中斷處理程序會在返回前標(biāo)記它的軟中斷,使其在稍后被執(zhí)行。軟中斷被標(biāo)記后,可以用softirq_pending()檢查到這個標(biāo)記并按照索引號將softirq_pending()的返回值的相應(yīng)位置1。
在合適的時刻,該軟中斷就會運行,在下列地方,待處理的軟中斷會被檢查和執(zhí)行:
在處理完一個硬中斷以后
在ksoftirqd內(nèi)核線程中
在那些顯式檢查和執(zhí)行待處理的軟中斷的代碼中,如網(wǎng)絡(luò)子系統(tǒng)中
不管是用什么辦法喚起,軟中斷都要在do_softirq()中執(zhí)行,該函數(shù)很簡單,如果有待處理的軟中斷,do_softirq()會遍歷每一個,調(diào)用它們的處理程序。
軟中斷在do_softirq()中執(zhí)行。do_softirq()經(jīng)過簡化后的核心部分:
u32 pending = sofeirq_pending(cpu);
if(pending) {
struct softirq_action *h = softirq_vec;
softirq_pending(cpu) = 0;
do {
if(pending&1) h->action(h); //調(diào)用action函數(shù)
h++;
pending>>=1;
}while(pending);
}
軟中斷保留給系統(tǒng)中對時間要求最嚴(yán)格以及最重要的下半部使用。內(nèi)核定時器和tasklets都是建立在軟中斷上的,如果你想加入一個新的軟中斷,首先要想想為什么用tasklet實現(xiàn)不了,tasklet可以動態(tài)生成,由于它們對加鎖的要求不高,所以使用起來也很方便,當(dāng)然,對于時間要求養(yǎng)并能自己高效的完成加鎖工作的應(yīng)用,軟中斷會是正確的選擇。
1、 分配索引:在編譯期間,可以通過<linux/interrupt.h>中定義的一個枚舉類型來靜態(tài)的聲明軟中斷。
2、 注冊處理程序:接著,在運行時通過調(diào)用open_softirq()注冊軟件中斷處理程序,該函數(shù)有三個參數(shù):索引號、處理函數(shù)和data域存放的數(shù)值。例如網(wǎng)絡(luò)子系統(tǒng),通過以下方式注冊自己的軟中斷:
open_softirq(NET_TX_SOFTIRQ,net_tx_action,NULL);
open_softirq(NET_TX_SOFTIRQ,net_rx_action,NULL);
軟中斷處理程序的執(zhí)行的時候,允許響應(yīng)中斷,但自己不能睡眠。
3、 觸發(fā)你的軟中斷:
通過在枚舉類型的列表中添加新項以及調(diào)用open_softirq()進行注冊以后,新的軟中斷處理程序就能夠運行。raise_softirq()函數(shù)可以將一個軟中斷設(shè)置為掛起狀態(tài),讓他在下次調(diào)用do_softirq()函數(shù)時投入運行。一個例子:
raise_softirq(NET_TX_SOFTIRQ);
這會觸發(fā)NET_TX_SOFTIRQ軟中斷。它的處理程序net_tx_action()就會在內(nèi)核下一次執(zhí)行軟中斷時投入運行。該函數(shù)在觸發(fā)一個軟中斷前要禁止中斷,觸發(fā)后再恢復(fù)回原來的狀態(tài)。在中斷處理程序中觸發(fā)軟中斷是最常見的形式。這樣,內(nèi)核在執(zhí)行完中斷處理程序后,馬上就會調(diào)用do_softirq。于是軟中斷開始執(zhí)行中斷處理程序留給它去完成的剩余任務(wù)。
6.3 Tasklet
tasklet是利用軟中斷實現(xiàn)的一種下半部機制。它和進程沒有任何關(guān)系。它和軟中斷本質(zhì)上很相似,行為表現(xiàn)也相近,但是,它的接口更簡單,鎖保護也要求較低。
軟中斷和tasklet怎樣選擇呢?
通常你應(yīng)該用tasklet,軟中斷一般用的很少,它只在那些執(zhí)行頻率很高和連續(xù)性要求很高的情況下才需要,而tasklet卻有更廣泛的用途。
因為tasklet是通過軟中斷實現(xiàn)的,所以它本身也是軟中斷。
(1)tasklet結(jié)構(gòu)體:tasklet由tasklet_struct結(jié)構(gòu)表示。每個結(jié)構(gòu)體單獨代表一個tasklet,它在<linux/interrupt.h>中定義:
struct tasklet_struct {
struct task_struct *next; /*指向鏈表中的下一個tasklet*/
unsigned long state; /* tasklet的狀態(tài)*/
atomic_t count; /* 引用計數(shù)器*/
void (*func) (unsigned long); /* tasklet處理函數(shù)*/
unsigned long data; /*給tasklet處理函數(shù)的參數(shù)*/
};
結(jié)構(gòu)體中的func成員是tasklet的處理程序,data是它唯一的參數(shù)。state成員只能在0、TASKLET_STATE_SCHED和TASKLET_STATE_RUN之間取值。TASKLET_STATE_SCHED表明tasklet已經(jīng)被調(diào)度,正準(zhǔn)備投入運行,TASKLET_STATE_RUN表示該tasklet正在運行。只有count為0時,tasklet才被激活,否則被允許,不允許執(zhí)行。
調(diào)度tasklet
已調(diào)度的tasklet存放在兩個單處理器數(shù)據(jù)結(jié)構(gòu):tasklet_vec和task_hi_vec中。它們都是由tasklet_struct結(jié)構(gòu)體構(gòu)成的鏈表。鏈表中的每個tasklet_struct代表一個不同的tasklet。
tasklet是由tasklet_schedule()和tasklet_hi_schedule()函數(shù)進行調(diào)度的,它們接受一個指向tasklet_struct結(jié)構(gòu)的指針作為參數(shù)。
Tasklet的實現(xiàn)通過軟中斷來實現(xiàn)的,tasklet_schedule()調(diào)度函數(shù)執(zhí)行一些初始工作,緊接著喚起TASKLET_SOFTIRQ或HI_SOFTIRQ軟中斷,這樣在下一次調(diào)用do_softirq()時就會執(zhí)行tasklet。
那么do_softirq()函數(shù)什么時候執(zhí)行呢?
do_softirq()會盡可能早的在下一個合適的時機執(zhí)行,由于大部分tasklet和軟中斷都是在中斷處理程序中被設(shè)置成待處理狀態(tài),所以最近一個中斷返回的時候看起來就是執(zhí)行do_softirq()的最佳時機。因為TASKLET_SOFTIRQ和HI_SOFTIRQ已經(jīng)被觸發(fā)了,所以do_softirq會執(zhí)行相應(yīng)的軟中斷處理程序。
Tasklet_action()和Tasklet_hi_action()兩個處理程序就是tasklet處理的核心。
總結(jié):所有的Tasklets都通過重復(fù)運用TASKLET_SOFTIRQ或HI_SOFTIRQ這兩個軟中斷實現(xiàn),當(dāng)一個tasklet被調(diào)度時,內(nèi)核就會喚起這兩個軟中斷中的一個,隨后,該軟中斷會被特定的函數(shù)處理,執(zhí)行所有已調(diào)度的tasklet,這個函數(shù)保證同一時間里只有一個給定類別的tasklet會被執(zhí)行(但其它不同類型的tasklet可以同時執(zhí)行),所有這些復(fù)雜性都被一個簡潔的接口隱藏起來了。
聲明你自己的tasklet
可以靜態(tài)創(chuàng)建,也可以動態(tài)創(chuàng)建,分別對應(yīng)直接引用和間接引用。選擇哪種方式取決于你到底是有(或者是想要)一個對tasklet的直接引用還是間接引用,靜態(tài)創(chuàng)建一個tasklet(也就是有一個直接引用),使用下面<linux/interrupt.h>中定義的兩個宏中的一個:
DECLARE_TASKLET(name,func, data)
實現(xiàn)了定義名稱為name的tasklet并將其與func這個函數(shù)綁定,而傳入這個函數(shù)的參數(shù)為data。
DECLARE_TASKLET_DISABLED(name, func, data);
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, dev);運行代碼實際上等價于:
structtasklet_struct my_tasklet = { NULL, 0, ATOMIC_INIT(0),my_tasklet_handler, dev }; 這樣就創(chuàng)建了一個名為my_tasklet,處理程序為tasklet_handler并且已經(jīng)被激活的tasklet。
還可以通過一個間接引用(一個指針)賦給一個動態(tài)創(chuàng)建的tasklet_struct結(jié)構(gòu)的方式來初始化一個tasklet:
Tasklet_init(t,tasklet_handler,dev);/*動態(tài)而不是靜態(tài)創(chuàng)建*/
編寫你自己的tasklet處理程序
必須符合規(guī)定的函數(shù)類型:
voidtasklet_handler(unsigned long data)
因為是靠軟件中斷實現(xiàn),所以tasklet不能睡眠,這意味著你不能在tasklet中使用信號量或者其它什么阻塞式的函數(shù)。
調(diào)度你自己的tasklet
通過調(diào)用task_schedule()函數(shù)并傳遞給它相應(yīng)的tasklet_struct的指針,該tasklet就會被調(diào)度以便執(zhí)行。
tasklet_schedule(&my_tasklet); /*把my_tasklet標(biāo)記為掛起*/
下面我們看一下軟中斷和tasklet的異同:
在前期準(zhǔn)備工作上,首先要給軟中斷分配索引,而tasklet則要用宏對處理程序聲明。在給軟中斷分配索引后,還要通過open_softirq()函數(shù)來注冊處理程序。這樣看來,tasklet是一步到位,直接到了處理函數(shù),而軟中斷需要做更多工作。接下來軟中斷要等待觸發(fā)(raise_softirq()或raise_softirq_irqoff),而tasklet則是等待tasklet_schedule()和tasklet_hi_schedule()對其進行調(diào)度。兩者雖然在命名上不同,但殊途同歸,最終的結(jié)果都是等待do_softirq()去執(zhí)行處理函數(shù),即將下半部設(shè)置為待執(zhí)行狀態(tài)以便稍后執(zhí)行。另外,在tasklet的tasklet_schedule()中,需要完成的動作之一便是喚起(觸發(fā))TASKLET_SOFTIRQ或HI_SOFTIRQ軟中斷,說明tasklet仍然是基于軟中斷的。在進入do_softirq()之后,所做的工作仍然有所不同,不再論述。
軟中斷和工作隊列都是異步發(fā)生的(就是說,在中斷處理返回的時候)
6.4工作隊列
工作隊列(work queue)是另外一種將工作推后執(zhí)行的形式,他和我們前面討論過的其他形式完全不同。工作隊列可以把工作推后,交由一個內(nèi)核線程去執(zhí)行——這個下半部總是會在進程上下文執(zhí)行。這樣,通過工作隊列執(zhí)行的代碼能占盡進程上下文的所有優(yōu)勢,最重要的是工作隊列允許重新調(diào)度甚至是睡眠。
如果你需要用一個可以重新調(diào)度的實體來執(zhí)行你的下半部處理,你應(yīng)該使用工作隊列,它是唯一能在進程上下文運行的下半部實現(xiàn)的機制,也只有它才可以睡眠,這意味著你在你需要獲得大量的內(nèi)存時,在你需要獲取信號量時,在你需要執(zhí)行阻塞式的IO操作時,它都會非常有用,如果你不需要用一個內(nèi)核線程來推后執(zhí)行工作,那么就考慮使用tasklet吧!
工作隊列子系統(tǒng)是一個用于創(chuàng)建內(nèi)核線程的接口,通過它創(chuàng)建的進程負(fù)責(zé)執(zhí)行由內(nèi)核其他部分排到隊列里的其他任務(wù)。它創(chuàng)建的這些內(nèi)核線程被稱作工作者線程。工作隊列可以讓驅(qū)動程序創(chuàng)建一個專門的工作者線程來處理需要推后的工作。不過,工作隊列子系統(tǒng)提供了一個默認(rèn)的工作者線程來處理這些工作。因此,工作隊列最基本的表現(xiàn)形式就轉(zhuǎn)變成了一個把需要推后執(zhí)行的任務(wù)交給特定的通用線程這樣一個接口。
表示線程的數(shù)據(jù)結(jié)構(gòu)
工作者線程用workqueue_struct結(jié)構(gòu)表示:
struct workqueue_struct {
struct cpu_workqueue_struct cpu_wq[NR_CPUS];
const char *name;
struct list_head list;
};
該結(jié)構(gòu)內(nèi)是一個由cpu_workqueue_struct結(jié)構(gòu)組成的數(shù)組,定義在kernel/workqueue.c中,數(shù)組的每一項對應(yīng)一個系統(tǒng)中的處理器。每個工作者線程都對應(yīng)這樣的cpu_workqueue_struct結(jié)構(gòu)體。cpu_workqueue_struct是kernel/workqueue.c中的核心數(shù)據(jù)結(jié)構(gòu):
struct cpu_workqueue_struct {
spinlock_t lock; /* 鎖定以便保護該結(jié)構(gòu)體*/
long romove_sequeue; /* 最近一個被加上的(下一個要運行的)*/
long insert_sequeue; /*下一個要加上的 */
wait_queue_head_t more_work;
wait_queue_head_t work_done;
struct workqueue_struct *wq; /* 有關(guān)聯(lián)的workqueue_struct結(jié)構(gòu)*/
task_t *thread; /* 有關(guān)聯(lián)的線程*/
int run_depth; /* run_workqueue()循環(huán)深度 */
};
由此可以看出,每個工作者線程類型關(guān)聯(lián)一個自己的workqueue_struct。在該結(jié)構(gòu)體里面,給每個線程分配一個cpu_workqueue_struct,因而也就是給每個處理器分配一個,因為每個處理器都有一個該類型的線程。
表示工作的數(shù)據(jù)結(jié)構(gòu)
所有工作者線程都是用普通的內(nèi)核線程實現(xiàn)的,它們都要執(zhí)行worker_thread()函數(shù)。在它初始化完以后,這個函數(shù)(worker_thread)開始休眠。當(dāng)有操作被插入到隊列的時候,線程就會被喚醒,以便執(zhí)行這些操作。當(dāng)沒有剩余的操作時,它又會繼續(xù)睡眠。
工作用<linux/workqueue.h>中定義的work_struct結(jié)構(gòu)體表示:
struct work_struct {
unsigned long pending; /* 這個工作是否正在等待處理*/
struct list_head entry; /* l連接所有工作的鏈表*/
void (* func) (void *); /* 處理函數(shù)*/
void *wq_data; /* 內(nèi)部使用*/
struct timer_list timer; /* 延遲的工作隊列所用到的定時器 */
};
這些結(jié)構(gòu)體被連接成鏈表,在每個處理器的每種類型的隊列都對應(yīng)這樣一個鏈表。當(dāng)一個工作者線程被喚醒時,它會執(zhí)行它的鏈表上的所有工作。當(dāng)工作完畢時,他會將相應(yīng)的work_struct對象從鏈表中移去,當(dāng)鏈表上不再有對象的時候,它就會繼續(xù)睡眠。
(1)創(chuàng)建推后的工作
首先要做的是實際創(chuàng)建一些需要推后執(zhí)行的工作??梢酝ㄟ^DECLARE_WORK在編譯時靜態(tài)的創(chuàng)建該結(jié)構(gòu)體:
DECLARE_WORK(name, void (*func) (void *), void *data);
這樣就會靜態(tài)的創(chuàng)建一個名為name,處理函數(shù)為func,參數(shù)為data的work_struct結(jié)構(gòu)體。也可以在運行時通過指針創(chuàng)建一個工作:
INIT_WORK(struct work_struct *work, void (*func)(void *),void *data);
這樣就動態(tài)的初始化了一個由work指向的工作。
(2)工作隊列的處理函數(shù)
原型是:void work_handler(void *data)
這個函數(shù)會由一個工作者線程執(zhí)行,因此,函數(shù)會運行在進程上下文中,默認(rèn)情況下,允許響應(yīng)中斷,并且不持有任何鎖,如果需要,函數(shù)可以睡眠,注意的是,盡管操作處理函數(shù)運行在進程上下文中,但它不能訪問用戶空間,因為內(nèi)核線程在用戶空間沒有相關(guān)的內(nèi)存映射,通常在系統(tǒng)調(diào)用發(fā)生時,內(nèi)核會代表用戶空間的進程運行,此時它才能訪問用戶空間,也只有在此時它才會映射用戶空間的內(nèi)存。
(3)對工作進行調(diào)度
現(xiàn)在工作已經(jīng)創(chuàng)建,我們可以調(diào)度它了,要把給定工作的處理函數(shù)提交給默認(rèn)的events工作線程,只需調(diào)用:schedule_work(&work); work馬上就會被調(diào)度,一旦其所在的處理器上的工作者線程被喚醒,它就會被執(zhí)行。
(4)刷新操作
刷新工作隊列的函數(shù)就是確保在卸載模塊之前,要確保一些操作已經(jīng)執(zhí)行完畢了,該函數(shù)如下:
Voidflush_scheduled_work(void);
該函數(shù)會一直等待,直到隊列中所有對象都被執(zhí)行以后才返回,在等待所以待處理的工作執(zhí)行的時候,該函數(shù)會進入休眠狀態(tài),所以只能在進程上下文中使用它。
(5)創(chuàng)建新的工作隊列
當(dāng)缺省的隊列不能滿足你的需要時,你應(yīng)該創(chuàng)建一個新的工作隊列和與之對應(yīng)的工作者線程。
6.5下半部之間的選擇
1 從設(shè)計的角度考慮
軟中斷提供的執(zhí)行序列化的保障最少,這就要求軟中斷必須采取一些步驟確保共享數(shù)據(jù)的安全。如果被考察的代碼本身多線索化的工作就做得非常好,軟中斷就很好,對于時間要求嚴(yán)格和執(zhí)行頻率很高的話,它執(zhí)行的也快。如果代碼本身多線索化的工作就做得不充分,就選擇tasklet比較好,由于兩個同種類型的tasklet不能同時執(zhí)行,實現(xiàn)起來也很簡單一些。
2 如果你需要把任務(wù)推到進程上下文中完成,只能選擇工作隊列。
如果不需要睡眠,那么軟中斷和工作隊列就更合適。工作隊列造成的開銷最大,因為他要牽扯到內(nèi)核線程甚至是上下文切換。
3 說到易用性,工作隊列最好,使用缺省的events隊列簡直不費吹灰之力。接下來就tasklet。他的的接口很簡單,最后才是軟中斷,它必須靜態(tài)創(chuàng)建。
6.6在下半部之間加鎖
使用tasklet的一個好處是在于它自己負(fù)責(zé)執(zhí)行的序列化保障,兩個相同類型的tasklet不允許同時執(zhí)行,即使在不同的處理器上也不行,意味著你無須考慮相同類型的tasklet內(nèi)部的同步問題。當(dāng)然,tasklet之間的同步(兩個不同類型的tasklet共享同一數(shù)據(jù)時)需要正確使用鎖機制。
因為軟中斷根本不保障執(zhí)行序列化,(即使相同類型的軟中斷也有可能有兩個實例在同時執(zhí)行)所以所有的共享數(shù)據(jù)都需要合適的鎖。
如果進程上下文和一個下半部共享數(shù)據(jù),在訪問這些數(shù)據(jù)之前,你需要禁止下半部的處理并得到鎖的使用權(quán),所做的這些是為了本地和SMP的保護并且防止死鎖的出現(xiàn)。
如果中斷上下文和一個下半部共享數(shù)據(jù),在訪問數(shù)據(jù)之前,你需要禁止中斷并得到鎖的使用權(quán),所做的這些是為了本地和SMP的保護并且防止死鎖的出現(xiàn)。
任何在工作隊列中被共享的數(shù)據(jù)也需要使用鎖機制,其中有關(guān)鎖的要點和在一般內(nèi)核代碼中沒什么區(qū)別,因為工作隊列本來就是在進程上下文中執(zhí)行的。
禁止下半部