搞內核研究的經常對中斷這個概念肯定不陌生,經常我們會接觸很多與中斷相關的術語,按照軟件和硬件進行分類:
硬件CPU相關:
IRQ、IDT、cli&sti
軟件操作系統相關:
APC、DPC、IRQL
一直以來對中斷這一部分內容弄的一知半解,操作系統和CPU之間如何協同工作也是很模糊。最近花了點時間認真把這塊知識進行了梳理,不當之處,還請高手指出,先行謝過了!
本文旨在解答下面這些問題:
1.IRQ和IRQL之間是什么關系?
2.Windows是如何在軟件層面上虛擬出IRQL這套中斷機制的
3.APC和DPC都是軟件中斷,既然是中斷那么對應的IDT表項中的處理例程在哪里呢?
0x00 Intel 80386處理器的中斷
首先,讓我們忘記Windows,從最開始的80386處理器開始,看看Intel設計它的時候是如何處理中斷這個東西的。
先來看看這個誕生於1985年的CPU長什么樣子:
看看那些伸出來的引腳,下面是它的引腳標注圖:
注意用紅圈標注的兩個引腳,這兩個就是80386處理器為中斷留出的兩個引腳。其中INTR是可屏蔽中斷輸入口,NMI是不可屏蔽中斷輸入口。
那么中斷是如何輸入給處理器的呢?那么多外部設備,而這只有一個引腳(暫時只考慮可屏蔽中斷),這里就需要為CPU配備一個管理中斷的秘書——可編程中斷控制器PIC。這個秘書需要干哪些活呢?外部設備的中斷都從它來進入中央處理器,所以它負責從外設接收中斷信號,並根據優先級向CPU發起中斷請求。最開始的這個PIC角色是一個代號為8259A的芯片在進行扮演,這貨長這樣:
下面是它的引腳圖:
其中IR0-IR7共8個引腳負責連接外部設備, 8259A PIC的每個IR口都連接着一條IRQ線,用於接收外設的中斷信號。INT負責連接CPU的INTR引腳,用於向CPU發起中斷請求。通常情況下,使用兩片8259A芯片進行級聯,一片連接CPU,稱為主片,另一片連接到主PIC的IR2引腳,稱為從片,這樣總共就可以連接8+7=15個外設了。如下圖所示:
在8259A中,默認情況下的優先級是主片IR0的中斷請求優先級最高,主片IR7最低,從片IR0-7所有中斷請求優先級都相當於IR2。所以IRQ線的優先級由高到低次序為IRQ0,IRQ1,IRQ8-15,IRQ3-7。這是默認情況,可以通過編程改變。
在8259a芯片內部有幾個重要的寄存器:
中斷請求寄存器: IRR,8bit,對應IR0-IR7,當對應引腳產生中斷信號時,該bit位置1。
中斷服務寄存器: ISR,8bit,對應IR0-IR7,當對應引腳的中斷正在被CPU處理時,該bit位置1。
中斷屏蔽寄存器: IMR,8bit,對應IR0-IR7,當對應位為1時,表示屏蔽該引腳產生的中斷信號。
還有一個中斷優先級判決器: PR,當中斷引腳有信號時,結合這次產生中斷的IRQ號和ISR中記錄的當前正在處理的中斷信息,根據優先級來決定是否把這個新的中斷信號報告給CPU,以此來產生中斷嵌套。
下面是這15條IRQ線分別連接的外設:
現在我們來看看這個秘書是如何和CPU之間進行協調工作的。
現在假設我們敲擊了一個鍵盤按鍵,鍵盤有中斷事件產生,這一事件通過IRQ1這根線告知了主PIC,主PIC經過內部一些判斷處理后通過INT發送電信號到CPU側的INTR。CPU在執行完當前的指令后,檢查到INTR有信號,說明有中斷請求來了,再檢查eflags中的IF不為零,表示當前允許中斷,則發送信號給PIC的-INTA,告訴它把本次中斷的向量號發送過來。主PIC收到-INTA管腳上的信號后,通過D0-D7引腳,輸出此次中斷的中斷向量號到數據總線(這里簡化了交互過程,實際上有兩次INTA信號的發送)。CPU拿到這個號后,就可以從IDT中尋找中斷服務例程(ISR)進行處理了,后面的事大家都知道了。
那PIC中的中斷向量號是怎么來的呢?各個IRQ是如何對應到IDT中的各個項呢?這里就利用了中斷控制器的可編程性來決定的了。
PIC全稱為可編程中斷控制器,那么它的可編程體現在哪些方面呢?參考資料2《i8259A中斷控制器分析一》一文有比較詳細的描述,大體包括編程指定主從片的IRQ線對應的中斷在IDT表中的中斷向量號、8259a中斷控制器的中斷方式、優先級方式、中斷嵌套方式,中斷屏蔽方式、中斷結束方式等等,這些都可以由操作系統編程指定。具體的編程格式在參考資料3《i8259A中斷控制器分析二》一文中有圖文介紹。
回到上一個問題,IRQ線上的中斷如何和IDT中的條目對應起來,操作系統在初始化的時候,會通過對8259a芯片編程(讀寫I/O端口),將指定PIC芯片的起始向量號,並要求低三位為0,起始向量號按照8對齊,這樣規定的原因是,當中斷發生時,低三位將自動填充對應的IRQ號,這樣就可以和起始向量號相加直接送給數據總線從而被CPU拿到。具體到Windows中,系統初始化的時候對PIC的編程為:指定主片的起始中斷向量號為0x30,指定從片的起始中斷向量號為0x38。這樣,通過中斷控制器連接的15個外設將被平坦的映射到IDT中0x30-0x40這一范圍中。Windows內核啟動初始化過程中使用了hal!HalpInitializePICs對8259a芯片進行編程,ReactOS中代碼如下:
其中0x20,0x21是主片的IO端口,0xa0,0xa1是從片的IO端口:
PRIMARY_VECTOR_BASE定義為:
具體8259a的編程方法就是讀寫IO端口,設置對應的控制命令,不用深入研究。我們來看Windows編程8259a的時候指定了哪些東西。
1、指定了主片的工作方式為級聯、中斷方式為電信號邊沿觸發
2、指定了主片IRQ的中斷向量映射基址:0x30
3、指定了主片的級聯方式為使用了自己的IRQ2這個管腳
4、指定了主片的工作模式為80x86模式,中斷結束方式為普通結束模式
5、指定了從片的工作方式為級聯、中斷方式為電信號邊沿觸發
6、指定了從片IRQ的中斷向量映射基址:0x38
7、指定了從片的工作方式級聯方式為主片的IRQ2這個管腳
8、指定了從片的工作模式為80x86模式,中斷結束方式為普通結束模式
至此我們可以知道,在使用8259A中斷控制器的計算機上,通過IRQ線連接的那15個外設可屏蔽中斷是被操作系統線性的映射到了IDT中的一個范圍段。在Windows中是0x30-0x40(PS:在Linux中是0x20-0x2F),同時指定了中斷控制器的中斷方式為邊沿觸發,結束模式為普通結束模式(也就是需要CPU側告知中斷處理有沒有結束並設置對應bit位,不能自動設置)。
0x02 8259a上的Windows IRQL
下面來看看IRQL。
從前面我們看到,硬件層面已經對中斷的處理提供了很好的支持,需要操作系統做的也就兩點:首先,初始化的時候對PIC進行編程設置其工作方式並對IRQ進行映射,讓這些中斷對應到IDT中的各個項,其次,實現這些IDT中的中斷服務例程。似乎這樣就夠了,那Windows弄出來的一套IRQL又是什么東西呢?
看看《Windows Internals》一書對IRQL的定義:
寫驅動的時候經常會接觸到IRQL這個概念,它實現了Windows里的中斷優先級制度,高優先級的中斷總是可以優先被處理,而低優先級的中斷則不得不等待高優先級中斷被處理完后才得到處理。軟件虛擬出來的這一套機制怎么能管到硬件的優先級呢?這是如何實現的呢?
先來解決兩個問題:
1、IRQ和IRQL的關系是什么?
2、使用KeRaiseIrql提升當前IRQL后,為什么就能保證不被低優先級的中斷打擾?
對於第一個問題,在使用8259a中斷控制器的計算機中,IRQL=27-IRQ,其就是一個線性關系。
關於第二個問題,《Windows Internals》一書是這樣解答的:
下面我們具體來看Windows的實現:
IRQL是一個完全虛擬出來的概念,Windows為了實現這一個虛擬的機制,完全虛擬了一個中斷控制器,它在KPCR中:
+0x024 Irql : UChar //IRQL
+0x028 IRR : Uint4B //虛擬中斷請求寄存器
+0x02c IrrActive : Uint4B //虛擬中斷在服務寄存器
+0x030 IDR : Uint4B //虛擬中斷屏蔽寄存器
在前面第一部分提到過,通過兩片8259a芯片連接的15個中斷源被映射到處理器IDT中的一段范圍,具體Windows而言,是在0x30-0x40這個范圍。這15個IDT中的中斷描述符所描述的中斷處理例程(ISR)不同於int 3所對應的KiTrap03和int 0e所對應的KiTrap0E,他們的ISR指向的代碼位於各自的中斷對象KINTERRUPT的DispatchCode。下面是這個結構的定義:
typedef struct _KINTERRUPT { CSHORT Type; CSHORT Size; LIST_ENTRY InterruptListEntry; PKSERVICE_ROUTINE ServiceRoutine; PVOID ServiceContext; KSPIN_LOCK SpinLock; ULONG TickCount; PKSPIN_LOCK ActualLock; PVOID DispatchAddress; ULONG Vector; KIRQL Irql; KIRQL SynchronizeIrql; BOOLEAN FloatingSave; BOOLEAN Connected; CHAR Number; UCHAR ShareVector; KINTERRUPT_MODE Mode; ULONG ServiceCount; ULONG DispatchCount; ULONG DispatchCode[106]; } KINTERRUPT, *PKINTERRUPT;
DispatchCode里面的代碼是根據一個模板來的,這些ISR處理開始和KiTrap03這些一樣,首先會建立陷阱幀,然后會獲取自己所在KINTERRUPT對象地址,得到這兩個參數之后,便開始使用KiInterruptDispatch或KiChainedDispatch(如果對該中斷注冊了多個KINTERRUPT結構構成了鏈表使用此函數)進行中斷派遣。而在這兩個具體的派遣中都會先調用HalBeginSystemInterrupt,然后才會執行對應中斷的實際處理工作,最后會執行HalEndSystemInterrupt完成此次中斷處理。下面我們重點來看看這兩個函數。
BOOLEAN
HalBeginSystemInterrupt(
IN KIRQL Irql
IN CCHAR Vector,
OUT PKIRQL OldIrql);
輸入參數Irql表示本次發生的中斷對應的的IRQL,Vector表示中斷向量號,如前所述,這兩個參數都是DispatchCode從自己所在KINTERRUPT對象中取出來的。
HalBeginSystemInterrupt內部使用IRQL參數在一個表格中進行了分發,這個表中除了個別函數不同外(其實也只是多了一層判斷),其他表項都是一致的,在ReactOS中名為HalpDismissIrqGeneric,該函數直接轉而調用其下划線版本_HalpDismissIrqGeneric。這里就是IRQL優先級實現的核心所在了。該函數不長,下面是ReactOS中的代碼(在Windows2000代碼中是匯編形式不如ReactOS使用的C語言形式直觀,所以采用了ReactOS的代碼進行說明):
首先,判斷本次發生的中斷對應的IRQL與當前處理器(KPCR)中的IRQL進行比較,如果大於了當前處理器的IRQL,則表示來了一個優先級更高的中斷,這時設置KPCR中的IRQL為這個新的更高的數值,后面返回了TRUE,表示需要處理這次中斷請求。如果不大於當前處理器的IRQL的話,首先把本次中斷記錄記錄到KPCR中的虛擬中斷控制器的IRR值,然后就直接通過KiI8259MaskTable表中選取當前處理器IRQL對應的屏蔽碼寫入PIC,用以屏蔽那些IRQL比自己低的中斷源,后面返回FALSE,表示不處理這次中斷請求。為什么不在設置處理器新IRQL的時候就進行設置屏蔽碼呢?《Windows Internals》是這樣解釋的:
HalpDismissIrqGeneric的返回值將直接作為HalBeginSystemInterrupt的返回值。以中斷派遣函數KiInterruptDispatch為例看看它是如何使用這個返回值的:
可以看出,如果HalBeginSystemInterrupt返回了FALSE,則直接導致本次中斷處理提前結束。只有當HalBeginSystemInterrupt返回了TRUE時,才繼續執行真正的中斷處理例程。最后, 情況下都會調用KiExitInterrupt結束中斷處理過程,看一下這個函數。結合KiInterruptDispatch的代碼,可以看出,只有當HalBeginSystemInterrupt返回的是TRUE時,下面的if條件才會成立,從而進入HalEndSystemInterrupt。
最后看一下HalEndSystemInterrupt,前面提到如果發生的中斷對應的IRQL低於處理器的IRQL,則不會執行其ISR,但會在KPCR中的虛擬中斷控制器的IRR中記錄起來,等到處理器執行完了高IRQL的任務時,到了HalEndSystemInterrupt的時候,就會降低處理器的IRQL並重新設置PIC的中斷屏蔽碼,另外很重要的就是去檢查IRR中的記錄,如果記錄中有比降低后的IRQL高的記錄,則派遣該中斷。
最后總結一下使用8259a中斷控制器的計算機中Windows的IRQL。
首先,系統啟動時對8259a芯片編程,設置其工作方式,並將15個中斷源(IRQ)映射到IDT中的0x30-0x40這一段。
第二,Windows自己定義了一個稱為中斷請求級的IRQL概念用來描述中斷的優先級別,IRQL是一個DWORD,共計32個級別,Windows使用一個簡單的線性關系來映射IRQ和IRQL:IRQL=27-IRQ。
第三,被映射中斷請求的0x30-0x40這一段的中斷描述符的每個ISR都指向了一個KINTERRUPT結構中的DispatchCode,這段DispatchCode使用中斷派遣函數KiInterruptDispatch或KiChainedDispatch進行中斷派遣。
第四,派遣過程為:先使用HalBeginSystemInterrupt對本次中斷的IRQL進行判斷來決定是否需要處理本次中斷,若不需要,則設置中斷控制器的屏蔽碼,防止再被打擾,同時將本次中斷登記在KPCR中的虛擬中斷控制器IRR中。若需要則提升IRQL,進而執行該中斷的實際處理例程,執行完畢后使用HalEndSystemInterrupt降低IRQL,然后檢查IRR有沒有記錄沒被處理的中斷以便在這個時候進行處理。
0x03 進入奔騰時代——APIC
下回再聊。