儘管有些設備僅僅通過控制其寄存器就可以得到控制,但現實中的大部分設備卻要比這複雜一些。因爲大部分設備的處理時間與處理器不在同一個週期,且一定會比處理器慢的多,這就造成了一種讓處理器等待設備的現象,顯然這是不行的,而有一種解決方法就是中斷操作。
中斷僅僅就是一個信號,當硬件需要獲得處理器對它的關注時,就可以發送這個信號。 Linux 處理中斷的方式非常類似在用戶空間處理信號的方式。大多數情況下,一個驅動只需要爲它的設備的中斷註冊一個處理例程,並當中斷到來時進行正確的處理。本質上來講,中斷處理例程和其他的代碼並行運行。因此,它們不可避免地引起併發問題,並競爭數據結構和硬件。 透徹地理解併發控制技術對中斷來講非常重要。
安裝中斷處理例程
中斷信號線
內核維護了一箇中斷信號線的註冊表,改註冊表類似於I/O端口的註冊表。模塊在使用中斷之前要先請求一箇中斷通道(或者中斷請求IRQ),然後在使用後釋放該通道。
<linux/sched.h> int request_irq(unsigned long irq, irqreturn_t (*handler)(int,void *,struct pt_regs *), unsigned long flags, const char *dev_name, void *dev_id); 表示註冊中斷,返回值: 0 指示成功,或返回一個負的錯誤碼,如 -EBUSY 表示另一個驅動已經佔用了你所請求的中斷線。 參數: unsigned int irq:要申請的中斷號 irqreturn_t (*handler)(int,void *,struct pt_regs *):要安裝的中斷處理函數的指針 unsigned long flags:與中斷管理相關的位掩碼選項 const char *dev_name:傳遞給request_irq的字符串,用來在/proc/interrupts顯示中斷的擁有者 void *dev_id:用於共享的中斷信號線,它是唯一的標識,在中斷線空閒時可以使用它,驅動程序也可以用它來指向自己的私有數據區(來標識哪個設備產生中斷)。若中斷沒有被共享,dev_id 可以設置爲 NULL,但推薦用它指向設備的數據結構。 flags: SA_INTERRUPT :快速中斷標誌。快速中斷處理例程運行在當前處理器禁止中斷的狀態下。SA_SHIRQ : 在設備間共享中斷標誌。SA_SAMPLE_RANDOM :該位表示產生的中斷能對 /dev/random 和 /dev/urandom 使用的熵池(entropy pool)有貢獻。 讀取這些設備會返回真正的隨機數,從而有助於應用程序軟件選擇用於加密的安全密鑰。 若設備以真正隨機的週期產生中斷,就應當設置這個標誌。若設備中斷是可預測的,這個標誌不值得設置。可能被攻擊者影響的設備不應當設置這個標誌。更多信息看 drivers/char/random.c 的註釋。void free_irq(unsigned int irq,void *dev_id); 釋放中斷 |
中斷處理例程可在驅動初始化時或在設備第一次打開時安裝。推薦在設備第一次打開、硬件被告知產生中斷前時申請中斷,因爲可以共享有限的中斷資源。這樣調用 free_irq 的位置是設備最後一次被關閉、硬件被告知不用再中斷處理器之後。但這種方式的缺點是必須爲每個設備維護一個打開計數。
以下是中斷申請的示例(並口):
|
i386 和 x86_64 體系定義了一個函數來查詢一箇中斷線是否可用:
|
x86中斷處理內幕
這個描述是從 2.6 內核 arch/i386/kernel/irq.c, arch/i386/kernel/ apic.c, arch/i386/kernel/entry.S, arch/i386/kernel/i8259.c, 和 include/asm-i386/hw_irq.h 中得出,儘管基本概念相同,硬件細節與其他平臺上不同。
底層中斷處理代碼在彙編語言文件 entry.S。在所有情況下,這個代碼將中斷號壓棧並且跳轉到一個公共段,公共段會調用 do_IRQ(在 irq.c 中定義)。do_IRQ 做的第一件事是應答中斷以便中斷控制器能夠繼續其他事情。它接着獲取給定 IRQ 號的一個自旋鎖,阻止其他 CPU 處理這個 IRQ,然後清除幾個狀態位(包括IRQ_WAITING )然後查找這個 IRQ 的處理例程。若沒有找到,什麼也不做;釋放自旋鎖,處理任何待處理的軟件中斷,最後 do_IRQ 返回。從中斷中返回的最後一件事可能是一次處理器的重新調度。
IRQ的探測是通過爲每個缺乏處理例程的IRQ設置 IRQ_WAITING 狀態位來完成。當中斷髮生, 因爲沒有註冊處理例程,do_IRQ 清除這個位並且接着返回。 當probe_irq_off被一個函數調用,只需搜索沒有設置 IRQ_WAITING 的 IRQ。
快速和慢速處理例程
快速中斷是那些能夠很快處理的中斷,而處理慢速中斷會花費更長的時間。在處理慢速中斷時處理器重新使能中斷,避免快速中斷被延時過長。在現代內核中,快速和慢速中斷的區別已經消失,剩下的只有一個:快速中斷(使用 SA_INTERRUPT )執行時禁止所有在當前處理器上的其他中斷。注意:其他的處理器仍然能夠處理中斷。
除非你充足的理由在禁止其他中斷情況下來運行中斷處理例程,否則不應當使用SA_INTERRUPT.
/proc接口
當硬件中斷到達處理器時, 內核提供的一個內部計數器會遞增,產生的中斷報告顯示在文件 /proc/interrupts中。這一方法可以用來檢查設備是否按預期地工作。此文件只顯示當前已安裝處理例程的中斷的計數。若以前request_irq的一箇中斷,現在已經free_irq了,那麼就不會顯示在這個文件中,但是它可以顯示終端共享的情況。
/proc/stat記錄了幾個關於系統活動的底層統計信息, 包括(但不僅限於)自系統啓動以來收到的中斷數。 stat 的每一行以一個字符串開始, 是該行的關鍵詞:intr 標誌是中斷計數。第一個數是所有中斷的總數, 而其他每一個代表一個單獨的中斷線的計數, 從中斷 0 開始(包括當前沒有安裝處理例程的中斷),無法顯示終端共享的情況。
以上兩個文件的一個不同是:/proc/interrupts幾乎不依賴體系,而/proc/stat的字段數依賴內核下的硬件中斷,其定義在<asm/irq.h>中。ARM的定義爲:
|
自動檢測 IRQ 號
驅動初始化時最迫切的問題之一是決定設備要使用的IRQ 線,驅動需要信息來正確安裝處理例程。自動檢測中斷號對驅動的可用性來說是一個基本需求。有時自動探測依賴一些設備具有的默認特性,以下是典型的並口中斷探測程序:
|
有的驅動允許用戶在加載時覆蓋默認值:
|
當目標設備有能力告知驅動它要使用的中斷號時,自動探測中斷號只是意味着探測設備,無需做額外的工作探測中斷。
但不是每個設備都對程序員友好,對於他們還是需要一些探測工作。這個工作技術上非常簡單: 驅動告知設備產生中斷並且觀察發生了什麼。如果一切順利,則只有一箇中斷信號線被激活。儘管探l測在理論上簡單,但實現可能不簡單。有 2 種方法來進行探測中斷:調用內核定義的輔助函數和DIY探測。
內核幫助下的探測:
linux提供了一個底層設施來探測中斷號。它只能在非共享中斷的模式下工作,但是大多數硬件有能力工作在共享中斷的模式下,並可提供更好的找到配置中斷號的方法,內核提供的這一設施由兩個函數組成,在頭文件<linux/interrupt.h>中聲明:
<linux/interrupt.h> unsigned long probe_irq_on(void); /*返回一個未分配中斷的位掩碼。驅動必須保留返回的位掩碼,並在後邊傳遞給probe_irq_off,在調用它之後,驅動程序應當至少安排它的設備產生一次中斷*/ int probe_irq_off(unsigned long); /*在請求設備產生一次中斷後,驅動調用這個函數,並將probe_irq_on產生的位掩碼作爲參數傳遞給probe_irq_off,probe_irq_off返回在probe_on之後發生的中斷號。如果沒有中斷髮生,返回0,如果產生了多次中斷,返回一個負值。*/ |
程序員應當注意在調用 probe_irq_on 之後啓用設備上的中斷, 並在調用 probe_irq_off 前禁用。此外還必須記住在 probe_irq_off 之後服務設備中待處理的中斷。
以下是LDD3中的並口示例代碼,(並口的管腳 9 和 10 連接在一起,探測五次失敗後放棄):
|
最好只在模塊初始化時探測中斷線一次。
大部分體系定義了這兩個函數( 即便是空的 )來簡化設備驅動的移植。
DIY探測:
DIY探測與前面原理相同: 使能所有未使用的中斷, 接着等待並觀察發生什麼。我們對設備的瞭解:通常一個設備能夠使用3或4個IRQ 號中的一個來進行配置,只探測這些 IRQ 號使我們能不必測試所有可能的中斷就探測到正確的IRQ 號。
下面的LDD3中的代碼通過測試所有"可能的"中斷並且察看發生的事情來探測中斷。 trials 數組列出要嘗試的中斷, 以 0 作爲結尾標誌; tried 數組用來跟蹤哪個中斷號已經被這個驅動註冊。
|
以下是handler的源碼:
|
若事先不知道"可能的" IRQ ,就需要探測所有空閒的中斷,所以不得不從 IRQ 0 探測到 IRQ NR_IRQS-1
實現中斷處理例程
1.中斷處理例程是在中斷時間內運行的,因此行爲會受到一些限制。這些限制跟我們在內核定時器中看到的一樣。
處理例程不能向用戶空間發送或者接收數據,因爲它不是在任何進程上下文中執行的
處理例程也不能做任何可能發生休眠的操作,例如調用wait_event,使用不帶GFP_ATOMIC標誌的內存分配操作,或者鎖住一個信號量等等
處理例程不能調用schdule函數
2.中斷處理理財的功能就是將有關中斷接收的信息反饋給設備,並根據正在服務的終端的不同含義對對數據進行相應的讀或寫。中斷處理例程第一步常常包括清除設備的一箇中斷標誌位,大部分硬件設備在清除"中斷掛起"位前不會再產生中斷。這也要根據硬件的工作原理決定, 這一步也可能需要在最後做而不是開始; 這裏沒有通用的規則。一些設備不需要這步, 因爲它們沒有一個"中斷掛起"位; 這樣的設備是少數。
3.中斷處理的一個典型 任務:如果中斷通知進程所等待的事件已經發生,比如新的數據已經到達,就會喚醒在設備上休眠的進程。
不管是快速或慢速處理例程,程序員應編寫執行時間儘可能短的處理例程。 如果需要進行長時間計算, 最好的方法是使用 tasklet 或者 workqueue 在一個更安全的時間來調度計算任務。
啓用和禁止中斷
有時設備驅動必須在一段時間(希望較短)內阻塞中斷髮生。並必須在持有一個自旋鎖時阻塞中斷,以避免死鎖系統。注意:應儘量少禁止中斷,即使是在設備驅動中,且這個技術不應當用於驅動中的互斥機制。
有時(但是很少!)一個驅動需要禁止一個特定中斷。但不推薦這樣做,特別是不能禁止共享中斷(在現代系統中, 共享的中斷是很常見的)。內核提供了 3 個函數,是內核 API 的一部分,聲明在 <asm/irq.h>:
|
在 2.6 內核, 可使用下面 2 個函數中的任一個(定義在 <asm/system.h>)關閉當前處理器上所有中斷:
|
頂半部和底半部
中斷處理需要很快完成,並且不需要阻塞太長,所以中斷處理的一個主要問題就是中斷處理例程中完成耗時的任務。
linux通過將中斷處理分成兩部分來完成這個任務:
1.頂半部:實際響應中斷的例程(request_irq註冊的那個例程)
2.底半部:被頂半部調用並在稍後更安全的一個時間裏執行的函數。
他們最大的不同在底半部處理例程執行時,所有中斷都是打開的(這就是所謂的在更安全的時間內運行)。典型的情況是:頂半部保存設備數據到一個設備特定的緩存並調度它的底半部,最後退出:這個操作非常快。底半部接着進行任何其他需要的工作。這種方式的好處是在底半部工作期間,頂半部仍然可以繼續爲新中斷服務。
Linux 內核有 2 個不同的機制可用來實現底半部處理:
(1) tasklet (首選機制),它非常快, 但是所有的 tasklet 代碼必須是原子的;
(2)工作隊列,它可能有更高的延時,但允許休眠。
tasklet和工作隊列在《時間、延遲及延緩操作》已經介紹過,具體的實現代碼請看實驗源碼!
中斷共享
Linux內核支持在所有總線上中斷共享。
安裝共享的處理例程
通過 request_irq來安裝共享中斷與非共享中斷有2點不同:
(1)當request_irq時,flags中必須指定SA_SHIRQ位;
(2)dev_id必須唯一。任何指向模塊地址空間的指針都行,但dev_id絕不能設置爲NULL。
內核爲每個中斷維護一箇中斷共享處理例程列表,dev_id就是區別不同處理例程的簽名。釋放處理例程通過執行free_irq實現。 dev_id用來從這個中斷的共享處理例程列表中選擇正確的處理例程來釋放,這就是爲什麼dev_id必須是唯一的.
請求一個共享的中斷時,如果滿足下列條件之一,則request_irq成功:
(1)中斷線空閒;
(2)所有已經註冊該中斷信號線的處理例程也標識了IRQ是共享。
一個共享的處理例程必須能夠識別自己的中斷,並且在自己的設備沒有被中斷時快速退出(返回IRQ_NONE)。
共享處理例程沒有探測函數可用,但使用的中斷信號線是空閒時標準的探測機制纔有效。
一個使用共享處理例程的驅動需要小心:不能使用enable_irq或disable_irq,否則,對其他共享這條線的設備就無法正常工作了。即便短時間禁止中斷,另一設備也可能產生延時而爲設備和其用戶帶來問題。所以程序員必須記住:他的驅動並不是獨佔這個IRQ,它的行爲應當比獨佔這個中斷線更加"社會化"。
中斷驅動的I/O
當與驅動程序管理的硬件間的數據傳送可能因爲某種原因而延遲,驅動編寫者應當實現緩存。一個好的緩存機制需採用中斷驅動的I/O,一個輸入緩存在中斷時被填充,並由讀取設備的進程取走緩衝區的數據,一個輸出緩存由寫設備的進程填充,並在中斷時送出數據。
爲正確進行中斷驅動的數據傳送,硬件應能夠按照下列語義產生中斷:
輸入:當新數據到達時並處理器準備好接受時,設備中斷處理器。
輸出:當設備準備好接受新數據或確認一個成功的數據傳送時,設備產生中斷。