如果你想實(shí)際地"看到"產(chǎn)生的中斷, 向硬件設(shè)備寫不足夠; 一個(gè)軟件處理必須在系統(tǒng)中配置. 如果 Linux 內(nèi)核還沒有被告知來期待你的中斷, 它簡(jiǎn)單地確認(rèn)并忽略它.
中斷線是一個(gè)寶貴且常常有限的資源, 特別當(dāng)它們只有 15 或者 16 個(gè)時(shí). 內(nèi)核保持了中斷線的一個(gè)注冊(cè), 類似于 I/O 端口的注冊(cè). 一個(gè)模塊被希望來請(qǐng)求一個(gè)中斷通道(或者 IRQ, 對(duì)于中斷請(qǐng)求), 在使用它之前, 并且當(dāng)結(jié)束時(shí)釋放它. 在很多情況下, 也希望模塊能夠與其他驅(qū)動(dòng)共享中斷線, 如同我們將看到的. 下面的函數(shù), 聲明在 <linux/interrupt.h>, 實(shí)現(xiàn)中斷注冊(cè)接口:
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 返回給請(qǐng)求函數(shù)的返回值或者是 0 指示成功, 或者是一個(gè)負(fù)的錯(cuò)誤碼, 如同平常. 函數(shù)返回 -EBUSY 來指示另一個(gè)驅(qū)動(dòng)已經(jīng)使用請(qǐng)求的中斷線是不尋常的. 函數(shù)的參數(shù)如下:
unsigned int irq
請(qǐng)求的中斷號(hào)
irqreturn_t (*handler)
安裝的處理函數(shù)指針. 我們?cè)诒菊潞竺嬗懻摻o這個(gè)函數(shù)的參數(shù)以及它的返回值.
unsigned long flags
如你會(huì)希望的, 一個(gè)與中斷管理相關(guān)的選項(xiàng)的位掩碼(后面描述).
const char *dev_name
這個(gè)傳遞給 request_irq 的字串用在 /proc/interrupts 來顯示中斷的擁有者(下一節(jié)看到)
void *dev_id
用作共享中斷線的指針. 它是一個(gè)獨(dú)特的標(biāo)識(shí), 用在當(dāng)釋放中斷線時(shí)以及可能還被驅(qū)動(dòng)用來指向它自己的私有數(shù)據(jù)區(qū)(來標(biāo)識(shí)哪個(gè)設(shè)備在中斷). 如果中斷沒有被共享, dev_id 可以設(shè)置為 NULL, 但是使用這個(gè)項(xiàng)指向設(shè)備結(jié)構(gòu)不管如何是個(gè)好主意. 我們將在"實(shí)現(xiàn)一個(gè)處理"一節(jié)中看到 dev_id 的一個(gè)實(shí)際應(yīng)用.
flags 中可以設(shè)置的位如下:
SA_INTERRUPT
當(dāng)置位了, 這表示一個(gè)"快速"中斷處理. 快速處理在當(dāng)前處理器上禁止中斷來執(zhí)行(這個(gè)主題在"快速和慢速處理"一節(jié)涉及).
SA_SHIRQ
這個(gè)位表示中斷可以在設(shè)備間共享. 共享的概念在"中斷共享"一節(jié)中略述.
SA_SAMPLE_RANDOM
這個(gè)位表示產(chǎn)生的中斷能夠有貢獻(xiàn)給 /dev/random 和 /dev/urandom 使用的加密池. 這些設(shè)備在讀取時(shí)返回真正的隨機(jī)數(shù)并且設(shè)計(jì)來幫助應(yīng)用程序軟件為加密選擇安全鑰. 這樣的隨機(jī)數(shù)從一個(gè)由各種隨機(jī)事件貢獻(xiàn)的加密池中提取的. 如果你的設(shè)備以真正隨機(jī)的時(shí)間產(chǎn)生中斷, 你應(yīng)當(dāng)設(shè)置這個(gè)標(biāo)志. 如果, 另一方面, 你的中斷是可預(yù)測(cè)的( 例如, 一個(gè)幀抓取器的場(chǎng)消隱), 這個(gè)標(biāo)志不值得設(shè)置 -- 它無論如何不會(huì)對(duì)系統(tǒng)加密有貢獻(xiàn). 可能被攻擊者影響的設(shè)備不應(yīng)當(dāng)設(shè)置這個(gè)標(biāo)志; 例如, 網(wǎng)絡(luò)驅(qū)動(dòng)易遭受從外部計(jì)時(shí)的可預(yù)測(cè)報(bào)文并且不應(yīng)當(dāng)對(duì)加密池有貢獻(xiàn). 更多信息看 drivers/char/random.c 的注釋.
中斷處理可以在驅(qū)動(dòng)初始化時(shí)安裝或者在設(shè)備第一次打開時(shí). 盡管從模塊的初始化函數(shù)中安裝中斷處理可能聽來是個(gè)好主意, 它常常不是, 特別當(dāng)你的設(shè)備不共享中斷. 因?yàn)橹袛嗑€數(shù)目是有限的, 你不想浪費(fèi)它們. 你可以輕易使你的系統(tǒng)中設(shè)備數(shù)多于中斷數(shù).如果一個(gè)模塊在初始化時(shí)請(qǐng)求一個(gè) IRQ, 它阻止了任何其他的驅(qū)動(dòng)使用這個(gè)中斷, 甚至這個(gè)持有它的設(shè)備從不被使用. 在設(shè)備打開時(shí)請(qǐng)求中斷, 另一方面, 允許某些共享資源.
例如, 可能與一個(gè) modem 在同一個(gè)中斷上運(yùn)行一個(gè)幀抓取器, 只要你不同時(shí)使用這 2 個(gè)設(shè)備. 對(duì)用戶來說是很普通的在系統(tǒng)啟動(dòng)時(shí)為一個(gè)特殊設(shè)備加載模塊, 甚至這個(gè)設(shè)備很少用到. 一個(gè)數(shù)據(jù)獲取技巧可能使用同一個(gè)中斷作為第 2 個(gè)串口. 雖然不是太難避免在數(shù)據(jù)獲取時(shí)聯(lián)入你的互聯(lián)網(wǎng)服務(wù)提供商(ISP), 被迫卸載一個(gè)模塊為了使用 modem 確實(shí)令人不快.
調(diào)用 request_irq 的正確位置是當(dāng)設(shè)備第一次打開時(shí), 在硬件被指示來產(chǎn)生中斷前. 調(diào)用 free_irq 的位置是設(shè)備最后一次被關(guān)閉時(shí), 在硬件被告知不要再中斷處理器之后. 這個(gè)技術(shù)的缺點(diǎn)是你需要保持一個(gè)每設(shè)備的打開計(jì)數(shù), 以便于你知道什么時(shí)候中斷可以被禁止.
盡管這個(gè)討論, short 還在加載時(shí)請(qǐng)求它的中斷線. 這樣做是為了你可以運(yùn)行測(cè)試程序而不必運(yùn)行一個(gè)額外的進(jìn)程來保持設(shè)備打開. short, 因此, 從它的初始化函數(shù)( short_init )請(qǐng)求中斷, 不是在 short_open 中做, 象一個(gè)真實(shí)設(shè)備驅(qū)動(dòng).
下面代碼請(qǐng)求的中斷是 short_irq. 變量的真正賦值(即, 決定使用哪個(gè) IRQ )在后面顯示, 因?yàn)樗同F(xiàn)在的討論無關(guān). short_base 是使用的并口 I/O 基地址; 接口的寄存器 2 被寫入來使能中斷報(bào)告.
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);
}
}
代碼顯示, 安裝的處理是一個(gè)快速處理(SA_INTERRUPT), 不支持中斷共享(SA_SHIRQ 沒有), 并且不對(duì)系統(tǒng)加密有貢獻(xiàn)(SA_SAMPLE_RANDOM 也沒有). outb 調(diào)用接著為并口使能中斷報(bào)告.
由于某些合理原因, i386 和 x86_64 體系定義了一個(gè)函數(shù)來詢問一個(gè)中斷線的能力:
int can_request_irq(unsigned int irq, unsigned long flags);
這個(gè)函數(shù)當(dāng)試圖分配一個(gè)給定中斷成功時(shí)返回一個(gè)非零值. 但是, 注意, 在 can_request_irq 和 request_irq 的調(diào)用之間事情可能一直改變.
無論何時(shí)一個(gè)硬件中斷到達(dá)處理器, 一個(gè)內(nèi)部的計(jì)數(shù)器遞增, 提供了一個(gè)方法來檢查設(shè)備是否如希望地工作. 報(bào)告的中斷顯示在 /proc/interrupts. 下面的快照取自一個(gè)雙處理器 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 號(hào). 你能夠從沒有的 IRQ 中看到這個(gè)文件只顯示對(duì)應(yīng)已安裝處理的中斷. 例如, 第一個(gè)串口(使用中斷號(hào) 4)沒有顯示, 指示 modem 沒在使用. 事實(shí)上, 即便如果 modem 已更早使用了, 但是在這個(gè)快照時(shí)間沒有使用, 它不會(huì)顯示在這個(gè)文件中; 串口表現(xiàn)很好并且在設(shè)備關(guān)閉時(shí)釋放它們的中斷處理.
/proc/interrupts 的顯示展示了有多少中斷硬件遞交給系統(tǒng)中的每個(gè) CPU. 如同你可從輸出看到的, Linux 內(nèi)核常常在第一個(gè) CPU 上處理中斷, 作為一個(gè)使 cache 局部性最大化的方法.[37] 最后 2 列給出關(guān)于處理中斷的可編程中斷控制器的信息(驅(qū)動(dòng)編寫者不必關(guān)心), 以及已注冊(cè)的中斷處理的設(shè)備的名子(如同在給 request_irq 的參數(shù) dev_name 中指定的).
/proc 樹包含另一個(gè)中斷有關(guān)的文件, /proc/stat; 有時(shí)你會(huì)發(fā)現(xiàn)一個(gè)文件更加有用并且有時(shí)你會(huì)喜歡另一個(gè). /proc/stat 記錄了幾個(gè)關(guān)于系統(tǒng)活動(dòng)的低級(jí)統(tǒng)計(jì)量, 包括(但是不限于)自系統(tǒng)啟動(dòng)以來收到的中斷數(shù). stat 的每一行以一個(gè)文本字串開始, 是該行的關(guān)鍵詞; intr 標(biāo)志是我們?cè)谡业? 下列(截短了)快照是在前一個(gè)后馬上取得的:
intr 5167833 5154006 2 0 2 4907 0 2 68 4 0 4406 9291 50 0 0
第一個(gè)數(shù)是所有中斷的總數(shù), 而其他每一個(gè)代表一個(gè)單個(gè) IRQ 線, 從中斷 0 開始. 所有的計(jì)數(shù)跨系統(tǒng)中所有處理器而匯總的. 這個(gè)快照顯示, 中斷號(hào) 4 已使用 4907 次, 盡管當(dāng)前沒有安裝處理. 如果你在測(cè)試的驅(qū)動(dòng)請(qǐng)求并釋放中斷在每個(gè)打開和關(guān)閉循環(huán), 你可能發(fā)現(xiàn) /proc/stat 比 /proc/interrupts 更加有用.
2 個(gè)文件的另一個(gè)不同是, 中斷不是體系依賴的(也許, 除了末尾幾行), 而 stat 是; 字段數(shù)依賴內(nèi)核之下的硬件. 可用的中斷數(shù)目少到在 SPARC 上的 15 個(gè), 多到 IA-64 上的 256個(gè), 并且其他幾個(gè)系統(tǒng)都不同. 有趣的是要注意, 定義在 x86 中的中斷數(shù)當(dāng)前是 224, 不是你可能期望的 16; 如同在 include/asm-i386/irq.h 中解釋的, 這依賴 Linux 使用體系的限制, 而不是一個(gè)特定實(shí)現(xiàn)的限制( 例如老式 PC 中斷控制器的 16 個(gè)中斷源).
下面是一個(gè) /proc/interrupts 的快照, 取自一臺(tái) 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
驅(qū)動(dòng)在初始化時(shí)最有挑戰(zhàn)性的問題中的一個(gè)是如何決定設(shè)備要使用哪個(gè) IRQ 線. 驅(qū)動(dòng)需要信息來正確安裝處理. 盡管程序員可用請(qǐng)求用戶在加載時(shí)指定中斷號(hào), 這是個(gè)壞做法, 因?yàn)榇蟛糠謺r(shí)間用戶不知道這個(gè)號(hào), 要么因?yàn)樗慌渲锰€要么因?yàn)樵O(shè)備是無跳線的. 大部分用戶希望他們的硬件"僅僅工作"并且不感興趣如中斷號(hào)的問題. 因此自動(dòng)檢測(cè)中斷號(hào)是一個(gè)驅(qū)動(dòng)可用性的基本需求.
有時(shí)自動(dòng)探測(cè)依賴知道一些設(shè)備有很少改變的缺省動(dòng)作的特性. 在這個(gè)情況下, 驅(qū)動(dòng)可能假設(shè)缺省值適用. 這確切地就是 short 如何缺省對(duì)并口動(dòng)作的. 實(shí)現(xiàn)是直接的, 如 short 自身顯示的:
if (short_irq < 0) /* not yet specified: force the default on */
switch(short_base) {
case 0x378: short_irq = 7; break;
case 0x278: short_irq = 2; break;
case 0x3bc: short_irq = 5; break;
}
代碼根據(jù)選擇的 I/O 基地址賦值中斷號(hào), 而允許用戶在加載時(shí)覆蓋缺省值, 使用如:
insmod ./short.ko irq=x
short_base defaults to 0x378, so short_irq defaults to 7.
有些設(shè)備設(shè)計(jì)得更高級(jí)并且簡(jiǎn)單地"宣布"它們要使用的中斷. 在這個(gè)情況下, 驅(qū)動(dòng)獲取中斷號(hào)通過從設(shè)備的一個(gè) I/O 端口或者 PCI 配置空間讀一個(gè)狀態(tài)字節(jié). 當(dāng)目標(biāo)設(shè)備是一個(gè)有能力告知驅(qū)動(dòng)它要使用哪個(gè)中斷的設(shè)備時(shí), 自動(dòng)探測(cè)中斷號(hào)只是意味著探測(cè)設(shè)備, 探測(cè)中斷沒有其他工作要做. 幸運(yùn)的是大部分現(xiàn)代硬件這樣工作; 例如, PCI 標(biāo)準(zhǔn)解決了這個(gè)問題通過要求外設(shè)來聲明它們要使用哪個(gè)中斷線. PCI 標(biāo)準(zhǔn)在 12 章討論.
不幸的是, 不是每個(gè)設(shè)備是對(duì)程序員友好的, 并且自動(dòng)探測(cè)可能需要一些探測(cè). 這個(gè)技術(shù)非常簡(jiǎn)單: 驅(qū)動(dòng)告知設(shè)備產(chǎn)生中斷并且觀察發(fā)生了什么. 如果所有事情進(jìn)展地好, 只有一個(gè)中斷線被激活.
盡管探測(cè)在理論上簡(jiǎn)單的, 實(shí)際的實(shí)現(xiàn)可能不清晰. 我們看 2 種方法來進(jìn)行這個(gè)任務(wù): 調(diào)用內(nèi)核定義的幫助函數(shù)和實(shí)現(xiàn)我們自己的版本.
Linux 內(nèi)核提供了一個(gè)低級(jí)設(shè)施來探測(cè)中斷號(hào). 它只為非共享中斷, 但是大部分能夠在共享中斷狀態(tài)工作的硬件提供了更好的方法來盡量發(fā)現(xiàn)配置的中斷號(hào).這個(gè)設(shè)施包括 2 個(gè)函數(shù), 在<linux/interrupt.h> 中聲明( 也描述了探測(cè)機(jī)制 ).
unsigned long probe_irq_on(void);
這個(gè)函數(shù)返回一個(gè)未安排的中斷的位掩碼. 驅(qū)動(dòng)必須保留返回的位掩碼, 并且在后面?zhèn)鬟f給 probe_irq_off. 在這個(gè)調(diào)用之后, 驅(qū)動(dòng)應(yīng)當(dāng)安排它的設(shè)備產(chǎn)生至少一次中斷.
int probe_irq_off(unsigned long);
在設(shè)備已請(qǐng)求一個(gè)中斷后, 驅(qū)動(dòng)調(diào)用這個(gè)函數(shù), 作為參數(shù)傳遞之前由 probe_irq_on 返回的位掩碼. probe_irq_off 返回在"probe_on"之后發(fā)出的中斷號(hào). 如果沒有中斷發(fā)生, 返回 0 (因此, IRQ 0 不能探測(cè), 但是沒有用戶設(shè)備能夠在任何支持的體系上使用它). 如果多于一個(gè)中斷發(fā)生( 模糊的探測(cè) ), probe_irq_off 返回一個(gè)負(fù)值.
程序員應(yīng)當(dāng)小心使能設(shè)備上的中斷, 在調(diào)用 probe_irq_on 之后以及在調(diào)用 probe_irq_off 后禁止它們. 另外, 你必須記住服務(wù)你的設(shè)備中掛起的中斷, 在 probe_irq_off 之后.
short 模塊演示了如何使用這樣的探測(cè). 如果你加載模塊使用 probe=1, 下列代碼被執(zhí)行來探測(cè)你的中斷線, 如果并口連接器的管腳 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 < 0 && count++ < 5);
if (short_irq < 0)
printk("short: probe failed %i times, giving up\n", count);
注意 udelay 的使用, 在調(diào)用 probe_irq_off 之前. 依賴你的處理器的速度, 你可能不得不等待一小段時(shí)間來給中斷時(shí)間來真正被遞交.
探測(cè)可能是一個(gè)長(zhǎng)時(shí)間的任務(wù). 雖然對(duì)于 short 這不是真的, 例如, 探測(cè)一個(gè)幀抓取器, 需要一個(gè)至少 20 ms 的延時(shí)( 對(duì)處理器是一個(gè)時(shí)代 ), 并且其他的設(shè)備可能要更長(zhǎng). 因此, 最好只探測(cè)中斷線一次, 在模塊初始化時(shí), 獨(dú)立于你是否在設(shè)備打開時(shí)安裝處理(如同你應(yīng)當(dāng)做的), 或者在初始化函數(shù)當(dāng)中(這個(gè)不推薦).
有趣的是注意在一些平臺(tái)上(PoweerPC, M68K, 大部分 MIPS 實(shí)現(xiàn), 以及 2 個(gè) SPARC 版本)探測(cè)是不必要的, 并且, 因此, 之前的函數(shù)只是空的占位者, 有時(shí)稱為"無用的 ISA 廢話". 在其他平臺(tái)上, 探測(cè)只為 ISA 設(shè)備實(shí)現(xiàn). 無論如何, 大部分體系定義了函數(shù)( 即便它們是空的 )來簡(jiǎn)化移植現(xiàn)存的設(shè)備驅(qū)動(dòng).
探測(cè)也可以在驅(qū)動(dòng)自身實(shí)現(xiàn)沒有太大麻煩. 它是一個(gè)少有的驅(qū)動(dòng)必須實(shí)現(xiàn)它自己的探測(cè), 但是看它是如何工作的能夠給出對(duì)這個(gè)過程的內(nèi)部認(rèn)識(shí). 為此目的, short 模塊進(jìn)行 do-it-yourself 的 IRQ 線探測(cè), 如果它使用 probe=2 加載.
這個(gè)機(jī)制與前面描述的相同: 使能所有未使用的中斷, 接著等待并觀察發(fā)生什么. 我們能夠, 然而, 利用我們對(duì)設(shè)備的知識(shí). 常常地一個(gè)設(shè)備能夠配置為使用一個(gè) IRQ 號(hào)從 3 個(gè)或者 4 個(gè)一套; 只探測(cè)這些 IRQ 使我們能夠探測(cè)正確的一個(gè), 不必測(cè)試所有的可能中斷.
short 實(shí)現(xiàn)假定 3, 5, 7, 和 9 是唯一可能的 IRQ 值. 這些數(shù)實(shí)際上是一些并口設(shè)備允許你選擇的數(shù).
下面的代碼通過測(cè)試所有"可能的"中斷并且查看發(fā)生的事情來探測(cè)中斷. trials 數(shù)組列出要嘗試的中斷, 以 0 作為結(jié)尾標(biāo)志; tried 數(shù)組用來跟蹤哪個(gè)處理實(shí)際上被這個(gè)驅(qū)動(dòng)注冊(cè).
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]; i++)
tried[i] = request_irq(trials[i], 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 <=0 && count++ < 5);
/* end of loop, uninstall the handler */
for (i = 0; trials[i]; i++)
if (tried[i] == 0)
free_irq(trials[i], NULL);
if (short_irq < 0)
printk("short: probe failed %i times, giving up\n", count);
你可能事先不知道"可能的" IRQ 值是什么. 在這個(gè)情況, 你需要探測(cè)所有空閑的中斷, 不是限制你自己在幾個(gè) trials[]. 為探測(cè)所有的中斷, 你不得不從 IRQ 0 到 IRQ NR_IRQS-1 探測(cè), 這里 NR_IRQS 在 <asm/irq.h> 中定義并且是獨(dú)立于平臺(tái)的.
現(xiàn)在我們只缺少探測(cè)處理自己了. 處理者的角色是更新 short_irq, 根據(jù)實(shí)際收到哪個(gè)中斷. short_irq 中的 0 值意味著"什么沒有", 而一個(gè)負(fù)值意味著"模糊的". 這些值選擇來和 probe_irq_off 相一致并且允許同樣的代碼來調(diào)用任一種 short.c 中的探測(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ù).
老版本的 Linux 內(nèi)核盡了很大努力來區(qū)分"快速"和"慢速"中斷. 快速中斷是那些能夠很快處理的, 而處理慢速中斷要特別地長(zhǎng)一些. 慢速中斷可能十分苛求處理器, 并且它值得在處理的時(shí)候重新使能中斷. 否則, 需要快速注意的任務(wù)可能被延時(shí)太長(zhǎng).
在現(xiàn)代內(nèi)核中, 快速和慢速中斷的大部分不同已經(jīng)消失. 剩下的僅僅是一個(gè): 快速中斷(那些使用 SA_INTERRUPT 被請(qǐng)求的)執(zhí)行時(shí)禁止所有在當(dāng)前處理器上的其他中斷. 注意其他的處理器仍然能夠處理中斷, 盡管你從不會(huì)看到 2 個(gè)處理器同時(shí)處理同一個(gè) IRQ.
這樣, 你的驅(qū)動(dòng)應(yīng)當(dāng)使用哪個(gè)類型的中斷? 在現(xiàn)代系統(tǒng)上, SA_INTERRUPT 只是打算用在幾個(gè), 特殊的情況例如時(shí)鐘中斷. 除非你有一個(gè)充足的理由來運(yùn)行你的中斷處理在禁止其他中斷情況下, 你不應(yīng)當(dāng)使用 SA_INTERRUPT.
這個(gè)描述應(yīng)當(dāng)滿足大部分讀者, 盡管有人喜好硬件并且對(duì)她的計(jì)算機(jī)有經(jīng)驗(yàn)可能有興趣深入一些. 如果你不關(guān)心內(nèi)部的細(xì)節(jié), 你可跳到下一節(jié).
這個(gè)描述是從 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)核而推知的; 盡管一般的概念保持一致, 硬件細(xì)節(jié)在其他平臺(tái)上不同.
中斷處理的最低級(jí)是在 entry.S, 一個(gè)匯編語言文件處理很多機(jī)器級(jí)別的工作. 通過一點(diǎn)匯編器的技巧和一些宏定義, 一點(diǎn)代碼被安排到每個(gè)可能的中斷. 在每個(gè)情況下, 這個(gè)代碼將中斷號(hào)壓棧并且跳轉(zhuǎn)到一個(gè)通用段, 稱為 do_IRQ, 在 irq.c 中定義.
do_IRQ 做的第一件事是確認(rèn)中斷以便中斷控制器能夠繼續(xù)其他事情. 它接著獲取給定 IRQ 號(hào)的一個(gè)自旋鎖, 因此阻止任何其他 CPU 處理這個(gè) IRQ. 它清除幾個(gè)狀態(tài)位(包括稱為 IRQ_WAITING 的一個(gè), 我們很快會(huì)看到它)并且接著查看這個(gè)特殊 IRQ 的處理者. 如果沒有處理者, 什么不作; 自旋鎖釋放, 任何掛起的軟件中斷被處理, 最后 do_IRQ 返回.
常常, 但是, 如果一個(gè)設(shè)備在中斷, 至少也有一個(gè)處理者注冊(cè)給它的 IRQ. 函數(shù) handle_IRQ_event 被調(diào)用來實(shí)際調(diào)用處理者. 如果處理者是慢速的( SA_INTERRUPT 沒有設(shè)置 ), 中斷在硬件中被重新使能, 并且調(diào)用處理者. 接著僅僅是清理, 運(yùn)行軟件中斷, 以及回到正常的工作. "常規(guī)工作"很可能已經(jīng)由于中斷而改變了(處理者可能喚醒一個(gè)進(jìn)程, 例如), 因此從中斷中返回的最后的事情是一個(gè)處理器的可能的重新調(diào)度.
探測(cè) IRQ 通過設(shè)置 IRQ_WAITING 狀態(tài)位給每個(gè)當(dāng)前缺乏處理者的 IRQ 來完成. 當(dāng)中斷發(fā)生, do_IRQ 清除這個(gè)位并且接著返回, 因?yàn)闆]有注冊(cè)處理者. probe_irq_off, 當(dāng)被一個(gè)函數(shù)調(diào)用, 需要只搜索不再有 IRQ_WAITING 設(shè)置的 IRQ.
至今, 我們已學(xué)習(xí)了注冊(cè)一個(gè)中斷處理, 但是沒有編寫一個(gè). 實(shí)際上, 對(duì)于一個(gè)處理者, 沒什么不尋常的 -- 它是普通的 C 代碼.
唯一的特別之處是一個(gè)處理者在中斷時(shí)運(yùn)行, 因此, 它能做的事情遭受一些限制. 這些限制與我們?cè)趦?nèi)核定時(shí)器上看到的相同. 一個(gè)處理者不能傳遞數(shù)據(jù)到或者從用戶空間, 因?yàn)樗辉谶M(jìn)程上下文執(zhí)行. 處理者也不能做任何可能睡眠的事情, 例如調(diào)用 wait_event, 使用除 GFP_ATOMIC 之外任何東西來分配內(nèi)存, 或者加鎖一個(gè)旗標(biāo). 最后, 處理者不能調(diào)用調(diào)度.
一個(gè)中斷處理的角色是給它的設(shè)備關(guān)于中斷接收的回應(yīng)并且讀或?qū)憯?shù)據(jù), 根據(jù)被服務(wù)的中斷的含義. 第一步常常包括清除接口板上的一位; 大部分硬件設(shè)備不產(chǎn)生別的中斷直到它們的"中斷掛起"位被清除. 根據(jù)你的硬件如何工作的, 這一步可能需要在最后做而不是開始; 這里沒有通吃的規(guī)則. 一些設(shè)備不需要這步, 因?yàn)樗鼈儧]有一個(gè)"中斷掛起"位; 這樣的設(shè)備是一少數(shù), 盡管并口是其中之一. 由于這個(gè)理由, short 不必清除這樣一個(gè)位.
一個(gè)中斷處理的典型任務(wù)是喚醒睡眠在設(shè)備上的進(jìn)程, 如果中斷指示它們?cè)诘却氖录? 例如新數(shù)據(jù)的到達(dá).
為堅(jiān)持幀抓取者的例子, 一個(gè)進(jìn)程可能請(qǐng)求一個(gè)圖像序列通過連續(xù)讀設(shè)備; 讀調(diào)用阻塞在讀取每個(gè)幀之前, 而中斷處理喚醒進(jìn)程一旦每個(gè)新幀到達(dá). 這個(gè)假定抓取器中斷處理器來指示每個(gè)新幀的成功到達(dá).
程序員應(yīng)當(dāng)小心編寫一個(gè)函數(shù)在最小量的時(shí)間內(nèi)執(zhí)行, 不管是一個(gè)快速或慢速處理者. 如果需要進(jìn)行長(zhǎng)時(shí)間計(jì)算, 最好的方法是使用一個(gè) tasklet 或者 workqueue 來調(diào)度計(jì)算在一個(gè)更安全的時(shí)間(我們將在"上和下半部"一節(jié)中見到工作如何被延遲.).
我們?cè)?short 中的例子代碼響應(yīng)中斷通過調(diào)用 do_gettimeofday 和 打印當(dāng)前時(shí)間到一個(gè)頁大小的環(huán)形緩存. 它接著喚醒任何讀進(jìn)程, 因?yàn)楝F(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;
}
這個(gè)代碼, 盡管簡(jiǎn)單, 代表了一個(gè)中斷處理的典型工作. 依次地, 它稱為 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;
}
這個(gè)函數(shù)已經(jīng)仔細(xì)編寫來回卷指向環(huán)形緩存的指針, 沒有暴露一個(gè)不正確的值. 這里的 barrier 調(diào)用來阻止編譯器在這個(gè)函數(shù)的其他 2 行之間優(yōu)化. 如果沒有 barrier, 編譯器可能決定優(yōu)化掉 new 變量并且直接賦值給 *index. 這個(gè)優(yōu)化可能暴露一個(gè) index 的不正確值一段時(shí)間, 在它回卷的地方. 通過小心阻止對(duì)其他線程可見的不一致的值, 我們能夠安全操作環(huán)形緩存指針而不用鎖.
用來讀取中斷時(shí)填充的緩存的設(shè)備文件是 /dev/shortint. 這個(gè)設(shè)備特殊文件, 同 /dev/shortprint 一起, 不在第 9 章介紹, 因?yàn)樗氖褂脤?duì)中斷處理是特殊的. /dev/shortint 內(nèi)部特別地為中斷產(chǎn)生和報(bào)告剪裁過. 寫到設(shè)備會(huì)每隔一個(gè)字節(jié)產(chǎn)生一個(gè)中斷; 讀取設(shè)備給出了每個(gè)中斷被報(bào)告的時(shí)間.
如果你連接并口連接器的管腳 9 和 10, 你可產(chǎn)生中斷通過拉高并口數(shù)據(jù)字節(jié)的高位. 這可通過寫二進(jìn)制數(shù)據(jù)到 /dev/short0 或者通過寫任何東西到 /dev/shortint 來完成.
[38]下列代碼為 /dev/shortint 實(shí)現(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 < 0) /* wrapped */
count0 = short_buffer + PAGE_SIZE - short_tail;
if (count0 < count)
count = count0;
if (copy_to_user(buf, (char *)short_tail, count))
return -EFAULT;
short_incr_bp (&short_tail, count);
return count;
}
ssize_t short_i_write (struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
int written = 0, odd = *f_pos & 1;
unsigned long port = short_base; /* output to the parallel data latch */
void *address = (void *) short_base;
if (use_mem)
{
while (written < count)
iowrite8(0xff * ((++written + odd) & 1), address);
} else
{
while (written < count)
outb(0xff * ((++written + odd) & 1), port);
}
*f_pos += count;
return written;
}
其他設(shè)備特殊文件, /dev/shortprint, 使用并口來驅(qū)動(dòng)一個(gè)打印機(jī); 你可用使用它, 如果你想避免連接一個(gè) D-25 連接器管腳 9 和 10. shortprint 的寫實(shí)現(xiàn)使用一個(gè)環(huán)形緩存來存儲(chǔ)要打印的數(shù)據(jù), 而寫實(shí)現(xiàn)是剛剛展示的那個(gè)(因此你能夠讀取你的打印機(jī)吃進(jìn)每個(gè)字符用的時(shí)間).
為了支持打印機(jī)操作, 中斷處理從剛剛展示的那個(gè)已經(jīng)稍微修改, 增加了發(fā)送下一個(gè)數(shù)據(jù)字節(jié)到打印機(jī)的能力, 如果沒有更多數(shù)據(jù)傳送.
盡管 short 忽略了它們, 一個(gè)傳遞給一個(gè)中斷處理的參數(shù): irq, dev_id, 和 regs. 我們看一下每個(gè)的角色.
中斷號(hào)( int irq )作為你可能在你的 log 消息中打印的信息是有用的, 如果有. 第二個(gè)參數(shù), void dev_id, 是一類客戶數(shù)據(jù); 一個(gè) void 參數(shù)傳遞給 request_irq, 并且同樣的指針接著作為一個(gè)參數(shù)傳回給處理者, 當(dāng)中斷發(fā)生時(shí). 你常常傳遞一個(gè)指向你的在 dev_id 中的設(shè)備數(shù)據(jù)結(jié)構(gòu)的指針, 因此一個(gè)管理相同設(shè)備的幾個(gè)實(shí)例的驅(qū)動(dòng)不需要任何額外的代碼, 在中斷處理中找出哪個(gè)設(shè)備要負(fù)責(zé)當(dāng)前的中斷事件.
這個(gè)參數(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 */
/* .... */
}
和這個(gè)處理者關(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;
}
最后一個(gè)參數(shù), struct pt_regs *regs, 很少用到. 它持有一個(gè)處理器的上下文在進(jìn)入中斷狀態(tài)前的快照. 寄存器可用來監(jiān)視和調(diào)試; 對(duì)于常規(guī)地設(shè)備驅(qū)動(dòng)任務(wù), 正常地不需要它們.
中斷處理應(yīng)當(dāng)返回一個(gè)值指示是否真正有一個(gè)中斷要處理. 如果處理者發(fā)現(xiàn)它的設(shè)備確實(shí)需要注意, 它應(yīng)當(dāng)返回 IRQ_HANDLED; 否則返回值應(yīng)當(dāng)是 IRQ_NONE. 你也可產(chǎn)生返回值, 使用這個(gè)宏:
IRQ_RETVAL(handled)
這里, handled 是非零, 如果你能夠處理中斷. 內(nèi)核用返回值來檢測(cè)和抑制假中斷. 如果你的設(shè)備沒有給你方法來告知是否它確實(shí)中斷, 你應(yīng)當(dāng)返回 IRQ_HANDLED.
有時(shí)設(shè)備驅(qū)動(dòng)必須阻塞中斷的遞交一段時(shí)間(希望地短)(我們?cè)诘?5 章的 "自旋鎖"一節(jié)看到過這樣的一個(gè)情況). 常常, 中斷必須被阻塞當(dāng)持有一個(gè)自旋鎖來避免死鎖系統(tǒng)時(shí). 有幾個(gè)方法來禁止不涉及自旋鎖的中斷. 但是在我們討論它們之前, 注意禁止中斷應(yīng)當(dāng)是一個(gè)相對(duì)少見的行為, 即便在設(shè)備驅(qū)動(dòng)中, 并且這個(gè)技術(shù)應(yīng)當(dāng)從不在驅(qū)動(dòng)中用做互斥機(jī)制.
有時(shí)(但是很少!)一個(gè)驅(qū)動(dòng)需要禁止一個(gè)特定中斷線的中斷遞交. 內(nèi)核提供了 3 個(gè)函數(shù)為此目的, 所有都聲明在 <asm/irq.h>. 這些函數(shù)是內(nèi)核 API 的一部分, 因此我們描述它們, 但是它們的使用在大部分驅(qū)動(dòng)中不鼓勵(lì). 在其他的中, 你不能禁止共享的中斷線, 并且, 在現(xiàn)代的系統(tǒng)中, 共享的中斷是規(guī)范. 已說過的, 它們?cè)谶@里:
void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);
調(diào)用任一函數(shù)可能更新在可編程控制器(PIC)中的特定 irq 的掩碼, 因此禁止或使能跨所有處理器的特定 IRQ. 對(duì)這些函數(shù)的調(diào)用能夠嵌套 -- 如果 disable_irq 被連續(xù)調(diào)用 2 次, 需要 2 個(gè) enable_irq 調(diào)用在 IRQ 被真正重新使能前. 可能調(diào)用這些函數(shù)從一個(gè)中斷處理中, 但是在處理它時(shí)使能你自己的 IRQ 常常不是一個(gè)好做法.
disable_irq 不僅禁止給定的中斷, 還等待一個(gè)當(dāng)前執(zhí)行的中斷處理結(jié)束, 如果有. 要知道如果調(diào)用 disable_irq 的線程持有中斷處理需要的任何資源(例如自旋鎖), 系統(tǒng)可能死鎖. disable_irq_nosync 與 disable_irq 不同, 它立刻返回. 因此, 使用disable_irq_nosync 快一點(diǎn), 但是可能使你的設(shè)備有競(jìng)爭(zhēng)情況.
但是為什么禁止中斷? 堅(jiān)持說并口, 我們看一下 plip 網(wǎng)絡(luò)接口. 一個(gè) plip 設(shè)備使用裸并口來傳送數(shù)據(jù). 因?yàn)橹挥?5 位可以從并口連接器讀出, 它們被解釋為 4 個(gè)數(shù)據(jù)位和一個(gè)時(shí)鐘/握手信號(hào). 當(dāng)一個(gè)報(bào)文的第一個(gè) 4 位被 initiator (發(fā)送報(bào)文的接口) 傳送, 時(shí)鐘線被拉高, 使接收接口來中斷處理器. plip 處理者接著被調(diào)用來處理新到達(dá)的數(shù)據(jù).
在設(shè)備已經(jīng)被提醒了后, 數(shù)據(jù)傳送繼續(xù), 使用握手線來傳送數(shù)據(jù)到接收接口(這可能不是最好的實(shí)現(xiàn), 但是有必要與使用并口的其他報(bào)文驅(qū)動(dòng)兼容). 如果接收接口不得不為每個(gè)接收的字節(jié)處理 2 次中斷, 性能可能不可忍受. 因此, 驅(qū)動(dòng)在接收?qǐng)?bào)文的時(shí)候禁止中斷; 相反, 一個(gè)查詢并延時(shí)的循環(huán)用來引入數(shù)據(jù).
類似地, 因?yàn)閺慕邮掌鞯桨l(fā)送器的握手線用來確認(rèn)數(shù)據(jù)接收, 發(fā)送接口禁止它的 IRQ 線在報(bào)文發(fā)送時(shí).
如果你需要禁止所有中斷如何? 在 2.6 內(nèi)核, 可能關(guān)閉在當(dāng)前處理器上所有中斷處理, 使用任一個(gè)下面 2 個(gè)函數(shù)(定義在 <asm/system.h>):
void local_irq_save(unsigned long flags);
void local_irq_disable(void);
一個(gè)對(duì) local_irq_save 的調(diào)用在當(dāng)前處理器上禁止中斷遞交, 在保存當(dāng)前中斷狀態(tài)到 flags 之后. 注意, flags 是直接傳遞, 不是通過指針. local_irq_disable 關(guān)閉本地中斷遞交而不保存狀態(tài); 你應(yīng)當(dāng)使用這個(gè)版本只在你知道中斷沒有在別處被禁止.
完成打開中斷, 使用:
void local_irq_restore(unsigned long flags);
void local_irq_enable(void);
第一個(gè)版本恢復(fù)由 local_irq_save 存儲(chǔ)于 flags 的狀態(tài), 而 local_irq_enable 無條件打開中斷. 不象 disable_irq, local_irq_disable 不跟蹤多次調(diào)用. 如果調(diào)用鏈中有多于一個(gè)函數(shù)可能需要禁止中斷, 應(yīng)該使用 local_irq_save.
在 2.6 內(nèi)核, 沒有方法全局性地跨整個(gè)系統(tǒng)禁止所有的中斷. 內(nèi)核開發(fā)者決定, 關(guān)閉所有中斷的開銷太高, 并且在任何情況下沒有必要有這個(gè)能力. 如果你在使用一個(gè)舊版本驅(qū)動(dòng), 它調(diào)用諸如 cli 和 sti, 你需要在它在 2.6 下工作前更新它為使用正確的加鎖
[37] 盡管, 一些大系統(tǒng)明確使用中斷平衡機(jī)制來在系統(tǒng)間分散中斷負(fù)載.
[38] 這個(gè) shortint 設(shè)備完成它的任務(wù), 通過交替地寫入 0x00 和 0xff 到并口.
更多建議: