Linux裝置驅動程式學習(11)-中斷處理

來源:互聯網
上載者:User
可以讓裝置在產生某個事件時通知處理器的方法就是中斷。一個“中斷”僅是一個訊號,當硬體需要獲得處理器對它的關注時,就可以發送這個訊號。 Linux 處理中斷的方式非常類似在使用者空間處理訊號的方式。 大多數情況下,一個驅動只需要為它的裝置的中斷註冊一個處理常式,併當中斷到來時進行正確的處理。本質上來講,中斷處理常式和其他的代碼並行運行。因此,它們不可避免地引起並發問題,並競爭資料結構和硬體。 透徹地理解並發控制技術對中斷來講非常重要。

安裝中斷處理常式

核心維護了一個中斷訊號線的註冊表,類似於 I/O 連接埠的註冊表。模組在使用中斷前要先請求一個中斷通道(或者 IRQ插斷要求),並在使用後釋放它。所用的函式宣告在 <linux/interrupt.h> (在此檔案中並未真正包含,是通過它include的檔案間接包含的,函數在/kernel/irq/Manage.h中),中斷註冊和釋放的函數介面如下:

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 的傳回值: 0 指示成功,或返回一個負的錯誤碼,如 -EBUSY 表示另一個驅動已經佔用了你所請求的中斷線。

函數的參數如下:

unsigned int irq :請求的中斷號 irqreturn_t (*handler) :安裝的處理函數指標。 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 的注釋。
中斷處理常式可在驅動初始化時或在裝置第一次開啟時安裝。推薦在裝置第一次開啟、硬體被告知產生中斷前時申請中斷,因為可以共用有限的中斷資源。這樣調用 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 的調用之間給定中斷可能被佔用*/
快速和慢速處理常式

快速中斷是那些能夠很快處理的中斷,而處理慢速中斷會花費更長的時間。在處理慢速中斷時處理器重新使能中斷,避免快速中斷被延時過長。在現代核心中,快速和慢速中斷的區別已經消失,剩下的只有一個:快速中斷(使用 SA_INTERRUPT )執行時禁止所有在當前處理器上的其他中斷。注意:其他的處理器仍然能夠處理中斷。

除非你充足的理由在禁止其他中斷情況下來運行中斷處理常式,否則不應當使用SA_INTERRUPT.

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。

/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

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

但不是每個裝置都對程式員友好,對於他們還是需要一些探測工作。這個工作技術上非常簡單: 驅動告知裝置產生中斷並且觀察發生了什麼。如果一切順利,則只有一個中斷訊號線被啟用。儘管探測在理論上簡單,但實現可能不簡單。有 2 種方法來進行探測中斷: 調用核心定義的輔助函數DIY探測

(1)調用核心定義的輔助函數


Linux 核心提供了一個底層設施來探測中斷號,且只能在非共用中斷模式下工作,它包括 2 個函數, 在<linux/interrupt.h> 中聲明( 也描述了探測機制 ):
unsigned long probe_irq_on(void);/*這個函數返回一個未分配中斷的位元遮罩。驅動必須保留返回的位元遮罩, 並在後面傳遞給 probe_irq_off。在調用probe_irq_on之後, 驅動應當安排它的裝置產生至少一次中斷*/int probe_irq_off(unsigned long);/*在請求裝置產生一個中斷後, 驅動調用這個函數, 並將 probe_irq_on 返回的位元遮罩作為參數傳遞給probe_irq_off。probe_irq_off 返回在"probe_on"之後發生的中斷號。如果沒有中斷髮生, 返回 0 ;如果產生了多次中斷,probe_irq_off 返回一個負值*/
程式員應當注意在調用 probe_irq_on 之後啟用裝置上的中斷, 並在調用 probe_irq_off 前禁用。此外還必須記住在 probe_irq_off 之後服務裝置中待處理的中斷。
以下是LDD3中的並口範例程式碼,(並口的管腳 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;}} while (short_irq < 0 && count++ < 5);if (short_irq < 0)        printk("short: probe failed %i times, giving up\n", count);
最好只在模組初始化時探測中斷線一次。
大部分體系定義了這兩個函數( 即便是空的 )來簡化裝置驅動的移植。

(2)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 。

處理常式的參數及傳回值

傳遞給一個中斷處理常式的參數有: int irq、void *dev_id和 struct pt_regs *regs。

int irq (中斷號):若要列印 log 訊息時,是很有用。

void *dev_id:一種使用者資料類型(驅動程式可用的私人資料),傳遞給 request_irq的 void* 參數,會在中斷髮生時作為參數傳給處理常式。我們通常傳遞一個指向裝置資料結構的指標到 dev_id 中,這樣一個管理若干相同裝置的驅動在中斷處理常式中不需要任何額外的代碼,就可以找出哪個裝置產生了當前的中斷事件。

struct pt_regs *regs很少用到。

中斷處理常式的典型使用如下:

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 *//* .... */}
和這個處理常式關聯的開啟代碼如下:

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;}
中斷處理常式應當返回一個值指示是否真正處理了一個中斷。如果處理常式發現裝置確實需要處理, 應當返回 IRQ_HANDLED; 否則傳回值 IRQ_NONE。以下宏可產生傳回值:

IRQ_RETVAL(handled) /*若要處理中斷,handled應是非零*/
有位網友在處理傳回值是按慣例return 0;,導致了oops。吸取經驗教訓,我們應特別注意這種傳回值,以下是有關中斷處理常式的傳回值的核心定義(#include <linux/irqreturn.h> ),看了就知道導致oops的原因了,以後應多多注意:

typedef int irqreturn_t;#define IRQ_NONE    (0)#define IRQ_HANDLED    (1)#define IRQ_RETVAL(x) ((x) != 0)
實現中斷處理常式

中斷處理常式唯一的特別之處在中斷時運行,它能做的事情受到了一些限制. 這些限制與我們在核心定時器上看到的相同:
(1)中斷處理常式不能與使用者空間傳遞資料, 因為它不在進程上下文執行;
(2)中斷處理常式也不能做任何可能休眠的事情, 例如調用 wait_event, 使用除 GFP_ATOMIC 之外任何東西來分配記憶體, 或者鎖住一個訊號量;
(3)處理者不能調用schedule()。

中斷處理常式的作用是將關於中斷接收的資訊反饋給裝置並根據被服務的中斷的含義讀、寫資料。中斷處理常式第一步常常包括清除裝置的一個中斷標誌位,大部分硬體裝置在清除"中斷掛起"位前不會再產生中斷。這也要根據硬體的工作原理決定, 這一步也可能需要在最後做而不是開始; 這裡沒有通用的規則。一些裝置不需要這步, 因為它們沒有一個"中斷掛起"位; 這樣的裝置是少數。

一個中斷處理的典型任務是:如果中斷通知它所等待的事件已經發生(例如新資料到達),就會喚醒休眠在裝置上的進程。

不管是快速或慢速處理常式,程式員應編寫執行時間儘可能短的處理常式。 如果需要進行長時間計算, 最好的方法是使用 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(unsigned long flags);/*在儲存當前中斷狀態到 flags 之後禁止中斷*/void local_irq_disable(void);/* 關閉中斷而不儲存狀態*//*如果調用鏈中有多個函數可能需要禁止中斷, 應使用 local_irq_save*//*開啟中斷使用:*/void local_irq_restore(unsigned long flags);void local_irq_enable(void);/*在 2.6 核心, 沒有方法全域禁用整個系統上的所有中斷*/
頂半部和底半部

中斷處理需要很快完成並且不使中斷阻塞太長,所以中斷處理的一個主要問題是如何在處理常式中完成耗時的任務。
Linux (連同許多其他系統)通過將中斷處理分為兩部分來解決這個問題:
頂半部”是實際響應中斷的常式(request_irq 註冊的那個常式)。

“底半部”是被頂半部調度,並在稍後更安全的時間內執行的函數。

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

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,一個輸入緩衝在中斷時被填充,並由讀取裝置的進程取走緩衝區的資料,一個輸出緩衝由寫裝置的進程填充,並在中斷時送出資料。

為正確進行中斷驅動的資料傳送,硬體應能夠按照下列語義產生中斷:

輸入:當新資料到達時並處理器準備好接受時,裝置中斷處理器。

輸出:當裝置準備好接受新資料或確認一個成功的資料傳送時,裝置產生中斷。

ARM9 開發板實驗

這次的實驗和硬體相關,利用了友善之臂SBC2440V4提供的四個中斷按鍵。

K1:中斷+tasklet

K2:中斷+工作隊列

K3:中斷+共用工作隊列

K4:中斷+linux核心緩衝kfifo

具體的實現方法請看源碼!

中斷模組源碼:IO_irq.tar.gz

測試程式:IO_irq_test.tar.gz

實驗資料:

[Tekkaman2440@SBC2440V4]#cd /lib/modules/[Tekkaman2440@SBC2440V4]#insmod IO_irq.ko[Tekkaman2440@SBC2440V4]#cat /proc/devicesCharacter devices:  1 mem  2 pty  3 ttyp  4 /dev/vc/0  4 tty  4 ttyS  5 /dev/tty  5 /dev/console  5 /dev/ptmx  7 vcs 10 misc 13 input 14 sound 81 video4linux 89 i2c 90 mtd116 alsa128 ptm136 pts153 spi180 usb189 usb_device204 s3c2410_serial252 IO_irq253 usb_endpoint254 rtcBlock devices:  1 ramdisk256 rfd  7 loop 31 mtdblock 93 nftl 96 inftl179 mmc[Tekkaman2440@SBC2440V4]#mknod -m 666 IO_irq c 252 0[Tekkaman2440@SBC2440V4]#/tmp/IO_irq_testIO_irq: opened !IO_irq: the module can not lseek!please input the command :**************KEY = 1*****************NO prevkeynow jiffies =0x00059ae5count = 1**************KEY = 1 END*******************************key1_tasklet_start*****************time:00059ae7 delta: 2 inirq:1 pid: 0 cpu:0 command:swapper**************key1_tasklet_end*******************************KEY = 2*****************prevkey=1 at 0x00059ae5NO prekey2now jiffies =0x00059c54count = 1result = 1**************KEY = 2 END*******************************key2_workqueue_start*****************time:00059c5f delta: 11 inirq:0 pid:832 cpu:0 command:tekkamanwork/0**************key2_workqueu_end*******************************KEY = 3*****************prevkey=2 at 0x00059c54NO prekey3now jiffies =0x00059f63count = 1result = 1**************KEY = 3 END*******************************key3_workqueue_start*****************time:00059f66 delta: 3 inirq:0 pid: 5 cpu:0 command:events/0**************key3_workqueu_end*****************IO_irq: Invalid input ! only 1、2、3、4、5、6、7、8、q !please input the command :6IO_irq: ioctl IO_KFIFO_SIZE len=414please input the command :8**********KEY = 4**********prevkey=3 at 0x00059f63NO prekey4now jiffies =0x0005a2fdcount = 1*********KEY = 4 END*********************KEY = 4**********prevkey=4 at 0x0005a2fdprekey4 at 0x0005a2fdnow jiffies =0x0005a304count = 2*********KEY = 4 END*********************KEY = 4**********prevkey=4 at 0x0005a304prekey4 at 0x0005a304now jiffies =0x0005a304count = 3*********KEY = 4 END***********please input the command :6IO_irq: ioctl IO_KFIFO_SIZE len=142please input the command :7IO_status= 1f !IO_irq: ioctl IO_KFIFO_RESET please input the command :6IO_irq: ioctl IO_KFIFO_SIZE len=0please input the command :8please input the command :qIO_irq: release ![Tekkaman2440@SBC2440V4]#

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.