中斷處理程序是被內核調用來響應中斷的,它運行在中斷上下文,中斷處理程序是上半部,當接收到一個中斷,它就立即開始執行,但只做有嚴格時限的工作,例如對接收的中斷進行應答或復位硬件,這些工作都是在所有中斷被禁止的情況下完成。能夠被允許稍後完成的工作會推遲到下半部去。
中斷處理程序的注冊是通過request_irq函數,由於該函數內部有分配內存的操作,所以它不能在中斷上下文或其他不允許阻塞的代碼中調用。Linux中的中斷處理程序是無須重入的,因為當一個給定的中斷處理程序正在執行時,所有其他的中斷都是打開的,而當前中斷線總是被禁止的,由此可見,同一個中斷處理程序絕不會被同時調用以處理嵌套的中斷。中斷處理程序不用關心中斷棧和內核棧的設置,盡量節約內核棧空間就是了。
鎖提供保護機制,防止來自其他處理器的並發訪問,而禁止中斷提供的保護機制則是防止來自其他中斷處理程序的並發訪問。禁止中斷包括禁止當前處理器的所有中斷和禁止一條中斷線兩種,禁止所有中斷可以使用local_irq_save和local_irq_restore函數,禁止一個中斷線可以使用disable_irq函數。
查看使用的中斷號可以用cat /proc/interrupts得到。中斷系統的狀態可以通過幾個函數獲得,irqs_disabled函數查看本地中斷傳遞是否被禁止;in_interrupt函數查看是否在中斷上下文;in_irq查看是否是當前正在執行中斷處理程序。
為什麼要引入下半部呢?因為中斷處理流程的上半部有一些局限:其一,中斷以異步方式執行,它有可能打斷其他重要代碼,為了避免打斷時間過長,中斷處理程序應該執行的快些。其二,如果當前有一個中斷處理程序正在執行,需要做一個禁止其他中斷的操作,禁止中斷後硬件和操作系統無法通信了,所以也有中斷處理快點。其三,中斷處理往往需要對硬件操作,所以也需要快點。其四,中斷處理程序不在進程上下文,不能睡眠,這也限制了它們所做的事。
下半部的任務就是執行與中斷處理密切相關中斷處理程序本身不執行的工作。中斷處理程序往往需要通過操作硬件對中斷的到達進行確認,有時它還會從硬件拷貝數據。我們將對時間非常敏感,與硬件相關,要保證不被其他中斷打斷的事情放在中斷處理程序中執行,其他任務考慮放到下半部執行。下半部執行的關鍵在於當它們運行的時候,運行相應所有的中斷。
在2.6內核版本中,下半部實現的機制包括軟中斷、tasklet和工作隊列。
軟中斷是一組靜態定義的下半部接口,有32個,可以在所以處理器上同時執行,軟中斷必須在編譯期間就進行靜態注冊,這裡的軟中斷不是系統調用中提到的軟中斷。軟中斷在下面地方會被執行:其一,一個硬件中斷代碼處返回時。其二,在ksoftirqd內核線程中(每個處理器都有一組輔助處理軟中斷和tasklet的內核線程,當大量軟中斷出現時,內核會喚醒一組內核線程來處理這些負載,這個內核線程就是ksoftirqd線程)。其三,在那些顯式檢查和執行處待理的軟件中斷的代碼中,如網絡子系統。只有網絡和SCSI子系統直接使用軟中斷,對於世界要求嚴格並能自己高效地完成加鎖工作的應用,軟中斷是正確的選擇。
tasklet有兩種類軟中斷代表:HI_SOFTIRQ和TASKLET_SOFTIRQ,前者優先級更高。所有的tasklet都通過重復運用HI_SOFTIRQ和TASKLET_SOFTIRQ這兩個軟中斷實現。當一個tasklet被調用時,內核就會喚醒這兩個軟中斷中的一個,隨後該軟中斷會被特定的函數處理,執行所有已調度的tasklet,這個函數保證同一時間裡只有一個給定類別的tasklet會被執行。Tasklet不能睡眠,這意味著你不能在tasklet中使用信號量或其他什麼阻塞式的函數,同時由於tasklet運行時允許響應中斷,所以你必須做好預防(如屏蔽中斷然後獲得一個鎖),另外,你可以調用tasklet_disable函數禁止某個指定的tasklet,你也可以使用tasklet_kill從掛起隊列中去掉一個tasklet。
DECLARE_TASKLET(test_tasklet,test_tasklet_func,0); //定義
void test_tasklet_func(void) //處理函數
{
printk("tasklet is executing!\n");
}
tasklet_schedule(&test_tasklet); //調度
工作隊列可以把工作推後,交給一個內核線程去執行,這個下半部分總是會在進程上下文中執行,工作隊列運行重新調度甚至是睡眠,它是唯一能在進程上下文中運行的下半部實現機制,也只有它可以睡眠。盡管操作處理函數運行在進程上下文,但它不能訪問用戶空間,因為內核線程在用戶空間沒有相關的內存映射,通常只有發生系統調用時,內核才會代表用戶空間的進行運行。
工作隊列子系統提供了一個缺省的工作者線程,我們只要把需要推後執行的任務交給特定的通用線程就好了,缺省的工作者線程叫events/n,我們一般使用這個缺省的工作者線程,但是如果你需要在工作者線程中執行大量的處理操作,創建自己的工作者線程就更好了。系統的每個CPU都會有一個工作者線程,每個工作者線程都是由struct cpu_workqueue_struct結構體表示,而struct workqueue_struct則表示給定類型(即同類型)的所有工作者線程。
INIT_WORK(&button_dev->work, gpio_keys_report_event); //定義
static void gpio_keys_report_event(struct work_struct *work) //處理函數
{
key_values[0] = '0' ; //清除按鍵標識
input_report_key(channel, BTN_0, !!ev_press); //向input子系統報告按鍵事件
input_sync(channel); //同步操作
ev_press = 0; //清除按鍵值
}
schedule_work(&button_dev->work); //調度工作隊列處理函數
cancel_work_sync(&button_dev->work); //刪除工作隊列
tasklet基於軟件中斷,而工作隊列是靠內核線程實現的,如果你有休眠的需要,那麼你使用工作隊列,否則最好使用tasklet機制。為了保證共享數據,一般先得到一個鎖,然後使用local_bh_disable函數禁止下半部,但local_bh_disable函數並不能禁止工作隊列的執行,因為工作隊列不涉及異步執行,但是由於軟中斷和tasklet是異步發送的,所以內核必須禁止它們。