中斷和異常
中斷通常被定義為一個(gè)事件,該事件改變處理器執(zhí)行的指令順序。這樣的事件與CPU芯片內(nèi)外部硬件電路產(chǎn)生的電信號相對應(yīng)。
中斷通常分為同步中斷和異步中斷:
2 同步中斷是當(dāng)指令執(zhí)行時(shí)由CPU控制單元產(chǎn)生的,之所以稱為同步,是因?yàn)橹挥性谝粭l指令終止執(zhí)行后CPU才會發(fā)出中斷。
◎ 異步中斷是由其他硬件設(shè)備依照CPU時(shí)鐘信號隨機(jī)產(chǎn)生的。
在Intel微處理器手冊中:
◎ 把同步中斷稱為異常(exception)
◎ 把異步中斷稱為中斷(interrupt)
這兩類中斷的共同特點(diǎn)是什么?如果CPU當(dāng)前不處于核心態(tài),則發(fā)起從用戶態(tài)到核心態(tài)的切換。接下來,在內(nèi)核中執(zhí)行一個(gè)專門的例程,稱為中斷服務(wù)例程(interrupt service routine)?;蛑袛嗵幚沓绦颍╥nterrupthandler)
另一方面,異常是由程序的錯誤產(chǎn)生的,或者是由內(nèi)核必須處理的異常條件產(chǎn)生的。第一種情況下,內(nèi)核通過發(fā)送一個(gè)每個(gè)Unix/Linux程序員都熟悉的信號來處理異常。第二種情況下,內(nèi)核執(zhí)行恢復(fù)異常需要的所有步驟,例如缺頁異常等。
中斷信號提供了一種特殊的方式,使處理器轉(zhuǎn)而去運(yùn)行正常控制流之外的代碼。當(dāng)一個(gè)中斷信號達(dá)到時(shí),CPU必須停止它當(dāng)前正在做的事情,并且切換到一個(gè)新的活動。為了這做到這一點(diǎn),就要在內(nèi)核態(tài)堆棧保存程序計(jì)數(shù)器的當(dāng)前值(即EIP和CS寄存器的內(nèi)容),并把與中斷類型相關(guān)的一個(gè)地址放進(jìn)程序計(jì)數(shù)器。
這可能會讓我們想起系統(tǒng)調(diào)度的進(jìn)程切換,發(fā)生在內(nèi)核用一個(gè)進(jìn)程替換另一個(gè)進(jìn)程時(shí)。但是中斷處理與進(jìn)程切換有一個(gè)明顯的差異:由中斷或異常處理程序執(zhí)行的代碼不是一個(gè)進(jìn)程。更準(zhǔn)確的說,它是一個(gè)內(nèi)核控制路徑,代表中斷發(fā)生時(shí)正在運(yùn)行的進(jìn)程執(zhí)行。作為一個(gè)內(nèi)核控制路徑,中斷處理程序比一個(gè)進(jìn)程要“輕”(中斷的上下文很少,建立或終止中斷處理需要的時(shí)間也很少)
中斷處理是由內(nèi)核執(zhí)行的最敏感的任務(wù)之一,因?yàn)樗仨殱M足下列約束:
◎ 當(dāng)內(nèi)核正打算去完成一些別的事情時(shí),中斷隨時(shí)會到來。因此,內(nèi)核的目標(biāo)就是讓中斷盡可能快地處理完,盡其所能把更多的處理向后推遲。因此,內(nèi)核響應(yīng)中斷后需要進(jìn)行的操作分為兩部分:關(guān)鍵而緊急的部分,內(nèi)核立即執(zhí)行;其余推遲的部分,內(nèi)核隨后執(zhí)行。
◎ 因?yàn)橹袛嚯S時(shí)會到來,所以內(nèi)核可能正在處理其中的一個(gè)中斷時(shí),另一個(gè)不同類型的中斷又發(fā)生了。內(nèi)核應(yīng)該盡可能地允許這種情況發(fā)生,因?yàn)檫@能維持更多的I/O設(shè)備得到處理的機(jī)會。因此,中斷處理程序必須編寫成使相應(yīng)的內(nèi)核控制路徑能以嵌套的方式執(zhí)行。當(dāng)最后一個(gè)內(nèi)核控制路徑終止時(shí),內(nèi)核必須能恢復(fù)被中斷進(jìn)程的執(zhí)行,或者,如果中斷信號已導(dǎo)致了重新調(diào)度,內(nèi)核也應(yīng)能切換到另外的進(jìn)程。
◎ 盡管內(nèi)核在處理前一個(gè)中斷時(shí)可以接受一個(gè)新的中斷,但在內(nèi)核代碼中還是存在一些臨界區(qū),在臨界區(qū)中,中斷必須被禁止。必須盡可能地限制這樣的臨界區(qū),因?yàn)楦鶕?jù)以前的要求,內(nèi)核,尤其是中斷處理程序,應(yīng)該在大部分時(shí)間內(nèi)以開中斷的方式運(yùn)行。
中斷這個(gè)名詞使用得并不是很謹(jǐn)慎,為什么?由于中斷是用來表示由CPU和外部硬件發(fā)出的信號所產(chǎn)生的。但是中斷不能由處理器外部的外設(shè)直接產(chǎn)生,而必須借助于一個(gè)稱為可編程中斷控制器(programmable interrupt controller)的標(biāo)準(zhǔn)組件來請求,該組件存在于每個(gè)系統(tǒng)中。
外部設(shè)備,會有電路連接到用于向中斷控制器發(fā)送中斷請求的組件。控制器在執(zhí)行了各種電工任務(wù)之后,將中斷請求轉(zhuǎn)發(fā)到CPU的中斷輸入中。因?yàn)橥獠吭O(shè)備不能直接發(fā)出中斷,而必須通過中斷控制器的標(biāo)準(zhǔn)組件來請求中斷,所以這種請求更正確的叫法是IRQ,或中斷請求(Interrupt Request)。
每個(gè)能夠發(fā)出中斷請求的硬件設(shè)備控制器都有這么一條名為IRQ的輸出線。所有現(xiàn)有的IRQ線都會與這個(gè)中斷控制器(PIC)的硬件電路的輸入引腳相連。下面來看看這種中斷控制器執(zhí)行下列動作:
1) 監(jiān)視IRQ線,檢查產(chǎn)生的信號 。如果有一條或兩條以上的IRQ線上產(chǎn)生信號,就選擇引腳編號較小的IRQ線。
2) 如果一個(gè)引發(fā)信號出現(xiàn)在IRQ線上:
a) 把接收到的引發(fā)信號轉(zhuǎn)換成對應(yīng)的向量(索引)。
b) 把這個(gè)向量存放在中斷控器的一個(gè)I/O端口,從而允許CPU通過數(shù)據(jù)總線讀取此向量。
c) 把引發(fā)信號發(fā)送到處理器的INTR引腳,即產(chǎn)生一個(gè)中斷。
d) 等待,直到CPU通過把這個(gè)中斷信號寫進(jìn)可編程中斷控制器的一個(gè)I/O端口來確認(rèn)它;當(dāng)這種情況發(fā)生時(shí),清INTR線。
3) 返回到第一步。
IRQ線是從0開始順序編號的,因此,第一條IRQ線通常表示成IRQ0。與IRQn關(guān)聯(lián)的Intel缺省向量是n+32。如前所述,通過向中斷控制器端口發(fā)布合適的指令,就可以修改IRQ和向量之間的映射。
可以有選擇地禁止每條IRQ線。因此,可以對PIC編程從而禁止IRQ,也就是說,可以告訴PIC停止對給定的IRQ線發(fā)布中斷,或者激活它們,禁止的中斷是丟失不了的,它們一旦激活,PIC就又把它們發(fā)送到CPU。這個(gè)特點(diǎn)被大多數(shù)中斷處理程序使用,因?yàn)檫@允許中斷處理程序逐次地處理同一類型的IRQ。
在CPU得知發(fā)生中斷后,它將進(jìn)一步的處理委托給一個(gè)軟件例程,該例程可能會修復(fù)故障、提供專門的處理或?qū)⑼獠渴录ㄖ脩暨M(jìn)程。由于每個(gè)中斷和異常都有唯一的編號,內(nèi)核使用一個(gè)數(shù)組項(xiàng)是指向處理程序函數(shù)的指針。相關(guān)的中斷號根據(jù)數(shù)組項(xiàng)在數(shù)組中位置判斷。
如下圖所示:
如下圖,中斷處理劃分為3部分。首先,必須建立一個(gè)適當(dāng)?shù)沫h(huán)境,使得處理程序函數(shù)能夠在其中執(zhí)行,接下來調(diào)用處理程序自身,最后將系統(tǒng)復(fù)原到中斷之前的狀態(tài)。調(diào)用中斷處理程序前后的兩部分,分別稱為進(jìn)入路徑和退出路徑。
進(jìn)入和退出任務(wù)還負(fù)責(zé)確保處理器從用戶態(tài)切換到核心態(tài)。進(jìn)入路徑的一個(gè)關(guān)鍵任務(wù)是,從用戶態(tài)棧切換到核心態(tài)棧。但是這一點(diǎn)還不夠。因?yàn)閮?nèi)核還要使用CPU資源執(zhí)行其代碼,進(jìn)入路徑必須保存用戶應(yīng)用程序當(dāng)前的寄存器,以便 在中斷活動結(jié)束后恢復(fù)。這與進(jìn)程調(diào)度間用上下文切換的機(jī)制是相同的。在進(jìn)入核心態(tài)時(shí),只保存整個(gè)寄存器集合的一部分。內(nèi)核并不使用全部寄存器。(如內(nèi)核代碼中不使用浮點(diǎn)操作,因而不保存浮點(diǎn)寄存器)。平臺相關(guān)的數(shù)據(jù)結(jié)構(gòu)pt_regs列出了核心態(tài)可能修改的所有寄存器,它的定義考慮到了不同的CPU之間的差別。
在退出路徑中,內(nèi)核會檢查下列事項(xiàng)。
◎ 調(diào)度器是否應(yīng)該選擇一個(gè)新進(jìn)程代替舊的進(jìn)程。
◎ 是否有信號必須投遞到原進(jìn)程
從中斷返回之后,只有確認(rèn)了這兩個(gè)問題,內(nèi)核才能完成其常規(guī)任務(wù),即還原寄存器集合、切換到用戶態(tài)棧、切換到適用于用戶應(yīng)用程序的適當(dāng)?shù)奶幚砥鳡顟B(tài),或切換到一個(gè)不同的保護(hù)環(huán)。
術(shù)語中斷處理程序的使用可能引起岐義。因?yàn)樗怯糜谥复鶦PU對ISR(中斷服務(wù)程序)的調(diào)用,包括了進(jìn)入/退出路徑和ISR本身。當(dāng)然,如果只指代在進(jìn)入路徑和退出路徑之間進(jìn)行由C語言實(shí)現(xiàn)的例程,將更為準(zhǔn)確。
中斷技術(shù)上的實(shí)現(xiàn)有兩方面:
1) 匯編語言代碼:與處理器高度相關(guān),用于處理特定平臺上相關(guān)的底層細(xì)節(jié);
2) 抽象接口:是設(shè)備驅(qū)動程序及其他內(nèi)核代碼安裝和管理IRQ處理程序所需的。
描述匯編語言部分的功能會涉及無數(shù)細(xì)節(jié),可以參考處理器體系方面的手冊。
為響應(yīng)外部設(shè)備的IRQ,內(nèi)核必須為每個(gè)潛在的IRQ提供一個(gè)函數(shù)。該函數(shù)必須能夠動態(tài)注冊和注銷。靜態(tài)表組織方式是不夠的,因?yàn)榭赡転樵O(shè)備編寫模塊,而且設(shè)備可能與系統(tǒng)的其他部分通過中斷進(jìn)行交互。
IRQ相關(guān)信息管理的關(guān)鍵點(diǎn)是一個(gè)全局?jǐn)?shù)組,每個(gè)數(shù)組項(xiàng)對應(yīng)一個(gè)IRQ編號。因?yàn)閿?shù)組位置和中斷號是相同的,很容易定位與特定的IRQ相關(guān)的數(shù)組項(xiàng):IRQ0在位置0,IRQ15在位置15,等等,IRQ最終映射到哪個(gè)處理器中斷,在這里不相關(guān)的。
盡管各個(gè)數(shù)組項(xiàng)使用的是一個(gè)體系結(jié)構(gòu)無關(guān)的數(shù)據(jù)類型,但I(xiàn)RQ的最大可能數(shù)目是通過一個(gè)平臺相關(guān)的常數(shù)NR_IRQS指定的,大多數(shù)體系結(jié)構(gòu)一,該常數(shù)定義在處理器相關(guān)的頭文件irq.h中。不同處理器間及同一處理器家庭內(nèi),該常數(shù)的值變化都很大,主要取決于輔助CPU管理IRQ的輔助芯片。
與IRQ的最大數(shù)目相比,我們對各數(shù)組項(xiàng)的數(shù)據(jù)類型更感興趣。在了解細(xì)節(jié)之前,需要概述內(nèi)核的IRQ處理子系統(tǒng)。
之前的一些版本包含了大量平臺代碼來處理IRQ,在許多地方是相同的。因而,在內(nèi)核版本2.6開發(fā)期間,引入了一個(gè)新的通用的IRQ子系統(tǒng)。它能夠以統(tǒng)一的方式處理不同的中斷控制器和不同類型的中斷?;旧纤?個(gè)抽象層組成。如下圖:
◎ 高層ISR(high-level interrupt service routines):針對設(shè)備驅(qū)動程序端的中斷,執(zhí)行由此引起的所有必要的工作。
◎ 中斷電流處理(interrupt flow handling):處理不同的中斷電流類型之間的各種差別,如邊沿觸發(fā)和電平觸發(fā)。
邊沿觸發(fā):意味著硬件通過感知線路上的電位差來檢測中斷。
電平觸發(fā):根據(jù)特定的電勢值檢測中斷,與電勢是否改變無關(guān)。
◎ 芯片級硬件封裝(chip-level hardware encapsulation):需要與電子學(xué)層次上產(chǎn)生中斷的底層硬件直接通信。該抽象可以視為中斷控制器的某種“設(shè)備驅(qū)動程序”。
用于表示irq的結(jié)構(gòu)如下:(摘自Linux kernel 3.5)
在上面的代碼中,注釋部分基本上簡單的介紹了下各個(gè)字段的意義,下面會針對一些特殊的字段進(jìn)行說明。
在介紹下面的主要字段之前,先說下這個(gè)結(jié)構(gòu)中的irq_data字段,該字段保存了一些在中斷處理過程各個(gè)中斷處理階段都會用到的數(shù)據(jù),該結(jié)構(gòu)如下所示:(摘自Linux kernel 3.5)
大概的意思在注釋中可以看到,下面將結(jié)合該結(jié)構(gòu)和irq_desc結(jié)構(gòu)針對一些比較有用的字段做以介紹。
從內(nèi)核中高層代碼的角度來看,每個(gè)IRQ都可以由該結(jié)構(gòu)完全描述。上面介紹的3個(gè)抽象層在該結(jié)構(gòu)中表示如下:
◎ 電流層ISR由handle_irq提供。irq_data結(jié)構(gòu)中的handler_data可以指向任意數(shù)據(jù),該數(shù)據(jù)可以是特定于IRQ或處理程序。每當(dāng)發(fā)生中斷時(shí),特定于體系結(jié)構(gòu)的代碼都會調(diào)用handle_irq。該函數(shù)負(fù)責(zé)使用chip中提供的特定于控制器的方法,進(jìn)行處理中斷所必需的一些底層操作。用于不同中斷類型的默認(rèn)函數(shù)由內(nèi)核提供。
◎ action提供了一個(gè)操作鏈,需要在中斷發(fā)生時(shí)執(zhí)行。由中斷通知的設(shè)備驅(qū)動程序,可以將與之相關(guān)的處理程序函數(shù)放置在此處。有一個(gè)專門的數(shù)據(jù)結(jié)構(gòu)用于表示這些操作。
◎ 電流處理和芯片相關(guān)操作被封裝在ird_data結(jié)構(gòu)中的chip中。為此引入了一個(gè)專門的數(shù)據(jù)結(jié)構(gòu),irq_chip。該結(jié)構(gòu)相關(guān)的東西之后介紹。
◎ name指定了電流層處理程序的名稱,將顯示在/proc/interrupts中。對邊沿觸發(fā)是“edge”,對電平觸發(fā)中斷,通常是“l(fā)evel”。
一些其它的字段意義如下:
◎ depth有兩個(gè)任務(wù)。它可用于確定IRQ電路是啟用的還是禁用的,正值表示禁用的,而0表示啟用的。為什么用正值表示禁用的IRQ呢?因?yàn)檫@使得內(nèi)核能夠區(qū)分啟用和禁用的IRQ電路,以及重復(fù)禁用同一中斷的情形。這個(gè)值相當(dāng)于一個(gè)計(jì)數(shù)器,內(nèi)核其余部分的代碼每次禁用某個(gè)中斷,則將對應(yīng)的計(jì)數(shù)器加1;每次 斷被再次啟用,則將計(jì)數(shù)器減1 。在depth歸0時(shí),硬件才能再次使用對應(yīng)的IRQ。這各方法能夠支持對嵌套禁用中斷的正確處理。
◎ IRQ不僅可以在處理程序安裝期間改變其狀態(tài),而且可以在運(yùn)行時(shí)改變:status描述了IRQ的當(dāng)前狀態(tài)。<irq.h>中定義了各種常數(shù),可用于描述IRQ電路當(dāng)前的狀態(tài)。每個(gè)常數(shù)表示位串中一個(gè)置位的標(biāo)志位,只要不相互沖突,幾個(gè)標(biāo)志可以同時(shí)設(shè)置。
根據(jù)status當(dāng)前的值,內(nèi)核很容易獲知某個(gè)IRQ的狀態(tài),而無需了解底層實(shí)現(xiàn)的硬件相關(guān)特性。當(dāng)然,只設(shè)置對應(yīng)的標(biāo)志位是不會產(chǎn)生預(yù)期效果的。如:通過設(shè)置IRQ_DISABLED標(biāo)志來禁用中斷是不可能的,還必須將新狀態(tài)通知底層硬件。因而,該標(biāo)準(zhǔn)只能通過特定于控制器的函數(shù)設(shè)置,這些函數(shù)同時(shí)還負(fù)責(zé)將設(shè)置信息同步到底層硬件。
剛剛提到過,電流處理和芯片相關(guān)操作被封裝在ird_data結(jié)構(gòu)中的chip中。為此還引入了一個(gè)專門的數(shù)據(jù)結(jié)構(gòu),下面來詳細(xì)說下這個(gè)結(jié)構(gòu)。這個(gè)結(jié)構(gòu)是一個(gè)操作的集合,它提供的函數(shù)用于改變IRQ的狀態(tài),這也是它們還負(fù)責(zé)設(shè)置irq_desc結(jié)構(gòu)中的status字段的原因。該結(jié)構(gòu)如下:(摘自Linux kernel 3.5)
該結(jié)構(gòu)需要考慮內(nèi)核中出現(xiàn)的各個(gè)IRQ實(shí)現(xiàn)的所有特性。因而,一個(gè)該結(jié)構(gòu)的特定實(shí)例,通常只定義所有可能方法的一個(gè)子集。
name包含一個(gè)短的字符串,用于標(biāo)識硬件控制器。
各個(gè)函數(shù)指針的語義如下:
irq_startup指向一個(gè)函數(shù),用于第一次初始化一個(gè)IRQ。大多數(shù)情況下,初始化工作僅限于啟用該IRQ。因而, irq_startup函數(shù)實(shí)際上就工作轉(zhuǎn)給enable。
irq_enable激活一個(gè)IRQ。換句話說,它執(zhí)行IRQ由禁用狀態(tài)到啟用狀態(tài)的轉(zhuǎn)換。為此。必須向I/O內(nèi)存或I/O端口中硬件相關(guān)的位置寫入特定于硬件的數(shù)值。
irq_disable與enable相對應(yīng),用于禁用IRQ。而shutdown完全關(guān)閉一個(gè)中斷源。如果不支持該特性,那么這個(gè)函數(shù)實(shí)際上是disable的別名。
irq_ack與中斷控制器的硬件密切相關(guān)。在某些模型中,IRQ請求的到達(dá)必須顯式確認(rèn),后續(xù)的請求才能進(jìn)行處理。如果芯片組沒有這樣的要求,該指針可以指向一個(gè)空函數(shù),或NULL指針。irq_mask_ack確認(rèn)一個(gè)中斷,并在接下來屏蔽該中斷。
在現(xiàn)代的中斷控制器不需要內(nèi)核進(jìn)行太多的電流控制,控制器幾乎可以管理所有事務(wù)。在處理中斷時(shí)需要一個(gè)到硬件的回調(diào),由irq_eoi提供。eoi表示endof interrupt,即中斷結(jié)束。
在多處理器系統(tǒng)中,可使用irq_set_affinity指定用哪個(gè)CPU來處理特定的IRQ。這使得可以將IRQ分配給某些CPU。該方法在單處理器系統(tǒng)上沒用,可以設(shè)置為NULL。
set_type設(shè)置IRQ的電流類型。該方法主要使用在ARM、PowerPC和SuperH機(jī)器上,其他系統(tǒng)不需要該方法,可以將set_type設(shè)置為NULL。
還記得在在剛才在“中斷處理子系統(tǒng)的各部分交互方式”的圖中看到的,剛才介紹了下IRQ控制器的抽象對象,當(dāng)內(nèi)核獲取到相應(yīng)的中斷請求后,是如何執(zhí)行對應(yīng)的中斷處理函數(shù)。這里又要引出一個(gè)新的結(jié)構(gòu),irqaction結(jié)構(gòu)。每個(gè)處理程序函數(shù)都對應(yīng)該結(jié)構(gòu)的一個(gè)實(shí)例:該結(jié)構(gòu)如下:(摘自LinuxKernel 3.5)
該結(jié)構(gòu)中最重要的成員是處理程序函數(shù)本身,即handler成員,這是一個(gè)函數(shù)指針,位于結(jié)構(gòu)的起始處。在設(shè)備請求一個(gè)系統(tǒng)中斷,而中斷控制器通過引用發(fā)中斷將該請求轉(zhuǎn)發(fā)到處理器的時(shí)候,內(nèi)核將調(diào)用該處理程序函數(shù)。在考慮如何注冊處理程序函數(shù)時(shí),我們再仔細(xì)考察其參數(shù)的語義。但請注意,處理程序的類型為irq_handler_t,與電流處理程序的類型irq_flow_handler_t顯然是不同的。
name和dev_id唯一地標(biāo)識一個(gè)中斷處理程序。name是一個(gè)短字符串,用于標(biāo)識設(shè)備,而dev_id是一個(gè)指針,指向在所有內(nèi)核數(shù)據(jù)結(jié)構(gòu)中唯一標(biāo)識了該設(shè)備的數(shù)據(jù)結(jié)構(gòu)實(shí)例。
如果幾個(gè)設(shè)備共享一個(gè)IRQ,那么IRQ編號自身不能標(biāo)識該設(shè)備,此時(shí),在刪除處理程序函數(shù)時(shí),將需要上述信息。
flag是一個(gè)標(biāo)志變量,通過位圖描述了IRQ的一些特性,位圖中各個(gè)標(biāo)志位照例可通過預(yù)定義的常數(shù)。<interrupt.h>中定義了下列常數(shù)。
◎ 對共享的IRQ設(shè)置IRQF_SHARED,表示有多于一個(gè)設(shè)備使用該IRQ電路。
◎ 如果IRQ對內(nèi)核熵池有貢獻(xiàn),將設(shè)置IRQF_SAMPLE_RANDOM。
◎ IRQF_DISABLED表示IRQ的處理程序必須在禁用中斷的情況下執(zhí)行。
◎ IRQF_TIMER表示時(shí)鐘中斷
next用于實(shí)現(xiàn)共享的IRQ處理程序。幾個(gè)irqaction實(shí)例聚集到一個(gè)鏈表中。鏈表的所有元素都必須處理同一個(gè)IRQ編號。在一個(gè)鏈表中的中斷都屬于可共享的IRQ,這在后面會有一些介紹。
下圖給出了所描述各數(shù)據(jù)結(jié)構(gòu)的一個(gè)概覽,說明其彼此交互的方式。因?yàn)橥ǔT谝粋€(gè)系統(tǒng)上只有一種類型的中斷控制器會占據(jù)支配地位,所以在一般情況下所有的irq_desc的handler成員都指向kirq_chip的同一個(gè)實(shí)例。
在這里將介紹下電流處理是如何實(shí)現(xiàn)的。在內(nèi)核版本2.6重寫中斷邏輯之前,此領(lǐng)域中的現(xiàn)狀令人感到相當(dāng)痛苦,在電流處理中會涉及大量體系結(jié)構(gòu)相關(guān)的代碼。幸好,情況現(xiàn)在有了很大的改善,有一個(gè)通用框架幾乎可用于所有硬件,僅有少量例外。
首先,需要提到內(nèi)核提供的一些標(biāo)準(zhǔn)函數(shù),用于注冊irq_chip和設(shè)置電流處理程序:
該函數(shù)將一個(gè)IRQ芯片以irq_chip實(shí)例的形式關(guān)聯(lián)到某個(gè)特定的中斷上。除了從irq_desc選取適當(dāng)?shù)某蓡T并設(shè)置chip之外,如果沒有提供特定于芯片的實(shí)現(xiàn),該函數(shù)還將設(shè)置默認(rèn)的處理程序。如果chip指針為NULL,將使用通用的“無控制器”irq_chip實(shí)例no_irq_chip,該實(shí)現(xiàn)只提供了空操作。
這兩個(gè)函數(shù)為某個(gè)給定的IRQ編號設(shè)置電流處理程序。第二種變體表示,處理程序必須處理共享的中斷。這會置位irq_desc[irq]->status中的標(biāo)志位IRQ_NOREQUEST和IRQ_NOPROBE:設(shè)置第一標(biāo)志,是因?yàn)楣蚕碇袛嗍遣荒塥?dú)占使用的,設(shè)置第二個(gè)標(biāo)志,是因?yàn)樵谟卸鄠€(gè)設(shè)備的IRQ電路上,使用中斷探測顯然是個(gè)壞主意。
兩個(gè)函數(shù)在內(nèi)部都使用了__irq_set_handler,該函數(shù)執(zhí)行一些合理性的檢查,然后設(shè)置irq_desc[irq]->handle_irq。
該兩個(gè)函數(shù)是一種快捷方式,它相當(dāng)于連續(xù)調(diào)用上述兩個(gè)函數(shù)。_name的函數(shù)變體工作方式相同,但可以為電流處理程序指定一個(gè)名稱,保存在irq_desc[irq]->name中。
在討論電流處理程序?qū)崿F(xiàn)方式之前,需要介紹處理程序所用的類型。irq_flow_handler_t指定了IRQ電流處理程序函數(shù)的原型:
電流處理程序的參數(shù)包括IRQ編號和一個(gè)指向負(fù)責(zé)該中斷的irq_handler指針,該信息接下來可用于實(shí)現(xiàn)正確的電流處理。
在前面說過,不同的硬件需要不同的電流處理,例如,邊沿觸發(fā)和電平觸發(fā)就需要不同的處理。內(nèi)核對各種類型提供了幾個(gè)默認(rèn)的電流處理程序。它們有一個(gè)共同點(diǎn):每個(gè)電流處理程序在其工作結(jié)束后,都要負(fù)責(zé)調(diào)用高層ISR。handle_IRQ_event負(fù)責(zé)激活高層的處理程序,這將在后面討論?,F(xiàn)在主要講如何處理電流處理。
◎ 邊沿觸發(fā)
現(xiàn)在的硬件大部分采用的是邊沿觸發(fā)中斷,因此首先講述這一類型。默認(rèn)處理程序?qū)崿F(xiàn)在handle_edge_irq中。其代碼流程圖如下圖:
在處理邊沿觸發(fā)的IRQ時(shí)無須屏蔽,這與電平觸發(fā)IRQ是相反的。這對SMP系統(tǒng)有一個(gè)重要的含義:當(dāng)在一個(gè)CPU上處理一個(gè)IRQ時(shí),另一個(gè)同樣編號的IRQ可以出現(xiàn)在另一個(gè)CPU上,稱為第二個(gè)CPU。這意味著,當(dāng)電流處理處理程序在由第一個(gè)IRQ觸發(fā)的CPU上運(yùn)行時(shí),還可能被再次調(diào)用。但為什么應(yīng)該有兩個(gè)CPU同時(shí)運(yùn)行同一個(gè)IRQ處理程序呢?內(nèi)核想要避免這種情況:處理程序只應(yīng)在一個(gè)CPU上運(yùn)行。handle_edge_irq的開始部分必須處理這種情況。如果在irq_desc實(shí)例中的irq_data字段內(nèi)的state_use_accessors字段被設(shè)置了IRQD_IRQ_INPROGRESS標(biāo)志。則IRQ在另一個(gè)CPU上已經(jīng)處于處理過程中。通過設(shè)置IRQD_IRQ_INPROGRESS標(biāo)志,內(nèi)核能夠記錄還有另一個(gè)IRQ需要在稍后處理。在屏蔽該IRQ并通過mask_ack_irq向控制器發(fā)送一個(gè)確認(rèn)后,處理過程可以放。因而第二個(gè)CPU可以恢復(fù)正常的工作,而第一個(gè)CPU將在稍后處理該IRQ。
在IRQ被禁用,或沒有可用的ISR處理程序,都會放棄處理。
現(xiàn)在,開始IRQ處理本身所涉及的工作。在用芯片相關(guān)的函數(shù)chip->irq_ack向中斷控制器發(fā)送一個(gè)確認(rèn)。然后調(diào)用handle_irq_event函數(shù),該函數(shù)先將IRQS_PENDING的標(biāo)志清除,然后設(shè)置IRQD_IRQ_INPROGRESS標(biāo)志。這表示IRQ正在處理過程中,可用于避免同一處理程序在多個(gè)CPU上執(zhí)行。
假定只有一個(gè)IRQ需要處理。在這種情況下,這時(shí)handle_irq_event函數(shù)會激活高層ISR處理程序,然后可以清除IRQD_IRQ_INPROGRESS標(biāo)志。
IRQ的處理是在一個(gè)循環(huán)中進(jìn)行。假定我們剛好處于調(diào)用handle_irq_event之后的位置上。在第一個(gè)IRQ的ISR處理程序運(yùn)行時(shí),可能同時(shí)有第二個(gè)IRQ請求發(fā)送過來,前前面已經(jīng)說過,這通過IRQ_PENDING表示,如果設(shè)置了該標(biāo)志(同時(shí)該IRQ沒有禁用),那么有另一個(gè)IRQ正在等待處理,循環(huán)將從頭再次開始。
但在這種情況下,IRQ已經(jīng)被屏蔽(在設(shè)置IRQ_PENDING時(shí)同時(shí)也調(diào)用了mask_ack_irq函數(shù)將該中斷屏蔽掉了)。因而必須用unmask_irq解除IRQ的屏蔽,并清除IRQ_MASKED標(biāo)志,這確保在handle_irq_event執(zhí)行期間只能發(fā)生一個(gè)中斷(被屏蔽后將不會在收到同一個(gè)編號的中斷)。
◎ 電平觸發(fā)
與邊沿觸發(fā)中斷相比,電平觸發(fā)中斷稍微容易處理一些。這也反映在電流處理程序handle_level_irq的代碼流程圖中。如下圖:
電平觸發(fā)在處理時(shí)必須屏蔽,因此需要完成的第一件事就是調(diào)用mask_ack_irq,該函數(shù)屏蔽并確認(rèn)IRQ,這是通過調(diào)用chip->irq_mask_ack,如果該方法不可用,則連續(xù)調(diào)用chip->irq_ack和chip->irq_mask。在多處理器系統(tǒng)上,可能發(fā)生競態(tài)條件,盡管IRQ已經(jīng)在另一個(gè)CPU上處理,但仍然在當(dāng)前CPU上調(diào)用了handle_level_irq。這可以通過檢查IRQD_IRQ_INPROGRESS標(biāo)志來判斷,這種情況下,IRQ已經(jīng)在另一個(gè)CPU上處理,因而在當(dāng)前CPU上可以放棄處理。
如果沒有對該IRQ注冊處理程序,也可以立即放棄處理,因?yàn)闊o事可做。另一個(gè)導(dǎo)致放棄處理的原因是設(shè)置了IRQ_DISABLE。盡管被禁用,有問題的硬件仍然可能發(fā)出IRQ,但可以被忽略。
接下來調(diào)用handle_irq_event。該函數(shù)在邊沿觸發(fā)中已有相關(guān)說明。最后需要解除對IRQ的屏蔽。但內(nèi)核需要考慮到ISR可能禁用中斷的情況,在這種情況下,ISR仍然保持屏蔽狀態(tài)。否則,便調(diào)用unmask_irq來解除屏蔽。
由于設(shè)備驅(qū)動程序動態(tài)注冊ISR的工作,可以使所述的數(shù)據(jù)結(jié)構(gòu)非常簡單的進(jìn)行。在內(nèi)核版本2.6重寫中斷子系統(tǒng)之前,該函數(shù)是由平臺相關(guān)代碼實(shí)現(xiàn)的。其原型在所有體系結(jié)構(gòu)上都是相同的,因?yàn)閷帉懫脚_無關(guān)的驅(qū)動程序來說,這是一個(gè)絕對的先決條件?,F(xiàn)在,該函數(shù)由通用代碼實(shí)現(xiàn):
該函數(shù)其實(shí)是request_thread_irq的一個(gè)包裹函數(shù),該函數(shù)首先生成一個(gè)新的irqaction的實(shí)例,然后用函數(shù)參數(shù)填充其內(nèi)容。當(dāng)然,其中特別重要的是處理程序函數(shù)的handler。所有進(jìn)一步的工作都委托給__setup_irq函數(shù),它將執(zhí)行下列步驟:
如果設(shè)置了IRQF_SAMPLE_RANDOM,則該中斷將對內(nèi)核熵池有所貢獻(xiàn),熵池用于隨機(jī)數(shù)發(fā)生器/dev/radom。之后調(diào)用rand_initialize_irq將該IRQ添加到對應(yīng)的數(shù)據(jù)結(jié)構(gòu)中。
由request_thread_irq生成的irqaction實(shí)例被添加到所屬IRQ編號對應(yīng)的例程鏈表尾部,該鏈表表頭為irq_desc[irq]->action。在處理中斷共享中斷時(shí),內(nèi)核就通過這種方式來確保中斷發(fā)生時(shí)調(diào)用處理程序的順序與其注冊順序相同。
如果安裝的處理程序是該IRQ編號對應(yīng)鏈接中的第一個(gè),則調(diào)用handler->tup初始化函數(shù)。如果該IRQ此前已經(jīng)安裝了處理程序,則沒有必要再調(diào)用該函數(shù)。
register_irq_proc在proc文件系統(tǒng)中建立目錄/proc/irq/NULL。而register_handler_proc生成/proc/irq/NUM/name。接下來,系統(tǒng)中就可以看到對應(yīng)的IRQ通道在使用了。
釋放中斷的方案,與前述過程剛好相反。首先,通過硬件相關(guān)的函數(shù)chip->shutdown通知中斷控制器該IRQ已經(jīng)刪除,接下來將相關(guān)數(shù)據(jù)項(xiàng)從內(nèi)核的一般數(shù)據(jù)結(jié)構(gòu)中刪除。輔助函數(shù)free_irq承擔(dān)這些任務(wù)。在重寫IRQ子系統(tǒng)之前它是一個(gè)體系結(jié)構(gòu)相關(guān)的函數(shù)。
在IRQ處理程序需要刪除一個(gè)共享的中斷時(shí),IRQ編號本身不足以標(biāo)識該IRQ。在這種情況下,為提供唯一標(biāo)識,還必須使用前面講述的dev_id。內(nèi)核掃描所有注冊的處理程序的鏈表,直至找到一個(gè)匹配的處理程序。這時(shí)才能移除該項(xiàng)。
前面講述的機(jī)制喉適用于由系統(tǒng)外設(shè)的中斷請求所引發(fā)的中斷。但內(nèi)核還必須考慮由處理器本身或者用戶進(jìn)程中的軟件機(jī)制所引發(fā)的中斷。與IRQ相比,內(nèi)核無需提供接口,供此類中斷動態(tài)注冊處理程序這是因?yàn)?,所使用的編號在初始化時(shí)就是已知的,此后不會改變。中斷和異常的注冊在內(nèi)核初始化時(shí)進(jìn)行,其分配在運(yùn)行時(shí)并不改變。
在注冊了IRQ處理程序后,每次發(fā)生中斷時(shí)將執(zhí)行處理程序例程。仍然會出現(xiàn)如何協(xié)調(diào)不同平臺差異的問題,由于事情的特定性質(zhì)所致,使得差別不僅涉及平臺相關(guān)實(shí)現(xiàn)中的各個(gè)C函數(shù),還深入到用于底層處理、人工優(yōu)化的匯編語言代碼。
我們可以確定各個(gè)平臺之間的幾個(gè)結(jié)構(gòu)上的相似性。例如,前文討論過,各個(gè)平臺上的中斷操作都由3部分組成。進(jìn)入路徑從用戶態(tài)切換到核心態(tài),接下來執(zhí)行實(shí)際的處理程序例程,最后從核心態(tài)切換回用戶態(tài)。盡管涉及大量的匯編語言代碼,至少有一些C代碼片段在所有平臺上都是相似的。
到核心態(tài)的切換,是基于每個(gè)中斷之后由處理器自動執(zhí)行匯編代碼。該代碼的任務(wù)如上面所講,其中通常定義了各個(gè)入口點(diǎn),在中斷發(fā)生時(shí)處理器可以將控制流轉(zhuǎn)到這些入口點(diǎn)。
只有那些最為必要的操作直接在匯編語言代碼中執(zhí)行。內(nèi)核試圖盡快地返回到常規(guī)的C語言,因?yàn)镃語言代碼更容易處理。為此,必須創(chuàng)建一個(gè)環(huán)境,與C編譯器預(yù)期兼容。
在C語言中調(diào)用函數(shù)時(shí),需要將所需的靈氣按一定的順序放到棧上。在用戶態(tài)和核心態(tài)之間切換時(shí),還需要將最重要的寄存器保存到棧上,以便以后恢復(fù)。這兩個(gè)操作由平臺相關(guān)的匯編語言代碼執(zhí)行。在大多數(shù)平臺上,控制流接下來傳遞到C函數(shù)do_IRQ,其實(shí)現(xiàn)也是平臺相關(guān)的,但情況仍然得到了很大的簡化。該函數(shù)原型如下:
pt_regs用于保存內(nèi)核使用的寄存器集合。各個(gè)寄存器的值被依次壓棧(通過匯編語言代碼)。在C函數(shù)調(diào)用之前,一直保存在棧上。
pt_regs的定義可以確保棧上的各個(gè)寄存器項(xiàng)與該結(jié)構(gòu)的各個(gè)上對應(yīng)。這些值并不是僅僅保存用于后續(xù)的使用,C代碼也可以讀取這些值。
此外,寄存器集合也可以被復(fù)制到地址空間中棧以外的其它位置。在這咱情況下,do_IRQ的一個(gè)參數(shù)是指向pt_regs的指針,但這并沒有改變以下事實(shí):寄存器的內(nèi)存已經(jīng)被保存,可以由C代碼讀取。
pt_regs的定義是平臺相關(guān)的,因而不同的處理器提供了不同的寄存器集合。
只有在內(nèi)核使用內(nèi)核棧來處理IRQ的情況下,上面描述的情形才是正確的。但不一定總是如此,IA-32體系結(jié)構(gòu)提供了配置選項(xiàng)CONFIG_4KSTACKS。如果啟用該配置,內(nèi)核棧的長度由8KB縮減到4KB。由于IA-32計(jì)算機(jī)上頁面的大小是4KB,實(shí)現(xiàn)內(nèi)核棧所需的頁數(shù)目由2個(gè)減少到一個(gè)。由于單個(gè)內(nèi)存頁比兩個(gè)連續(xù)的內(nèi)存頁更容易分配,在系統(tǒng)中有大量活動進(jìn)程時(shí),這使得虛擬內(nèi)存子系統(tǒng)的工作會稍微容易些。遺憾的,對常規(guī)的內(nèi)核工作以及IRQ處理例程所需的空間來說,4KB并不總是夠用,因而引入了另外的兩個(gè)棧。
◎ 用于硬件IRQ處理的棧
◎ 用于軟件IRQ處理的棧
常規(guī)的內(nèi)核棧對每個(gè)進(jìn)程都會分配,而這兩個(gè)額外的棧是針對各CPU分別分配的,在硬件中斷發(fā)生時(shí),內(nèi)核需要切換到適當(dāng)?shù)臈!?/span>
電流處理程序例程的調(diào)用方式,因體系結(jié)構(gòu)而不同,我們假定內(nèi)核棧只使用了一個(gè)頁幀,即每個(gè)進(jìn)程的內(nèi)核棧為4KB。如果設(shè)置了CONFIG_4KSTACKS,內(nèi)核棧的配置就是這樣。在上面說過,如果在這種情況下,內(nèi)核需要一個(gè)獨(dú)立的棧處理IRQ。
首先開始先調(diào)用set_irq_regs將一個(gè)指向寄存器集合的指針保存在一個(gè)全局的CPU變量中(中斷發(fā)生之前,變量中保存的舊指針會保留下來,借后續(xù)使用)。需要訪問寄存器集合的中斷處理程序,可以從該變量中訪問。
接下調(diào)用irq_enter負(fù)責(zé)更新一些統(tǒng)計(jì)量。對于具備動態(tài)時(shí)鐘周期特性的系統(tǒng),如果系統(tǒng)已經(jīng)有很長一段時(shí)間沒有發(fā)生時(shí)鐘中斷,則更新全局計(jì)時(shí)變量jiffies。接下來內(nèi)核必須切換到IRQ棧。當(dāng)前棧可以通過調(diào)用輔助函數(shù)current_thead_info獲得,該函數(shù)返回一個(gè)指向當(dāng)前使用的thread_info實(shí)例的指針。而指向適當(dāng)?shù)腎RQ棧的指針可以從上下文中的hardirq_ctx獲得。有如下兩種可能的情況:
進(jìn)程已經(jīng)在使用IRQ棧了,因?yàn)槭窃谔幚砬短椎腎RQ,在這種情況下,內(nèi)核不需要做什么,所有的設(shè)置都已經(jīng)完成。可以調(diào)用ird_desc[irq]->handle_irq來激活保存在IRQ數(shù)據(jù)庫中的IRQ。
當(dāng)前棧不是IRQ棧,(curctx != irqctx),需要在二者之間切換,在這種情況下,內(nèi)核執(zhí)行所需的底層匯編語言操作來切換棧,然后調(diào)用ird_desc[irq]->handle_irq,最后再將棧切換回去。
接下來調(diào)用irq_exit函數(shù),該函數(shù)負(fù)責(zé)記錄一些統(tǒng)計(jì)量,另外還要調(diào)用do_softirq來處理任何待決的軟件IRQ。最后再次調(diào)用set_irq_regs,將指向struct pt_regs的指針恢復(fù)到上一次調(diào)用之前的值。這確保嵌套的處理程序能夠正確工作。
經(jīng)過do_IRQ的工作最終將調(diào)到不同的電流處理函數(shù),也就是之前所說的handle_edge_irq或handle_level_irq函數(shù),在介紹這兩個(gè)函數(shù)的時(shí)候,函數(shù)最終都會去調(diào)用handle_irq_event的函數(shù),而這個(gè)函數(shù)最終又會調(diào)用到handle_irq_event_percpu函數(shù),都會采用這個(gè)函數(shù)來激活與特定IRQ相關(guān)的高層ISR?,F(xiàn)在需要仔細(xì)的說下這個(gè)函數(shù)。該函數(shù)原型如下:
該函數(shù)主要的任務(wù)就是逐一調(diào)用所注冊的IRQ處理程序action。代碼大概如下面這樣:
在共享IRQ時(shí),內(nèi)核無法找出引發(fā)中斷請求的設(shè)備。該工作完全留自帶程序例程,其中將使用設(shè)備相關(guān)的寄存器或其他硬件特征來查找中斷來源。未受影響的例程也需要識別出該中斷并非來自于相關(guān)設(shè)備,應(yīng)該盡可能快的將控制返回。但處理程序例程也無法向高層代碼報(bào)告該中斷是否是針對它的。內(nèi)核總是依次執(zhí)行所有處理程序例程,而不考慮實(shí)際上哪個(gè)處理程序與該中斷相關(guān)。
但內(nèi)核總可以檢查是否有負(fù)責(zé)該IRQ的處理程序。irqreturn_t定義為處理程序函數(shù)的返回類型,它只是一個(gè)簡單的整形變量??梢越邮誌RQ_NONE和IRQ_HANDLED,這取決于處理程序是否處理了該IRQ。
在執(zhí)行所有處理程序例程期間,內(nèi)核將返回結(jié)果用邏輯“或”操作合并起來。內(nèi)核最后可以據(jù)此判斷IRQ是否被處理。
中斷這塊的內(nèi)容大概說完了,不過這里也只是從硬件得到響應(yīng)后的硬件中斷,因?yàn)樵陂_始也說過,由于CPU的資源寶貴,而在中斷處理期間又不能有任何的搶占操作,所以在硬件中斷的過程中,各個(gè)中斷處理例程只是把一些必須的操作在這里面執(zhí)行完,然后就把控制權(quán)交回,真正做后續(xù)處理的是后面將介紹的軟中斷。下一篇,將要針對軟中斷進(jìn)行一些介紹。
本篇文章參考了《深入理解Linux內(nèi)核架構(gòu)》。有些代碼結(jié)合最新3.5的內(nèi)核進(jìn)行了分析。