linux設備驅動程序學習(10) 中斷處理

儘管有些設備僅僅通過控制其寄存器就可以得到控制,但現實中的大部分設備卻要比這複雜一些。因爲大部分設備的處理時間與處理器不在同一個週期,且一定會比處理器慢的多,這就造成了一種讓處理器等待設備的現象,顯然這是不行的,而有一種解決方法就是中斷操作。

中斷僅僅就是一個信號,當硬件需要獲得處理器對它的關注時,就可以發送這個信號。 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 的位置是設備最後一次被關閉、硬件被告知不用再中斷處理器之後。但這種方式的缺點是必須爲每個設備維護一個打開計數。

以下是中斷申請的示例(並口):

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 { /*打開中斷硬件的中斷能力*/
                outb(0x10,short_base+2);
        }
}

i386 和 x86_64 體系定義了一個函數來查詢一箇中斷線是否可用:

int can_request_irq(unsigned int irq, unsigned long flags);

/*當能夠成功分配給定中斷,則返回非零值。但注意,在 can_request_irq 和 request_irq 的調用之間給定中斷可能被佔用*/

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的定義爲:

#define NR_IRQS    128

 

自動檢測 IRQ 號

驅動初始化時最迫切的問題之一是決定設備要使用的IRQ 線,驅動需要信息來正確安裝處理例程。自動檢測中斷號對驅動的可用性來說是一個基本需求。有時自動探測依賴一些設備具有的默認特性,以下是典型的並口中斷探測程序:

if (short_irq < 0) /* 依靠使並口的端口號,確定中斷*/
switch(short_base) {
case 0x378: short_irq = 7; break;
case 0x278: short_irq = 2; break;
case 0x3bc: short_irq = 5; break;
}

有的驅動允許用戶在加載時覆蓋默認值:

insmod xxxxx.ko irq=x

當目標設備有能力告知驅動它要使用的中斷號時,自動探測中斷號只是意味着探測設備,無需做額外的工作探測中斷。

但不是每個設備都對程序員友好,對於他們還是需要一些探測工作。這個工作技術上非常簡單: 驅動告知設備產生中斷並且觀察發生了什麼。如果一切順利,則只有一箇中斷信號線被激活。儘管探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 連接在一起,探測五次失敗後放棄):

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

最好只在模塊初始化時探測中斷線一次。
大部分體系定義了這兩個函數( 即便是空的 )來簡化設備驅動的移植。

 

DIY探測:

DIY探測與前面原理相同: 使能所有未使用的中斷, 接着等待並觀察發生什麼。我們對設備的瞭解:通常一個設備能夠使用3或4個IRQ 號中的一個來進行配置,只探測這些 IRQ 號使我們能不必測試所有可能的中斷就探測到正確的IRQ 號。

下面的LDD3中的代碼通過測試所有"可能的"中斷並且察看發生的事情來探測中斷。 trials 數組列出要嘗試的中斷, 以 0 作爲結尾標誌; tried 數組用來跟蹤哪個中斷號已經被這個驅動註冊。

int trials[]= {3, 5, 7, 9, 0};
int tried[]={0, 0, 0, 0, 0};
int i, count = 0;


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 */
         /* 等待中斷,若在這段時間有中斷產生,handler會改變 short_irq */
        /* 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");
        }
 } 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);

以下是handler的源碼:

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;
}

若事先不知道"可能的" IRQ ,就需要探測所有空閒的中斷,所以不得不從 IRQ 0 探測到 IRQ NR_IRQS-1

實現中斷處理例程

1.中斷處理例程是在中斷時間內運行的,因此行爲會受到一些限制。這些限制跟我們在內核定時器中看到的一樣。

處理例程不能向用戶空間發送或者接收數據,因爲它不是在任何進程上下文中執行的

處理例程也不能做任何可能發生休眠的操作,例如調用wait_event,使用不帶GFP_ATOMIC標誌的內存分配操作,或者鎖住一個信號量等等

處理例程不能調用schdule函數

2.中斷處理理財的功能就是將有關中斷接收的信息反饋給設備,並根據正在服務的終端的不同含義對對數據進行相應的讀或寫。中斷處理例程第一步常常包括清除設備的一箇中斷標誌位,大部分硬件設備在清除"中斷掛起"位前不會再產生中斷。這也要根據硬件的工作原理決定, 這一步也可能需要在最後做而不是開始; 這裏沒有通用的規則。一些設備不需要這步, 因爲它們沒有一個"中斷掛起"位; 這樣的設備是少數。

3.中斷處理的一個典型 任務:如果中斷通知進程所等待的事件已經發生,比如新的數據已經到達,就會喚醒在設備上休眠的進程。

不管是快速或慢速處理例程,程序員應編寫執行時間儘可能短的處理例程。 如果需要進行長時間計算, 最好的方法是使用 tasklet 或者 workqueue 在一個更安全的時間來調度計算任務。

 

啓用和禁止中斷

 

有時設備驅動必須在一段時間(希望較短)內阻塞中斷髮生。並必須在持有一個自旋鎖時阻塞中斷,以避免死鎖系統。注意:應儘量少禁止中斷,即使是在設備驅動中,且這個技術不應當用於驅動中的互斥機制。

禁止單箇中斷
有時(但是很少!)一個驅動需要禁止一個特定中斷。但不推薦這樣做,特別是不能禁止共享中斷(在現代系統中, 共享的中斷是很常見的)。內核提供了 3 個函數,是內核 API 的一部分,聲明在 <asm/irq.h>:

void disable_irq(int irq);/*禁止給定的中斷, 並等待當前的中斷處理例程結束。如果調用 disable_irq 的線程持有任何中斷處理例程需要的資源(例如自旋鎖), 系統可能死鎖*/
void disable_irq_nosync(int irq);/*禁止給定的中斷後立刻返回(可能引入競態)*/
void enable_irq(int irq);

 
調用任一函數可能更新在可編程控制器(PIC)中的特定 irq 的掩碼, 從而禁止或使能所有處理器特定的 IRQ。這些函數的調用能夠嵌套,即如果 disable_irq 被連續調用 2 次,則需要 2 個 enable_irq 重新使能 IRQ 。可以在中斷處理例程中調用這些函數,但在處理某個IRQ時再打開它是不好的做法。
 
禁止所有中斷
在 2.6 內核, 可使用下面 2 個函數中的任一個(定義在 <asm/system.h>)關閉當前處理器上所有中斷:

void local_irq_save(unsignedlong flags);/*在保存當前中斷狀態到 flags 之後禁止中斷*/
void local_irq_disable(void);/* 關閉中斷而不保存狀態*/
/*如果調用鏈中有多個函數可能需要禁止中斷, 應使用 local_irq_save*/
/*打開中斷使用:*/
void local_irq_restore(unsignedlong flags); 

void local_irq_enable(void);
/*2.6內核,沒有方法全局禁用整個系統上的所有中斷*/

 

頂半部和底半部

中斷處理需要很快完成,並且不需要阻塞太長,所以中斷處理的一個主要問題就是中斷處理例程中完成耗時的任務。

linux通過將中斷處理分成兩部分來完成這個任務:

1.頂半部:實際響應中斷的例程(request_irq註冊的那個例程)

2.底半部:被頂半部調用並在稍後更安全的一個時間裏執行的函數。

他們最大的不同在底半部處理例程執行時,所有中斷都是打開的(這就是所謂的在更安全的時間內運行)。典型的情況是:頂半部保存設備數據到一個設備特定的緩存並調度它的底半部,最後退出:這個操作非常快。底半部接着進行任何其他需要的工作。這種方式的好處是在底半部工作期間,頂半部仍然可以繼續爲新中斷服務。

Linux 內核有 2 個不同的機制可用來實現底半部處理:

1 tasklet (首選機制),它非常快, 但是所有的 tasklet 代碼必須是原子的;

2)工作隊列,它可能有更高的延時,但允許休眠。

tasklet和工作隊列在《時間、延遲及延緩操作》已經介紹過,具體的實現代碼請看實驗源碼!

 

中斷共享

Linux內核支持在所有總線上中斷共享。

安裝共享的處理例程

通過 request_irq來安裝共享中斷與非共享中斷有2點不同:

1)當request_irq時,flags中必須指定SA_SHIRQ位;

2dev_id必須唯一。任何指向模塊地址空間的指針都行,但dev_id絕不能設置爲NULL

內核爲每個中斷維護一箇中斷共享處理例程列表,dev_id就是區別不同處理例程的簽名。釋放處理例程通過執行free_irq實現。 dev_id用來從這個中斷的共享處理例程列表中選擇正確的處理例程來釋放,這就是爲什麼dev_id必須是唯一的.

 

請求一個共享的中斷時,如果滿足下列條件之一,則request_irq成功:

1)中斷線空閒;

2)所有已經註冊該中斷信號線的處理例程也標識了IRQ是共享。

一個共享的處理例程必須能夠識別自己的中斷,並且在自己的設備沒有被中斷時快速退出(返回IRQ_NONE)。

共享處理例程沒有探測函數可用,但使用的中斷信號線是空閒時標準的探測機制纔有效。

一個使用共享處理例程的驅動需要小心:不能使用enable_irqdisable_irq,否則,對其他共享這條線的設備就無法正常工作了。即便短時間禁止中斷,另一設備也可能產生延時而爲設備和其用戶帶來問題。所以程序員必須記住:他的驅動並不是獨佔這個IRQ,它的行爲應當比獨佔這個中斷線更加"社會化"

中斷驅動的I/O

當與驅動程序管理的硬件間的數據傳送可能因爲某種原因而延遲,驅動編寫者應當實現緩存。一個好的緩存機制需採用中斷驅動的I/O,一個輸入緩存在中斷時被填充,並由讀取設備的進程取走緩衝區的數據,一個輸出緩存由寫設備的進程填充,並在中斷時送出數據。

爲正確進行中斷驅動的數據傳送,硬件應能夠按照下列語義產生中斷:

輸入:當新數據到達時並處理器準備好接受時,設備中斷處理器。

輸出:當設備準備好接受新數據或確認一個成功的數據傳送時,設備產生中斷。

發佈了32 篇原創文章 · 獲贊 6 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章