標籤:
搞核心研究的經常對中斷這個概念肯定不陌生,經常我們會接觸很多與中斷相關的術語,按照軟體和硬體進行分類:
硬體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=0x27-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完成此次中斷處理。下面我們重點來看看這兩個函數。
BOOLEANHalBeginSystemInterrupt( 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
下回再聊。
Windows中斷那些事兒