中斷還是中斷,我講了很多次的中斷了,今天還是要講中斷,為啥呢?因為在操作系統中,中斷是必須要講的..
那么什么叫中斷呢, 中斷還是打斷,這樣一說你就不明白了。唉,中斷還真是有點像打斷。我們知道linux管理所有的硬件設備,要做的第一件事先是通信。然后,我們天天在說一句話:處理器的速度跟外圍硬件設備的速度往往不在一個數量級上,甚至幾個數量級的差別,這時咋辦,你總不能讓處理器在那里傻等著你硬件做好了告訴我一聲吧。這很容易就和日常生活聯系起來了,這樣效率太低,不如我處理器做別的事情,你硬件設備準備好了,告訴我一聲就得了。這個告訴,咱們說的輕松,做起來還是挺費勁啊!怎么著,簡單一點,輪訓(polling)可能就是一種解決方法,缺點是操作系統要做太多的無用功,在那里傻傻的做著不重要而要重復的工作,這里有更好的辦法---中斷,這個中斷不要緊,關鍵在于從硬件設備的角度上看,已經實現了從被動為主動的歷史性突破。
中斷的例子我就不說了,這個很顯然啊。分析中斷,本質上是一種特殊的電信號,由硬件設備發向處理器,處理器接收到中斷后,會馬上向操作系統反應此信號的帶來,然后就由OS負責處理這些新到來的數據,中斷可以隨時發生,才不用操心與處理器的時間同步問題。不同的設備對應的中斷不同,他們之間的不同從操作系統級來看,差別就在于一個數字標識-----中斷號。專業一點就叫中斷請求(IRQ)線,通常IRQ都是一些數值量。有些體系結構上,中斷好是固定的,有的是動態分配的,這不是問題所在,問題在于特定的中斷總是與特定的設備相關聯,并且內核要知道這些信息,這才是最關鍵的,不是么?哈哈.
用書上一句話說:討論中斷就不得不提及異常,異常和中斷不一樣,它在產生時必須要考慮與處理器的時鐘同步,實際上,異常也常常稱為同步中斷,在處理器執行到由于編程失誤而導致的錯誤指令的時候,或者是在執行期間出現特殊情況,必須要靠內核來處理的時候,處理器就會產生一個異常。因為許多處理器體系結構處理異常以及處理中斷的方式類似,因此,內核對它們的處理也很類似。這里的討論,大部分都是適合異常,這時可以看成是處理器本身產生的中斷。
中斷產生告訴中斷控制器,繼續告訴操作系統內核,內核總是要處理的,是不?這里內核會執行一個叫做中斷處理程序或中斷處理例程的函數。這里特別要說明,中斷處理程序是和特定中斷相關聯的,而不是和設備相關聯,如果一個設備可以產生很多中斷,這時該設備的驅動程序也就需要準備多個這樣的函數。一個中斷處理程序是設備驅動程序的一部分,這個我們在linux設備驅動中已經說過,就不說了,后面我也會提到一些。前邊說過一個問題:中斷是可能隨時發生的,因此必須要保證中斷處理程序也能隨時執行,中斷處理程序也要盡可能的快速執行,只有這樣才能保證盡可能快地恢復中斷代碼的執行。
但是,不想說但是,大學第一節逃課的情形現在仍記憶猶新:又想馬兒跑,又想馬兒不吃草,怎么可能!但現實問題或者不像想象那樣悲觀,我們的中斷說不定還真有奇跡發生。這個奇跡就是將中斷處理切為兩個部分或兩半。中斷處理程序上半部(top half)---接收到一個中斷,它就立即開始開始執行,但只做嚴格時限的工作,這些工作都是在所有中斷被禁止的情況下完成的。同時,能夠被允許稍后完成的工作推遲到下半部(bottom half)去,此后,下半部會被執行,通常情況下,下半部都會在中斷處理程序返回時立即執行。我會在后面談論linux所提供的是實現下半部的各種機制。
說了那么多,現在開始第一個問題:如何注冊一個中斷處理程序。我們在linux驅動程序理論里講過,通過一下函數可注冊一個中斷處理程序:
1 | int request_irq(unsigned int irq,irqreturn_t (*handler)( int , void *, struct pt_regs *),unsigned long irqflags, const char * devname, void *dev_id)
|
有關這個中斷的一些參數說明,我就不說了,一旦注冊了一個中斷處理程序,就肯定會有釋放中斷處理,這是調用下列函數:
1 | void free_irq(unsigned int irq, void *dev_id)
|
這里需要說明的就是要必須要從進程上下文調用free_irq().好了,現在給出一個例子來說明這個過程,首先聲明一個中斷處理程序:
1 | static irqreturn_t intr_handler( int irq, void *dev_id, struct pt_regs *regs)
|
注意:這里的類型和前邊說到的request_irq()所要求的參數類型是匹配的,參數不說了。對于返回值,中斷處理程序的返回值是一個特殊類型,irqrequest_t,可能返回兩個特殊的值:IRQ_NONE和IRQ_HANDLED.當中斷處理程序檢測到一個中斷時,但該中斷對應的設備并不是在注冊處理函數期間指定的產生源時,返回IRQ_NONE;當中斷處理程序被正確調用,且確實是它所對應的設備產生了中斷時,返回IRQ_HANDLED.C此外,也可以使用宏IRQ_RETVAL(x),如果x非0值,那么該宏返回IRQ_HANDLED,否則,返回IRQ_NONE.利用這個特殊的值,內核可以知道設備發出的是否是一種虛假的(未請求)中斷。如果給定中斷線上所有中斷處理程序返回的都是IRQ_NONE,那么,內核就可以檢測到出了問題。最后,需要說明的就是那個static了,中斷處理程序通常會標記為static,因為它從來不會被別的文件中的代碼直接調用。另外,中斷處理程序是無需重入的,當一個給定的中斷處理程序正在執行時,相應的中斷線在所有處理器上都會被屏蔽掉,以防止在同一個中斷上接收另外一個新的中斷。通常情況下,所有其他的中斷都是打開的,所以這些不同中斷線上的其他中斷都能被處理,但當前中斷總是被禁止的。由此可見,同一個中斷處理程序絕對不會被同時調用以處理嵌套的中斷。
下面要說到的一個問題是和共享的中斷處理程序相關的。共享和非共享在注冊和運行方式上比較相似的。差異主要有以下幾點:
1.request_irq()的參數flags必須設置為SA_SHIRQ標志。 2.對每個注冊的中斷處理來說,dev_id參數必須唯一。指向任一設備結構的指針就可以滿足這一要求。通常會選擇設備結構,因為它是唯一的,而且中 斷處理程序可能會用到它,不能給共享的處理程序傳遞NULL值。 3.中斷處理程序必須能夠區分它的設備是否真的產生了中斷。這既需要硬件的支持,也需要處理程序有相關的處理邏輯。如果硬件不支持這一功能,那中 斷處理程序肯定會束手無策,它根本沒法知道到底是否與它對應的設備發生了中斷,還是共享這條中斷線的其他設備發出了中斷。 |
在指定SA_SHIRQ標志以調用request_irq()時,只有在以下兩種情況下才能成功:中斷當前未被注冊或者在該線上的所有已注冊處理程序都指定了SA_SHIRQ.A。注意,在這一點上2.6與以前的內核是不同的,共享的處理程序可以混用SA_INTERRUPT. 一旦內核接收到一個中斷后,它將依次調用在該中斷線上注冊的每一個處理程序。因此一個處理程序必須知道它是否應該為這個中斷負責。如果與它相關的設備并沒有產生中斷,那么中斷處理程序應該立即退出,這需要硬件設備提供狀態寄存器(或類似機制),以便中斷處理程序進行檢查。毫無疑問,大多數設備都提這種功能。
當執行一個中斷處理程序或下半部時,內核處于中斷上下文(interrupt context)中。對比進程上下文,進程上下文是一種內核所處的操作模式,此時內核代表進程執行,可以通過current宏關聯當前進程。此外,因為進程是進程上下文的形式連接到內核中,因此,在進程上下文可以隨時休眠,也可以調度程序。但中斷上下文卻完全不是這樣,它可以休眠,因為我們不能從中斷上下文中調用函數。如果一個函數睡眠,就不能在中斷處理程序中使用它,這也是對什么樣的函數能在中斷處理程序中使用的限制。還需要說明一點的是,中斷處理程序沒有自己的棧,相反,它共享被中斷進程的內核棧,如果沒有正在運行的進程,它就使用idle進程的棧。因為中斷程序共享別人的堆棧,所以它們在棧中獲取空間時必須非常節省。內核棧在32位體系結構上是8KB,在64位體系結構上是16KB.執行的進程上下文和產生的所有中斷都共享內核棧。
下面給出中斷從硬件到內核的路由過程(截圖選自liuux內核分析與設計p61),然后做出總結:

圖一 中斷從硬件到內核的路由
上面的圖內部說明已經很明確了,我這里就不在詳談。在內核中,中斷的旅程開始于預定義入口點,這類似于系統調用。對于每條中斷線,處理器都會跳到對應的一個唯一的位置。這樣,內核就可以知道所接收中斷的IRQ號了。初始入口點只是在棧中保存這個號,并存放當前寄存器的值(這些值屬于被中斷的任務);然后,內核調用函數do_IRQ().從這里開始,大多數中斷處理代碼是用C寫的。do_IRQ()的聲明如下:
1 | unsigned int do_IRQ( struct pt_regs regs)
|
因為C的調用慣例是要把函數參數放在棧的頂部,因此pt_regs結構包含原始寄存器的值,這些值是以前在匯編入口例程中保存在棧上的。中斷的值也會得以保存,所以,do_IRQ()可以將它提取出來,X86的代碼為:
1 | int irq = regs.orig_eax & 0xff
|
計算出中斷號后,do_IRQ()對所接收的中斷進行應答,禁止這條線上的中斷傳遞。在普通的PC機器上,這些操作是由mask_and_ack_8259A()來完成的,該函數由do_IRQ()調用。接下來,do_IRQ()需要確保在這條中斷線上有一個有效的處理程序,而且這個程序已經啟動但是當前沒有執行。如果這樣的話, do_IRQ()就調用handle_IRQ_event()來運行為這條中斷線所安裝的中斷處理程序,有關處理例子,可以參考linux內核設計分析一書,我這里就不細講了。在handle_IRQ_event()中,首先是打開處理器中斷,因為前面已經說過處理器上所有中斷這時是禁止中斷(因為我們說過指定SA_INTERRUPT)。接下來,每個潛在的處理程序在循環中依次執行。如果這條線不是共享的,第一次執行后就退出循環,否則,所有的處理程序都要被執行。之后,如果在注冊期間指定了SA_SAMPLE_RANDOM標志,則還要調用函數add_interrupt_randomness(),這個函數使用中斷間隔時間為隨機數產生熵。最后,再將中斷禁止(do_IRQ()期望中斷一直是禁止的),函數返回。該函數做清理工作并返回到初始入口點,然后再從這個入口點跳到函數ret_from_intr().該函數類似初始入口代碼,以匯編編寫,它會檢查重新調度是否正在掛起,如果重新調度正在掛起,而且內核正在返回用戶空間(也就是說,中斷了用戶進程),那么schedule()被調用。如果內核正在返回內核空間(也就是中斷了內核本身),只有在preempt_count為0時,schedule()才會被調用(否則,搶占內核是不安全的)。在schedule()返回之前,或者如果沒有掛起的工作,那么,原來的寄存器被恢復,內核恢復到曾經中斷的點。在x86上,初始化的匯編例程位于arch/i386/kernel/entry.S,C方法位于arch/i386/kernel/irq.c其它支持的結構類似。
下邊給出PC機上位于/proc/interrupts文件的輸出結果,這個文件存放的是系統中與中斷相關的統計信息,這里就解釋一下這個表:

上面是這個文件的輸入,第一列是中斷線(中斷號),第二列是一個接收中斷數目的計數器,第三列是處理這個中斷的中斷控制器,最后一列是與這個中斷有關的設備名字,這個名字是通過參數devname提供給函數request_irq()的。最后,如果中斷是共享的,則這條中斷線上注冊的所有設備都會列出來,如4號中斷。
Linux內核給我們提供了一組接口能夠讓我們控制機器上的中斷狀態,這些接口可以在<asm/system.h>和<asm/irq.h>中找到。一般來說,控制中斷系統的原因在于需要提供同步,通過禁止中斷,可以確保某個中斷處理程序不會搶占當前的代碼。此外,禁止中斷還可以禁止內核搶占。然而,不管是禁止中斷還是禁止內核搶占,都沒有提供任何保護機制來防止來自其他處理器的并發訪問。Linux支持多處理器,因此,內核代碼一般都需要獲取某種鎖,防止來自其他處理器對共享數據的并發訪問,獲取這些鎖的同時也伴隨著禁止本地中斷。鎖提供保護機制,防止來自其他處理器的并發訪問,而禁止中斷提供保護機制,則是防止來自其他中斷處理程序的并發訪問。
在linux設備驅動理論帖里詳細介紹過linux的中斷操作接口,這里就大致過一下,禁止/使能本地中斷(僅僅是當前處理器)用:
1 2 | local_irq_disable();
local_irq_enable();
|
如果在調用local_irq_disable()之前已經禁止了中斷,那么該函數往往會帶來潛在的危險,同樣的local_irq_enable()也存在潛在的危險,因為它將無條件的激活中斷,盡管中斷可能在開始時就是關閉的。所以我們需要一種機制把中斷恢復到以前的狀態而不是簡單地禁止或激活,內核普遍關心這點,是因為內核中一個給定的代碼路徑可以在中斷激活餓情況下達到,也可以在中斷禁止的情況下達到,這取決于具體的調用鏈。面對這種情況,在禁止中斷之前保存中斷系統的狀態會更加安全一些。相反,在準備激活中斷時,只需把中斷恢復到它們原來的狀態:
1 2 3 | unsigned long flags;
local_irq_save(flags);
local_irq_restore(flags);
|
參數包含具體體系結構的數據,也就是包含中斷系統的狀態。至少有一種體系結構把棧信息與值相結合(SPARC),因此flags不能傳遞給另一個函數(換句話說,它必須駐留在同一個棧幀中),基于這個原因,對local_irq_save()的調用和local_irq_restore()的調用必須在同一個函數中進行。前面的所有的函數既可以在中斷中調用,也可以在進程上下文使用。
前面我提到過禁止整個CPU上所有中斷的函數。但有時候,好奇的我就想,我干么沒要禁止掉所有的中斷,有時,我只需要禁止系統中一條特定的中斷就可以了(屏蔽掉一條中斷線),這就有了我下面給出的接口:
1 2 3 4 | void disable_irq(unsigned int irq);
void disable_irq_nosync(unsigned int irq);
void enable_irq(unsigned int irq);
void synchronise_irq(unsigned int irq);
|
對有關函數的說明和注意,我前邊已經說的很清楚了,這里飄過。另外,禁止多個中斷處理程序共享的中斷線是不合適的。禁止中斷線也就禁止了這條線上所有設備的中斷傳遞,因此,用于新設備的驅動程序應該傾向于不使用這些接口。另外,我們也可以通過宏定義在<asm/system.h>中的宏irqs_disable()來獲取中斷的狀態,如果中斷系統被禁止,則它返回非0,否則,返回0;用定義在<asm/hardirq.h>中的兩個宏in_interrupt()和in_irq()來檢查內核的當前上下文的接口。由于代碼有時要做一些像睡眠這樣只能從進程上下文做的事,這時這兩個函數的價值就體現出來了。
最后,作為對這篇博客的總結,這里給出我前邊提到的用于控制中斷的方法列表:
