中斷和中斷處理程序
- 1 中斷
- 異常
- 2 中斷處理程序
- 上半部與下半部的對比
- 3 注冊中斷處理程序
- 釋放中斷處理程序
- 4 編寫中斷處理程序
- 重入和中斷處理程序
- 共享的中斷處理程序
- 中斷處理程序實例
- 5 中斷上下文
- 6 中斷處理機制的實現
- 7 中斷控制
- 禁止和激活中斷
- 禁止指定中斷線
- 中斷系統的狀態
- 8 總結
Linux內核要對連接到計算機上的所有硬件設備進行管理,要與它們進行通信。但是,處理器的速度跟外圍硬件設備的速度往往不是一個數量級的,硬件的響應很慢,內核應該在此期間處理其他事物,等到硬件真正完成了請求的操作之后,再回過頭來對它進行處理。中斷機制讓硬件在需要的時候再向內核發出信號,內核來處理硬件的請求。
1 中斷
中斷使得硬件得以與處理器進行通信。硬件設備生成中斷的時候并不考慮與處理器的時鐘同步,換句話說,中斷隨時都可以產生,因此,內核隨時可能因為新到來的中斷而被打斷。
從物理學的角度看,中斷是一種電信號,由硬件設備生成,并直接送入中斷控制器的輸入引腳上,然后再由中斷控制器向處理器發送相應的信號。處理器一經檢測到此信號,便中斷自己的當前工作轉而處理中斷。此后,處理器會通知操作系統已經產生中斷,這樣,操作系統就可以對這個中斷進行適當的處理了。
不同的設備對應的中斷不同,而每個中斷都通過一個唯一的數字標識,這些中斷值通常被稱為中斷請求(IRQ)線。特定的中斷總是與特定的設備相關聯,并且內核要知道這些消息。
異常
異常與中斷不同,它在產生時必須考慮與處理器時鐘同步。異常也常稱為同步中斷。在處理器執行到由于編程失誤而導致的錯誤指令的時候,或者是在執行期間出現特殊情況(例如缺頁),必須靠內核來處理的時候,處理器就會產生一個異常。
2 中斷處理程序
在響應一個特定中斷的時候,內核會執行一個函數,該函數叫做中斷處理程序或中斷服務例程(interrupt service routine,ISR)。產生中斷的每個設備都有一個相應的中斷處理程序。中斷處理程序通常不是和特定設備關聯,而是和特定中斷關聯的,也就是說,如果一個設備可以產生多種不同的中斷,那么該設備就可以對應多個中斷處理程序,相應的,該設備的驅動程序也就需要準備多個這樣的函數。
在Linux中,中斷處理程序看起來就是普普通通的C函數,只不過這些函數必須按照特定的類型聲明,以便內核能夠以標準的方式傳遞程序的信息。中斷處理程序與其他內核函數的真正區別在于:中斷處理程序是被內核調用來響應中斷的,而它們運行在我們稱之為中斷上下文的特殊上下文中。
上半部與下半部的對比
一般把中斷處理切為兩個部分,中斷處理程序是上半部,接受到一個中斷,它就立即開始執行,但只做有嚴格時限的工作,例如對接受的中斷進行應答或復位硬件,這些工作都是在所有中斷被禁止的情況下完成的。能夠被允許稍后完成的工作會被推遲到下半部去。此后,在合適的時機,下半部就開中斷執行,Linux提供了實現下半部的各種機制,下一篇文章會討論,現在我們要記住上半部是中斷處理程序,只做有嚴格時限的工作。
3 注冊中斷處理程序
中斷處理程序是驅動程序的組成部分。每一設備都有相關的驅動程序,如果設備使用中斷,那么相應的驅動程序就注冊一個中斷處理程序。
驅動程序可以通過下面的函數注冊并激活一個中斷處理程序:
/* request_irq分配一個中斷線 */
int request_irq(unsigned int irq,irqreturn_t (*handler)(int,void *,struct pt_regs *),unsigned long irqflags,const char * devname,void *dev_id)
第一個參數irq表示要分配的中斷號。對某些設備,如傳統PC設備上的系統時鐘或鍵盤,這個值通常是預先定死的。而對于大多數其他設備,這個值要么是可以通過探測獲取,要么可以通過編程動態確定。
第二個參數handle是實際的中斷處理程序。只要操作系統一接收到該中斷,該函數就被調用。
第三個參數irqflags可以為0,也可能是下列一個或多個標志的位掩碼:
- SA_INTERRUPT:此標志表明給定的中斷處理程序是一個快速中斷處理程序。在本地處理器上,快速中斷處理程序在禁止所有中斷的情況下運行。
- SA_SAMPLE_RANDOM:此標志表明這個設備產生的中斷對內核熵池有貢獻。內核熵池負責提供從各種隨機事件導出的真正的隨機數。
- SA_SHIRQ:此標志表明可以在多個中斷處理程序之間共享中斷線。
第四個參數devname是給中斷相關的設備起的名字。這些名字會被/proc/irq和/proc/Interrupt文件使用,以便與用戶通信。
第五個參數dev_id主要用于共享中斷線。如果無需共享中斷線,那么將該參數賦值為NULL就可以了,但是,如果中斷線是被共享的,那么必須傳遞唯一的信息,用來區分用的是共享中斷線上的那個中斷處理程序。實踐中往往會通過它傳遞驅動程序的設備結構。
request_irq()函數可能會睡眠,因此,不能在中斷上下文或其他不允許阻塞的代碼中用該函數,至于request_irq()會睡眠,那是在注冊的過程中,內核需要在/proc/irq文件創建一個中斷對應的項,會調用kmalloc()來請求分配內存,函數kmalloc()是可以睡眠的。
在一個驅動程序中請求一個中斷線,并在通過request_irq()注冊中斷處理程序:
if(request_irq(irqn,my_interrupt,SA_SHIRQ,"my_device",dev))
{printk(KERN_ERR "my_device:connot register IRQ %d \n",irqn);return -RIO;
}
irqn是請求的中斷線,my_interrupt是中斷處理程序,中斷線可以共享,設備名為"my_device",而且我們通過dev_id傳遞dev結構體。
釋放中斷處理程序
卸載驅動程序時,需要注銷相應的中斷處理程序,并釋放中斷線。可以調用void free_irq(unsigned int irq,void *dev_id)
來釋放中斷線。
如果指定的中斷線不是共享的,那么,該函數刪除中斷處理程序的同時將禁用這條中斷線。如果中斷線是共享的,則僅刪除dev_id所對應的處理程序,而這條中斷線本身只有在刪除了最后一個程序程序時才會被禁用。
4 編寫中斷處理程序
以下是一個典型的中斷處理程序聲明:
static irqreturn_t intr_handler(int irq,void *dev_id,struct pt_regs *regs);
它的類型與request_irq()參數中handler所要求的參數類型相匹配。第一個參數irq就是這個中斷處理程序要響應的中斷的中斷線號。第二個參數dev_id是用一個通用指針,它與中斷處理程序注冊時傳遞給request_irq()的采納數dev_id必須一致,可以用來區分共享同一個中斷處理程序的多個設備,最后一個參數regs是一個指向結構的指針,該結構包含處理中斷之前處理器的寄存器和狀態。除了調試的時候,它們很少使用到。
中斷處理程序的返回值是一個特殊類型:irqreturn_t。中斷處理程序可能返回兩個特殊的值,IRQ_NONE和IRQ_HANDLED。當中斷處理程序檢測到一個中斷,但該中斷對應的設備并不是在注冊處理函數期間指定的產生源時,返回IRQ_NONE;當中斷處理程序被正確調用,且確實是它所對應的設備產生的中斷,返回IRQ_HANDLED。
重入和中斷處理程序
Linux中的中斷處理程序是無需重入的。當一個給定的中斷處理程序在執行時,相應的中斷線在所有處理器上都會被屏蔽掉,以防止在同一中斷線上接受到另一個新的中斷。
共享的中斷處理程序
共享中斷線的處理程序和非共享中斷線的處理程序在注冊和運行方式上比較相似,但主要有以下差異:
- request_irq()的flags參數必須設置SA_SHIRQ標志
- 對每個注冊的中斷處理程序來說,dev_id必須是唯一的。指向任意設備結構的指針就可以滿足這一要求,通常會選擇設備結構,因為它是唯一的,而且中斷處理程序可能會用到它。不能共享就傳NULL
- 中斷處理程序必須能區分它的設備是否真的產生了中斷,
中斷處理程序實例
讓我們考察一個實際的中斷處理程序,它來自RTC(real_time clock)驅動程序,可以在drivers/char/rtc.c中找到。RTC驅動程序裝載時,rtc_init()函數會被調用,對這個驅動程序進行初始化。它的職責之一就是注冊中斷處理程序:
/** XXX Interrupt pin #7 in Espresso is shared between RTC and* PCI Slot 2 INTA# (and some INTx# in Slot 1). SA_INTERRUPT here* is asking for trouble with add-on boards. Change to SA_SHIRQ.*/if (request_irq(rtc_irq, rtc_interrupt, SA_INTERRUPT, "rtc", (void *)&rtc_port)) {/** Standard way for sparc to print irq's is to use* __irq_itoa(). I think for EBus it's ok to use %d.*/printk(KERN_ERR "rtc: cannot register IRQ %d\n", rtc_irq);return -EIO;}
中斷線號由rtc_irq指定,rtc_interrupt是我們的中斷處理程序,驅動程序的名稱為rtc,因為這個設備不允許共享中斷線,且處理程序沒有用到什么特殊的值,因此給dev_id的值是NULL。
5 中斷上下文
當執行一個中斷處理程序或下半部時,內核處于中斷上下文中。進程上下文是一種內核所處的操作模式,此時內核代表進程執行,在進程上下文中,可以通過current宏關聯到當前進程,此外,因為進程是以進程上下文的形式連接到內核的,因此,在進程上下文可以睡眠,也可以調用調度程序。
中斷上下文和進程并沒什么瓜葛。因為沒有進程的背景,所有中斷上下文不可睡眠。
中斷處理程序棧的設置是一個配置選項。它們共享所屬中斷進程的內核棧。內核棧的大小是兩頁。因為這種設置,中斷處理程序共享別人的堆棧,所以它們從棧中獲取內容非常節省空間。
6 中斷處理機制的實現
中斷處理在linux中的實現非常依賴體系結構,實現依賴于處理器、所使用的中斷控制器的類型、體系結構的設計以及機器本身。
下圖是中斷從硬件到內核的步驟。
設備產生中斷,通過總線把電信號發送給中斷控制器。如果中斷線是激活的,那么中斷控制器就會把中斷發往處理器。在大多數體系結構中,這個工作就是通過電信號給處理器的特定管腳發送一個信號。除非在處理器禁止該中斷,否則,處理器會立即停止它正在做的事,關閉中斷系統,然后跳到中斷處理程序的入口點去運行。
對于每條中斷線,處理器都會跳到對應的唯一的位置。這樣,內核就可知道所接收中斷的IRQ號了。入口點只是在棧中保存這個號,并存當前寄存器的值;然后,內核調用函數do_IRQ()。
do_IRQ()的聲明如下:
unsigned int do_IRQ(struct pt_regs regs);
因為C的調用慣例是要把函數參數放在棧的頂部,因此pt_regs結構包含原始寄存器的值,這些值是以前在匯編入口例程中保存在棧的的,中斷的值也會得到保存,所以,do_IRQ()可以將它提取出來。
計算出中斷號后,do_IRQ()對所接收的中斷進行應答,禁止這條線上的中斷傳遞。do_IRQ()需要確保在這條中斷線上有一個有效的處理程序,而且這個程序已經啟動但是當前沒有執行。如果是這樣的話,do_IRQ()就調用handle_IRQ_event()來運行為這條中斷線所安裝的中斷處理程序,在kernel/irq/handle.c文件中
/** Have got an event to handle:*/
fastcall int handle_IRQ_event(unsigned int irq, struct pt_regs *regs,struct irqaction *action)
{int ret, retval = 0, status = 0;if (!(action->flags & SA_INTERRUPT))local_irq_enable();do {ret = action->handler(irq, action->dev_id, regs);if (ret == IRQ_HANDLED)status |= action->flags;retval |= ret;action = action->next;} while (action);if (status & SA_SAMPLE_RANDOM)add_interrupt_randomness(irq);local_irq_disable();return retval;
}
7 中斷控制
Linux內核提供了一組接口用于操作機器上的中斷狀態。這些接口為我們提供了禁止當前處理器的中斷系統,或屏蔽掉整個機器的一條中斷線的能力,這些接口是與體系有關的,可以在<asm/system.h>和<asm/irq.h>中找到。
禁止和激活中斷
用于禁止當前處理器(僅僅是當前處理器)上的本地中斷,隨后又激活它們的語句是:
local_irq_disable();
local_irq_enable();
這兩個函數通常以單個匯編指令來實現(取決于體系結構)。實際上,在x86中,local_irq_disable()僅僅是cli指令,而local_irq_enable()只不過是sti指令。
local_irq_disable是禁止當前處理器上的所有中斷,local_irq_enable是開啟當前處理器上的所有中斷,即使在禁止前有些中斷是關閉的,在調用local_irq_enable()后也會被激活。所以我們需要一種機制把中斷恢復到以前的狀態而不是簡單地禁止或激活。在禁止之前保存當前的中斷系統,激活時恢復就行。
unsigned long flags;
lcoal_irq_save(flags)
local_irq_restore(flags);
flags必須是unsigned long類型。local_irq_save () 是保存本地中斷傳遞的當前狀態,然后禁止本地中斷傳遞。local_irq_restore () 是恢復本地中斷到flags的狀態。
禁止指定中斷線
在某些情況下,只禁止整個系統中一條特定的中斷線就夠了。為此,Linux提供了四個接口:
void disable_irq(unsigned int irq);
void disable_irq_nosync(unsigned int irq);
void enable_irq(unsigned int irq);
void synchronize_irq(unsigned int irq);
disable_irq和disable_irq_nosync禁止中斷控制器上指定的中斷線,即禁止給定中斷向系統中所有處理器的傳遞。我們來看看disable_irq的具體實現:
/*** disable_irq - disable an irq and wait for completion* @irq: Interrupt to disable** Disable the selected interrupt line. Enables and disables* are nested. This functions waits for any pending IRQ* handlers for this interrupt to complete before returning.* If you use this function while holding a resource the IRQ* handler may need you will deadlock.** This function may be called - with care - from IRQ context.*/
void disable_irq(unsigned int irq)
{struct irqdesc *desc = irq_desc + irq;disable_irq_nosync(irq);if (desc->action)synchronize_irq(irq);
}
此函數在返回之前等待中斷irq的任何掛起的 中斷處理程序完成。disable_irq會阻塞,所以不能在在中斷處理函數中使用,如果在中斷處理程序中使用,會導致死鎖,電腦會死機。
disable_irq_nosync不會阻塞,會立即返回,可在中斷處理函數中使用。
enable_irq函數的參數是int型變量,代表操作中斷對應的中斷號
函數synchronize_irq等待一個特定的中斷處理程序的退出,所以該函數也會阻塞。
這些函數的調用都可以嵌套,但要記住在一條指定的中斷線上,對disable_irq()或disable_irq_nosync()的每次調用,都需要相應地調用一次enable_irq()。只有在對enable_irq()完成最后一次調用后,才真正重新激活了中斷線。例如,如果disable_irq()被調用了兩次,那么直達第二次調用enable_irq()后,才能真正地激活中斷線。
禁止多個中斷處理程序共享的中斷線是不合適的。禁止中斷線也就禁止了這條線上所有設備的中斷傳遞。因此,用于新設備的驅動程序應該盡量不使用這些接口。
中斷系統的狀態
宏irqs_disable()定義在<asm/system.h>中,如果本地處理器上的中斷系統被禁止,則它返回非0,否則返回0.
在<asm/hardirq.h>中定義了的兩個宏提供了一個用來檢查內核的當前上下文的接口,它們是:
in_interrupt();
in_irq();
in_interrupt用來檢測內核是否處于中斷上下文中,如果是的話,返回非0。說明此刻內核正在執行中斷處理程序,或者正在執行下半段處理程序,如果返回0,此時內核處于進程上下文中。宏in_irq只有在內核確實正在執行中斷處理程序時才返回非0.
8 總結
中斷就是由硬件打斷操作系統。內核提供的接口包括注冊和注銷中斷處理程序,禁止中斷,屏蔽中斷線,以及檢查中斷系統的狀態。
因為中斷打斷了其他代碼的執行(進程,內核本身,甚至其他中斷處理程序),它們必須趕快執行完。為了大量的工作與必須快速執行完之間求得平衡,內核把處理中斷的工作分為兩部分。中斷處理程序,也就是上半部,下半部我們還沒有討論。