一. 中斷上半部,下半部理解
設備的中斷會打斷內核中進程的正常調度和運行,系統對更高吞吐率的追求勢必要求中斷服務程序盡可能地短小精悍。但是,這個良好的願望往往與現實並不吻合。在大多數真實的系統中,當中斷到來時,要完成的工作往往並不會是短小的,它可能要進行較大量的耗時處理。
如上圖描述了Linux內核的中斷處理機制
為了在中斷執行時間盡可能短和中斷處理需完成大量工作之間找到一個平衡點,Linux將中斷處理程序分解為兩個半部:頂半部(top half)和底半部(bottom half)。
頂半部完成盡可能少的比較緊急的功能,它往往只是簡單地讀取寄存器中的中斷狀態並清除中斷標志后就進行“登記中斷”的工作。“登記中斷”意味着將底半部處理程序掛到該設備的底半部執行隊列中去。這樣,頂半部執行的速度就會很快,可以服務更多的中斷請求。
現在,中斷處理工作的重心就落在了底半部的頭上,它來完成中斷事件的絕大多數任務。底半部幾乎做了中斷處理程序所有的事情,而且可以被新的中斷打斷,這也是底半部和頂半部的最大不同,因為頂半部往往被設計成不可中斷。底半部則相對來說並不是非常緊急的,而且相對比較耗時,不在硬件中斷服務程序中執行。
盡管頂半部、底半部的結合能夠改善系統的響應能力,但是,僵化地認為Linux設備驅動中的中斷處理一定要分兩個半部則是不對的。如果中斷要處理的工作本身很少,則完全可以直接在頂半部全部完成。
其實上面這一段大致說明一個問題,那就是:中斷要盡可能耗時比較短,盡快恢復系統正常調試,所以把中斷觸發、中斷執行分開,也就是所說的“上半部分(中斷觸發)、底半部(中斷執行)”,其實就是我們后面說的中斷上下文。下半部分一般有tasklet、工作隊列實現,觸摸屏中中斷實現以工作隊列形式實現的
二. 中斷下半部的處理
對於一個中斷,如何划分出上下兩部分呢?哪些處理放在上半步,哪些放在下半部?
這里有一些經驗可供借鑒:
如果一個任務對時間十分敏感,將其放在上半部。
如果一個任務和硬件有關,將其放在上半部。
如果一個任務要保證不被其他中斷打斷,將其放在上半部。
其他所有任務,考慮放在下半部。
三. 實現下半部中斷的三種機制
目前使用下面三種方法:
1.軟中斷
2.tasklet
3.工作隊列
四. 軟中斷
軟中斷是一組靜態定義的下半部接口,有 32 個,可以在所有處理器上同時執行,類型相同也可以;在編譯時靜態注冊。
軟中斷的流程如下:
軟中斷執行函數如下:
1 asmlinkage void do_softirq(void) 2 { 3 __u32 pending; 4 unsigned long flags; 5 6 /* 判斷是否在中斷處理中,如果正在中斷處理,就直接返回 */ 7 if (in_interrupt()) 8 return; 9 10 /* 保存當前寄存器的值 */ 11 local_irq_save(flags); 12 13 /* 取得當前已注冊軟中斷的位圖 */ 14 pending = local_softirq_pending(); 15 16 /* 循環處理所有已注冊的軟中斷 */ 17 if (pending) 18 __do_softirq(); 19 20 /* 恢復寄存器的值到中斷處理前 */ 21 local_irq_restore(flags); 22 }
代碼之中第一次就判斷是否在中斷處理中,如果在立刻退出函數。這說明了什么?說明了如果有其他軟中斷觸發,執行到此處由於先前的軟中斷已經在處理,則其他軟中斷會返回。所以,軟中斷不能被另外一個軟中斷搶占!唯一可以搶占軟中斷的是中斷處理程序,所以軟中斷允許響應中斷。雖然不能在本處理器上搶占,但是其他的軟中斷甚至同類型可以再其他處理器上同時執行。由於這點,所以對臨界區需要加鎖保護。
軟中斷留給對時間要求最嚴格的下半部使用。目前只有網絡,內核定時器和 tasklet 建立在軟中斷上。
Tasklet
注意,這第二種機制是基於軟中斷實現的,靈活性強,動態創建的下半部實現機制。兩個不同類型的 tasklet 可以在不同處理器上運行,但相同的不可以,可以通過代碼動態注冊。
在 SMP 上,調用 tasklet 是會檢測 TASKLET_STATE_SCHED 標志,如果同類型在運行,就退出函數。
tasklet 由於是基於軟中斷實現的,所以也允許響應中斷。但不能睡眠(我認為不能睡眠原因是它們內部有 spin lock)。
工作隊列
工作隊列(work queue)是另外一種將中斷的部分工作推后的一種方式,它可以實現一些tasklet不能實現的工作,比如工作隊列機制可以睡眠。這種差異的本質原因是,在工作隊列機制中,將推后的工作交給一個稱之為工作者線程(worker thread)的內核線程去完成(單核下一般會交給默認的線程events/0)。因此,在該機制中,當內核在執行中斷的剩余工作時就處在進程上下文(process context)中。也就是說由工作隊列所執行的中斷代碼會表現出進程的一些特性,最典型的就是可以重新調度甚至睡眠。
對於tasklet機制(中斷處理程序也是如此),內核在執行時處於中斷上下文(interrupt context)中。而中斷上下文與進程毫無瓜葛,所以在中斷上下文中就不能睡眠。因此,選擇tasklet還是工作隊列來完成下半部分應該不難選擇。當推后的那部分中斷程序需要睡眠時,工作隊列毫無疑問是你的最佳選擇;否則,還是用tasklet吧。
中斷上下文
在了解中斷上下文時,先來回顧另一個熟悉概念:進程上下文(這個中文翻譯真的不是很好理解,用“環境”比它好很多)。一般的進程運行在用戶態,如果這個進程進行了系統調用,那么此時用戶空間中的程序就進入了內核空間,並且稱內核代表該進程運行於內核空間中。由於用戶空間和內核空間具有不同的地址映射,並且用戶空間的進程要傳遞很多變量、參數給內核,內核也要保存用戶進程的一些寄存器、變量等,以便系統調用結束后回到用戶空間繼續執行。這樣就產生了進程上下文。
所謂的進程上下文,就是一個進程在執行的時候,CPU的所有寄存器中的值、進程的狀態以及堆棧中的內容。當內核需要切換到另一個進程時(上下文切換),它需要保存當前進程的所有狀態,即保存當前進程的進程上下文,以便再次執行該進程時,能夠恢復切換時的狀態繼續執行。上述所說的工作隊列所要做的工作都交給工作者線程來處理,因此它可以表現出進程的一些特性,比如說可以睡眠等。
對於中斷而言,是硬件通過觸發信號,導致內核調用中斷處理程序,進入內核空間。這個過程中,硬件的一些變量和參數也要傳遞給內核,內核通過這些參數進行中斷處理,中斷上下文就可以理解為硬件傳遞過來的這些參數和內核需要保存的一些環境,主要是被中斷的進程的環境。因此處於中斷上下文的tasklet不會有睡眠這樣的特性。
工作隊列實現方法
使用方法和tasklet類似
相關操作:
struct work_struct my_wq; //定義一個工作隊列
void my_wq_func(unsigned long); //定義一個處理函數
通過INIT_WORK()可以初始化這個工作隊列並將工作隊列與處理函數綁定
INIT_WORK(&my_wq,(void ()(void ))my_wq_func,NULL);
/*初始化工作隊列並將其與處理函數綁定*/
schedule_work(&my_wq);/調度工作隊列執行/
1 /*定義工作隊列和關聯函數*/ 2 struct work_struct xxx_wq(unsigned long); 3 4 /*中斷處理底半部*/ 5 void xxx_do_work(unsigned long){...} 6 7 /*中斷處理頂半部*/ 8 irqreturn_t xxx_interrupt(int irq,void *dev_id) 9 { 10 //TODO 11 schedule_work(&my_wq); 12 } 13 14 /*設備驅動模塊加載函數*/ 15 int xxx_init(void) 16 { 17 //TODO 18 //申請中斷 19 //原型:int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev) 20 request = request_irq(xxx_wq,xxx_interrupt,IRQF_DISABLED,"XXX",NULL); 21 ... 22 // 初始化工作隊列 23 INIT_WORK(&my_wq,(void(*)(void *))xxx_do_work,NULL ) 24 } 25 26 /*設備驅動模塊卸載函數*/ 27 void xxx_exit(void) 28 { 29 //TODO 30 //釋放中斷 31 free_irq(xxx_irq,xxx_interrupt); 32 //TODO 33 }
中斷中為何不能使用信號量?中斷上下為何不能睡眠?
信號量會導致睡眠
中斷發生以后,CPU跳到內核設置好的中斷處理代碼中去,由這部分內核代碼來處理中斷。這個處理過程中的上下文就是中斷上下文。
為什么可能導致睡眠的函數都不能在中斷上下文中使用呢? 首先睡眠的含義是將進程置於“睡眠”狀態,在這個狀態的進程不能被調度執行。然后,在一定的時機,這個進程可能會被重新置為“運行”狀態,從而可能被調度執行。 可見,“睡眠”與“運行”是針對進程而言的,代表進程的task_struct結構記錄着進程的狀態。內核中的“調度器”通過task_struct對進程進行調度。
但是,中斷上下文卻不是一個進程,它並不存在task_struct,所以它是不可調度的。所以,在中斷上下文就不能睡眠。
那么,中斷上下文為什么不存在對應的task_struct結構呢?
中斷的產生是很頻繁的(至少每毫秒(看配置,可能10毫秒或其他值)會產生一個時鍾中斷),並且中斷處理過程會很快。如果為中斷上下文維護一個對應的task_struct結構,那么這個結構頻繁地分配、回收、並且影響調度器的管理,這樣會對整個系統的吞吐量有所影響。
但是在某些追求實時性的嵌入式linux中,中斷也可能被賦予task_struct結構。這是為了避免大量中斷不斷的嵌套,導致一段時間內CPU總是運行在中斷上下文,使得某些優先級非常高的進程得不到運行。這種做法能夠提高系統的實時性,但是代價中吞吐量的降低