- 論壇徽章:
- 0
|
第 10 章 中斷處理
盡管一些設(shè)備可只使用它們的 I/O 區(qū)來控制, 大部分真實的設(shè)備比那個要復(fù)雜點. 設(shè)備不得不和外部世界打交道, 常常包括諸如旋轉(zhuǎn)的磁盤, 移動的磁帶, 連到遠處的線纜, 等等. 很多必須在一個時間片中完成, 不同于, 并且遠慢于處理器. 因為幾乎一直是不希望使處理器等待外部事件, 對于設(shè)備必須有一種方法使處理器知道有事情發(fā)生了.
當(dāng)然, 那種方法是中斷. 一個中斷不過是一個硬件在它需要處理器的注意時能夠發(fā)出的信號. Linux 處理中斷非常類似它處理用戶空間信號的方式. 對大部分來說, 一個驅(qū)動只需要為它的設(shè)備中斷注冊一個處理函數(shù), 并且當(dāng)它們到來時正確處理它們. 當(dāng)然, 在這個簡單圖像之下有一些復(fù)雜; 特別地, 中斷處理有些受限于它們能夠進行的動作, 這是它們?nèi)绾芜\行而導(dǎo)致的結(jié)果.
沒有一個真實的硬件設(shè)備來產(chǎn)生中斷, 就難演示中斷的使用. 因此, 本章使用的例子代碼使用并口工作. 這些端口在現(xiàn)代硬件上開始變得稀少, 但是, 運氣地, 大部分人仍然能夠有一個有可用的端口的系統(tǒng). 我們將使用來自上一章的簡短模塊; 添加一小部分它能夠產(chǎn)生并處理來自并口的中斷. 模塊的名子, short, 實際上意味著 short int ( 它是 C, 對不?), 來提醒我們它處理中斷.
但是, 在我們進入主題之前, 是時候提出一個注意事項. 中斷處理, 由于它們的特性, 與其他的代碼并行地運行. 因此, 它們不可避免地引起并發(fā)問題和對數(shù)據(jù)結(jié)構(gòu)和硬件的競爭. 如果你屈服于誘惑以越過第 5 章的討論, 我們理解. 但是我們也建議你轉(zhuǎn)回去并且現(xiàn)在看一下. 一個堅實的并發(fā)控制技術(shù)的理解是重要的, 在使用中斷時.
10.1. 準(zhǔn)備并口
盡管并口簡單, 它能夠觸發(fā)中斷. 這個能力被打印機用來通知 lp 驅(qū)動它準(zhǔn)備好接收緩存中的下一個字符.
如同大部分設(shè)備, 并口實際上不產(chǎn)生中斷, 在它被指示這樣作之前; 并口標(biāo)準(zhǔn)規(guī)定設(shè)置 port 2 (0x37a, 0x27a, 或者任何)的 bit 4 就使能中斷報告. short 在模塊初始化時進行一個簡單的 outb 調(diào)用來設(shè)置這個位.
一旦中斷使能, 任何時候在管腳 10 (所謂的 ACK 位)上的電信號從低變到高, 并口產(chǎn)生一個中斷. 最簡單的方法來強制接口產(chǎn)生中斷( 沒有掛一個打印機到端口 )是連接并口連接器的管腳 9 和 管腳 10. 一根短線, 插到你的系統(tǒng)后面的并口連接器的合適的孔中, 就建立這個連接. 并口外面的管腳圖示于圖
并口的管腳
管腳 9 是并口數(shù)據(jù)字節(jié)的最高位. 如果你寫二進制數(shù)據(jù)到 /dev/short0, 你產(chǎn)生幾個中斷. 然而, 寫 ASCII 文本到這個端口不會產(chǎn)生任何中斷, 因為 ASCII 字符集沒有最高位置位的項.
如果你寧愿避免連接管腳到一起, 而你手上確實有一臺打印機, 你可用使用一個真正的打印機來運行例子中斷處理, 如同下面展示的. 但是, 注意我們介紹的探測函數(shù)依賴管腳 9 和管腳 10 之間的跳線在位置上, 并且你需要它使用你的代碼來試驗探測.
10.2. 安裝一個中斷處理
如果你想實際地"看到"產(chǎn)生的中斷, 向硬件設(shè)備寫不足夠; 一個軟件處理必須在系統(tǒng)中配置. 如果 Linux 內(nèi)核還沒有被告知來期待你的中斷, 它簡單地確認并忽略它.
中斷線是一個寶貴且常常有限的資源, 特別當(dāng)它們只有 15 或者 16 個時. 內(nèi)核保持了中斷線的一個注冊, 類似于 I/O 端口的注冊. 一個模塊被希望來請求一個中斷通道(或者 IRQ, 對于中斷請求), 在使用它之前, 并且當(dāng)結(jié)束時釋放它. 在很多情況下, 也希望模塊能夠與其他驅(qū)動共享中斷線, 如同我們將看到的. 下面的函數(shù), 聲明在 , 實現(xiàn)中斷注冊接口:
int request_irq(unsigned int irq,
irqreturn_t (*handler)(int, void *, struct pt_regs *),
unsigned long flags,
const char *dev_name,
void *dev_id);
void free_irq(unsigned int irq, void *dev_id);
從 request_irq 返回給請求函數(shù)的返回值或者是 0 指示成功, 或者是一個負的錯誤碼, 如同平常. 函數(shù)返回 -EBUSY 來指示另一個驅(qū)動已經(jīng)使用請求的中斷線是不尋常的. 函數(shù)的參數(shù)如下:
unsigned int irq
請求的中斷號
irqreturn_t (*handler)
安裝的處理函數(shù)指針. 我們在本章后面討論給這個函數(shù)的參數(shù)以及它的返回值.
unsigned long flags
如你會希望的, 一個與中斷管理相關(guān)的選項的位掩碼(后面描述).
const char *dev_name
這個傳遞給 request_irq 的字串用在 /proc/interrupts 來顯示中斷的擁有者(下一節(jié)看到)
void *dev_id
用作共享中斷線的指針. 它是一個獨特的標(biāo)識, 用在當(dāng)釋放中斷線時以及可能還被驅(qū)動用來指向它自己的私有數(shù)據(jù)區(qū)(來標(biāo)識哪個設(shè)備在中斷). 如果中斷沒有被共享, dev_id 可以設(shè)置為 NULL, 但是使用這個項指向設(shè)備結(jié)構(gòu)不管如何是個好主意. 我們將在"實現(xiàn)一個處理"一節(jié)中看到 dev_id 的一個實際應(yīng)用.
flags 中可以設(shè)置的位如下:
SA_INTERRUPT
當(dāng)置位了, 這表示一個"快速"中斷處理. 快速處理在當(dāng)前處理器上禁止中斷來執(zhí)行(這個主題在"快速和慢速處理"一節(jié)涉及).
SA_SHIRQ
這個位表示中斷可以在設(shè)備間共享. 共享的概念在"中斷共享"一節(jié)中略述.
SA_SAMPLE_RANDOM
這個位表示產(chǎn)生的中斷能夠有貢獻給 /dev/random 和 /dev/urandom 使用的加密池. 這些設(shè)備在讀取時返回真正的隨機數(shù)并且設(shè)計來幫助應(yīng)用程序軟件為加密選擇安全鑰. 這樣的隨機數(shù)從一個由各種隨機事件貢獻的加密池中提取的. 如果你的設(shè)備以真正隨機的時間產(chǎn)生中斷, 你應(yīng)當(dāng)設(shè)置這個標(biāo)志. 如果, 另一方面, 你的中斷是可預(yù)測的( 例如, 一個幀抓取器的場消隱), 這個標(biāo)志不值得設(shè)置 -- 它無論如何不會對系統(tǒng)加密有貢獻. 可能被攻擊者影響的設(shè)備不應(yīng)當(dāng)設(shè)置這個標(biāo)志; 例如, 網(wǎng)絡(luò)驅(qū)動易遭受從外部計時的可預(yù)測報文并且不應(yīng)當(dāng)對加密池有貢獻. 更多信息看 drivers/char/random.c 的注釋.
中斷處理可以在驅(qū)動初始化時安裝或者在設(shè)備第一次打開時. 盡管從模塊的初始化函數(shù)中安裝中斷處理可能聽來是個好主意, 它常常不是, 特別當(dāng)你的設(shè)備不共享中斷. 因為中斷線數(shù)目是有限的, 你不想浪費它們. 你可以輕易使你的系統(tǒng)中設(shè)備數(shù)多于中斷數(shù).如果一個模塊在初始化時請求一個 IRQ, 它阻止了任何其他的驅(qū)動使用這個中斷, 甚至這個持有它的設(shè)備從不被使用. 在設(shè)備打開時請求中斷, 另一方面, 允許某些共享資源.
例如, 可能與一個 modem 在同一個中斷上運行一個幀抓取器, 只要你不同時使用這 2 個設(shè)備. 對用戶來說是很普通的在系統(tǒng)啟動時為一個特殊設(shè)備加載模塊, 甚至這個設(shè)備很少用到. 一個數(shù)據(jù)獲取技巧可能使用同一個中斷作為第 2 個串口. 雖然不是太難避免在數(shù)據(jù)獲取時聯(lián)入你的互聯(lián)網(wǎng)服務(wù)提供商(ISP), 被迫卸載一個模塊為了使用 modem 確實令人不快.
調(diào)用 request_irq 的正確位置是當(dāng)設(shè)備第一次打開時, 在硬件被指示來產(chǎn)生中斷前. 調(diào)用 free_irq 的位置是設(shè)備最后一次被關(guān)閉時, 在硬件被告知不要再中斷處理器之后. 這個技術(shù)的缺點是你需要保持一個每設(shè)備的打開計數(shù), 以便于你知道什么時候中斷可以被禁止.
盡管這個討論, short 還在加載時請求它的中斷線. 這樣做是為了你可以運行測試程序而不必運行一個額外的進程來保持設(shè)備打開. short, 因此, 從它的初始化函數(shù)( short_init )請求中斷, 不是在 short_open 中做, 象一個真實設(shè)備驅(qū)動.
下面代碼請求的中斷是 short_irq. 變量的真正賦值(即, 決定使用哪個 IRQ )在后面顯示, 因為它和現(xiàn)在的討論無關(guān). short_base 是使用的并口 I/O 基地址; 接口的寄存器 2 被寫入來使能中斷報告.
if (short_irq >= 0)
{
result = request_irq(short_irq, short_interrupt,
SA_INTERRUPT, "short", NULL);
if (result) {
printk(KERN_INFO "short: can't get assigned irq %i\n",
short_irq);
short_irq = -1;
} else { /* actually enable it -- assume this *is* a parallel port */
outb(0x10,short_base+2);
}
}
代碼顯示, 安裝的處理是一個快速處理(SA_INTERRUPT), 不支持中斷共享(SA_SHIRQ 沒有), 并且不對系統(tǒng)加密有貢獻(SA_SAMPLE_RANDOM 也沒有). outb 調(diào)用接著為并口使能中斷報告.
由于某些合理原因, i386 和 x86_64 體系定義了一個函數(shù)來詢問一個中斷線的能力:
int can_request_irq(unsigned int irq, unsigned long flags);
這個函數(shù)當(dāng)試圖分配一個給定中斷成功時返回一個非零值. 但是, 注意, 在 can_request_irq 和 request_irq 的調(diào)用之間事情可能一直改變.
10.2.1. /proc 接口
無論何時一個硬件中斷到達處理器, 一個內(nèi)部的計數(shù)器遞增, 提供了一個方法來檢查設(shè)備是否如希望地工作. 報告的中斷顯示在 /proc/interrupts. 下面的快照取自一個雙處理器 Pentium 系統(tǒng):
root@montalcino:/bike/corbet/write/ldd3/src/short# m /proc/interrupts
CPU0 CPU1
0: 4848108 34 IO-APIC-edge timer
2: 0 0 XT-PIC cascade
8: 3 1 IO-APIC-edge rtc
10: 4335 1 IO-APIC-level aic7xxx
11: 8903 0 IO-APIC-level uhci_hcd
12: 49 1 IO-APIC-edge i8042
NMI: 0 0
LOC: 4848187 4848186
ERR: 0
MIS: 0
第一列是 IRQ 號. 你能夠從沒有的 IRQ 中看到這個文件只顯示對應(yīng)已安裝處理的中斷. 例如, 第一個串口(使用中斷號 4)沒有顯示, 指示 modem 沒在使用. 事實上, 即便如果 modem 已更早使用了, 但是在這個快照時間沒有使用, 它不會顯示在這個文件中; 串口表現(xiàn)很好并且在設(shè)備關(guān)閉時釋放它們的中斷處理.
/proc/interrupts 的顯示展示了有多少中斷硬件遞交給系統(tǒng)中的每個 CPU. 如同你可從輸出看到的, Linux 內(nèi)核常常在第一個 CPU 上處理中斷, 作為一個使 cache 局部性最大化的方法.[
37
] 最后 2 列給出關(guān)于處理中斷的可編程中斷控制器的信息(驅(qū)動編寫者不必關(guān)心), 以及已注冊的中斷處理的設(shè)備的名子(如同在給 request_irq 的參數(shù) dev_name 中指定的).
/proc 樹包含另一個中斷有關(guān)的文件, /proc/stat; 有時你會發(fā)現(xiàn)一個文件更加有用并且有時你會喜歡另一個. /proc/stat 記錄了幾個關(guān)于系統(tǒng)活動的低級統(tǒng)計量, 包括(但是不限于)自系統(tǒng)啟動以來收到的中斷數(shù). stat 的每一行以一個文本字串開始, 是該行的關(guān)鍵詞; intr 標(biāo)志是我們在找的. 下列(截短了)快照是在前一個后馬上取得的:
intr 5167833 5154006 2 0 2 4907 0 2 68 4 0 4406 9291 50 0 0
第一個數(shù)是所有中斷的總數(shù), 而其他每一個代表一個單個 IRQ 線, 從中斷 0 開始. 所有的計數(shù)跨系統(tǒng)中所有處理器而匯總的. 這個快照顯示, 中斷號 4 已使用 4907 次, 盡管當(dāng)前沒有安裝處理. 如果你在測試的驅(qū)動請求并釋放中斷在每個打開和關(guān)閉循環(huán), 你可能發(fā)現(xiàn) /proc/stat 比 /proc/interrupts 更加有用.
2 個文件的另一個不同是, 中斷不是體系依賴的(也許, 除了末尾幾行), 而 stat 是; 字段數(shù)依賴內(nèi)核之下的硬件. 可用的中斷數(shù)目少到在 SPARC 上的 15 個, 多到 IA-64 上的 256個, 并且其他幾個系統(tǒng)都不同. 有趣的是要注意, 定義在 x86 中的中斷數(shù)當(dāng)前是 224, 不是你可能期望的 16; 如同在 include/asm-i386/irq.h 中解釋的, 這依賴 Linux 使用體系的限制, 而不是一個特定實現(xiàn)的限制( 例如老式 PC 中斷控制器的 16 個中斷源).
下面是一個 /proc/interrupts 的快照, 取自一臺 IA-64 系統(tǒng). 如你所見, 除了不同硬件的通用中斷源的路由, 輸出非常類似于前面展示的 32-位 系統(tǒng)的輸出.
CPU0 CPU1
27: 1705 34141 IO-SAPIC-level qla1280
40: 0 0 SAPIC perfmon
43: 913 6960 IO-SAPIC-level eth0
47: 26722 146 IO-SAPIC-level usb-uhci
64: 3 6 IO-SAPIC-edge ide0
80: 4 2 IO-SAPIC-edge keyboard
89: 0 0 IO-SAPIC-edge PS/2 Mouse
239: 5606341 5606052 SAPIC timer
254: 67575 52815 SAPIC IPI
NMI: 0 0
ERR: 0
10.2.2. 自動檢測 IRQ 號
驅(qū)動在初始化時最有挑戰(zhàn)性的問題中的一個是如何決定設(shè)備要使用哪個 IRQ 線. 驅(qū)動需要信息來正確安裝處理. 盡管程序員可用請求用戶在加載時指定中斷號, 這是個壞做法, 因為大部分時間用戶不知道這個號, 要么因為他不配置跳線要么因為設(shè)備是無跳線的. 大部分用戶希望他們的硬件"僅僅工作"并且不感興趣如中斷號的問題. 因此自動檢測中斷號是一個驅(qū)動可用性的基本需求.
有時自動探測依賴知道一些設(shè)備有很少改變的缺省動作的特性. 在這個情況下, 驅(qū)動可能假設(shè)缺省值適用. 這確切地就是 short 如何缺省對并口動作的. 實現(xiàn)是直接的, 如 short 自身顯示的:
if (short_irq
代碼根據(jù)選擇的 I/O 基地址賦值中斷號, 而允許用戶在加載時覆蓋缺省值, 使用如:
insmod ./short.ko irq=x
short_base defaults to 0x378, so short_irq defaults to 7.
有些設(shè)備設(shè)計得更高級并且簡單地"宣布"它們要使用的中斷. 在這個情況下, 驅(qū)動獲取中斷號通過從設(shè)備的一個 I/O 端口或者 PCI 配置空間讀一個狀態(tài)字節(jié). 當(dāng)目標(biāo)設(shè)備是一個有能力告知驅(qū)動它要使用哪個中斷的設(shè)備時, 自動探測中斷號只是意味著探測設(shè)備, 探測中斷沒有其他工作要做. 幸運的是大部分現(xiàn)代硬件這樣工作; 例如, PCI 標(biāo)準(zhǔn)解決了這個問題通過要求外設(shè)來聲明它們要使用哪個中斷線. PCI 標(biāo)準(zhǔn)在 12 章討論.
不幸的是, 不是每個設(shè)備是對程序員友好的, 并且自動探測可能需要一些探測. 這個技術(shù)非常簡單: 驅(qū)動告知設(shè)備產(chǎn)生中斷并且觀察發(fā)生了什么. 如果所有事情進展地好, 只有一個中斷線被激活.
盡管探測在理論上簡單的, 實際的實現(xiàn)可能不清晰. 我們看 2 種方法來進行這個任務(wù): 調(diào)用內(nèi)核定義的幫助函數(shù)和實現(xiàn)我們自己的版本.
10.2.2.1. 內(nèi)核協(xié)助的探測
Linux 內(nèi)核提供了一個低級設(shè)施來探測中斷號. 它只為非共享中斷, 但是大部分能夠在共享中斷狀態(tài)工作的硬件提供了更好的方法來盡量發(fā)現(xiàn)配置的中斷號.這個設(shè)施包括 2 個函數(shù), 在 中聲明( 也描述了探測機制 ).
unsigned long probe_irq_on(void);
這個函數(shù)返回一個未安排的中斷的位掩碼. 驅(qū)動必須保留返回的位掩碼, 并且在后面?zhèn)鬟f給 probe_irq_off. 在這個調(diào)用之后, 驅(qū)動應(yīng)當(dāng)安排它的設(shè)備產(chǎn)生至少一次中斷.
int probe_irq_off(unsigned long);
在設(shè)備已請求一個中斷后, 驅(qū)動調(diào)用這個函數(shù), 作為參數(shù)傳遞之前由 probe_irq_on 返回的位掩碼. probe_irq_off 返回在"probe_on"之后發(fā)出的中斷號. 如果沒有中斷發(fā)生, 返回 0 (因此, IRQ 0 不能探測, 但是沒有用戶設(shè)備能夠在任何支持的體系上使用它). 如果多于一個中斷發(fā)生( 模糊的探測 ), probe_irq_off 返回一個負值.
程序員應(yīng)當(dāng)小心使能設(shè)備上的中斷, 在調(diào)用 probe_irq_on 之后以及在調(diào)用 probe_irq_off 后禁止它們. 另外, 你必須記住服務(wù)你的設(shè)備中掛起的中斷, 在 probe_irq_off 之后.
short 模塊演示了如何使用這樣的探測. 如果你加載模塊使用 probe=1, 下列代碼被執(zhí)行來探測你的中斷線, 如果并口連接器的管腳 9 和 10 連接在一起:
int count = 0;
do
{
unsigned long mask;
mask = probe_irq_on();
outb_p(0x10,short_base+2); /* enable reporting */
outb_p(0x00,short_base); /* clear the bit */
outb_p(0xFF,short_base); /* set the bit: interrupt! */
outb_p(0x00,short_base+2); /* disable reporting */
udelay(5); /* give it some time */
short_irq = probe_irq_off(mask);
if (short_irq == 0) { /* none of them? */
printk(KERN_INFO "short: no irq reported by probe\n");
short_irq = -1;
}
/*
* if more than one line has been activated, the result is
* negative. We should service the interrupt (no need for lpt port)
* and loop over again. Loop at most five times, then give up
*/
} while (short_irq
注意 udelay 的使用, 在調(diào)用 probe_irq_off 之前. 依賴你的處理器的速度, 你可能不得不等待一小段時間來給中斷時間來真正被遞交.
探測可能是一個長時間的任務(wù). 雖然對于 short 這不是真的, 例如, 探測一個幀抓取器, 需要一個至少 20 ms 的延時( 對處理器是一個時代 ), 并且其他的設(shè)備可能要更長. 因此, 最好只探測中斷線一次, 在模塊初始化時, 獨立于你是否在設(shè)備打開時安裝處理(如同你應(yīng)當(dāng)做的), 或者在初始化函數(shù)當(dāng)中(這個不推薦).
有趣的是注意在一些平臺上(PoweerPC, M68K, 大部分 MIPS 實現(xiàn), 以及 2 個 SPARC 版本)探測是不必要的, 并且, 因此, 之前的函數(shù)只是空的占位者, 有時稱為"無用的 ISA 廢話". 在其他平臺上, 探測只為 ISA 設(shè)備實現(xiàn). 無論如何, 大部分體系定義了函數(shù)( 即便它們是空的 )來簡化移植現(xiàn)存的設(shè)備驅(qū)動.
10.2.2.2. Do-it-yourself 探測
探測也可以在驅(qū)動自身實現(xiàn)沒有太大麻煩. 它是一個少有的驅(qū)動必須實現(xiàn)它自己的探測, 但是看它是如何工作的能夠給出對這個過程的內(nèi)部認識. 為此目的, short 模塊進行 do-it-yourself 的 IRQ 線探測, 如果它使用 probe=2 加載.
這個機制與前面描述的相同: 使能所有未使用的中斷, 接著等待并觀察發(fā)生什么. 我們能夠, 然而, 利用我們對設(shè)備的知識. 常常地一個設(shè)備能夠配置為使用一個 IRQ 號從 3 個或者 4 個一套; 只探測這些 IRQ 使我們能夠探測正確的一個, 不必測試所有的可能中斷.
short 實現(xiàn)假定 3, 5, 7, 和 9 是唯一可能的 IRQ 值. 這些數(shù)實際上是一些并口設(shè)備允許你選擇的數(shù).
下面的代碼通過測試所有"可能的"中斷并且查看發(fā)生的事情來探測中斷. trials 數(shù)組列出要嘗試的中斷, 以 0 作為結(jié)尾標(biāo)志; tried 數(shù)組用來跟蹤哪個處理實際上被這個驅(qū)動注冊.
int trials[] =
{
3, 5, 7, 9, 0
};
int tried[] = {0, 0, 0, 0, 0};
int i, count = 0;
/*
* install the probing handler for all possible lines. Remember
* the result (0 for success, or -EBUSY) in order to only free
* what has been acquired */
for (i = 0; trials; i++)
tried = request_irq(trials, short_probing,
SA_INTERRUPT, "short probe", NULL);
do
{
short_irq = 0; /* none got, yet */
outb_p(0x10,short_base+2); /* enable */
outb_p(0x00,short_base);
outb_p(0xFF,short_base); /* toggle the bit */
outb_p(0x00,short_base+2); /* disable */
udelay(5); /* give it some time */
/* the value has been set by the handler */
if (short_irq == 0) { /* none of them? */
printk(KERN_INFO "short: no irq reported by probe\n");
}
/*
* If more than one line has been activated, the result is
* negative. We should service the interrupt (but the lpt port
* doesn't need it) and loop over again. Do it at most 5 times
*/
} while (short_irq
你可能事先不知道"可能的" IRQ 值是什么. 在這個情況, 你需要探測所有空閑的中斷, 不是限制你自己在幾個 trials[]. 為探測所有的中斷, 你不得不從 IRQ 0 到 IRQ NR_IRQS-1 探測, 這里 NR_IRQS 在 中定義并且是獨立于平臺的.
現(xiàn)在我們只缺少探測處理自己了. 處理者的角色是更新 short_irq, 根據(jù)實際收到哪個中斷. short_irq 中的 0 值意味著"什么沒有", 而一個負值意味著"模糊的". 這些值選擇來和 probe_irq_off 相一致并且允許同樣的代碼來調(diào)用任一種 short.c 中的探測.
irqreturn_t short_probing(int irq, void *dev_id, struct pt_regs *regs)
{
if (short_irq == 0) short_irq = irq; /* found */
if (short_irq != irq) short_irq = -irq; /* ambiguous */
return IRQ_HANDLED;
}
處理的參數(shù)在后面描述. 知道 irq 是在處理的中斷應(yīng)當(dāng)是足夠的來理解剛剛展示的函數(shù).
10.2.3. 快速和慢速處理
老版本的 Linux 內(nèi)核盡了很大努力來區(qū)分"快速"和"慢速"中斷. 快速中斷是那些能夠很快處理的, 而處理慢速中斷要特別地長一些. 慢速中斷可能十分苛求處理器, 并且它值得在處理的時候重新使能中斷. 否則, 需要快速注意的任務(wù)可能被延時太長.
在現(xiàn)代內(nèi)核中, 快速和慢速中斷的大部分不同已經(jīng)消失. 剩下的僅僅是一個: 快速中斷(那些使用 SA_INTERRUPT 被請求的)執(zhí)行時禁止所有在當(dāng)前處理器上的其他中斷. 注意其他的處理器仍然能夠處理中斷, 盡管你從不會看到 2 個處理器同時處理同一個 IRQ.
這樣, 你的驅(qū)動應(yīng)當(dāng)使用哪個類型的中斷? 在現(xiàn)代系統(tǒng)上, SA_INTERRUPT 只是打算用在幾個, 特殊的情況例如時鐘中斷. 除非你有一個充足的理由來運行你的中斷處理在禁止其他中斷情況下, 你不應(yīng)當(dāng)使用 SA_INTERRUPT.
這個描述應(yīng)當(dāng)滿足大部分讀者, 盡管有人喜好硬件并且對她的計算機有經(jīng)驗可能有興趣深入一些. 如果你不關(guān)心內(nèi)部的細節(jié), 你可跳到下一節(jié).
10.2.3.1. x86上中斷處理的內(nèi)幕
這個描述是從 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 它們出現(xiàn)于 2.6 內(nèi)核而推知的; 盡管一般的概念保持一致, 硬件細節(jié)在其他平臺上不同.
中斷處理的最低級是在 entry.S, 一個匯編語言文件處理很多機器級別的工作. 通過一點匯編器的技巧和一些宏定義, 一點代碼被安排到每個可能的中斷. 在每個情況下, 這個代碼將中斷號壓棧并且跳轉(zhuǎn)到一個通用段, 稱為 do_IRQ, 在 irq.c 中定義.
do_IRQ 做的第一件事是確認中斷以便中斷控制器能夠繼續(xù)其他事情. 它接著獲取給定 IRQ 號的一個自旋鎖, 因此阻止任何其他 CPU 處理這個 IRQ. 它清除幾個狀態(tài)位(包括稱為 IRQ_WAITING 的一個, 我們很快會看到它)并且接著查看這個特殊 IRQ 的處理者. 如果沒有處理者, 什么不作; 自旋鎖釋放, 任何掛起的軟件中斷被處理, 最后 do_IRQ 返回.
常常, 但是, 如果一個設(shè)備在中斷, 至少也有一個處理者注冊給它的 IRQ. 函數(shù) handle_IRQ_event 被調(diào)用來實際調(diào)用處理者. 如果處理者是慢速的( SA_INTERRUPT 沒有設(shè)置 ), 中斷在硬件中被重新使能, 并且調(diào)用處理者. 接著僅僅是清理, 運行軟件中斷, 以及回到正常的工作. "常規(guī)工作"很可能已經(jīng)由于中斷而改變了(處理者可能喚醒一個進程, 例如), 因此從中斷中返回的最后的事情是一個處理器的可能的重新調(diào)度.
探測 IRQ 通過設(shè)置 IRQ_WAITING 狀態(tài)位給每個當(dāng)前缺乏處理者的 IRQ 來完成. 當(dāng)中斷發(fā)生, do_IRQ 清除這個位并且接著返回, 因為沒有注冊處理者. probe_irq_off, 當(dāng)被一個函數(shù)調(diào)用, 需要只搜索不再有 IRQ_WAITING 設(shè)置的 IRQ.
10.2.4. 實現(xiàn)一個處理
至今, 我們已學(xué)習(xí)了注冊一個中斷處理, 但是沒有編寫一個. 實際上, 對于一個處理者, 沒什么不尋常的 -- 它是普通的 C 代碼.
唯一的特別之處是一個處理者在中斷時運行, 因此, 它能做的事情遭受一些限制. 這些限制與我們在內(nèi)核定時器上看到的相同. 一個處理者不能傳遞數(shù)據(jù)到或者從用戶空間, 因為它不在進程上下文執(zhí)行. 處理者也不能做任何可能睡眠的事情, 例如調(diào)用 wait_event, 使用除 GFP_ATOMIC 之外任何東西來分配內(nèi)存, 或者加鎖一個旗標(biāo). 最后, 處理者不能調(diào)用調(diào)度.
一個中斷處理的角色是給它的設(shè)備關(guān)于中斷接收的回應(yīng)并且讀或?qū)憯?shù)據(jù), 根據(jù)被服務(wù)的中斷的含義. 第一步常常包括清除接口板上的一位; 大部分硬件設(shè)備不產(chǎn)生別的中斷直到它們的"中斷掛起"位被清除. 根據(jù)你的硬件如何工作的, 這一步可能需要在最后做而不是開始; 這里沒有通吃的規(guī)則. 一些設(shè)備不需要這步, 因為它們沒有一個"中斷掛起"位; 這樣的設(shè)備是一少數(shù), 盡管并口是其中之一. 由于這個理由, short 不必清除這樣一個位.
一個中斷處理的典型任務(wù)是喚醒睡眠在設(shè)備上的進程, 如果中斷指示它們在等待的事件, 例如新數(shù)據(jù)的到達.
為堅持幀抓取者的例子, 一個進程可能請求一個圖像序列通過連續(xù)讀設(shè)備; 讀調(diào)用阻塞在讀取每個幀之前, 而中斷處理喚醒進程一旦每個新幀到達. 這個假定抓取器中斷處理器來指示每個新幀的成功到達.
程序員應(yīng)當(dāng)小心編寫一個函數(shù)在最小量的時間內(nèi)執(zhí)行, 不管是一個快速或慢速處理者. 如果需要進行長時間計算, 最好的方法是使用一個 tasklet 或者 workqueue 來調(diào)度計算在一個更安全的時間(我們將在"上和下半部"一節(jié)中見到工作如何被延遲.).
我們在 short 中的例子代碼響應(yīng)中斷通過調(diào)用 do_gettimeofday 和 打印當(dāng)前時間到一個頁大小的環(huán)形緩存. 它接著喚醒任何讀進程, 因為現(xiàn)在有數(shù)據(jù)可用來讀取.
irqreturn_t short_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
struct timeval tv;
int written;
do_gettimeofday(&tv);
/* Write a 16 byte record. Assume PAGE_SIZE is a multiple of 16 */
written = sprintf((char *)short_head,"%08u.%06u\n",
(int)(tv.tv_sec % 100000000), (int)(tv.tv_usec));
BUG_ON(written != 16);
short_incr_bp(&short_head, written);
wake_up_interruptible(&short_queue); /* awake any reading process */
return IRQ_HANDLED;
}
這個代碼, 盡管簡單, 代表了一個中斷處理的典型工作. 依次地, 它稱為 short_incr_bp, 定義如下:
static inline void short_incr_bp(volatile unsigned long *index, int delta)
{
unsigned long new = *index + delta;
barrier(); /* Don't optimize these two together */
*index = (new >= (short_buffer + PAGE_SIZE)) ? short_buffer : new;
}
這個函數(shù)已經(jīng)仔細編寫來回卷指向環(huán)形緩存的指針, 沒有暴露一個不正確的值. 這里的 barrier 調(diào)用來阻止編譯器在這個函數(shù)的其他 2 行之間優(yōu)化. 如果沒有 barrier, 編譯器可能決定優(yōu)化掉 new 變量并且直接賦值給 *index. 這個優(yōu)化可能暴露一個 index 的不正確值一段時間, 在它回卷的地方. 通過小心阻止對其他線程可見的不一致的值, 我們能夠安全操作環(huán)形緩存指針而不用鎖.
用來讀取中斷時填充的緩存的設(shè)備文件是 /dev/shortint. 這個設(shè)備特殊文件, 同 /dev/shortprint 一起, 不在第 9 章介紹, 因為它的使用對中斷處理是特殊的. /dev/shortint 內(nèi)部特別地為中斷產(chǎn)生和報告剪裁過. 寫到設(shè)備會每隔一個字節(jié)產(chǎn)生一個中斷; 讀取設(shè)備給出了每個中斷被報告的時間.
如果你連接并口連接器的管腳 9 和 10, 你可產(chǎn)生中斷通過拉高并口數(shù)據(jù)字節(jié)的高位. 這可通過寫二進制數(shù)據(jù)到 /dev/short0 或者通過寫任何東西到 /dev/shortint 來完成.
[
38
]下列代碼為 /dev/shortint 實現(xiàn)讀和寫:
ssize_t short_i_read (struct file *filp, char __user *buf, size_t count,
loff_t *f_pos)
{
int count0;
DEFINE_WAIT(wait);
while (short_head == short_tail)
{
prepare_to_wait(&short_queue, &wait, TASK_INTERRUPTIBLE);
if (short_head == short_tail)
schedule();
finish_wait(&short_queue, &wait);
if (signal_pending (current)) /* a signal arrived */
return -ERESTARTSYS; /* tell the fs layer to handle it */
} /* count0 is the number of readable data bytes */ count0 = short_head - short_tail;
if (count0
其他設(shè)備特殊文件, /dev/shortprint, 使用并口來驅(qū)動一個打印機; 你可用使用它, 如果你想避免連接一個 D-25 連接器管腳 9 和 10. shortprint 的寫實現(xiàn)使用一個環(huán)形緩存來存儲要打印的數(shù)據(jù), 而寫實現(xiàn)是剛剛展示的那個(因此你能夠讀取你的打印機吃進每個字符用的時間).
為了支持打印機操作, 中斷處理從剛剛展示的那個已經(jīng)稍微修改, 增加了發(fā)送下一個數(shù)據(jù)字節(jié)到打印機的能力, 如果沒有更多數(shù)據(jù)傳送.
10.2.5. 處理者的參數(shù)和返回值
盡管 short 忽略了它們, 一個傳遞給一個中斷處理的參數(shù): irq, dev_id, 和 regs. 我們看一下每個的角色.
中斷號( int irq )作為你可能在你的 log 消息中打印的信息是有用的, 如果有. 第二個參數(shù), void *dev_id, 是一類客戶數(shù)據(jù); 一個 void* 參數(shù)傳遞給 request_irq, 并且同樣的指針接著作為一個參數(shù)傳回給處理者, 當(dāng)中斷發(fā)生時. 你常常傳遞一個指向你的在 dev_id 中的設(shè)備數(shù)據(jù)結(jié)構(gòu)的指針, 因此一個管理相同設(shè)備的幾個實例的驅(qū)動不需要任何額外的代碼, 在中斷處理中找出哪個設(shè)備要負責(zé)當(dāng)前的中斷事件.
這個參數(shù)在中斷處理中的典型使用如下:
static irqreturn_t sample_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
struct sample_dev *dev = dev_id;
/* now `dev' points to the right hardware item */
/* .... */
}
和這個處理者關(guān)聯(lián)的典型的打開代碼看來如此:
static void sample_open(struct inode *inode, struct file *filp)
{
struct sample_dev *dev = hwinfo + MINOR(inode->i_rdev);
request_irq(dev->irq, sample_interrupt,
0 /* flags */, "sample", dev /* dev_id */);
/*....*/
return 0;
}
最后一個參數(shù), struct pt_regs *regs, 很少用到. 它持有一個處理器的上下文在進入中斷狀態(tài)前的快照. 寄存器可用來監(jiān)視和調(diào)試; 對于常規(guī)地設(shè)備驅(qū)動任務(wù), 正常地不需要它們.
中斷處理應(yīng)當(dāng)返回一個值指示是否真正有一個中斷要處理. 如果處理者發(fā)現(xiàn)它的設(shè)備確實需要注意, 它應(yīng)當(dāng)返回 IRQ_HANDLED; 否則返回值應(yīng)當(dāng)是 IRQ_NONE. 你也可產(chǎn)生返回值, 使用這個宏:
IRQ_RETVAL(handled)
這里, handled 是非零, 如果你能夠處理中斷. 內(nèi)核用返回值來檢測和抑制假中斷. 如果你的設(shè)備沒有給你方法來告知是否它確實中斷, 你應(yīng)當(dāng)返回 IRQ_HANDLED.
10.2.6. 使能和禁止中斷
有時設(shè)備驅(qū)動必須阻塞中斷的遞交一段時間(希望地短)(我們在第 5 章的 "自旋鎖"一節(jié)看到過這樣的一個情況). 常常, 中斷必須被阻塞當(dāng)持有一個自旋鎖來避免死鎖系統(tǒng)時. 有幾個方法來禁止不涉及自旋鎖的中斷. 但是在我們討論它們之前, 注意禁止中斷應(yīng)當(dāng)是一個相對少見的行為, 即便在設(shè)備驅(qū)動中, 并且這個技術(shù)應(yīng)當(dāng)從不在驅(qū)動中用做互斥機制.
10.2.6.1. 禁止單個中斷
有時(但是很少!)一個驅(qū)動需要禁止一個特定中斷線的中斷遞交. 內(nèi)核提供了 3 個函數(shù)為此目的, 所有都聲明在 . 這些函數(shù)是內(nèi)核 API 的一部分, 因此我們描述它們, 但是它們的使用在大部分驅(qū)動中不鼓勵. 在其他的中, 你不能禁止共享的中斷線, 并且, 在現(xiàn)代的系統(tǒng)中, 共享的中斷是規(guī)范. 已說過的, 它們在這里:
void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);
調(diào)用任一函數(shù)可能更新在可編程控制器(PIC)中的特定 irq 的掩碼, 因此禁止或使能跨所有處理器的特定 IRQ. 對這些函數(shù)的調(diào)用能夠嵌套 -- 如果 disable_irq 被連續(xù)調(diào)用 2 次, 需要 2 個 enable_irq 調(diào)用在 IRQ 被真正重新使能前. 可能調(diào)用這些函數(shù)從一個中斷處理中, 但是在處理它時使能你自己的 IRQ 常常不是一個好做法.
disable_irq 不僅禁止給定的中斷, 還等待一個當(dāng)前執(zhí)行的中斷處理結(jié)束, 如果有. 要知道如果調(diào)用 disable_irq 的線程持有中斷處理需要的任何資源(例如自旋鎖), 系統(tǒng)可能死鎖. disable_irq_nosync 與 disable_irq 不同, 它立刻返回. 因此, 使用disable_irq_nosync 快一點, 但是可能使你的設(shè)備有競爭情況.
但是為什么禁止中斷? 堅持說并口, 我們看一下 plip 網(wǎng)絡(luò)接口. 一個 plip 設(shè)備使用裸并口來傳送數(shù)據(jù). 因為只有 5 位可以從并口連接器讀出, 它們被解釋為 4 個數(shù)據(jù)位和一個時鐘/握手信號. 當(dāng)一個報文的第一個 4 位被 initiator (發(fā)送報文的接口) 傳送, 時鐘線被拉高, 使接收接口來中斷處理器. plip 處理者接著被調(diào)用來處理新到達的數(shù)據(jù).
在設(shè)備已經(jīng)被提醒了后, 數(shù)據(jù)傳送繼續(xù), 使用握手線來傳送數(shù)據(jù)到接收接口(這可能不是最好的實現(xiàn), 但是有必要與使用并口的其他報文驅(qū)動兼容). 如果接收接口不得不為每個接收的字節(jié)處理 2 次中斷, 性能可能不可忍受. 因此, 驅(qū)動在接收報文的時候禁止中斷; 相反, 一個查詢并延時的循環(huán)用來引入數(shù)據(jù).
類似地, 因為從接收器到發(fā)送器的握手線用來確認數(shù)據(jù)接收, 發(fā)送接口禁止它的 IRQ 線在報文發(fā)送時.
10.2.6.2. 禁止所有中斷
如果你需要禁止所有中斷如何? 在 2.6 內(nèi)核, 可能關(guān)閉在當(dāng)前處理器上所有中斷處理, 使用任一個下面 2 個函數(shù)(定義在 ):
void local_irq_save(unsigned long flags);
void local_irq_disable(void);
一個對 local_irq_save 的調(diào)用在當(dāng)前處理器上禁止中斷遞交, 在保存當(dāng)前中斷狀態(tài)到 flags 之后. 注意, flags 是直接傳遞, 不是通過指針. local_irq_disable 關(guān)閉本地中斷遞交而不保存狀態(tài); 你應(yīng)當(dāng)使用這個版本只在你知道中斷沒有在別處被禁止.
完成打開中斷, 使用:
void local_irq_restore(unsigned long flags);
void local_irq_enable(void);
第一個版本恢復(fù)由 local_irq_save 存儲于 flags 的狀態(tài), 而 local_irq_enable 無條件打開中斷. 不象 disable_irq, local_irq_disable 不跟蹤多次調(diào)用. 如果調(diào)用鏈中有多于一個函數(shù)可能需要禁止中斷, 應(yīng)該使用 local_irq_save.
在 2.6 內(nèi)核, 沒有方法全局性地跨整個系統(tǒng)禁止所有的中斷. 內(nèi)核開發(fā)者決定, 關(guān)閉所有中斷的開銷太高, 并且在任何情況下沒有必要有這個能力. 如果你在使用一個舊版本驅(qū)動, 它調(diào)用諸如 cli 和 sti, 你需要在它在 2.6 下工作前更新它為使用正確的加鎖
[
37
] 盡管, 一些大系統(tǒng)明確使用中斷平衡機制來在系統(tǒng)間分散中斷負載.
[
38
] 這個 shortint 設(shè)備完成它的任務(wù), 通過交替地寫入 0x00 和 0xff 到并口.
10.3. 前和后半部
中斷處理的一個主要問題是如何在處理中進行長時間的任務(wù). 常常大量的工作必須響應(yīng)一個設(shè)備中斷來完成, 但是中斷處理需要很快完成并且不使中斷阻塞太長. 這 2 個需要(工作和速度)彼此沖突, 留給驅(qū)動編寫者一點困擾.
Linux (許多其他系統(tǒng)一起)解決這個問題通過將中斷處理分為 2 半. 所謂的前半部是實際響應(yīng)中斷的函數(shù) -- 你使用 request_irq 注冊的那個. 后半部是由前半部調(diào)度來延后執(zhí)行的函數(shù), 在一個更安全的時間. 最大的不同在前半部處理和后半部之間是所有的中斷在后半部執(zhí)行時都使能 -- 這就是為什么它在一個更安全時間運行. 在典型的場景中, 前半部保存設(shè)備數(shù)據(jù)到一個設(shè)備特定的緩存, 調(diào)度它的后半部, 并且退出: 這個操作非?. 后半部接著進行任何其他需要的工作, 例如喚醒進程, 啟動另一個 I/O 操作, 等等. 這種設(shè)置允許前半部來服務(wù)一個新中斷而同時后半部仍然在工作.
幾乎每個認真的中斷處理都這樣劃分. 例如, 當(dāng)一個網(wǎng)絡(luò)接口報告有新報文到達, 處理者只是獲取數(shù)據(jù)并且上推給協(xié)議層; 報文的實際處理在后半部進行.
Linux 內(nèi)核有 2 個不同的機制可用來實現(xiàn)后半部處理, 我們都在第 7 章介紹. tasklet 常常是后半部處理的首選機制; 它們非?, 但是所有的 tasklet 代碼必須是原子的. tasklet 的可選項是工作隊列, 它可能有一個更高的運行周期但是允許睡眠.
下面的討論再次使用 short 驅(qū)動. 當(dāng)使用一個模塊選項加載時, short 能夠被告知在前/后半部模式使用一個 tasklet 或者工作隊列處理者來進行中斷處理. 在這個情況下, 前半部快速地執(zhí)行; 它簡單地記住當(dāng)前時間并且調(diào)度后半部處理. 后半部接著負責(zé)將時間編碼并且喚醒任何可能在等待數(shù)據(jù)的用戶進程.
10.3.1. Tasklet 實現(xiàn)
記住 tasklet 是一個特殊的函數(shù), 可能被調(diào)度來運行, 在軟中斷上下文, 在一個系統(tǒng)決定的安全時間中. 它們可能被調(diào)度運行多次, 但是 tasklet 調(diào)度不累積; ; tasklet 只運行一次, 即便它在被投放前被重復(fù)請求. 沒有 tasklet 會和它自己并行運行, 因為它只運行一次, 但是 tasklet 可以與 SMP 系統(tǒng)上的其他 tasklet 并行運行. 因此, 如果你的驅(qū)動有多個 tasklet, 它們必須采取某類加鎖來避免彼此沖突.
tasklet 也保證作為函數(shù)運行在第一個調(diào)度它們的同一個 CPU 上. 因此, 一個中斷處理可以確保一個 tasklet 在處理者結(jié)束前不會開始執(zhí)行. 但是, 另一個中斷當(dāng)然可能在 tasklet 在運行時被遞交, 因此, tasklet 和中斷處理之間加鎖可能仍然需要.
tasklet 必須使用 DECLARE_TASKLET 宏來聲明:
DECLARE_TASKLET(name, function, data);
name 是給 tasklet 的名子, function 是調(diào)用來執(zhí)行 tasklet (它帶一個 unsigned long 參數(shù)并且返回 void )的函數(shù), 以及 data 是一個 unsigned long 值來傳遞給 tasklet 函數(shù).
short 驅(qū)動聲明它的 tasklet 如下:
void short_do_tasklet(unsigned long);
DECLARE_TASKLET(short_tasklet, short_do_tasklet, 0);
函數(shù) tasklet_schedule 用來調(diào)度一個 tasklet 運行. 如果 short 使用 tasklet=1 來加載, 它安裝一個不同的中斷處理來保存數(shù)據(jù)并且調(diào)度 tasklet 如下:
irqreturn_t short_tl_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
do_gettimeofday((struct timeval *) tv_head); /* cast to stop 'volatile' warning
*/
short_incr_tv(&tv_head);
tasklet_schedule(&short_tasklet);
short_wq_count++; /* record that an interrupt arrived */
return IRQ_HANDLED;
}
實際的 tasklet 函數(shù), short_do_tasklet, 將在系統(tǒng)方便時很快執(zhí)行. 如同前面提過, 這個函數(shù)進行處理中斷的大量工作; 它看來如此:
void short_do_tasklet (unsigned long unused)
{
int savecount = short_wq_count, written;
short_wq_count = 0; /* we have already been removed from the queue */
/*
* The bottom half reads the tv array, filled by the top half,
* and prints it to the circular text buffer, which is then consumed
* by reading processes */
/* First write the number of interrupts that occurred before this bh */
written = sprintf((char *)short_head,"bh after %6i\n",savecount);
short_incr_bp(&short_head, written);
/*
* Then, write the time values. Write exactly 16 bytes at a time,
* so it aligns with PAGE_SIZE */
do {
written = sprintf((char *)short_head,"%08u.%06u\n",
(int)(tv_tail->tv_sec % 100000000),
(int)(tv_tail->tv_usec));
short_incr_bp(&short_head, written);
short_incr_tv(&tv_tail);
} while (tv_tail != tv_head);
wake_up_interruptible(&short_queue); /* awake any reading process */
}
在別的東西中, 這個 tasklet 記錄了從它上次被調(diào)用以來有多少中斷到達. 一個如 short 一樣的設(shè)備能夠在短時間內(nèi)產(chǎn)生大量中斷, 因此在后半部執(zhí)行前有幾個中斷到達就不是不尋常的. 驅(qū)動必須一直準(zhǔn)備這種可能性并且必須能夠從前半部留下的信息中決定有多少工作要做.
10.3.2. 工作隊列
回想, 工作隊列在將來某個時候調(diào)用一個函數(shù), 在一個特殊工作者進程的上下文中. 因為這個工作隊列函數(shù)在進程上下文運行, 它在需要時能夠睡眠. 但是, 你不能從一個工作隊列拷貝數(shù)據(jù)到用戶空間, 除非你使用我們在 15 章演示的高級技術(shù); 工作者進程不存取任何其他進程的地址空間.
short 驅(qū)動, 如果設(shè)置 wq 選項為一個非零值來加載, 為它的后半部處理使用一個工作隊列. 它使用系統(tǒng)缺省的工作隊列, 因此不要求特殊的設(shè)置代碼; 如果你的驅(qū)動有特別的運行周期要求(或者可能在工作隊列函數(shù)長時間睡眠), 你可能需要創(chuàng)建你自己的, 專用的工作隊列. 我們確實需要一個 work_struct 結(jié)構(gòu), 它聲明和初始化使用下列:
static struct work_struct short_wq;
/* this line is in short_init() */
INIT_WORK(&short_wq, (void (*)(void *)) short_do_tasklet, NULL);
我們的工作者函數(shù)是 short_do_tasklet, 我們已經(jīng)在前面一節(jié)看到.
當(dāng)使用一個工作隊列, short 還建立另一個中斷處理, 看來如此:
irqreturn_t short_wq_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
/* Grab the current time information. */
do_gettimeofday((struct timeval *) tv_head);
short_incr_tv(&tv_head);
/* Queue the bh. Don't worry about multiple enqueueing */
schedule_work(&short_wq);
short_wq_count++; /* record that an interrupt arrived */
return IRQ_HANDLED;
}
如你所見, 中斷處理看來非常象這個 tasklet 版本, 除了它調(diào)用 schedule_work 來安排后半部處理.
10.4. 中斷共享
中斷沖突的概念幾乎是 PC 體系的同義詞. 過去, 在 PC 上的 IRQ 線不能服務(wù)多于一個設(shè)備, 并且它們從不足夠. 結(jié)果, 失望的用戶花費大量時間開著它們的計算機, 盡力找到一個方法來使它們所有的外設(shè)一起工作.
現(xiàn)代的硬件, 當(dāng)然, 已經(jīng)設(shè)計來允許中斷共享; PCI 總線要求它. 因此, Linux 內(nèi)核支持在所有總線上中斷共享, 甚至是那些(例如 ISA 總線)傳統(tǒng)上不被支持的. 2.6 內(nèi)核的設(shè)備驅(qū)動應(yīng)當(dāng)編寫來使用共享中斷, 如果目標(biāo)硬件能夠支持這個操作模式. 幸運的是, 使用共享中斷在大部分時間是容易的.
10.4.1. 安裝一個共享的處理者
共享中斷通過 request_irq 來安裝就像不共享的一樣, 但是有 2 個不同:
SA_SHIRQ 位必須在 flags 參數(shù)中指定, 當(dāng)請求中斷時.
dev_id 參數(shù)必須是獨特的. 任何模塊地址空間的指針都行, 但是 dev_id 明確地不能設(shè)置為 NULL.
內(nèi)核保持著一個與中斷相關(guān)聯(lián)的共享處理者列表, 并且 dev_id 可認為是區(qū)別它們的簽名. 如果 2 個驅(qū)動要在同一個中斷上注冊 NULL 作為它們的簽名, 在卸載時事情可能就亂了, 在中斷到的時候引發(fā)內(nèi)核 oops. 由于這個理由, 如果在注冊共享中斷時傳給了一個 NULL dev_id , 現(xiàn)代內(nèi)核會大聲抱怨. 當(dāng)請求一個共享的中斷, request_irq 成功, 如果下列之一是真:
中斷線空閑.
所有這條線的已經(jīng)注冊的處理者也指定共享這個 IRQ.
無論何時 2 個或多個驅(qū)動在共享中斷線, 并且硬件中斷在這條線上中斷處理器, 內(nèi)核為這個中斷調(diào)用每個注冊的處理者, 傳遞它的 dev_id 給每個. 因此, 一個共享的處理者必須能夠識別它自己的中斷并且應(yīng)當(dāng)快速退出當(dāng)它自己的設(shè)備沒有被中斷時. 確認返回 IRQ_NONE 無論何時你的處理者被調(diào)用并且發(fā)現(xiàn)設(shè)備沒被中斷.
如果你需要探測你的設(shè)備, 在請求 IRQ 線之前, 內(nèi)核無法幫你. 沒有探測函數(shù)可給共享處理者使用. 標(biāo)準(zhǔn)的探測機制有效如果使用的線是空閑的, 但是如果這條線已經(jīng)被另一個有共享能力的驅(qū)動持有, 探測失敗, 即便你的驅(qū)動已正常工作. 幸運的是, 大部分設(shè)計為中斷共享的硬件能夠告知處理器它在使用哪個中斷, 因此減少明顯的探測的需要.
釋放處理者以正常方式進行, 使用 free_irq. 這里 dev_id 參數(shù)用來從這個中斷的共享處理者列表中選擇正確的處理者來釋放. 這就是為什么 dev_id 指針必須是獨特的.
一個使用共享處理者的驅(qū)動需要小心多一件事: 它不能使用 enable_irq 或者 disable_irq. 如果它用了, 對其他共享這條線的設(shè)備就亂了; 禁止另一個設(shè)備的中斷即便短時間也可能產(chǎn)生延時, 這對這個設(shè)備和它的用戶是有問題的. 通常, 程序員必須記住, 他的驅(qū)動不擁有這個 IRQ, 并且它的行為應(yīng)當(dāng)比它擁有這個中斷線更加"社會性".
10.4.2. 運行處理者
如同前面建議的, 當(dāng)內(nèi)核收到一個中斷, 所有的注冊的處理者被調(diào)用. 一個共享的處理者必須能夠在它需要的處理的中斷和其他設(shè)備產(chǎn)生的中斷之間區(qū)分.
使用 shared=1 選項來加載 short 安裝了下列處理者來代替缺省的:
irqreturn_t short_sh_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
int value, written;
struct timeval tv;
/* If it wasn't short, return immediately */
value = inb(short_base);
if (!(value & 0x80))
return IRQ_NONE;
/* clear the interrupting bit */
outb(value & 0x7F, short_base);
/* the rest is unchanged */
do_gettimeofday(&tv);
written = sprintf((char *)short_head,"%08u.%06u\n",
(int)(tv.tv_sec % 100000000), (int)(tv.tv_usec));
short_incr_bp(&short_head, written);
wake_up_interruptible(&short_queue); /* awake any reading process */
return IRQ_HANDLED;
}
這里應(yīng)該有個解釋. 因為并口沒有"中斷掛起"位來檢查, 處理者使用 ACK 位作此目的. 如果這個位是高, 正報告的中斷是給 short, 并且這個處理者清除這個位.
處理者通過并口數(shù)據(jù)端口的清零來復(fù)位這個位 -- short 假設(shè)管腳 9 和 10 連在一起. 如果其他一個和 short 共享這個 IRQ 的設(shè)備產(chǎn)生一個中斷, short 看到它的線仍然非激活并且什么不作.
當(dāng)然, 一個功能齊全的驅(qū)動可能將工作劃分位前和后半部, 但是容易添加并且不會有任何影響實現(xiàn)共享的代碼. 一個真實驅(qū)動還可能使用 dev_id 參數(shù)來決定, 在很多可能的中, 哪個設(shè)備在中斷.
注意, 如果你使用打印機(代替跳線)來測試使用 short 的中斷管理, 這個共享的處理者不象所說的一樣工作,因為打印機協(xié)議不允許共享, 并且驅(qū)動不知道是否這個中斷是來自打印機.
10.4.3. /proc 接口和共享中斷
在系統(tǒng)中安裝共享處理者不影響 /proc/stat, 它甚至不知道處理者. 但是, /proc/interrupts 稍稍變化.
所有同一個中斷號的安裝的處理者出現(xiàn)在 /proc/interrupts 的同一行. 下列輸出( 從一個 x86_64 系統(tǒng))顯示了共享中斷處理是如何顯示的:
CPU0
0: 892335412 XT-PIC timer
1: 453971 XT-PIC i8042
2: 0 XT-PIC cascade
5: 0 XT-PIC libata, ehci_hcd
8: 0 XT-PIC rtc
9: 0 XT-PIC acpi
10: 11365067 XT-PIC ide2, uhci_hcd, uhci_hcd, SysKonnect SK-98xx, EMU10K1
11: 4391962 XT-PIC uhci_hcd, uhci_hcd
12: 224 XT-PIC i8042
14: 2787721 XT-PIC ide0
15: 203048 XT-PIC ide1
NMI: 41234
LOC: 892193503
ERR: 102
MIS: 0
這個系統(tǒng)有幾個共享中斷線. IRQ 5 用來給串行 ATA 和 IEEE 1394 控制器; IRQ 10 有幾個設(shè)備, 包括一個 IDE 控制器, 2 個 USB 控制器, 一個以太網(wǎng)接口, 以及一個聲卡; 并且 IRQ 11 也被 2 個 USB 控制器使用.
10.5. 中斷驅(qū)動 I/O
無論何時一個數(shù)據(jù)傳送到或自被管理的硬件可能因為任何原因而延遲, 驅(qū)動編寫者應(yīng)當(dāng)實現(xiàn)緩存. 數(shù)據(jù)緩存幫助來分離數(shù)據(jù)傳送和接收從寫和讀系統(tǒng)調(diào)用, 并且整個系統(tǒng)性能受益.
一個好的緩存機制產(chǎn)生了中斷驅(qū)動的 I/O, 一個輸入緩存在中斷時填充并且被讀取設(shè)備的進程清空; 一個輸出緩存由寫設(shè)備的進程填充并且在中斷時清空. 一個中斷驅(qū)動的輸出的例子是 /dev/shortprint 的實現(xiàn).
為使中斷驅(qū)動的數(shù)據(jù)傳送成功發(fā)生, 硬件應(yīng)當(dāng)能夠產(chǎn)生中斷, 使用下列語義:
對于輸入, 設(shè)備中斷處理器, 當(dāng)新數(shù)據(jù)到達時, 并且準(zhǔn)備好被系統(tǒng)處理器獲取. 進行的實際動作依賴是否設(shè)備使用 I/O 端口, 內(nèi)存映射, 或者 DMA.
對于輸出, 設(shè)備遞交一個中斷, 或者當(dāng)它準(zhǔn)備好接受新數(shù)據(jù), 或者確認一個成功的數(shù)據(jù)傳送. 內(nèi)存映射的和能DMA的設(shè)備常常產(chǎn)生中斷來告訴系統(tǒng)它們完成了這個緩存.
在一個讀或?qū)懪c實際數(shù)據(jù)到達之間的時間關(guān)系在第 6 章的"阻塞和非阻塞操作"一節(jié)中介紹.
10.5.1. 一個寫緩存例子
我們已經(jīng)幾次提及 shortprint 驅(qū)動; 現(xiàn)在是時候真正看看. 這個模塊為并口實現(xiàn)一個非常簡單, 面向輸出的驅(qū)動; 它是足夠的, 但是, 來使能文件打印. 如果你選擇來測試這個驅(qū)動, 但是, 記住你必須傳遞給打印機一個文件以它理解的格式; 不是所有的打印機在給一個任意數(shù)據(jù)的流時很好響應(yīng).
shortprint 驅(qū)動維護一個一頁的環(huán)形輸出緩存. 當(dāng)一個用戶空間進程寫數(shù)據(jù)到這個設(shè)備, 數(shù)據(jù)被填入緩存, 但是寫方法實際沒有進行任何 I/O. 相反, shortp_write 的核心看來如此:
while (written 0))
goto out;
}
/* Move data into the buffer. */
if ((space + written) > count)
space = count - written;
if (copy_from_user((char *) shortp_out_head, buf, space)) {
up(&shortp_out_sem);
return -EFAULT;
}
shortp_incr_out_bp(&shortp_out_head, space);
buf += space;
written += space;
/* If no output is active, make it active. */
spin_lock_irqsave(&shortp_out_lock, flags);
if (! shortp_output_active)
shortp_start_output();
spin_unlock_irqrestore(&shortp_out_lock, flags);
}
out:
*f_pos += written;
一個旗標(biāo) ( shortp_out_sem ) 控制對這個環(huán)形緩存的存取; shortp_write 就在上面的代碼片段之前獲得這個旗標(biāo). 當(dāng)持有這個旗標(biāo), 它試圖輸入數(shù)據(jù)到這個環(huán)形緩存. 函數(shù) shortp_out_space 返回可用的連續(xù)空間的數(shù)量(因此, 沒有必要擔(dān)心緩存回繞); 如果這個量是 0, 驅(qū)動等到釋放一些空間. 它接著拷貝它能夠的數(shù)量的數(shù)據(jù)到緩存中.
一旦有數(shù)據(jù)輸出, shortp_write 必須確保數(shù)據(jù)被寫到設(shè)備. 數(shù)據(jù)的寫是通過一個工作隊列函數(shù)完成的; shortp_write 必須啟動這個函數(shù)如果它還未在運行. 在獲取了一個單獨的, 控制存取輸出緩存的消費者一側(cè)(包括 shortp_output_active)的數(shù)據(jù)的自旋鎖后, 它調(diào)用 shortp_start_output 如果需要. 接著只是注意多少數(shù)據(jù)被寫到緩存并且返回.
啟動輸出進程的函數(shù)看來如下:
static void shortp_start_output(void)
{
if (shortp_output_active) /* Should never happen */
return;
/* Set up our 'missed interrupt' timer */
shortp_output_active = 1;
shortp_timer.expires = jiffies + TIMEOUT;
add_timer(&shortp_timer);
/* And get the process going. */
queue_work(shortp_workqueue, &shortp_work);
}
處理硬件的事實是, 你可以, 偶爾, 丟失來自設(shè)備的中斷. 當(dāng)發(fā)生這個, 你確實不想你的驅(qū)動一直停止直到系統(tǒng)重啟; 這不是一個用戶友好的做事方式. 最好是認識到一個中斷已經(jīng)丟失, 收拾殘局, 繼續(xù). 為此, shortprint 甚至一個內(nèi)核定時器無論何時它輸出數(shù)據(jù)給設(shè)備. 如果時鐘超時, 我們可能丟失一個中斷. 我們很快會看到定時器函數(shù), 但是, 暫時, 讓我們堅持在主輸出功能上. 那是在我們的工作隊列函數(shù)里實現(xiàn)的, 它, 如同你上面看到的, 在這里被調(diào)度. 那個函數(shù)的核心看來如下:
spin_lock_irqsave(&shortp_out_lock, flags);
/* Have we written everything? */
if (shortp_out_head == shortp_out_tail)
{ /* empty */
shortp_output_active = 0;
wake_up_interruptible(&shortp_empty_queue);
del_timer(&shortp_timer);
}
/* Nope, write another byte */
else
shortp_do_write();
/* If somebody's waiting, maybe wake them up. */
if (((PAGE_SIZE + shortp_out_tail -shortp_out_head) % PAGE_SIZE) > SP_MIN_SPACE)
{
wake_up_interruptible(&shortp_out_queue);
}
spin_unlock_irqrestore(&shortp_out_lock, flags);
因為我們在使用共享變量的輸出一側(cè), 我們必須獲得自旋鎖. 接著我們看是否有更多的數(shù)據(jù)要發(fā)送; 如果無, 我們注意輸出不再激活, 刪除定時器, 并且喚醒任何在等待隊列全空的進程(這種等待當(dāng)設(shè)備被關(guān)閉時結(jié)束). 如果, 相反, 有數(shù)據(jù)要寫, 我們調(diào)用 shortp_do_write 來實際發(fā)送一個字節(jié)到硬件.
接著, 因為我們可能在輸出緩存中有空閑空間, 我們考慮喚醒任何等待增加更多數(shù)據(jù)給那個緩存的進程. 但是我們不是無條件進行喚醒; 相反, 我們等到有一個最低數(shù)量的空間. 每次我們從緩存拿出一個字節(jié)就喚醒一個寫者是無意義的; 喚醒進程的代價, 調(diào)度它運行, 并且使它重回睡眠, 太高了. 相反, 我們應(yīng)當(dāng)?shù)鹊竭M程能夠立刻移動相當(dāng)數(shù)量的數(shù)據(jù)到緩存. 這個技術(shù)在緩存的, 中斷驅(qū)動的驅(qū)動中是普通的.
為完整起見, 這是實際寫數(shù)據(jù)到端口的代碼:
static void shortp_do_write(void)
{
unsigned char cr = inb(shortp_base + SP_CONTROL);
/* Something happened; reset the timer */
mod_timer(&shortp_timer, jiffies + TIMEOUT);
/* Strobe a byte out to the device */
outb_p(*shortp_out_tail, shortp_base+SP_DATA);
shortp_incr_out_bp(&shortp_out_tail, 1);
if (shortp_delay)
udelay(shortp_delay);
outb_p(cr | SP_CR_STROBE, shortp_base+SP_CONTROL);
if (shortp_delay)
udelay(shortp_delay);
outb_p(cr & ~SP_CR_STROBE, shortp_base+SP_CONTROL);
}
這里, 我們復(fù)位定時器來反映一個事實, 我們已經(jīng)作了一些處理, 輸送字節(jié)到設(shè)備, 并且更新了環(huán)形緩存指針.
工作隊列函數(shù)沒有直接重新提交它自己, 因此只有一個單個字節(jié)會被寫入設(shè)備. 在某一處, 打印機將, 以它的緩慢方式, 消耗這個字節(jié)并且準(zhǔn)備好下一個; 它將接著中斷處理器. shortprint 中使用的中斷處理是簡短的:
static irqreturn_t shortp_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
if (! shortp_output_active)
return IRQ_NONE;
/* Remember the time, and farm off the rest to the workqueue function */
do_gettimeofday(&shortp_tv);
queue_work(shortp_workqueue, &shortp_work);
return IRQ_HANDLED;
}
因為并口不要求一個明顯的中斷確認, 中斷處理所有真正需要做的是告知內(nèi)核來再次運行工作隊列函數(shù).
如果中斷永遠不來如何? 至此我們已見到的驅(qū)動代碼將簡單地停止. 為避免發(fā)生這個, 我們設(shè)置了一個定時器在幾頁前. 當(dāng)定時器超時運行的函數(shù)是:
static void shortp_timeout(unsigned long unused)
{
unsigned long flags;
unsigned char status;
if (! shortp_output_active)
return;
spin_lock_irqsave(&shortp_out_lock, flags);
status = inb(shortp_base + SP_STATUS);
/* If the printer is still busy we just reset the timer */
if ((status & SP_SR_BUSY) == 0 || (status & SP_SR_ACK)) {
shortp_timer.expires = jiffies + TIMEOUT;
add_timer(&shortp_timer);
spin_unlock_irqrestore(&shortp_out_lock, flags);
return;
}
/* Otherwise we must have dropped an interrupt. */
spin_unlock_irqrestore(&shortp_out_lock, flags);
shortp_interrupt(shortp_irq, NULL, NULL);
}
如果沒有輸出要被激活, 定時器函數(shù)簡單地返回. 這避免了定時器重新提交自己, 當(dāng)事情在被關(guān)閉時. 接著, 在獲得了鎖之后, 我們查詢端口的狀態(tài); 如果它聲稱忙, 它完全還沒有時間來中斷我們, 因此我們復(fù)位定時器并且返回. 打印機能夠, 有時, 花很長時間來使自己準(zhǔn)備; 考慮一下缺紙的打印機, 而每個人在一個長周末都不在. 在這種情況下, 只有耐心等待直到事情改變.
但是, 如果打印機聲稱準(zhǔn)備好了, 我們一定丟失了它的中斷. 這個情況下, 我們簡單地手動調(diào)用我們的中斷處理來使輸出處理再動起來.
shortpirnt 驅(qū)動不支持從端口讀數(shù)據(jù); 相反, 它象 shortint 并且返回中斷時間信息. 但是一個中斷驅(qū)動的讀方法的實現(xiàn)可能非常類似我們已經(jīng)見到的. 從設(shè)備來的數(shù)據(jù)可能被讀入驅(qū)動緩存; 它可能被拷貝到用戶空間只在緩存中已經(jīng)累積了相當(dāng)數(shù)量的數(shù)據(jù), 完整的讀請求已被滿足, 或者某種超時發(fā)生.
10.6. 快速參考
本章中介紹了這些關(guān)于中斷管理的符號:
#include
int request_irq(unsigned int irq, irqreturn_t (*handler)( ), unsigned long flags, const char *dev_name, void *dev_id);
void free_irq(unsigned int irq, void *dev_id);
調(diào)用這個注冊和注銷一個中斷處理.
#include
int can_request_irq(unsigned int irq, unsigned long flags);
這個函數(shù), 在 i386 和 x86_64 體系上有, 返回一個非零值如果一個分配給定中斷線的企圖成功.
#include
SA_INTERRUPT
SA_SHIRQ
SA_SAMPLE_RANDOM
給 request_irq 的標(biāo)志. SA_INTERRUPT 請求安裝一個快速處理者( 相反是一個慢速的). SA_SHIRQ 安裝一個共享的處理者, 并且第 3 個 flag 聲稱中斷時戳可用來產(chǎn)生系統(tǒng)熵.
/proc/interrupts
/proc/stat
報告硬件中斷和安裝的處理者的文件系統(tǒng)節(jié)點.
unsigned long probe_irq_on(void);
int probe_irq_off(unsigned long);
驅(qū)動使用的函數(shù), 當(dāng)它不得不探測來決定哪個中斷線被設(shè)備在使用. probe_irq_on 的結(jié)果必須傳回給 probe_irq_off 在中斷產(chǎn)生之后. probe_irq_off 的返回值是被探測的中斷號.
IRQ_NONE
IRQ_HANDLED
IRQ_RETVAL(int x)
從一個中斷處理返回的可能值, 指示是否一個來自設(shè)備的真正的中斷出現(xiàn)了.
void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);
驅(qū)動可以使能和禁止中斷報告. 如果硬件試圖在中斷禁止時產(chǎn)生一個中斷, 這個中斷永遠丟失了. 一個使用一個共享處理者的驅(qū)動必須不使用這個函數(shù).
void local_irq_save(unsigned long flags);
void local_irq_restore(unsigned long flags);
使用 local_irq_save 來禁止本地處理器的中斷并且記住它們之前的狀態(tài). flags 可以被傳遞給 local_irq_restore 來恢復(fù)之前的中斷狀態(tài).
void local_irq_disable(void);
void local_irq_enable(void);
在當(dāng)前處理器熵?zé)o條件禁止和使能中斷的函數(shù).
本文來自ChinaUnix博客,如果查看原文請點:http://blog.chinaunix.net/u2/78225/showart_1270132.html |
|