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

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

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