一、中斷基本原理
中斷是計算機中非常重要的功能,其重要性不亞于人的神經(jīng)系統(tǒng)加脈搏。雖然圖靈機和馮諾依曼結構中沒有中斷,但是計算機如果真的沒有中斷的話,那么計算機就相當于是半個殘疾人。今天我們就來全面詳細地講一講中斷。
1.1 中斷的定義
我們先來看一下中斷的定義:
中斷機制:CPU在執(zhí)行指令時,收到某個中斷信號轉而去執(zhí)行預先設定好的代碼,然后再返回到原指令流中繼續(xù)執(zhí)行,這就是中斷機制。
可以發(fā)現(xiàn)中斷的定義非常簡單。我們根據(jù)中斷的定義來畫一張圖:
在圖靈機模型中,計算機是一直線性運行的。加入了中斷之后,計算機就可以透明地在進程執(zhí)行流中插入一段代碼來執(zhí)行。那么這么做的目的是什么呢?
1.2 中斷的作用
設計中斷機制的目的在于中斷機制有以下4個作用,這些作用可以幫助操作系統(tǒng)實現(xiàn)自己的功能。這四個作用分別是:
1.外設異步通知CPU:外設發(fā)生了什么事情或者完成了什么任務或者有什么消息要告訴CPU,都可以異步給CPU發(fā)通知。例如,網(wǎng)卡收到了網(wǎng)絡包,磁盤完成了IO任務,定時器的間隔時間到了,都可以給CPU發(fā)中斷信號。
2.CPU之間發(fā)送消息:在SMP系統(tǒng)中,一個CPU想要給另一個CPU發(fā)送消息,可以給其發(fā)送IPI(處理器間中斷)。
3.處理CPU異常:CPU在執(zhí)行指令的過程中遇到了異常會給自己發(fā)送中斷信號來處理異常。例如,做整數(shù)除法運算的時候發(fā)現(xiàn)被除數(shù)是0,訪問虛擬內(nèi)存的時候發(fā)現(xiàn)虛擬內(nèi)存沒有映射到物理內(nèi)存上。
4.實現(xiàn)系統(tǒng)調用:早期的系統(tǒng)調用就是靠中斷指令來實現(xiàn)的,后期雖然開發(fā)了專用的系統(tǒng)調用指令,但是其基本原理還是相似的。
1.3 中斷的產(chǎn)生
那么中斷信號又是如何產(chǎn)生的呢?中斷信號的產(chǎn)生有以下4個來源:
1.外設,外設產(chǎn)生的中斷信號是異步的,一般也叫做硬件中斷(注意硬中斷是另外一個概念)。硬件中斷按照是否可以屏蔽分為可屏蔽中斷和不可屏蔽中斷。例如,網(wǎng)卡、磁盤、定時器都可以產(chǎn)生硬件中斷。
2.CPU,這里指的是一個CPU向另一個CPU發(fā)送中斷,這種中斷叫做IPI(處理器間中斷)。IPI也可以看出是一種特殊的硬件中斷,因為它和硬件中斷的模式差不多,都是異步的。
3.CPU異常,CPU在執(zhí)行指令的過程中發(fā)現(xiàn)異常會向自己發(fā)送中斷信號,這種中斷是同步的,一般也叫做軟件中斷(注意軟中斷是另外一個概念)。CPU異常按照是否需要修復以及是否能修復分為3類:1.陷阱(trap),不需要修復,中斷處理完成后繼續(xù)執(zhí)行下一條指令,2.故障(fault),需要修復也有可能修復,中斷處理完成后重新執(zhí)行之前的指令,3.中止(abort),需要修復但是無法修復,中斷處理完成后,進程或者內(nèi)核將會崩潰。例如,缺頁異常是一種故障,所以也叫缺頁故障,缺頁異常處理完成后會重新執(zhí)行剛才的指令。
4.中斷指令,直接用CPU指令來產(chǎn)生中斷信號,這種中斷和CPU異常一樣是同步的,也可以叫做軟件中斷。例如,中斷指令int 0x80可以用來實現(xiàn)系統(tǒng)調用。
中斷信號的4個來源正好對應著中斷的4個作用。前兩種中斷都可以叫做硬件中斷,都是異步的;后兩種中斷都可以叫做軟件中斷,都是同步的。很多書上也把硬件中斷叫做中斷,把軟件中斷叫做異常。
1.4 中斷的處理
那么中斷信號又是如何處理的呢?也許你會覺得這不是很簡單嗎,前面的圖里面不是畫的很清楚嗎,中斷信號就是在正常的執(zhí)行流中插入一段中斷執(zhí)行流啊。雖然這種中斷處理方式簡單又直接,但是它還存在著問題。
執(zhí)行場景(execute context)
在繼續(xù)講解之前,我們先引入一個概念,執(zhí)行場景(execute context)。在中斷產(chǎn)生之前是沒有這個概念的,有了中斷之后,CPU就分為兩個執(zhí)行場景了,進程執(zhí)行場景(process context)和中斷執(zhí)行場景(interrupt context)。那么哪些是進程執(zhí)行場景哪些是中斷執(zhí)行場景呢?進程的執(zhí)行是進程執(zhí)行場景,同步中斷的處理也是進程執(zhí)行場景,異步中斷的處理是中斷執(zhí)行場景。可能有的人會對同步中斷的處理是進程執(zhí)行場景感到疑惑,但是這也很好理解,因為同步中斷處理是和當前指令相關的,可以看做是進程執(zhí)行的一部分。而異步中斷的處理和當前指令沒有關系,所以不是進程執(zhí)行場景。
進程執(zhí)行場景和中斷執(zhí)行場景有兩個區(qū)別:一是進程執(zhí)行場景是可以調度、可以休眠的,而中斷執(zhí)行場景是不可以調度不可用休眠的;二是在進程執(zhí)行場景中是可以接受中斷信號的,而在中斷執(zhí)行場景中是屏蔽中斷信號的。所以如果中斷執(zhí)行場景的執(zhí)行時間太長的話,就會影響我們對新的中斷信號的響應性,所以我們需要盡量縮短中斷執(zhí)行場景的時間。為此我們對異步中斷的處理有下面兩類辦法:
1.立即完全處理:
對于簡單好處理的異步中斷可以立即進行完全處理。
2.立即預處理 + 稍后完全處理:
對于處理起來比較耗時的中斷可以采取立即預處理加稍后完全處理的方式來處理。
為了方便表述,我們把立即完全處理和立即預處理都叫做中斷預處理,把稍后完全處理叫做中斷后處理。中斷預處理只有一種實現(xiàn)方式,就是直接處理。但是中斷后處理卻有很多種方法,其處理方法可以運行在中斷執(zhí)行場景,也可以運行在進程執(zhí)行場景,前者叫做直接中斷后處理,后者叫做線程化中斷后處理。
在Linux中,中斷預處理叫做上半部,中斷后處理叫做下半部。由于“上半部、下半部”詞義不明晰,我們在本文中都用中斷預處理、中斷后處理來稱呼。中斷預處理只有一種方法,叫做hardirq(硬中斷)。中斷后處理有很多種方法,分為兩類,直接中斷后處理有softirq(軟中斷)、tasklet(微任務),線程化中斷后處理有workqueue(工作隊列)、threaded_irq(中斷線程)。
硬中斷、軟中斷是什么意思呢?本來的異步中斷處理是直接把中斷處理完的,整個過程是屏蔽中斷的,現(xiàn)在,把整個過程分成了兩部分,前半部分還是屏蔽中斷的,叫做硬中斷,處理與硬件相關的緊急事物,后半部分不再屏蔽中斷,叫做軟中斷,處理剩余的事物。由于軟中斷中不再屏蔽中斷信號,所以提高了系統(tǒng)對中斷的響應性。
注意硬件中斷、軟件中斷,硬中斷、軟中斷是不同的概念,分別指的是中斷的來源和中斷的處理方式。
1.5 中斷向量號
不同的中斷信號需要有不同的處理方式,那么系統(tǒng)是怎么區(qū)分不同的中斷信號呢?是靠中斷向量號。每一個中斷信號都有一個中斷向量號,中斷向量號是一個整數(shù)。CPU收到一個中斷信號會根據(jù)這個信號的中斷的向量號去查詢中斷向量表,根據(jù)向量表里面的指示去調用相應的處理函數(shù)。
中斷信號和中斷向量號是如何對應的呢?對于CPU異常來說,其向量號是由CPU架構標準規(guī)定的。對于外設來說,其向量號是由設備驅動動態(tài)申請的。對于IPI中斷和指令中斷來說,其向量號是由內(nèi)核規(guī)定的。
那么中斷向量表是什么格式,應該如何設置呢,這個我們后面會講。
1.6 中斷框架結構
有了前面這么多基礎知識,下面我們對中斷機制做個概覽。
中斷信號的產(chǎn)生有兩類,分別是異步中斷和同步中斷,異步中斷包括外設中斷和IPI中斷,同步中斷包括CPU異常和指令中斷。無論是同步中斷還是異步中斷,都要經(jīng)過中斷向量表進行處理。對于同步中斷的處理是異常處理或者系統(tǒng)調用,它們都是進程執(zhí)行場景,所以沒有過多的處理方法,就是直接執(zhí)行。對于異步中斷的處理,由于直接調用處理是屬于中斷執(zhí)行場景,默認的中斷執(zhí)行場景是會屏蔽中斷的,這會降低系統(tǒng)對中斷的響應性,所以內(nèi)核開發(fā)出了很多的方法來解決這個問題。
下面的章節(jié)是對這個圖的詳細解釋,我們先講中斷向量表,再講中斷的產(chǎn)生,最后講中斷的處理。
本文后面都是以x86 CPU架構進行講解的。
二、中斷流程
CPU收到中斷信號后會首先保存被中斷程序的狀態(tài),然后再去執(zhí)行中斷處理程序,最后再返回到原程序中被中斷的點去執(zhí)行。具體是怎么做呢?我們以x86為例講解一下。
2.1 保存現(xiàn)場
CPU收到中斷信號后會首先把一些數(shù)據(jù)push到內(nèi)核棧上,保存的數(shù)據(jù)是和當前執(zhí)行點相關的,這樣中斷完成后就可以返回到原執(zhí)行點。如果CPU當前處于用戶態(tài),則會先切換到內(nèi)核態(tài),把用戶棧切換為內(nèi)核棧再去保存數(shù)據(jù)(內(nèi)核棧的位置是在當前線程的TSS中獲取的)。下面我們畫個圖看一下:
CPU都push了哪些數(shù)據(jù)呢?分為兩種情況。當CPU處于內(nèi)核態(tài)時,會push寄存器EFLAGS、CS、EIP的值到棧上,對于有些CPU異常還會push Error Code。Push CS、EIP是為了中斷完成后返回到原執(zhí)行點,push EFLAGS是為了恢復之前的CPU狀態(tài)。當CPU處于用戶態(tài)時,會先切換到內(nèi)核態(tài),把棧切換到內(nèi)核棧,然后push寄存器SS(old)、ESP(old)、EFLAGS、CS、EIP的值到新的內(nèi)核棧,對于有些CPU異常還會push Error Code。Push SS(old)、ESP(old),是為了中斷返回的時候可以切換回原來的棧。有些CPU異常會push Error Code,這樣可以方便中斷處理程序知道更具體的異常信息。不是所有的CPU異常都會push Error Code,具體哪些會哪些不會在3.1節(jié)中會講。
上圖是32位的情況,64位的時候會push 64位下的寄存器。
2.2 查找向量表
保存完被中斷程序的信息之后,就要去執(zhí)行中斷處理程序了。CPU會根據(jù)當前中斷信號的向量號去查詢中斷向量表找到中斷處理程序。CPU是如何獲得當前中斷信號的向量號的呢,如果是CPU異常可以在CPU內(nèi)部獲取,如果是指令中斷,在指令中就有向量號,如果是硬件中斷,則可以從中斷控制器中獲取中斷向量號。那CPU又是怎么找到中斷向量表呢,是通過IDTR寄存器。IDTR寄存器的格式如下圖所示:
IDTR寄存器由兩部分組成:一部分是IDT基地址,在32位上是32位,在64位上是64位,是虛擬內(nèi)存上的地址;一部分是IDT限長,是16位,單位是字節(jié),代表中斷向量表的長度。雖然x86支持256個中斷向量,但是系統(tǒng)不一定要用滿256個,IDT限長用來指定中斷向量表的大小。系統(tǒng)在啟動時分配一定大小的內(nèi)存用來做中斷向量表,然后通過LIDT指令設置IDTR寄存器的值,這樣CPU就知道中斷向量表的位置和大小了。
IDTR寄存器設置好之后,中斷向量表的內(nèi)容還是可以再修改的。該如何修改呢,這就需要我們知道中斷向量表的數(shù)據(jù)結構了。中斷向量表是一個數(shù)組結構,數(shù)組的每一項叫做中斷向量表條目,每個條目都是一個門描述符(gate descriptor)。門描述符一共有三種類型,不同類型的具體結構不同,三類門描述符分別是任務門描述符、中斷門描述符、陷阱門描述符。任務門不太常用,后面我們都默認忽略任務門。中斷門一般用于硬件中斷,陷阱門一般用于軟件中斷。32位下的門描述符是8字節(jié),下面是它們的具體結構:
Segment Selector是段選擇符,Offset是段偏移,兩個段偏移共同構成一個32的段偏移。p代表段是否加載到了內(nèi)存。dpl是段描述符特權級。d為0代表是16位描述符,d為1代表是32位描述符。Type 是8 9 10三位,代表描述符的類型。
下面看一下64位門描述符的格式:
可以看到64位和32位最主要的變化是把段偏移變成了64位。
關于x86的分段機制,這里就不展開討論了,簡介地介紹一下其在Linux內(nèi)核中的應用。Linux內(nèi)核并不使用x86的分段機制,但是x86上特權級的切換還是需要用到分段。所以Linux采取的方法是,定義了四個段__KERNEL_CS、__KERNEL_DS、__USER_CS、__USER_DS,這四個段的段基址都是0,段限長都是整個內(nèi)存大小,所以在邏輯上相當于不分段。但是這四個段的特權級不一樣,__KERNEL_CS、__KERNEL_DS是內(nèi)核特權級,用在內(nèi)核執(zhí)行時,__USER_CS、__USER_DS是用戶特權級,用在進程執(zhí)行時。由于中斷都運行在內(nèi)核,所以所有中斷的門描述符的段選擇符都是__KERNEL_CS,而段偏移實際上就是終端處理函數(shù)的虛擬地址。
CPU現(xiàn)在已經(jīng)把被中斷的程序現(xiàn)場保存到內(nèi)核棧上了,又得到了中斷向量號,然后就根據(jù)中斷向量號從中斷向量表中找到對應的門描述符,對描述符做一番安全檢查之后,CPU就開始執(zhí)行中斷處理函數(shù)(就是門描述符中的段偏移)。中斷處理函數(shù)的最末尾執(zhí)行IRET指令,這個指令會根據(jù)前面保存在棧上的數(shù)據(jù)跳回到原來的指令繼續(xù)執(zhí)行。
三、軟件中斷
對中斷的基本概念和整個處理流程有了大概的認識之后,我們來看一下軟件中斷的產(chǎn)生。軟件中斷有兩類,CPU異常和指令中斷。我們先來看CPU異常:
3.1 CPU異常
CPU在執(zhí)行指令的過程中遇到了異常就會給自己發(fā)送中斷信號。注意異常不一定是錯誤,只要是異于平常就都是異常。有些異常不但不是錯誤,它還是實現(xiàn)內(nèi)核重要功能的方法。CPU異常分為3類:1.陷阱(trap),陷阱并不是錯誤,而是想要陷入內(nèi)核來執(zhí)行一些操作,中斷處理完成后繼續(xù)執(zhí)行之前的下一條指令,2.故障(fault),故障是程序遇到了問題需要修復,問題不一定是錯誤,如果問題能夠修復,那么中斷處理完成后會重新執(zhí)行之前的指令,如果問題無法修復那就是錯誤,當前進程將會被殺死。3.中止(abort),系統(tǒng)遇到了很嚴重的錯誤,無法修改,一般系統(tǒng)會崩潰。
CPU異常的含義和其向量號都是架構標準提前定義好的,下面我們來看一下。
x86一共有256個中斷向量號,前32個(0-31)是Intel預留的,其中0-21(除了15)都已分配給特定的CPU異常。32-255是給硬件中斷和指令中斷保留的向量號。
3.2 指令中斷
指令中斷和CPU異常有很大的相似性,都屬于同步中斷,都是屬于因為執(zhí)行指令而產(chǎn)生了中斷。不同的是CPU異常不是在執(zhí)行特定的指令時發(fā)生的,也不是必然發(fā)生。而指令中斷是執(zhí)行特定的指令而發(fā)生的中斷,設計這些指令的目的就是為了產(chǎn)生中斷的,而且一定會產(chǎn)生中斷或者有些條件成立的情況下一定會產(chǎn)生中斷。其中指令INT n可以產(chǎn)生任意中斷,n可以取任意值。Linux用int 0x80來作為系統(tǒng)調用的指令。
四、硬件中斷
硬件中斷分為外設中斷和處理器間中斷(IPI),下面我們先來看一下外設中斷。
4.1 外設中斷
外設中斷和軟件中斷有一個很大的不同,軟件中斷是CPU自己給自己發(fā)送中斷,而外設中斷是需要外設發(fā)送中斷給CPU。外設想要給CPU發(fā)送中斷,那就必須要連接到CPU,不可能隔空發(fā)送。那么怎么連接呢,如果所有外設都直接連到CPU,顯然是不可能的。因為一個計算機系統(tǒng)中的外設是非常多的,而且多種多樣,CPU無法提前為所有外設設計和預留接口。所以需要一個中間設備,就像秘書一樣替CPU連接到所有的外設并接收中斷信號,再轉發(fā)給CPU,這個設備就叫做中斷控制器(Interrupt Controller )。
在x86上,在UP時代的時候,有一個中斷控制器叫做PIC(Programmable Interrupt Controller )。所有的外設都連接到PIC上,PIC再連接到CPU的中斷引腳上。外設給PIC發(fā)中斷,PIC再把中斷轉發(fā)給CPU。由于PIC的設計問題,一個PIC只能連接8個外設,所以后來把兩個PIC級聯(lián)起來,第二個PIC連接到第一個PIC的一個引腳上,這樣一共能連接15個外設。
到了SMP時代的時候,PIC顯然不能勝任工作了,于是Intel開發(fā)了APIC(Advanced PIC)。APIC分為兩個部分:一部分是Local APIC,有NR_CPU個,每個CPU都連接一個Local APIC;一部分是IO APIC,只有一個,所有的外設都連接到這個IO APIC上。IO APIC連接到所有的Local APIC上,當外設向IO APIC發(fā)送中斷時,IO APIC會把中斷信號轉發(fā)給某個Local APIC。有些per CPU的設備是直接連接到Local APIC的,可以通過Local APIC直接給自己的CPU發(fā)送中斷。
外設中斷并不是直接分配中斷向量號,而是直接分配IRQ號,然后IRQ+32就是其中斷向量號。有些外設的IRQ是內(nèi)核預先設定好的,有些是行業(yè)默認的IRQ號。
關于APIC的細節(jié)這里就不再闡述了,推薦大家去看《Interrupt in Linux (硬件篇)》,對APIC講的比較詳細。
4.2 處理器間中斷
在SMP系統(tǒng)中,多個CPU之間有時候也需要發(fā)送消息,于是就產(chǎn)生了處理器間中斷(IPI)。IPI既像軟件中斷又像硬件中斷,它的產(chǎn)生像軟件中斷,是在程序中用代碼發(fā)送的,而它的處理像硬件中斷,是異步的。我們這里把IPI看作是硬件中斷,因為一個CPU可以把另外一個CPU看做外設,就相當于是外設發(fā)來的中斷。
五、中斷處理
終于講到中斷處理了,我們再把之前的中間機制圖搬過來,再回顧一下:
無論是硬件中斷還是軟件中斷,都是通過中斷向量表進行處理的。但是不同的是,軟件中斷的處理程序是屬于進程執(zhí)行場景,所以直接把中斷處理程序設置好就行了,中斷處理程序怎么寫也沒有什么要顧慮的。而硬件中斷的處理程序就不同了,它是屬于中斷執(zhí)行場景。不僅其中斷處理函數(shù)中不能調用會阻塞、休眠的函數(shù),而且處理程序本身要盡量的短,越短越好。所以為了使硬件中斷處理函數(shù)盡可能的短,Linux內(nèi)核開發(fā)了一大堆方法。這些方法包括硬中斷(hardirq)、軟中斷(softirq)、微任務(tasklet)、中斷線程(threaded irq)、工作隊列(workqueue)。其實硬中斷嚴格來說不算是一種方法,因為它是中斷處理的必經(jīng)之路,它就是中斷向量表里面設置的處理函數(shù)。為了和軟中斷進行區(qū)分,才把硬中斷叫做硬中斷。硬中斷和軟中斷都是屬于中斷執(zhí)行場景,而中斷線程和工作隊列是屬于進程執(zhí)行場景。把硬件中斷的處理任務放到進程場景里面來做,大大提高了中斷處理的靈活性。
由于軟件中斷的處理都是直接處理,都是內(nèi)核本身直接寫好了的,一般都接觸不到,而硬件中斷的處理和硬件驅動密切相關,所以很多書上所講的中斷處理都是指的硬件中斷的處理。
5.1 異常處理
x86上的異常處理是怎么設置的呢?我們把前面的圖搬過來看一下:
我們對照著這個圖去捋代碼。首先我們需要分配一片內(nèi)存來存放中斷向量表,這個是在如下代碼中分配的。
linux-src/arch/x86/kernel/idt.c
/* Must be page-aligned because the real IDT is used in the cpu entry area */static gate_desc idt_table[IDT_ENTRIES] __page_aligned_bss;
linux-src/arch/x86/include/asm/desc_defs.h
struct idt_bits { u16 ist : 3, zero : 5, type : 5, dpl : 2, p : 1;} __attribute__((packed));
struct gate_struct { u16 offset_low; u16
segment; struct idt_bits bits; u16 offset_middle;#ifdef CONFIG_X86_64 u32
offset_high;
u32
reserved;#endif} __attribute__((packed));
typedef struct gate_struct gate_desc;
linux-src/arch/x86/include/asm/segment.h
#define IDT_ENTRIES 256
可以看到我們的中斷向量表idt_table是門描述符gate_desc的數(shù)組,數(shù)組大小是IDT_ENTRIES 256。門描述符gate_desc的定義和前面畫的圖是一致的,注意x86是小端序。
寄存器IDTR內(nèi)容包括IDT的基址和限長,為此我們專門定義一個數(shù)據(jù)結構包含IDT的基址和限長,然后就可以用這個變量通過LIDT指令來設置IDTR寄存器了。
linux-src/arch/x86/kernel/idt.c
static struct desc_ptr idt_descr __ro_after_init = { .size = IDT_TABLE_SIZE - 1, .address = (unsigned long) idt_table,};
linux-src/arch/x86/include/asm/desc.h
#define load_idt(dtr) native_load_idt(dtr)static __always_inline void native_load_idt(const struct desc_ptr *dtr){ asm volatile(“l(fā)idt %0”::“m” (*dtr));}
有一點需要注意的,我們并不是需要把idt_table完全初始化好了再去load_idt,我們可以先初始化一部分的idt_table,然后再去load_idt,之后可以不停地去完善idt_table。
我們先來看一下內(nèi)核是什么時候load_idt的,其實內(nèi)核有多次load_idt,不過實際上只需要一次就夠了。
調用棧如下:
start_kernel
setup_arch
idt_setup_early_traps
代碼如下:
linux-src/arch/x86/kernel/idt.c
void __init idt_setup_early_traps(void){ idt_setup_from_table(idt_table, early_idts, ARRAY_SIZE(early_idts), true); load_idt(&idt_descr);}
這是內(nèi)核在start_kernel里第一次設置IDTR,雖然之前的代碼里也有設置過IDTR,我們就不考慮了。load_idt之后,IDT就生效了,只不過這里IDT還沒有設置全,只設置了少數(shù)幾個CPU異常的處理函數(shù),我們來看一下是怎么設置的。
linux-src/arch/x86/kernel/idt.c
static __init voididt_setup_from_table(gate_desc *idt, const struct idt_data *t, int size, bool sys){ gate_desc desc;
for (; size 》 0; t++, size--) { idt_init_desc(&desc, t); write_idt_entry(idt, t-》vector, &desc);
if (sys)
set_bit(t-》vector, system_vectors); }}
static inline void idt_init_desc(gate_desc *gate, const struct idt_data *d){ unsigned long addr = (unsigned long) d-》addr;
gate-》offset_low = (u16) addr; gate-》segment
= (u16) d-》segment; gate-》bits
= d-》bits; gate-》offset_middle = (u16) (addr 》》 16);#ifdef CONFIG_X86_64 gate-》offset_high = (u32) (addr 》》 32); gate-》reserved = 0;#endif}
#define write_idt_entry(dt, entry, g)
native_write_idt_entry(dt, entry, g)static inline void native_write_idt_entry(gate_desc *idt, int entry, const gate_desc *gate){ memcpy(&idt[entry], gate, sizeof(*gate));}
在函數(shù)idt_setup_from_table里會定義一個gate_desc的臨時變量,然后用idt_data來初始化這個gate_desc,最后會把gate_desc復制到idt_table中對應的位置中去。這樣中斷向量表中的這一項就生效了。
下面我們再來看看idt_data數(shù)據(jù)是怎么來的:
linux-src/arch/x86/kernel/idt.c
static const __initconst struct idt_data early_idts[] = { INTG(X86_TRAP_DB,
asm_exc_debug), SYSG(X86_TRAP_BP, asm_exc_int3),};
#define G(_vector, _addr, _ist, _type, _dpl, _segment) {
.vector = _vector,
.bits.ist = _ist,
.bits.type = _type,
.bits.dpl = _dpl,
.bits.p = 1,
.addr = _addr, 。
segment = _segment, }
/* Interrupt gate */#define INTG(_vector, _addr)
G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL0, __KERNEL_CS)
/* System interrupt gate */#define SYSG(_vector, _addr)
G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL3, __KERNEL_CS)
linux-src/arch/x86/kernel/traps.c
DEFINE_IDTENTRY_DEBUG(exc_debug){ exc_debug_kernel(regs, debug_read_clear_dr6());}
EFINE_IDTENTRY_RAW(exc_int3){ /* * poke_int3_handler() is completely self contained code; it does (and * must) *NOT* call out to anything, lest it hits upon yet another
* INT3. */ if (poke_int3_handler(regs)) return;
/* * irqentry_enter_from_user_mode() uses static_branch_{,un}likely() * and therefore can trigger INT3, hence poke_int3_handler() must
* be done before. If the entry came from kernel mode, then use * nmi_enter() because the INT3 could have been hit in any context * including NMI.
*/ if (user_mode(regs)) { irqentry_enter_from_user_mode(regs);
instrumentation_begin();
do_int3_user(regs);
instrumentation_end();
irqentry_exit_to_user_mode(regs); } else {
irqentry_state_t irq_state = irqentry_nmi_enter(regs);
instrumentation_begin();
if (!do_int3(regs))
die(“int3”, regs, 0);
instrumentation_end(); irqentry_nmi_exit(regs, irq_state); }}
early_idts是idt_data的數(shù)組,在這里定義了兩個中斷向量表的條目,分別是X86_TRAP_DB和X86_TRAP_BP,它們的中斷處理函數(shù)分別是asm_exc_debug和asm_exc_int3。這里只是設置了兩個中斷向量表條目,并且把IDTR寄存器設置好了,后來就不需要再設置IDTR寄存器了。
下面我們看一下所有CPU異常的處理函數(shù)是怎么設置的。
先看調用棧:
start_kernel
trap_init
idt_setup_traps
代碼如下:
linux-src/arch/x86/kernel/idt.c
void __init idt_setup_traps(void){ idt_setup_from_table(idt_table, def_idts, ARRAY_SIZE(def_idts), true);}
static const __initconst struct idt_data def_idts[] = { INTG(X86_TRAP_DE, asm_exc_divide_error), ISTG(X86_TRAP_NMI,
asm_exc_nmi, IST_INDEX_NMI), INTG(X86_TRAP_BR,
asm_exc_bounds), INTG(X86_TRAP_UD,
asm_exc_invalid_op), INTG(X86_TRAP_NM, asm_exc_device_not_available),
INTG(X86_TRAP_OLD_MF,
asm_exc_coproc_segment_overrun), INTG(X86_TRAP_TS,
asm_exc_invalid_tss), INTG(X86_TRAP_NP,
asm_exc_segment_not_present), INTG(X86_TRAP_SS,
asm_exc_stack_segment), INTG(X86_TRAP_GP,
asm_exc_general_protection), INTG(X86_TRAP_SPURIOUS,
asm_exc_spurious_interrupt_bug), INTG(X86_TRAP_MF,
asm_exc_coprocessor_error), INTG(X86_TRAP_AC,
asm_exc_alignment_check), INTG(X86_TRAP_XF,
asm_exc_simd_coprocessor_error),
#ifdef CONFIG_X86_32 TSKG(X86_TRAP_DF,
GDT_ENTRY_DOUBLEFAULT_TSS),#else ISTG(X86_TRAP_DF,
asm_exc_double_fault, IST_INDEX_DF),#endif ISTG(X86_TRAP_DB, asm_exc_debug, IST_INDEX_DB),
#ifdef CONFIG_X86_MCE ISTG(X86_TRAP_MC,
asm_exc_machine_check, IST_INDEX_MCE),#endif
#ifdef CONFIG_AMD_MEM_ENCRYPT ISTG(X86_TRAP_VC,
asm_exc_vmm_communication, IST_INDEX_VC),#endif
SYSG(X86_TRAP_OF,
asm_exc_overflow),#if defined(CONFIG_IA32_EMULATION) SYSG(IA32_SYSCALL_VECTOR,
entry_INT80_compat),#elif defined(CONFIG_X86_32) SYSG(IA32_SYSCALL_VECTOR,
entry_INT80_32),#endif};
可以看到這次設置非常簡單,就是調用了一下idt_setup_from_table,并沒有調用load_idt。主要是數(shù)組def_idts里面包含了大部分的CPU異常處理。但是沒缺頁異常,缺頁異常是單獨設置。設置路徑如下:
調用棧:
start_kernel
setup_arch
idt_setup_early_pf
代碼如下:
linux-src/arch/x86/kernel/idt.c
void __init idt_setup_early_pf(void){ idt_setup_from_table(idt_table, early_pf_idts,
ARRAY_SIZE(early_pf_idts), true);}
static const __initconst struct idt_data early_pf_idts[] = { INTG(X86_TRAP_PF,
asm_exc_page_fault),};
現(xiàn)在CPU異常的中斷處理函數(shù)就全部設置完成了,想要研究具體哪個異常是怎么處理的同學,可以去跟蹤研究一下相應的函數(shù)。
5.2 硬中斷(hardirq)
硬件中斷的中斷處理和軟件中斷有一部分是相同的,有一部分卻有很大的不同。對于IPI中斷和per CPU中斷,其設置是和軟件中斷相同的,都是一步到位設置到具體的處理函數(shù)。但是對于余下的外設中斷,只是設置了入口函數(shù),并沒有設置具體的處理函數(shù),而且是所有的外設中斷的處理函數(shù)都統(tǒng)一到了同一個入口函數(shù)。然后在這個入口函數(shù)處會調用相應的irq描述符的handler函數(shù),這個handler函數(shù)是中斷控制器設置的。中斷控制器設置的這個handler函數(shù)會處理與這個中斷控制器相關的一些事物,然后再調用具體設備注冊的irqaction的handler函數(shù)進行具體的中斷處理。
我們先來看一下對中斷向量表條目的設置代碼。
調用棧如下:
start_kernel
init_IRQ
native_init_IRQ
idt_setup_apic_and_irq_gates
代碼如下:
linux-src/arch/x86/kernel/idt.c
/** * idt_setup_apic_and_irq_gates - Setup APIC/SMP and normal interrupt gates */void __init idt_setup_apic_and_irq_gates(void){ int i = FIRST_EXTERNAL_VECTOR; void *entry;
idt_setup_from_table(idt_table, apic_idts, ARRAY_SIZE(apic_idts), true);
for_each_clear_bit_from(i, system_vectors, FIRST_SYSTEM_VECTOR) {
entry = irq_entries_start + 8 * (i - FIRST_EXTERNAL_VECTOR);
set_intr_gate(i, entry); }
#ifdef CONFIG_X86_LOCAL_APIC for_each_clear_bit_from(i, system_vectors, NR_VECTORS) {
/* * Don‘t set the non assigned system vectors in the * system_vectors bitmap. Otherwise they show up in
* /proc/interrupts. */ entry = spurious_entries_start + 8 * (i - FIRST_SYSTEM_VECTOR);
set_intr_gate(i, entry); }#endif /* Map IDT into CPU entry area and reload it. */ idt_map_in_cea(); load_idt(&idt_descr);
/* Make the IDT table read only */ set_memory_ro((unsigned long)&idt_table, 1);
idt_setup_done = true;}
static const __initconst struct idt_data apic_idts[] = {#ifdef CONFIG_SMP INTG(RESCHEDULE_VECTOR,
asm_sysvec_reschedule_ipi), INTG(CALL_FUNCTION_VECTOR,
asm_sysvec_call_function),
INTG(CALL_FUNCTION_SINGLE_VECTOR, asm_sysvec_call_function_single), INTG(IRQ_MOVE_CLEANUP_VECTOR,
asm_sysvec_irq_move_cleanup), INTG(REBOOT_VECTOR,
asm_sysvec_reboot),#endif
#ifdef CONFIG_X86_THERMAL_VECTOR INTG(THERMAL_APIC_VECTOR,
asm_sysvec_thermal),#endif
#ifdef CONFIG_X86_MCE_THRESHOLD INTG(THRESHOLD_APIC_VECTOR,
asm_sysvec_threshold),#endif
#ifdef CONFIG_X86_MCE_AMD INTG(DEFERRED_ERROR_VECTOR,
asm_sysvec_deferred_error),#endif
#ifdef CONFIG_X86_LOCAL_APIC INTG(LOCAL_TIMER_VECTOR,
asm_sysvec_apic_timer_interrupt), INTG(X86_PLATFORM_IPI_VECTOR,
asm_sysvec_x86_platform_ipi),# ifdef CONFIG_HAVE_KVM INTG(POSTED_INTR_VECTOR,
asm_sysvec_kvm_posted_intr_ipi), INTG(POSTED_INTR_WAKEUP_VECTOR, asm_sysvec_kvm_posted_intr_wakeup_ipi), INTG(POSTED_INTR_NESTED_VECTOR,
asm_sysvec_kvm_posted_intr_nested_ipi),# endif# ifdef CONFIG_IRQ_WORK INTG(IRQ_WORK_VECTOR,
asm_sysvec_irq_work),# endif INTG(SPURIOUS_APIC_VECTOR,
asm_sysvec_spurious_apic_interrupt), INTG(ERROR_APIC_VECTOR,
asm_sysvec_error_interrupt),#endif};
static __init void set_intr_gate(unsigned int n, const void *addr){ struct idt_data data;
init_idt_data(&data, n, addr);
idt_setup_from_table(idt_table, &data, 1, false);}
linux-src/arch/x86/include/asm/desc.h
static inline void init_idt_data(struct idt_data *data, unsigned int n,
const void *addr){ BUG_ON(n 》 0xFF);
memset(data, 0, sizeof(*data)); data-》vector = n;
data-》addr = addr;
data-》segment = __KERNEL_CS; data-》bits.type = GATE_INTERRUPT; data-》bits.p = 1;}
linux-src/arch/x86/include/asm/idtentry.h
SYM_CODE_START(irq_entries_start)
vector=FIRST_EXTERNAL_VECTOR
.rept NR_EXTERNAL_VECTORS UNWIND_HINT_IRET_REGS0 :
.byte 0x6a, vector jmp asm_common_interrupt nop
/* Ensure that the above is 8 bytes max */
。 = 0b + 8 vector = vector+1
.endrSYM_CODE_END(irq_entries_start)
linux-src/arch/x86/kernel/irq.c
DEFINE_IDTENTRY_IRQ(common_interrupt){ struct pt_regs *old_regs = set_irq_regs(regs); struct irq_desc *desc;
/* entry code tells RCU that we’re not quiescent. Check it. */ RCU_LOCKDEP_WARN(!rcu_is_watching(), “IRQ failed to wake up RCU”);
desc = __this_cpu_read(vector_irq[vector]); if (likely(!IS_ERR_OR_NULL(desc))) {
handle_irq(desc, regs); } else { ack_APIC_irq();
if (desc == VECTOR_UNUSED) {
pr_emerg_ratelimited(“%s: %d.%u No irq handler for vector
”, __func__, smp_processor_id(),
vector); } else { __this_cpu_write(vector_irq[vector], VECTOR_UNUSED); } }
set_irq_regs(old_regs);}
static __always_inline void handle_irq(struct irq_desc *desc,
struct pt_regs *regs){ if (IS_ENABLED(CONFIG_X86_64))
generic_handle_irq_desc(desc);
else __handle_irq(desc, regs);}
linux-src/arch/x86/kernel/irqinit.c
DEFINE_PER_CPU(vector_irq_t, vector_irq) = { [0 。.. NR_VECTORS - 1] = VECTOR_UNUSED,};
linux-src/arch/x86/include/asm/hw_irq.h
typedef struct irq_desc* vector_irq_t[NR_VECTORS];
linux-src/include/linux/irqdesc.h
static inline void generic_handle_irq_desc(struct irq_desc *desc){ desc-》handle_irq(desc);}
從上面的代碼可以看出,對硬件中斷的設置分為兩個部分,一部分就像前面的軟件中斷的方式一樣,是從apic_idts數(shù)組設置的,設置的都是一些IPI和per CPU的中斷。另一部分是把所有剩余的硬件中斷的處理函數(shù)都設置為irq_entries_start,irq_entries_start會調用common_interrupt函數(shù)。在common_interrupt函數(shù)中會根據(jù)中斷向量號去讀取per CPU的數(shù)組變量vector_irq,得到一個irq_desc。最終會調用irq_desc中的handle_irq來處理這個中斷。
對于外設中斷為什么要采取這樣的處理方式呢?有兩個原因,1是因為外設中斷和中斷控制器相關聯(lián),這樣可以統(tǒng)一處理與中斷控制器相關的事物,2是因為外設中斷的驅動執(zhí)行比較晚,有些設備還是可以熱插拔的,直接把它們放到中斷向量表上比較麻煩。有個irq_desc這個中間層,設備驅動后面只需要調用函數(shù)request_irq來注冊ISR,只處理與設備相關的業(yè)務就可以了,而不用考慮和中斷控制器硬件相關的處理。
我們先來看一下vector_irq數(shù)組是怎么初始化的。
linux-src/arch/x86/kernel/apic/vector.c
void lapic_online(void){ unsigned int vector;
lockdep_assert_held(&vector_lock);
/* Online the vector matrix array for this CPU */ irq_matrix_online(vector_matrix);
/* * The interrupt affinity logic never targets interrupts to offline
* CPUs. The exception are the legacy PIC interrupts. In general
* they are only targeted to CPU0, but depending on the platform
* they can be distributed to any online CPU in hardware. The
* kernel has no influence on that. So all active legacy vectors * must be installed on all CPUs. All non legacy interrupts can be
* cleared. */ for (vector = 0; vector 《 NR_VECTORS; vector++)
this_cpu_write(vector_irq[vector], __setup_vector_irq(vector));}
static struct irq_desc *__setup_vector_irq(int vector){ int isairq = vector - ISA_IRQ_VECTOR(0);
/* Check whether the irq is in the legacy space */ if (isairq 《 0 || isairq 》= nr_legacy_irqs())
return VECTOR_UNUSED; /* Check whether the irq is handled by the IOAPIC */ if (test_bit(isairq, &io_apic_irqs))
return VECTOR_UNUSED; return irq_to_desc(isairq);}
linux-src/kernel/irq/irqdesc.c
struct irq_desc *irq_to_desc(unsigned int irq){return radix_tree_lookup(&irq_desc_tree, irq);}
可以看出vector_irq數(shù)組的初始化數(shù)據(jù)是從irq_desc_tree來的,我們再來看一下irq_desc_tree是怎么初始化的。
linux-src/kernel/irq/irqdesc.c
int __init early_irq_init(void){ int i, initcnt, node = first_online_node; struct irq_desc *desc;
init_irq_default_affinity();
/* Let arch update nr_irqs and return the nr of preallocated irqs */ initcnt = arch_probe_nr_irqs();
printk(KERN_INFO “NR_IRQS: %d, nr_irqs: %d, preallocated irqs: %d
”, NR_IRQS, nr_irqs, initcnt);
if (WARN_ON(nr_irqs 》 IRQ_BITMAP_BITS)) nr_irqs = IRQ_BITMAP_BITS;
if (WARN_ON(initcnt 》 IRQ_BITMAP_BITS)) initcnt = IRQ_BITMAP_BITS;
if (initcnt 》 nr_irqs) nr_irqs = initcnt;
for (i = 0; i 《 initcnt; i++) {
desc = alloc_desc(i, node, 0, NULL, NULL);
set_bit(i, allocated_irqs);
irq_insert_desc(i, desc); } return arch_early_irq_init();}
可以看到vector_irq數(shù)組的內(nèi)容是在系統(tǒng)初始化的時候通過alloc_desc函數(shù)為每個irq進行分配的。在alloc_desc中對irq_desc的初始化會把handle_irq函數(shù)指針默認初始化為handle_bad_irq,這個函數(shù)代表還沒有中斷控制器注冊這個函數(shù),handle_bad_irq只是簡單地確認一下中斷,然后做個錯誤記錄。
中斷控制器注冊handle_irq函數(shù)的代碼如下:
linux-src/kernel/irq/chip.c
void__irq_set_handler(unsigned int irq, irq_flow_handler_t handle, int is_chained,
const char *name){ unsigned long flags; struct irq_desc *desc = irq_get_desc_buslock(irq, &flags, 0);
if (!desc) return;
__irq_do_set_handler(desc, handle, is_chained, name); irq_put_desc_busunlock(desc, flags);}
static void__irq_do_set_handler(struct irq_desc *desc, irq_flow_handler_t handle,
int is_chained, const char *name){ if (!handle) {
handle = handle_bad_irq; } else { struct irq_data *irq_data = &desc-》irq_data;#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
/* * With hierarchical domains we might run into a * situation where the outermost chip is not yet set
* up, but the inner chips are there. Instead of * bailing we install the handler, but obviously we
* cannot enable/startup the interrupt at this point.
*/ while (irq_data) {
if (irq_data-》chip != &no_irq_chip) break;
/* * Bail out if the outer chip is not set up
* and the interrupt supposed to be started * right away.
*/ if (WARN_ON(is_chained)) return; /* Try the parent */
irq_data = irq_data-》parent_data; }#endif if (WARN_ON(!irq_data || irq_data-》chip == &no_irq_chip))
return; }
/* Uninstall? */ if (handle == handle_bad_irq) {
if (desc-》irq_data.chip != &no_irq_chip) mask_ack_irq(desc);
irq_state_set_disabled(desc); if (is_chained) desc-》action = NULL;
desc-》depth = 1; } desc-》handle_irq = handle; desc-》name = name;
if (handle != handle_bad_irq && is_chained) { unsigned int type = irqd_get_trigger_type(&desc-》irq_data);
/* * We‘re about to start this interrupt immediately,
* hence the need to set the trigger configuration.
* But the .set_type callback may have overridden the
* flow handler, ignoring that we’re dealing with a
* chained interrupt. Reset it immediately because we * do know better.
*/ if (type != IRQ_TYPE_NONE) {
__irq_set_trigger(desc, type);
desc-》handle_irq = handle; }
irq_settings_set_noprobe(desc);
irq_settings_set_norequest(desc);
irq_settings_set_nothread(desc);
desc-》action = &chained_action;
irq_activate_and_startup(desc, IRQ_RESEND); }}
不同的系統(tǒng)有不同的中斷控制器,其在啟動初始化的時候都會去注冊irq_desc的handle_irq函數(shù)。
下面我們再來看一下具體的硬件驅動應該如何注冊自己設備的ISR:
linux-src/include/linux/interrupt.h
static inline int __must_checkrequest_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev){ return request_threaded_irq(irq, handler, NULL, flags, name, dev);}
linux-src/kernel/irq/manage.c
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id);
驅動程序使用request_irq接口來注冊自己的ISR,ISR就是運行在硬中斷的,參數(shù)handler代表的就是ISR。request_irq又調用request_threaded_irq來實現(xiàn)自己。request_threaded_irq是用來創(chuàng)建中斷線程的函數(shù)接口,其中有兩個參數(shù)handler、thread_fn,都是函數(shù)指針,handler代表的是ISR,是進行中斷預處理的,thread_fn代表的是要創(chuàng)建的中斷線程的入口函數(shù),是進行中斷后處理的。中斷線程的細節(jié)我們在5.5中斷線程中再細講。
我們再來總結一下外設中斷的處理方式。外設中斷的向量表條目都被統(tǒng)一設置到同一個函數(shù)common_interrupt。在函數(shù)common_interrupt中又會根據(jù)irq參數(shù)去一個類型為irq_desc的vector_irq數(shù)組中尋找其對應的irq_desc,并用irq_desc的handle_irq來處理這個中斷。vector_irq數(shù)組是在系統(tǒng)啟動時初始化的,每個irq_desc的handle_irq都是中斷控制器初始化時設置的,handle_irq的處理是和中斷控制器密切相關的。具體的硬件驅動會通過request_irq接口來注冊ISR,每個ISR都會生成一個irqaction,這個irqaction會掛在irq_desc的鏈表上。這樣中斷發(fā)生時handle_irq就可以去執(zhí)行與irq相對應的每個ISR了。
5.3 軟中斷(softirq)
軟中斷是把中斷處理程序分成了兩段:前一段叫做硬中斷,執(zhí)行驅動的ISR,處理與硬件密切相關的事,在此期間是禁止中斷的;后一段叫做軟中斷,軟中斷中處理和硬件不太密切的事物,在此期間是開中斷的,可以繼續(xù)接受硬件中斷。軟中斷的設計提高了系統(tǒng)對中斷的響應性。下面我們先說軟中斷的執(zhí)行時機,然后再說軟中斷的使用接口。
軟中斷也是中斷處理程序的一部分,是在ISR執(zhí)行完成之后運行的,在ISR中可以向軟中斷中添加任務,然后軟中斷有事要做就會運行了。有些時候當軟中斷過多,處理不過來的時候,也會喚醒ksoftirqd/x線程來執(zhí)行軟中斷。
linux-src/kernel/irq/irqdesc.c
int handle_domain_irq(struct irq_domain *domain,
unsigned int hwirq, struct pt_regs *regs){ struct pt_regs *old_regs = set_irq_regs(regs);
struct irq_desc *desc; int ret = 0;
irq_enter();
/* The irqdomain code provides boundary checks */
desc = irq_resolve_mapping(domain, hwirq); if (likely(desc))
handle_irq_desc(desc); else ret = -EINVAL;
irq_exit(); set_irq_regs(old_regs); return ret;}
linux-src/kernel/softirq.c
void irq_exit(void){ __irq_exit_rcu(); rcu_irq_exit(); /* must be last! */ lockdep_hardirq_exit();}
static inline void __irq_exit_rcu(void){#ifndef __ARCH_IRQ_EXIT_IRQS_DISABLED local_irq_disable();#else
lockdep_assert_irqs_disabled();#endif account_hardirq_exit(current); preempt_count_sub(HARDIRQ_OFFSET);
if (!in_interrupt() && local_softirq_pending()) invoke_softirq();
tick_irq_exit();}
static inline void invoke_softirq(void){ if (ksoftirqd_running(local_softirq_pending())) return;
if (!force_irqthreads() || !__this_cpu_read(ksoftirqd)) {#ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK
/* * We can safely execute softirq on the current stack if * it is the irq stack, because it should be near empty
* at this stage. */ __do_softirq();#else /* * Otherwise, irq_exit() is called on the task stack that can
* be potentially deep already. So call softirq in its own stack
* to prevent from any overrun.
*/ do_softirq_own_stack();#endif } else { wakeup_softirqd(); }}
asmlinkage __visible void __softirq_entry __do_softirq(void){ unsigned long end = jiffies + MAX_SOFTIRQ_TIME; unsigned long old_flags = current-》flags; int max_restart = MAX_SOFTIRQ_RESTART;
struct softirq_action *h;
bool in_hardirq; __u32 pending; int softirq_bit;
static int i = 0; if(++i == 50) dump_stack();
/* * Mask out PF_MEMALLOC as the current task context is borrowed for the * softirq. A softirq handled, such as network RX, might set PF_MEMALLOC * again if the socket is related to swapping.
*/ current-》flags &= ~PF_MEMALLOC;
pending = local_softirq_pending();
softirq_handle_begin(); in_hardirq = lockdep_softirq_start(); account_softirq_enter(current);
restart: /* Reset the pending bitmask before enabling irqs */ set_softirq_pending(0);
local_irq_enable();
h = softirq_vec;
while ((softirq_bit = ffs(pending))) { unsigned int vec_nr; int prev_count;
h += softirq_bit - 1;
vec_nr = h - softirq_vec; prev_count = preempt_count();
kstat_incr_softirqs_this_cpu(vec_nr);
trace_softirq_entry(vec_nr); h-》action(h); trace_softirq_exit(vec_nr); if (unlikely(prev_count != preempt_count())) { pr_err(“huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?
”, vec_nr, softirq_to_name[vec_nr], h-》action, prev_count, preempt_count()); preempt_count_set(prev_count); } h++; pending 》》= softirq_bit; }
if (!IS_ENABLED(CONFIG_PREEMPT_RT) && __this_cpu_read(ksoftirqd) == current) rcu_softirq_qs();
local_irq_disable();
pending = local_softirq_pending(); if (pending) { if (time_before(jiffies, end) && !need_resched() && --max_restart) goto restart;
wakeup_softirqd(); }
account_softirq_exit(current); lockdep_softirq_end(in_hardirq); softirq_handle_end(); current_restore_flags(old_flags, PF_MEMALLOC);}
可以看到__do_softirq在執(zhí)行軟中斷前會打開中斷l(xiāng)ocal_irq_enable(),在執(zhí)行完軟中斷之后又會關閉中斷l(xiāng)ocal_irq_disable()。所以軟中斷執(zhí)行期間CPU是可以接收硬件中斷的。
下面我們再來看一下軟中斷的使用接口。軟中斷定義了一個softirq_action類型的數(shù)組,數(shù)組大小是NR_SOFTIRQS,代表軟中斷的類型,目前只有10種軟中斷類型。softirq_action結構體里面僅僅只有一個函數(shù)指針。當我們要設置某一類軟中斷的處理函數(shù)時使用接口open_softirq。當我們想要觸發(fā)某一類軟中斷的執(zhí)行時使用接口raise_softirq。
下面我們來看一下代碼:
linux-src/include/linux/interrupt.h
enum{ HI_SOFTIRQ=0, TIMER_SOFTIRQ, NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ, TASKLET_SOFTIRQ, SCHED_SOFTIRQ, HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS};
struct softirq_action{ void (*action)(struct softirq_action *);};
linux-src/kernel/softirq.c
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
void open_softirq(int nr, void (*action)(struct softirq_action *)){ softirq_vec[nr].action = action;}
void raise_softirq(unsigned int nr){ unsigned long flags;
local_irq_save(flags); raise_softirq_irqoff(nr); local_irq_restore(flags);}
inline void raise_softirq_irqoff(unsigned int nr){ __raise_softirq_irqoff(nr);
/* * If we‘re in an interrupt or softirq, we’re done * (this also catches softirq-disabled code)。 We will
* actually run the softirq once we return from * the irq or softirq. * * Otherwise we wake up ksoftirqd to make sure we
* schedule the softirq soon. */ if (!in_interrupt() && should_wake_ksoftirqd()) wakeup_softirqd();}
void __raise_softirq_irqoff(unsigned int nr){ lockdep_assert_irqs_disabled(); trace_softirq_raise(nr);
or_softirq_pending(1UL 《《 nr);}
所有軟中斷的處理函數(shù)都是在系統(tǒng)啟動的初始化函數(shù)里面用open_softirq接口設置的。raise_softirq一般是在硬中斷或者軟中斷中用來往軟中斷上push work使得軟中斷可以被觸發(fā)執(zhí)行或者繼續(xù)執(zhí)行。
5.4 微任務(tasklet)
新代碼要想使用softirq就必須修改內(nèi)核的核心代碼,添加新的softirq類型,這對于很多驅動程序來說是做不到的,于是內(nèi)核在softirq的基礎上開發(fā)了tasklet。使用tasklet不需要修改內(nèi)核的核心代碼,驅動程序直接使用tasklet的接口就可以了。
Tasklet其實是一種特殊的softirq,它是在softirq的基礎上進行了擴展。它利用的就是softirq中的HI_SOFTIRQ和TASKLET_SOFTIRQ。softirq在初始化的時候會設置這兩個softirq類型。然后其處理函數(shù)會去處理tasklet的鏈表。我們在使用tasklet的時候只需要定義一個tasklet_struct,并用我們想要執(zhí)行的函數(shù)初始化它,然后再用tasklet_schedule把它放入到隊列中,它就會被執(zhí)行了。下面我們來看一下代碼:
linux-src/kernel/softirq.c
void __init softirq_init(void){ int cpu;
for_each_possible_cpu(cpu) { per_cpu(tasklet_vec, cpu).tail =
&per_cpu(tasklet_vec, cpu).head; per_cpu(tasklet_hi_vec, cpu).tail =
&per_cpu(tasklet_hi_vec, cpu).head; }
open_softirq(TASKLET_SOFTIRQ, tasklet_action); open_softirq(HI_SOFTIRQ, tasklet_hi_action);}
static __latent_entropy void tasklet_action(struct softirq_action *a){
tasklet_action_common(a, this_cpu_ptr(&tasklet_vec), TASKLET_SOFTIRQ);}
static __latent_entropy void tasklet_hi_action(struct softirq_action *a){
tasklet_action_common(a, this_cpu_ptr(&tasklet_hi_vec), HI_SOFTIRQ);}
static void tasklet_action_common(struct softirq_action *a,
struct tasklet_head *tl_head,
unsigned int softirq_nr){ struct tasklet_struct *list;
local_irq_disable(); list = tl_head-》head;
tl_head-》head = NULL; tl_head-》tail = &tl_head-》head; local_irq_enable();
while (list) { struct tasklet_struct *t = list;
list = list-》next;
if (tasklet_trylock(t)) {
if (!atomic_read(&t-》count)) {
if (tasklet_clear_sched(t)) {
if (t-》use_callback)
t-》callback(t);
else t-》func(t-》data);
} tasklet_unlock(t);
continue; }
tasklet_unlock(t); }
local_irq_disable();
t-》next = NULL; *tl_head-》tail = t;
tl_head-》tail = &t-》next;
__raise_softirq_irqoff(softirq_nr);
local_irq_enable(); }}
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);
static void __tasklet_schedule_common(struct tasklet_struct *t,
struct tasklet_head __percpu *headp,
unsigned int softirq_nr){ struct tasklet_head *head; unsigned long flags;
local_irq_save(flags);
head = this_cpu_ptr(headp); t-》next = NULL; *head-》tail = t;
head-》tail = &(t-》next);
raise_softirq_irqoff(softirq_nr); local_irq_restore(flags);}
void __tasklet_schedule(struct tasklet_struct *t){ __tasklet_schedule_common(t, &tasklet_vec,
TASKLET_SOFTIRQ);}EXPORT_SYMBOL(__tasklet_schedule);
void __tasklet_hi_schedule(struct tasklet_struct *t){ __tasklet_schedule_common(t, &tasklet_hi_vec,
HI_SOFTIRQ);}EXPORT_SYMBOL(__tasklet_hi_schedule);
linux-src/include/linux/interrupt.h
static inline void tasklet_schedule(struct tasklet_struct *t){
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t-》state))
__tasklet_schedule(t);}
static inline void tasklet_hi_schedule(struct tasklet_struct *t){
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t-》state))
__tasklet_hi_schedule(t);}
Tasklet和softirq有一個很大的區(qū)別就是,同一個softirq可以在不同的CPU上并發(fā)執(zhí)行,而同一個tasklet不會在多個CPU上并發(fā)執(zhí)行。所以我們在編程的時候,如果使用的是tasklet就不用考慮多CPU之間的同步問題。
還有很重要的一點,tasklet不是獨立的,它是softirq的一部分,禁用軟中斷的同時也禁用了tasklet。
5.5 中斷線程(threaded_irq)
前面講的硬中斷,它是外設中斷處理中必不可少的一部分。Softirq和tasklet雖然不會禁用中斷,提高了系統(tǒng)對中斷的響應性,但是softirq的執(zhí)行優(yōu)先級還是比進程的優(yōu)先級高,有些確實不那么重要的任務其實可以放到進程里執(zhí)行,和普通進程共同競爭CPU。而且軟中斷里不能調用會阻塞、休眠的函數(shù),這對軟中斷函數(shù)的編程是很不利的,所以綜合各種因素,我們需要把中斷處理任務中的與硬件無關有不太緊急的部分放到進程里面來做。為此內(nèi)核開發(fā)了兩種方法,中斷線程和工作隊列。
我們這節(jié)先講中斷線程,其接口如下:
linux-src/include/linux/interrupt.h
extern int __must_checkrequest_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long flags, const char *name, void *dev);
如果我們要為某個外設注冊中斷處理程序,可以使用這個接口。其中handler是硬中斷,是處理與硬件密切相關的事物。其處理完成后,可以把接收到的數(shù)據(jù)、要繼續(xù)處理的事情放到某個位置,然后返回是否需要喚醒對應的中斷線程。如果需要的話,系統(tǒng)會喚醒其對應的中斷線程來繼續(xù)處理任務,這個線程的主函數(shù)就是第三個參數(shù)thread_fn。下面我們來看一下這個接口的實現(xiàn)。
linux-src/kernel/irq/manage.c
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id){ struct irqaction *action; struct irq_desc *desc; int retval;
if (irq == IRQ_NOTCONNECTED)
return -ENOTCONN;
/* * Sanity-check: shared interrupts must pass in a real dev-ID,
* otherwise we‘ll have trouble later trying to figure out
* which interrupt is which (messes up the interrupt freeing
* logic etc)。
* * Also shared interrupts do not go well with disabling auto enable.
* The sharing interrupt might request it while it’s still disabled
* and then wait for interrupts forever.
* * Also IRQF_COND_SUSPEND only makes sense for shared interrupts and
* it cannot be set along with IRQF_NO_SUSPEND.
*/ if (((irqflags & IRQF_SHARED) && !dev_id) ||
((irqflags & IRQF_SHARED) && (irqflags & IRQF_NO_AUTOEN)) ||
(?。╥rqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) ||
((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND)))
return -EINVAL;
desc = irq_to_desc(irq); if (!desc)
return -EINVAL;
if (!irq_settings_can_request(desc) ||
WARN_ON(irq_settings_is_per_cpu_devid(desc)))
return -EINVAL;
if (!handler) {
if (!thread_fn)
return -EINVAL;
handler = irq_default_primary_handler;
}
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;
action-》handler = handler; action-》thread_fn = thread_fn;
action-》flags = irqflags; action-》name = devname;
action-》dev_id = dev_id;
retval = irq_chip_pm_get(&desc-》irq_data);
if (retval 《 0) {
kfree(action);
return retval; }
retval = __setup_irq(irq, desc, action);
if (retval) {
irq_chip_pm_put(&desc-》irq_data);
kfree(action-》secondary);
kfree(action); }
#ifdef CONFIG_DEBUG_SHIRQ_FIXME if (!retval && (irqflags & IRQF_SHARED)) {
/* * It‘s a shared IRQ -- the driver ought to be prepared for it
* to happen immediately, so let’s make sure.。..
* We disable the irq to make sure that a ‘real’ IRQ doesn‘t
* run in parallel with our fake.
*/ unsigned long flags;
disable_irq(irq);
local_irq_save(flags);
handler(irq, dev_id);
local_irq_restore(flags);
enable_irq(irq);
}#endif return retval;}
static int__setup_irq(unsigned int irq, struct irq_desc *desc,
struct irqaction *new){ struct irqaction *old, **old_ptr;
unsigned long flags, thread_mask = 0; int ret, nested, shared = 0;
if (!desc)
return -EINVAL;
if (desc-》irq_data.chip == &no_irq_chip)
return -ENOSYS; if (!try_module_get(desc-》owner))
return -ENODEV;
new-》irq = irq;
/* * If the trigger type is not specified by the caller,
* then use the default for this interrupt.
*/ if (?。╪ew-》flags & IRQF_TRIGGER_MASK))
new-》flags |= irqd_get_trigger_type(&desc-》irq_data);
/* * Check whether the interrupt nests into another interrupt
* thread. */ nested = irq_settings_is_nested_thread(desc); if (nested) {
if (!new-》thread_fn) {
ret = -EINVAL;
goto out_mput; }
/* * Replace the primary handler which was provided from
* the driver for non nested interrupt handling by the
* dummy function which warns when called. */
new-》handler = irq_nested_primary_handler; } else {
if (irq_settings_can_thread(desc)) {
ret = irq_setup_forced_threading(new);
if (ret)
goto out_mput; } }
/* * Create a handler thread when a thread function is supplied
* and the interrupt does not nest into another interrupt
* thread.
*/ if (new-》thread_fn && !nested) {
ret = setup_irq_thread(new, irq, false);
if (ret)
goto out_mput;
if (new-》secondary) {
ret = setup_irq_thread(new-》secondary, irq, true);
if (ret)
goto out_thread;
} }
/* * Drivers are often written to work w/o knowledge about the
* underlying irq chip implementation, so a request for a
* threaded irq without a primary hard irq context handler
* requires the ONESHOT flag to be set. Some irq chips like
* MSI based interrupts are per se one shot safe. Check the
* chip flags, so we can avoid the unmask dance at the end of
* the threaded handler for those.
*/ if (desc-》irq_data.chip-》flags & IRQCHIP_ONESHOT_SAFE)
new-》flags &= ~IRQF_ONESHOT;
/* * Protects against a concurrent __free_irq() call which might wait
* for synchronize_hardirq() to complete without holding the optional
* chip bus lock and desc-》lock. Also protects against handing out
* a recycled oneshot thread_mask bit while it’s still in use by
* its previous owner.
*/ mutex_lock(&desc-》request_mutex);
/* * Acquire bus lock as the irq_request_resources() callback below
* might rely on the serialization or the magic power management
* functions which are abusing the irq_bus_lock() callback,
*/ chip_bus_lock(desc);
/* First installed action requests resources.
*/ if (!desc-》action) {
ret = irq_request_resources(desc);
if (ret) {
pr_err(“Failed to request resources for %s (irq %d) on irqchip %s
”, new-》name, irq, desc-》irq_data.chip-》name);
goto out_bus_unlock;
} }
/* * The following block of code has to be executed atomically
* protected against a concurrent interrupt and any of the other
* management calls which are not serialized via
* desc-》request_mutex or the optional bus lock.
*/ raw_spin_lock_irqsave(&desc-》lock, flags);
old_ptr = &desc-》action; old = *old_ptr; if (old) {
/* * Can‘t share interrupts unless both agree to and are
* the same type (level, edge, polarity)。 So both flag
* fields must have IRQF_SHARED set and the bits which
* set the trigger type must match. Also all must
* agree on ONESHOT.
* Interrupt lines used for NMIs cannot be shared.
*/ unsigned int oldtype;
if (desc-》istate & IRQS_NMI) {
pr_err(“Invalid attempt to share NMI for %s (irq %d) on irqchip %s.
”, new-》name, irq, desc-》irq_data.chip-》name);
ret = -EINVAL;
goto out_unlock; }
/* * If nobody did set the configuration before, inherit
* the one provided by the requester.
*/ if (irqd_trigger_type_was_set(&desc-》irq_data)) {
oldtype = irqd_get_trigger_type(&desc-》irq_data); } else {
oldtype = new-》flags & IRQF_TRIGGER_MASK;
irqd_set_trigger_type(&desc-》irq_data, oldtype);
}
if (?。ǎ╫ld-》flags & new-》flags) & IRQF_SHARED) ||
(oldtype != (new-》flags & IRQF_TRIGGER_MASK)) ||
((old-》flags ^ new-》flags) & IRQF_ONESHOT))
goto mismatch;
/* All handlers must agree on per-cpuness */
if ((old-》flags & IRQF_PERCPU) !=
(new-》flags & IRQF_PERCPU))
goto mismatch;
/* add new interrupt at end of irq queue */
do {
/*
* Or all existing action-》thread_mask bits,
* so we can find the next zero bit for this
* new action. */
thread_mask |= old-》thread_mask;
old_ptr = &old-》next;
old = *old_ptr;
} while (old);
shared = 1; }
/* * Setup the thread mask for this irqaction for ONESHOT. For
* !ONESHOT irqs the thread mask is 0 so we can avoid a
* conditional in irq_wake_thread()。
*/ if (new-》flags & IRQF_ONESHOT) {
/* * Unlikely to have 32 resp 64 irqs sharing one line,
* but who knows. */ if (thread_mask == ~0UL) {
ret = -EBUSY; goto out_unlock; }
/* * The thread_mask for the action is or’ed to
* desc-》thread_active to indicate that the
* IRQF_ONESHOT thread handler has been woken, but not
* yet finished. The bit is cleared when a thread
* completes. When all threads of a shared interrupt
* line have completed desc-》threads_active becomes
* zero and the interrupt line is unmasked. See
* handle.c:irq_wake_thread() for further information. *
* If no thread is woken by primary (hard irq context)
* interrupt handlers, then desc-》threads_active is
* also checked for zero to unmask the irq line in the
* affected hard irq flow handlers
* (handle_[fasteoi|level]_irq)。
* * The new action gets the first zero bit of
* thread_mask assigned. See the loop above which or‘s
* all existing action-》thread_mask bits.
*/ new-》thread_mask = 1UL 《《 ffz(thread_mask);
} else if (new-》handler == irq_default_primary_handler &&
?。╠esc-》irq_data.chip-》flags & IRQCHIP_ONESHOT_SAFE)) {
/*
* The interrupt was requested with handler = NULL, so
* we use the default primary handler for it. But it
* does not have the oneshot flag set. In combination
* with level interrupts this is deadly, because the
* default primary handler just wakes the thread, then
* the irq lines is reenabled, but the device still
* has the level irq asserted. Rinse and repeat.。.. *
* While this works for edge type interrupts, we play
* it safe and reject unconditionally because we can’t
* say for sure which type this interrupt really
* has. The type flags are unreliable as the
* underlying chip implementation can override them.
*/ pr_err(“Threaded irq requested with handler=NULL and !ONESHOT for %s (irq %d)
”, new-》name, irq);
ret = -EINVAL;
goto out_unlock; }
if (!shared) {
init_waitqueue_head(&desc-》wait_for_threads);
/* Setup the type (level, edge polarity) if configured: */
if (new-》flags & IRQF_TRIGGER_MASK) {
ret = __irq_set_trigger(desc,
new-》flags & IRQF_TRIGGER_MASK);
if (ret)
goto out_unlock; }
/* * Activate the interrupt. That activation must happen
* independently of IRQ_NOAUTOEN. request_irq() can fail
* and the callers are supposed to handle
* that. enable_irq() of an interrupt requested with
* IRQ_NOAUTOEN is not supposed to fail. The activation
* keeps it in shutdown mode, it merily associates
* resources if necessary and if that‘s not possible it
* fails. Interrupts which are in managed shutdown mode
* will simply ignore that activation request.
*/ ret = irq_activate(desc);
if (ret)
goto out_unlock;
desc-》istate &= ~(IRQS_AUTODETECT | IRQS_SPURIOUS_DISABLED |
IRQS_ONESHOT | IRQS_WAITING);
irqd_clear(&desc-》irq_data, IRQD_IRQ_INPROGRESS);
if (new-》flags & IRQF_PERCPU) {
irqd_set(&desc-》irq_data, IRQD_PER_CPU);
irq_settings_set_per_cpu(desc);
if (new-》flags & IRQF_NO_DEBUG)
irq_settings_set_no_debug(desc); }
if (noirqdebug)
irq_settings_set_no_debug(desc);
if (new-》flags & IRQF_ONESHOT)
desc-》istate |= IRQS_ONESHOT;
/* Exclude IRQ from balancing if requested */
if (new-》flags & IRQF_NOBALANCING) {
irq_settings_set_no_balancing(desc);
irqd_set(&desc-》irq_data, IRQD_NO_BALANCING); }
if (?。╪ew-》flags & IRQF_NO_AUTOEN) &&
irq_settings_can_autoenable(desc)) {
irq_startup(desc, IRQ_RESEND, IRQ_START_COND);
} else {
/*
* Shared interrupts do not go well with disabling
* auto enable. The sharing interrupt might request
* it while it’s still disabled and then wait for
* interrupts forever. */
WARN_ON_ONCE(new-》flags & IRQF_SHARED);
/* Undo nested disables: */
desc-》depth = 1; }
} else if (new-》flags & IRQF_TRIGGER_MASK) {
unsigned int nmsk = new-》flags & IRQF_TRIGGER_MASK;
unsigned int omsk = irqd_get_trigger_type(&desc-》irq_data);
if (nmsk != omsk)
/* hope the handler works with current trigger mode */
pr_warn(“irq %d uses trigger mode %u; requested %u
”, irq, omsk, nmsk); }
*old_ptr = new;
irq_pm_install_action(desc, new);
/* Reset broken irq detection when installing new handler */
desc-》irq_count = 0;
desc-》irqs_unhandled = 0;
/* * Check whether we disabled the irq via the spurious handler
* before. Reenable it and give it another chance.
*/ if (shared && (desc-》istate & IRQS_SPURIOUS_DISABLED)) {
desc-》istate &= ~IRQS_SPURIOUS_DISABLED;
__enable_irq(desc); }
raw_spin_unlock_irqrestore(&desc-》lock, flags);
chip_bus_sync_unlock(desc); mutex_unlock(&desc-》request_mutex);
irq_setup_timings(desc, new);
/* * Strictly no need to wake it up, but hung_task complains
* when no hard interrupt wakes the thread up.
*/ if (new-》thread)
wake_up_process(new-》thread);
if (new-》secondary)
wake_up_process(new-》secondary-》thread);
register_irq_proc(irq, desc); new-》dir = NULL; register_handler_proc(irq, new); return 0;
mismatch: if (?。╪ew-》flags & IRQF_PROBE_SHARED)) {
pr_err(“Flags mismatch irq %d. %08x (%s) vs. %08x (%s)
”, irq, new-》flags, new-》name, old-》flags, old-》name);#ifdef CONFIG_DEBUG_SHIRQ
dump_stack();#endif } ret = -EBUSY;
out_unlock: raw_spin_unlock_irqrestore(&desc-》lock, flags);
if (!desc-》action) irq_release_resources(desc);out_bus_unlock:
chip_bus_sync_unlock(desc); mutex_unlock(&desc-》request_mutex);
out_thread: if (new-》thread) { struct task_struct *t = new-》thread;
new-》thread = NULL; kthread_stop(t); put_task_struct(t);
} if (new-》secondary && new-》secondary-》thread) {
struct task_struct *t = new-》secondary-》thread;
new-》secondary-》thread = NULL; kthread_stop(t);
put_task_struct(t); }out_mput: module_put(desc-》owner); return ret;}
static intsetup_irq_thread(struct irqaction *new, unsigned int irq, bool secondary){ struct task_struct *t;
if (!secondary) { t = kthread_create(irq_thread, new, “irq/%d-%s”, irq,
new-》name); } else {
t = kthread_create(irq_thread, new, “irq/%d-s-%s”, irq,
new-》name); }
if (IS_ERR(t)) return PTR_ERR(t);
sched_set_fifo(t);
/* * We keep the reference to the task struct even if
* the thread dies to avoid that the interrupt code
* references an already freed task_struct.
*/ new-》thread = get_task_struct(t);
/* * Tell the thread to set its affinity. This is
* important for shared interrupt handlers as we do
* not invoke setup_affinity() for the secondary
* handlers as everything is already set up. Even for
* interrupts marked with IRQF_NO_BALANCE this is
* correct as we want the thread to move to the cpu(s)
* on which the requesting code placed the interrupt.
*/ set_bit(IRQTF_AFFINITY, &new-》thread_flags); return 0;}
中斷線程雖然實現(xiàn)很復雜,但是其使用接口還是很簡單的。
5.6 工作隊列(workqueue)
工作隊列是內(nèi)核中使用最廣泛的線程化中斷處理機制。系統(tǒng)中有一些默認的工作隊列,你也可以創(chuàng)建自己的工作隊列,工作隊列背后對應的是內(nèi)核線程。你可以創(chuàng)建一個work,然后push到某個工作隊列,然后這個工作隊列背后的內(nèi)核線程就會去執(zhí)行這些work。下面我們來看一下工作隊列的接口。
linux-src/include/linux/workqueue.h
struct work_struct { atomic_long_t data;
struct list_head entry;
work_func_t func;#ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map;#endif};
#define DECLARE_WORK(n, f)
struct work_struct n = __WORK_INITIALIZER(n, f)
#define __WORK_INITIALIZER(n, f) {
.data = WORK_DATA_STATIC_INIT(),
.entry = { &(n).entry, &(n).entry },
.func = (f),
__WORK_INIT_LOCKDEP_MAP(#n, &(n))
}
static inline bool schedule_work(struct work_struct *work){ return queue_work(system_wq, work);}
static inline bool schedule_work_on(int cpu, struct work_struct *work){ return queue_work_on(cpu, system_wq, work);}
這是創(chuàng)建work,把work push到系統(tǒng)默認的工作隊列上的接口,下面我們再來看一下創(chuàng)建自己的工作隊列的接口:
linux-src/include/linux/workqueue.h
struct workqueue_struct *alloc_workqueue(const char *fmt, unsigned int flags, int max_active, 。..);
#define create_workqueue(name)
alloc_workqueue(“%s”, __WQ_LEGACY | WQ_MEM_RECLAIM, 1, (name))
工作隊列還有很多很豐富的接口,這里就不一一介紹了。
關于工作隊列的實現(xiàn)原理,推薦閱讀:
http://www.wowotech.net/irq_subsystem/workqueue.html
http://www.wowotech.net/irq_subsystem/cmwq-intro.html
http://www.wowotech.net/irq_subsystem/alloc_workqueue.html
http://www.wowotech.net/irq_subsystem/queue_and_handle_work.html
六、中斷與同步
在只有線程的情況下,線程之間的同步邏輯還是很好理解的,但是有了中斷之后,硬中斷、軟中斷、線程相互之間的同步就變得復雜起來。下面我們就來看一下它們在運行的時候相互之間的搶占關系。
6.1 CPU運行模型
首先我們來看一下CPU最原始的運行模型,圖靈機模型,非常簡單,就是一條直線一直運行下去。
在圖靈機上加入中斷之后,CPU的運行模型也是比較簡單的。但是當我們考慮軟件中斷、硬件中斷的區(qū)別時,CPU運行模型就開始變得復雜起來了。
不同的中斷類型使得中斷執(zhí)行流有了不同的類型,這里一共分為三種類型,系統(tǒng)調用、CPU異常、硬件中斷?,F(xiàn)在這個還不算復雜,下面我們看一下它們之間的搶占情形。
在系統(tǒng)調用時會發(fā)生CPU異常,也可能會發(fā)生硬件中斷,在CPU異常的時候也可能發(fā)生硬件中斷。其實這三者也可以嵌套起來,請看下圖:
系統(tǒng)調用時發(fā)生了CPU異常,CPU異常時發(fā)生了硬件中斷。下面我們把硬件中斷的處理過程分為硬中斷和軟中斷兩部分,看看它們之間的關系。
硬件中斷的前半部分是硬中斷,后半部分是軟中斷,硬中斷中不能再嵌套硬中斷了,但是軟中斷中可以嵌套硬中斷。不過嵌套的硬中斷在返回時發(fā)現(xiàn)正在執(zhí)行軟中斷,就不會再重新還行軟中斷了,而是會回到原來的軟中斷執(zhí)行流中。軟中斷的執(zhí)行還有一種情況,如下圖所示:
這是因為線程在其臨界區(qū)中禁用了軟中斷,如果臨界區(qū)中發(fā)生了硬中斷還是會執(zhí)行的,但是硬中斷返回時不會去執(zhí)行軟中斷,因為軟中斷被禁用了。當線程的臨界區(qū)結束是會再打開軟中斷,此時發(fā)現(xiàn)有pending的軟中斷沒有處理,就會去執(zhí)行軟中斷。
還有一種比較特殊的情況,就是線程里套軟中斷,軟中斷里套硬中斷,硬中斷里套NMI中斷,如下圖所示:
首先軟中斷是不能獨立觸發(fā)的,必須是硬中斷觸發(fā)軟中斷。在圖中,第一個硬中斷是執(zhí)行完成了的,然后在軟中斷的執(zhí)行過程中又發(fā)生了硬中斷,第二個硬中斷還沒執(zhí)行完的時候在執(zhí)行過程中的時候又發(fā)生了NMI中斷。這樣就發(fā)生了四個不同等級的執(zhí)行流一一嵌套的情況,這也是隊列自旋鎖的鎖節(jié)點為啥要乘以4的原因。
6.2 中斷相關同步方法
軟中斷可以搶占線程,硬中斷可以搶占軟中斷也可以搶占線程,而返回來則不能搶占,所以如果我們的低等級執(zhí)行流代碼和高等級執(zhí)行流代碼有同步問題的話,就要考慮禁用高等級執(zhí)行流。下面我們來看一下它們的接口,首先看禁用硬中斷:
linux-src/include/linux/irqflags.h
#define local_irq_enable()
do { raw_local_irq_enable(); } while (0)#define local_irq_disable()
do { raw_local_irq_disable(); } while (0)#define local_irq_save(flags)
do { raw_local_irq_save(flags);
} while (0)#define local_irq_restore(flags)
do { raw_local_irq_restore(flags);
} while (0)
linux-src/include/linux/interrupt.h
extern void disable_irq_nosync(unsigned int irq);extern bool disable_hardirq(unsigned int irq);
extern void disable_irq(unsigned int irq);extern void disable_percpu_irq(unsigned int irq);
extern void enable_irq(unsigned int irq);extern void enable_percpu_irq(unsigned
int
irq,
unsigned int type);
你可以在一個CPU上禁用所有中斷,也可以在所有CPU上禁用某個硬件中斷,但是你不能在所有CPU上同時禁用所有硬件中斷。
再來看一下禁用軟中斷的接口:
linux-src/include/linux/bottom_half.h
static inline void local_bh_disable(void){ __local_bh_disable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);}
static inline void local_bh_enable(void){ __local_bh_enable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);}
我們只能禁用本地CPU的軟中斷,而且是整體禁用,不能只禁用某一類型的軟中斷。雖然在Linux中,下半部bh包括所有的下半部,但是此處的bh僅僅指軟中斷(包括tasklet),不包括中斷線程和工作隊列。
七、總結回顧
本文我們從中斷的概念開始講起,一路上分析了中斷的作用、中斷的產(chǎn)生、中斷的處理。其中內(nèi)容最多的是硬件中斷的處理,方法很多很繁雜。從6.1節(jié)CPU運行模型中,我們可以看到中斷對于推動整個系統(tǒng)運行的重要性。所以說中斷機制是計算機系統(tǒng)的神經(jīng)和脈搏,一點都不為過。想要學會Linux內(nèi)核,弄明白中斷機制是其中必不可少的一環(huán)。最后我們再來看一下中斷機制的圖:
參考文獻:
《Linux Kernel Development》
《Understanding the Linux Kernel》
《Professional Linux Kernel Architecture》
《Intel 64 and IA-32 Architectures Software Developer’s Manual Volume 3》
《Interrupt in Linux (硬件篇)》
編輯:黃飛
-
cpu
+關注
關注
68文章
10804瀏覽量
210845 -
Linux
+關注
關注
87文章
11209瀏覽量
208721
原文標題:深入理解Linux中斷機制
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。
發(fā)布評論請先 登錄
相關推薦
評論