10.2. 安裝一個中斷處理

2018-02-24 15:50 更新

10.2.?安裝一個中斷處理

如果你想實際地"看到"產(chǎn)生的中斷, 向硬件設(shè)備寫不足夠; 一個軟件處理必須在系統(tǒng)中配置. 如果 Linux 內(nèi)核還沒有被告知來期待你的中斷, 它簡單地確認(rèn)并忽略它.

中斷線是一個寶貴且常常有限的資源, 特別當(dāng)它們只有 15 或者 16 個時. 內(nèi)核保持了中斷線的一個注冊, 類似于 I/O 端口的注冊. 一個模塊被希望來請求一個中斷通道(或者 IRQ, 對于中斷請求), 在使用它之前, 并且當(dāng)結(jié)束時釋放它. 在很多情況下, 也希望模塊能夠與其他驅(qū)動共享中斷線, 如同我們將看到的. 下面的函數(shù), 聲明在 <linux/interrupt.h>, 實現(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 指示成功, 或者是一個負(fù)的錯誤碼, 如同平常. 函數(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)生的中斷能夠有貢獻(xiàn)給 /dev/random 和 /dev/urandom 使用的加密池. 這些設(shè)備在讀取時返回真正的隨機(jī)數(shù)并且設(shè)計來幫助應(yīng)用程序軟件為加密選擇安全鑰. 這樣的隨機(jī)數(shù)從一個由各種隨機(jī)事件貢獻(xiàn)的加密池中提取的. 如果你的設(shè)備以真正隨機(jī)的時間產(chǎn)生中斷, 你應(yīng)當(dāng)設(shè)置這個標(biāo)志. 如果, 另一方面, 你的中斷是可預(yù)測的( 例如, 一個幀抓取器的場消隱), 這個標(biāo)志不值得設(shè)置 -- 它無論如何不會對系統(tǒng)加密有貢獻(xiàn). 可能被攻擊者影響的設(shè)備不應(yīng)當(dāng)設(shè)置這個標(biāo)志; 例如, 網(wǎng)絡(luò)驅(qū)動易遭受從外部計時的可預(yù)測報文并且不應(yīng)當(dāng)對加密池有貢獻(xiàn). 更多信息看 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 還在加載時請求它的中斷線. 這樣做是為了你可以運行測試程序而不必運行一個額外的進(jìn)程來保持設(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)加密有貢獻(xiàn)(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 接口

無論何時一個硬件中斷到達(dá)處理器, 一個內(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 < 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 基地址賦值中斷號, 而允許用戶在加載時覆蓋缺省值, 使用如:


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ā)生了什么. 如果所有事情進(jìn)展地好, 只有一個中斷線被激活.

盡管探測在理論上簡單的, 實際的實現(xiàn)可能不清晰. 我們看 2 種方法來進(jìn)行這個任務(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ù), 在<linux/interrupt.h> 中聲明( 也描述了探測機(jī)制 ).

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 返回一個負(fù)值.

程序員應(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 < 0 && count++ < 5);
if (short_irq < 0)
        printk("short: probe failed %i times, giving up\n", count);

注意 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)部認(rèn)識. 為此目的, short 模塊進(jìn)行 do-it-yourself 的 IRQ 線探測, 如果它使用 probe=2 加載.

這個機(jī)制與前面描述的相同: 使能所有未使用的中斷, 接著等待并觀察發(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]; 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 值是什么. 在這個情況, 你需要探測所有空閑的中斷, 不是限制你自己在幾個 trials[]. 為探測所有的中斷, 你不得不從 IRQ 0 到 IRQ NR_IRQS-1 探測, 這里 NR_IRQS 在 <asm/irq.h> 中定義并且是獨立于平臺的.

現(xiàn)在我們只缺少探測處理自己了. 處理者的角色是更新 short_irq, 根據(jù)實際收到哪個中斷. short_irq 中的 0 值意味著"什么沒有", 而一個負(fù)值意味著"模糊的". 這些值選擇來和 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ī)有經(jīng)驗可能有興趣深入一些. 如果你不關(guān)心內(nèi)部的細(xì)節(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)核而推知的; 盡管一般的概念保持一致, 硬件細(xì)節(jié)在其他平臺上不同.

中斷處理的最低級是在 entry.S, 一個匯編語言文件處理很多機(jī)器級別的工作. 通過一點匯編器的技巧和一些宏定義, 一點代碼被安排到每個可能的中斷. 在每個情況下, 這個代碼將中斷號壓棧并且跳轉(zhuǎn)到一個通用段, 稱為 do_IRQ, 在 irq.c 中定義.

do_IRQ 做的第一件事是確認(rèn)中斷以便中斷控制器能夠繼續(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)由于中斷而改變了(處理者可能喚醒一個進(jìn)程, 例如), 因此從中斷中返回的最后的事情是一個處理器的可能的重新調(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ù)到或者從用戶空間, 因為它不在進(jìn)程上下文執(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è)備上的進(jìn)程, 如果中斷指示它們在等待的事件, 例如新數(shù)據(jù)的到達(dá).

為堅持幀抓取者的例子, 一個進(jìn)程可能請求一個圖像序列通過連續(xù)讀設(shè)備; 讀調(diào)用阻塞在讀取每個幀之前, 而中斷處理喚醒進(jìn)程一旦每個新幀到達(dá). 這個假定抓取器中斷處理器來指示每個新幀的成功到達(dá).

程序員應(yīng)當(dāng)小心編寫一個函數(shù)在最小量的時間內(nèi)執(zhí)行, 不管是一個快速或慢速處理者. 如果需要進(jìn)行長時間計算, 最好的方法是使用一個 tasklet 或者 workqueue 來調(diào)度計算在一個更安全的時間(我們將在"上和下半部"一節(jié)中見到工作如何被延遲.).

我們在 short 中的例子代碼響應(yīng)中斷通過調(diào)用 do_gettimeofday 和 打印當(dāng)前時間到一個頁大小的環(huán)形緩存. 它接著喚醒任何讀進(jì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)仔細(xì)編寫來回卷指向環(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é)的高位. 這可通過寫二進(jìn)制數(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 < 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ū)動一個打印機(jī); 你可用使用它, 如果你想避免連接一個 D-25 連接器管腳 9 和 10. shortprint 的寫實現(xiàn)使用一個環(huán)形緩存來存儲要打印的數(shù)據(jù), 而寫實現(xiàn)是剛剛展示的那個(因此你能夠讀取你的打印機(jī)吃進(jìn)每個字符用的時間).

為了支持打印機(jī)操作, 中斷處理從剛剛展示的那個已經(jīng)稍微修改, 增加了發(fā)送下一個數(shù)據(jù)字節(jié)到打印機(jī)的能力, 如果沒有更多數(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è)備要負(fù)責(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, 很少用到. 它持有一個處理器的上下文在進(jìn)入中斷狀態(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ū)動中用做互斥機(jī)制.

10.2.6.1.?禁止單個中斷

有時(但是很少!)一個驅(qū)動需要禁止一個特定中斷線的中斷遞交. 內(nèi)核提供了 3 個函數(shù)為此目的, 所有都聲明在 <asm/irq.h>. 這些函數(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)用來處理新到達(dá)的數(shù)據(jù).

在設(shè)備已經(jīng)被提醒了后, 數(shù)據(jù)傳送繼續(xù), 使用握手線來傳送數(shù)據(jù)到接收接口(這可能不是最好的實現(xiàn), 但是有必要與使用并口的其他報文驅(qū)動兼容). 如果接收接口不得不為每個接收的字節(jié)處理 2 次中斷, 性能可能不可忍受. 因此, 驅(qū)動在接收報文的時候禁止中斷; 相反, 一個查詢并延時的循環(huán)用來引入數(shù)據(jù).

類似地, 因為從接收器到發(fā)送器的握手線用來確認(rèn)數(shù)據(jù)接收, 發(fā)送接口禁止它的 IRQ 線在報文發(fā)送時.

10.2.6.2.?禁止所有中斷

如果你需要禁止所有中斷如何? 在 2.6 內(nèi)核, 可能關(guān)閉在當(dāng)前處理器上所有中斷處理, 使用任一個下面 2 個函數(shù)(定義在 <asm/system.h>):


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)明確使用中斷平衡機(jī)制來在系統(tǒng)間分散中斷負(fù)載.

[38] 這個 shortint 設(shè)備完成它的任務(wù), 通過交替地寫入 0x00 和 0xff 到并口.

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號