中斷處理 由于I/O操作的不确定因素,以及處理器和I/O裝置之間速度的不比對,裝置往往通過某種硬體信号異步地喚起處理器的注意。這些硬體信号就是所謂的中斷。每個中斷裝置都被配置設定給一個相關的辨別符,被稱為中斷請求(IRQ)号。當處理器檢測到某一IRQ号對應的中斷産生時,它将停止它現在的工作,并引用該IRQ所對應的中斷服務例程(ISR)。中斷處理函數ISR在中斷上下文執行。 中斷上下文 ISR 是與硬體互動的非常重要的代碼片段。它們被給予了立即執行的特權,以便最大化系統的性能。不過,如果ISR執行過慢、負載太重的化,就違背了自身的設計哲學。貴賓都被給予了優惠待遇,但是,盡量減少由此造成的對公衆的不便也是他們的義務。為了對粗暴打斷目前執行線程的行為進行補償,ISR不得不禮貌地執行于受限制的環境下,即所謂的中斷上下文(或原子上下文)。 下面給出了中斷上下文可為和不可為事項的清單: 1. 如果你的中斷上下文進入睡眠,它是一項應該被處以監禁的罪行。中斷處理函數不能通過調用schedule_timeout()等睡眠函數放棄處理器,在中斷處理函數中調用一個核心API之前,應該仔細分析它以確定其内部不會觸發阻塞等待。例如,input_register_device()表面上看起來沒有問題,但是它内部以GFP_KERNEL為參數調用了kmalloc()。從第2章《核心一瞥》可以看出,用這種方式調用kmalloc()的話,如果系統的空閑記憶體低于某門限,kmalloc()将睡眠等待swapper釋放記憶體。 2. 為了在中斷處理函數中保護臨界區,你不能使用互斥體,因為它們也許導緻睡眠。應該使用自旋鎖代替互斥體,但是一定要記住的是隻有真正需要的時候才采用它。 3. 中斷處理函數不能與使用者空間直接互動資料,因為它們經由程序上下文與使用者空間建立連接配接。這也是為什麼中斷處理函數不能睡眠的第2個理由:排程器工作于程序之間,如果中斷處理函數睡眠并被排程出去,它們怎麼傳回到運作隊列呢? 4. 中斷處理函數一方面需要快速地出來,另一方面又需要完成它的工作。為了規避這種沖突,中斷處理函數通常被分成2個部分。瘦小的頂半部标志一個響應以宣稱它已經服務了該中斷,而重大的工作負載都被丢給了肥胖的底半部。底半部的執行被延後,在其執行環境中,所有的中斷都是使能的。在讨論softirq和tasklet的時候,你将學習到真也難怪開發底半部。 5. 中斷處理函數不必是可重用的。當某中斷被執行的時候,在它傳回之前,相應的IRQ都被禁止了。是以,與程序上下文代碼不同的是,同一中斷處理函數的不同執行個體不可能同時運作在多個處理器上。 6. 中斷處理函數可以被更高優先級IRQ的中斷處理函數打斷。如果你請求核心将你的中斷處理函數作為快中斷處理的話,此類中斷嵌套将被禁止。快中斷服務函數運作的時候,本處理器上的所有中斷都會被禁止。在禁止中斷或将你的中斷辨別為快中斷之前,請意識到中斷屏蔽對系統性能的壞處。中斷屏蔽的時間越長,中斷延遲就會更長,或者說已經被産生的中斷得到服務的延遲就會越久。中斷延遲與系統真實的響應時間成反比。 函數中可以檢查in_interrupt()的傳回值以檢視自身是否位于中斷上下文。 與外部硬體産生的異步中斷不一樣,也存在同步到達的中斷。同步中斷意味着它們不會不期而遇,它們由處理器本身執行某指令而産生。外部中斷和同步中斷在核心中使用相同的機制處理。 同步中斷的例子包括: (1)異常,被用于報告嚴重的運作時錯誤; (2)軟中斷,如int 0x80指令,被使用者實作x86體系結構上的系統調用。 配置設定 IRQ 号 裝置驅動必須将它們的IRQ号與一個中斷處理函數連接配接。是以,它們需要知道它們正在驅動的裝置的IRQ号。IRQ的配置設定可以很直接,也可能需要複雜的探測過程。在PC體系結構中,例如,定時器中斷被配置設定了IRQ 0,RTC中斷也是IRQ 8。現代的中斷技術(如PCI)足夠強大,它能夠響應對IRQ的查詢(系統啟動過程中由BIOS配置設定),PCI驅動能夠通路裝置配置空間的相應區域并獲得IRQ。對于較老的裝置,如基于工業标準體系結構(ISA)的卡而言,驅動也許不得不利用特定硬體的知識以探測和解析IRQ。 通過/proc/interrupts可以檢視系統中活動的IRQ的清單。 裝置執行個體:輥輪 現在你已經學習了中斷處理的基本知識,現在我們來實作一個輥輪裝置執行個體的中斷處理。在一些手機和PDA上能找到輥輪,它支援3種動作(順時針旋轉,逆時針旋轉和按鍵),可便利菜單導航。本例輥輪中的任何運作都會向處理器産生IRQ 7。通用目的I/O(GPIO)端口D的低3位與輥輪裝置連接配接。這些引腳上産生的波形與圖4.3中不同的輥輪運動一緻。中斷處理函數的工作是通過檢視端口D的GPIO資料寄存器解析出輥輪的運動。
驅動必須首先請求IRQ并将一個中斷處理函數與其綁定: #define ROLLER_IRQ 7 static irqreturn_t roller_interrupt(int irq, void *dev_id); if (request_irq(ROLLER_IRQ, roller_interrupt, IRQF_DISABLED | IRQF_TRIGGER_RISING, "roll", NULL)) { printk(KERN_ERR "Roll: Can't register IRQ %d/n", ROLLER_IRQ); return -EIO; } 我們看一下傳遞給request_irq()的參數,本例中沒有查詢或探測IRQ号,而是直接寫死為ROLLER_IRQ。第2個參數roller_interrupt()是中斷處理函數。中斷處理函數的原型的傳回值類型為irqreturn_t,如果中斷處理成功,則傳回IRQ_HANDLED,否則,傳回IRQ_NONE。對于PCI等I/O而言,該傳回值的意義更重要,因為多個裝置可能共享同一IRQ。 IRQF_DISABLED 标志意味着這個中斷處理為快中斷,是以,在調用該處理函數的時候,核心将禁止所有的中斷。IRQF_TRIGGER_RISING暗示輥輪将在中斷線上産生一個上升沿以發出中斷。換句話說,輥輪是一個邊沿觸發的裝置。有一些裝置是電平觸發的,在CPU服務其中斷之前,它一直将中斷線保持在一個電平上。使用IRQF_TRIGGER_HIGH或IRQF_TRIGGER_LOW可以辨別一個中斷為高/低電平觸發。該參數其他的可能值包括IRQF_SAMPLE_RANDOM(第5章《字元裝置驅動》的《僞字元裝置驅動》一節會用到)、IRQF_SHARED(定義這個IRQ被多個裝置共享)。 下一個參數"roll",用于辨別這個裝置,在/proc/interrupts等檔案中也會利用它産生資料。最後一個參數(本例中為NULL),僅在共享中斷的時候有用,用于區分共享同一IRQ線的每個裝置。 從2.6.19核心開始,中斷處理接口發生了一些變化。以前的中斷處理函數的第3個參數為struct pt_regs *,它指向存放CPU寄存器的位址,在2.6.19中已經移除。另外,IRQF_xxx型中斷标志取代了SA_xxx型中斷标志。例如,在較早的核心中,你應該使用SA_INTERRUPT而不是IRQF_DISABLED來将中斷處理辨別為快中斷處理。 驅動初始化的時候申請IRQ并不是太好,因為這樣會導緻甚至裝置未被使用的時候,有價值的資源也被占用。是以,裝置驅動通常在裝置被應用打開的時候申請IRQ。類似地,IRQ也在應用關閉裝置的時候釋放IRQ,而不是在退出驅動子產品的時候進行。使用下面的方法可以釋放一個IRQ: free_irq(int irq, void *dev_id); 清單4.1給出了輥輪中斷處理的實作。 roller_interrupt() 有2個參數,IRQ和裝置辨別符(傳遞給request_irq()的最後一個參數)。請對照圖4.3檢視清單4.1。 清單4.1 輥輪中斷處理 spinlock_t roller_lock = SPIN_LOCK_UNLOCKED; static DECLARE_WAIT_QUEUE_HEAD(roller_poll); static irqreturn_t roller_interrupt(int irq, void *dev_id) { int i, PA_t, PA_delta_t, movement = 0; PA_t = PORTD & 0x07; for (i=0; (PA_t==PA_delta_t); i++){ PA_delta_t = PORTD & 0x07; } movement = determine_movement(PA_t, PA_delta_t); spin_lock(&roller_lock); store_movements(movement); spin_unlock(&roller_lock); wake_up_interruptible(&roller_poll); return IRQ_HANDLED; } int determine_movement(int PA_t, int PA_delta_t) { switch (PA_t){ case 0: switch (PA_delta_t){ case 1: movement = ANTICLOCKWISE; break; case 2: movement = CLOCKWISE; break; case 4: movement = KEYPRESSED; break; } break; case 1: switch (PA_delta_t){ case 3: movement = ANTICLOCKWISE; break; case 0: movement = CLOCKWISE; break; } break; case 2: switch (PA_delta_t){ case 0: movement = ANTICLOCKWISE; break; case 3: movement = CLOCKWISE; break; } break; case 3: switch (PA_delta_t){ case 2: movement = ANTICLOCKWISE; break; case 1: movement = CLOCKWISE; break; } case 4: movement = KEYPRESSED; break; } } 驅動入口點(如read()和poll())尾随roller_interrupt()進行操作。例如,當中斷處理函數解析完一個輥輪運動後,它喚醒正在等待的poll()線程(可能已經因為X Windows等應用發起的select()系統調用而睡眠)。請在學習完第5章字元裝置驅動的知識後,重新檢視清單4.1并實作輥輪裝置的完整驅動。 第7章《輸入裝置驅動》的清單7.3利用了核心的輸入接口,将輥輪轉化為輥滑鼠。 在本節結束前,我們介紹一下使能和禁止特定IRQ的函數。enable_irq(ROLLER_IRQ)用于使能輥輪運動的中斷發生,disable_irq(ROLLER_IRQ)則進行相反的工作。disable_irq_nosync(ROLLER_IRQ)禁止輥輪中斷,并且不等待任何正在執行的roller_interrupt()執行個體的傳回。disable_irq()的非同步變體執行地更快,但是可能導緻潛在的競态。隻有在你确認沒有競争的盡快下,才可以這樣使用。drivers/ide/ide-io.c由一個使用disable_irq_nosync()的例子,在初始化過程中,它阻止了一些中斷,因為一些系統中可能在此方面存在問題。 軟中斷( Softirq ) 和 Tasklet 正如以前讨論的那樣,中斷處理有2個沖突的要求:它們需要完成大量的裝置資料處理,但是又不得不盡可能快地退出。為了擺脫這一困境,中斷處理過程被分成2部分:一個急切的且搶占的與硬體互動的頂半部,和一個在所有中斷都使能情況下并非十分急切的處理大量工作的底半部。如頂半部不一樣,底半部是同步的,因為核心決定了它什麼時候會執行它們。如下機制都可用于核心中延後一個工作到底半部執行:softirq、tasklet和工作隊列(work queue)。 Softirq 是一種基本的底半部機制,有較強的加鎖需求。僅僅在一些對性能敏感的子系統(如網絡層、SCSI層和核心定時器)中才會使用softirq。Tasklet建立在softirq之上,使用起來更簡單。除非有嚴格的可擴充性和速度要求,都建議使用Tasklet。Softirq和Tasklet的主要不同是前者是可重用的而後者則不需要。Softirq的不同執行個體可運作在不同的處理器上,而tasklet則不允許。 為了論證Softirq和Tasklet的用法,假定前例中的輥輪由存在由于運動部件導緻的潛在問題(如旋輪偶爾被卡住)進而導緻不同于spec的波形。一個被卡住的旋輪會不停地産生假的中斷,并可能使系統當機。為了解決這個問題,可以捕獲波形,進行一些分析,并在發現卡住的情況下動态地從中斷模式切換到輪詢模式,如果旋輪恢複正常,軟體也恢複到正常模式。我們在中斷處理函數中捕獲波形,并在底半部分析它。清單4.2和4.3分别用Softirq和Tasklet對此進行了實作。 它們都是清單4.1的簡化的變體,它們将中斷處理簡化為2個函數:從GPIO端口D捕獲波形的roller_capture()和對波形進行算術分析并按需切換到輪詢模式的roller_analyze()。 清單4.2 使用Softirq 分擔中斷處理的負載 void __init roller_init() { open_softirq(ROLLER_SOFT_IRQ, roller_analyze, NULL); } void roller_analyze() { } static irqreturn_t roller_interrupt(int irq, void *dev_id) { roller_capture(); raise_softirq(ROLLER_SOFT_IRQ); return IRQ_HANDLED; } 為了定義一個softirq,你必須在include/linux/interrupt.h中靜态地添加一個入口。你不能動态地定義softirq。raise_softirq()用于宣布相應的softirq需要被執行。核心會在下一個可獲得的機會裡執行它。可能發生在退出硬中斷處理函數的時候,也可能在ksoftirqd核心線程中。 清單4.3使用tasklet分擔中斷處理的負載 struct roller_device_struct { struct tasklet_struct tsklt; } void __init roller_init() { struct roller_device_struct *dev_struct; tasklet_init(&dev_struct->tsklt, roller_analyze, dev); } void roller_analyze() { } static irqreturn_t roller_interrupt(int irq, void *dev_id) { struct roller_device_struct *dev_struct; roller_capture(); tasklet_schedule(&dev_struct->tsklt); return IRQ_HANDLED; } tasklet_init() 用于動态地初始化一個tasklet,該函數不會為tasklet_struct配置設定記憶體,相反地,你必須将已經配置設定好的位址傳遞給它。tasklet_schedule()用于宣布相應的tasklet需要被執行。和中斷類似,核心提供了一系列用于控制在多處理器系統中tasklet執行狀态的函數: (1)tasklet_enable()使能tasklet; (2)tasklet_disable()禁止tasklet,并等待正在執行的tasklet退出; (3)tasklet_disable_nosync()的語義和disable_irq_nosync()相似,它并不等待正在執行的tasklet退出。 你已經看到了中斷處理函數和底半部的不同,但是,也有幾個相似點。中斷處理函數和tasklet都不需要可重用。而且,二者都不能睡眠。另外,中斷處理函數、tasklet和softirq都不能被搶占。 工作隊列是中斷處理延後執行的第3種方式。它們在程序上下文執行,允許睡眠,是以可以使用mutex這類可能導緻睡眠的函數。在前一章分析核心輔助接口的時候,我們已經讨論了工作隊列。表4.1對softirq、tasklet和工作隊列進行了對比分析。
清單4.4 使用workqueue分擔中斷處理的負載
#include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/interrupt.h> #include <linux/workqueue.h> static void intr_print(void* data) { printk("/n%s/n", (char*)data); } static struct work_struct works; static char list[20]; static int intr_init(void) { strlcpy(list, "Hello world", 20); INIT_WORK(&works, (void*)intr_print, (void*)list); schedule_work(&works); return 0; } static void intr_exit(void) { printk("intr_exit"); } module_init(intr_init); module_exit(intr_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Shakespeare"); |
work queue例子如上所示
表4.1 softirq、tasklet和工作隊列對比
Softirq | Tasklet | Work Queue | |
執行上下文 | 延後的工作,運作于中斷上下文 | 延後的工作,運作于中斷上下文 | 延後的工作,運作于程序上下文 |
可重用 | 可以在不同的 CPU 上同時運作 | 不能在不同的 CPU 上同時運作,但是不同的 CPU 可以運作不同的 tasklet | 可以在不同的 CPU 上同時運作 |
睡眠 | 不能睡眠 | 不能睡眠 | 可以睡眠 |
搶占 | 不能搶占 / 排程 | 不能搶占 / 排程 | 可以搶占 / 排程 |
易用性 | 不容易使用 | 容易使用 | 容易使用 |
何時使用 | 如果延後的工作不會睡眠,而且有嚴格的可擴充性或速度要求 | 如果延後的工作不會睡眠 | 如果延後的工作會睡眠 |
在LKML正在進行一項去除tasklet的可行性的讨論。Tasklet比程序上下文的代碼優先級更高,是以它們可能存在延遲問題。另外,你已經學習到,它們不允許睡眠,且隻能在同一CPU上執行。是以,有人提議将現存的tasklet基于其場景随機應變地轉換為softirq或工作隊列。