Windows NT為每個硬件中斷和少數(shù)軟件事件賦予了一個優(yōu)先級,即中斷請求級(interrupt request level - IRQL)。IRQL為單CPU上的活動提供了同步方法,它基于下面規(guī)則:
圖4-1顯示了x86平臺上的IRQL值范圍。(通常,這個IRQL數(shù)值要取決于你所面對的平臺) 用戶模式程序執(zhí)行在PASSIVE_LEVEL上,可以被任何執(zhí)行在高于該IRQL上的活動搶先。許多設備驅(qū)動程序例程也執(zhí)行在PASSIVE_LEVEL上。第二章中討論的DriverEntry和AddDevice例程就屬于這類,大部分IRP派遣例程也屬于這類。
圖4-1. 中斷請求級
在DISPATCH_LEVEL級和PROFILE_LEVEL級之間是各種硬件中斷級。通常,每個有中斷能力的設備都有一個IRQL,它定義了該設備的中斷優(yōu)先級別。WDM驅(qū)動程序只有在收到一個副功能碼為IRP_MN_START_DEVICE的IRP_MJ_PNP請求后,才能確定其設備的IRQL。設備的配置信息作為參數(shù)傳遞給該請求,而設備的IRQL就包含在這個配置信息中。我們通常把設備的中斷級稱為設備IRQL,或DIRQL。
其它IRQL級的含義有時需要依靠具體的CPU結(jié)構(gòu)。這些IRQL通常僅被Windows NT內(nèi)核內(nèi)部使用,因此它們的含義與設備驅(qū)動程序的編寫不是特別密切相關。例如,我將要在本章后面詳細討論的APC_LEVEL,當系統(tǒng)在該級上為某線程調(diào)度APC(異步過程調(diào)用)例程時不會被同一CPU上的其它線程所干擾。在HIGH_LEVEL級上系統(tǒng)可以執(zhí)行一些特殊操作,如系統(tǒng)休眠前的內(nèi)存快照、處理bug check、處理假中斷,等等。
IRQL的變化
為了演示IRQL的重要性,參見圖4-2,該圖顯示了發(fā)生在單CPU上的一系列事件。在時間序列的開始處,CPU執(zhí)行在PASSIVE_LEVEL級上。在t1時刻,一個中斷到達,它的服務例程執(zhí)行在DIRQL1上,該級是在DISPATCH_LEVEL和PROFILE_LEVEL之間的某個DIRQL。在t2時刻,另一個中斷到達,它的服務例程執(zhí)行在DIRQL2上,比DIRQL1低一級。我們討論過搶先規(guī)則,所以CPU將繼續(xù)服務于第一個中斷。當?shù)谝粋€中斷服務例程在t3時刻完成時,該中斷服務程序可能會請求一個DPC。而DPC例程是執(zhí)行在DISPATCH_LEVEL上。所以當前存在的未執(zhí)行的最高優(yōu)先級的活動就是第二個中斷的服務例程,所以系統(tǒng)接著執(zhí)行第二個中斷的服務例程。這個例程在t4時刻結(jié)束,假設這之后再沒有其它中斷發(fā)生,CPU將降到DISPATCH_LEVEL級上執(zhí)行第一個中斷的DPC例程。當DPC例程在t5時刻完成后,IRQL又落回到原來的PASSIVE_LEVEL級。

圖4-2. 變化中的中斷優(yōu)先級
基本同步規(guī)則
遵循下面規(guī)則,你可以利用IRQL的同步效果:
所有對共享數(shù)據(jù)的訪問都應該在同一(提升的)IRQL上進行。
換句話說,不論何時何地,如果你的代碼訪問的數(shù)據(jù)對象被其它代碼共享,那么你應該使你的代碼執(zhí)行在高于PASSIVE_LEVEL的級上。一旦越過PASSIVE_LEVEL級,操作系統(tǒng)將不允許同IRQL的活動相互搶先,從而防止了潛在的沖突。然而這個規(guī)則不足以保護多處理器機器上的數(shù)據(jù),在多處理器機器中你還需要另外的防護措施——自旋鎖(spin lock)。如果你僅關心單CPU上的操作,那么使用IRQL就可以解決所有同步問題。但事實上,所有WDM驅(qū)動程序都必須設計成能夠運行在多處理器的系統(tǒng)上。
IRQL與線程優(yōu)先級
線程優(yōu)先級是與IRQL非常不同的概念。線程優(yōu)先級控制著線程調(diào)度器的調(diào)度動作,決定何時搶先運行線程以及下一次運行什么線程。然而,當IRQL級高于或等于DISPATCH_LEVEL級時線程切換停止,無論當前活動的是什么線程都將保持活動狀態(tài)直到IRQL降到DISPATCH_LEVEL級之下。而此時的“優(yōu)先級”僅指IRQL本身,由它控制到底哪個活動該執(zhí)行,而不是該切換到哪個線程的上下文。
IRQL和分頁
執(zhí)行在提升的IRQL級上的一個后果是,系統(tǒng)將不能處理頁故障(系統(tǒng)在APC級處理頁故障)。這意味著:
執(zhí)行在高于或等于DISPATCH_LEVEL級上的代碼絕對不能造成頁故障。
這也意味著執(zhí)行在高于或等于DISPATCH_LEVEL級上的代碼必須存在于非分頁內(nèi)存中。此外,所有這些代碼要訪問的數(shù)據(jù)也必須存在于非分頁內(nèi)存中。最后,隨著IRQL的提升,你能使用的內(nèi)核模式支持例程將會越來越少。
DDK文檔中明確指出支持例程的IRQL限定。例如,KeWaitForSingleObject例程有兩個限定:
- 調(diào)用者必須運行在低于或等于DISPATCH_LEVEL級上。
- 如果調(diào)用中指定了非0的超時,那么調(diào)用者必須嚴格地運行在低于DISPATCH_LEVEL的IRQL上。
上面這兩行想要說明的是:如果KeWaitForSingleObject真的被阻塞了指定長的時間(你指定的非0超時),那么你必定運行在低于DISPATCH_LEVEL的IRQL上,因為只有在這樣的IRQL上線程阻塞才是允許的。如果你所做的一切就是為了檢測事件是否進入信號態(tài),則可以執(zhí)行在DISPATCH_LEVEL級上。但你不能在ISR或其它運行在高于DISPATCH_LEVEL級上的例程中調(diào)用KeWaitForSingleObject例程。
IRQL的隱含控制
在大部分時間里,系統(tǒng)都是在正確的IRQL上調(diào)用驅(qū)動程序中的例程。雖然我們還沒有詳細地討論過這些例程,但我希望舉一個例子來表達這句話的含義。你首先遇到的I/O請求就是I/O管理器調(diào)用你的某個派遣例程來處理一個IRP。這個調(diào)用發(fā)生在PASSIVE_LEVEL級上,因為你需要阻塞調(diào)用者線程,還需要調(diào)用其它支持例程。當然,你不能在更高的IRQL級上阻塞一個線程,而PASSIVE_LEVEL也是唯一能讓你無限制地調(diào)用任何支持例程的IRQL級。
如果你的派遣例程通過調(diào)用IoStartPacket來排隊IRP,那么你第一個遇到的請求將發(fā)生在I/O管理器調(diào)用你的StartIo例程時。這個調(diào)用發(fā)生在DISPATCH_LEVEL級,因為系統(tǒng)需要在沒有其它例程(這些例程能在隊列中插入或刪除IRP)干擾的情況下訪問I/O隊列。回想一下前面提到的規(guī)則:所有對共享數(shù)據(jù)的訪問都應該在同一(提升的)IRQL級上進行。因為每個能訪問IRP隊列的例程都執(zhí)行在DISPATCH_LEVEL級上,所以任何例程在操作隊列期間都不可能被打斷(僅指在單CPU系統(tǒng))。
之后,設備可能生成一個中斷,而該中斷的服務例程(ISR)將在DIRQL級上被調(diào)用。設備上的某些寄存器也許不能被安全地共享。但是,如果你僅在DIRQL上訪問那些寄存器,可以保證在單CPU計算機上沒人能妨礙你的ISR執(zhí)行。如果驅(qū)動程序的其它代碼需要訪問這些關鍵的硬件寄存器,你應該讓這些代碼僅執(zhí)行在DIRQL級上。KeSynchronizeExecution服務函數(shù)可以幫助你強制執(zhí)行這個規(guī)則,我將在第七章的“與中斷處理連接”段中討論這個函數(shù)。
再往后,你應該安排一個DPC調(diào)用。DPC例程執(zhí)行在DISPATCH_LEVEL級上,它們需要訪問你的IRP隊列,并取出隊列中的下一個請求,然后把這個請求發(fā)送給StartIo例程。你可以調(diào)用IoStartNextPacket服務函數(shù)從隊列中提取下一個請求,但必須在DISPATCH_LEVEL級上調(diào)用。該函數(shù)在返回前將調(diào)用你的StartIo例程。注意,這里的IRQL吻合得相當巧妙:隊列訪問,調(diào)用IoStartNextPacket,和調(diào)用StartIo都需要發(fā)生在DISPATCH_LEVEL級上,并且系統(tǒng)也是在這個IRQL級上調(diào)用DPC例程的。
盡管明確地控制IRQL也是可能的,但幾乎沒有理由這樣做,因為你需要的IRQL和系統(tǒng)調(diào)用你時使用的IRQL總是相應的。所以不必不時地提高IRQL,例程希望的IRQL和系統(tǒng)使用的IRQL幾乎總是正確對應的。
IRQL的明確控制
如果必要,你還可以在當前處理器上臨時提升IRQL,然后再降回到原來的IRQL,使用KeRaiseIrql和KeLowerIrql函數(shù)。下面代碼運行在PASSIVE_LEVEL級上:
KIRQL oldirql; <--1
ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); <--2
KeRaiseIrql(DISPATCH_LEVEL, &oldirql); <--3
...
KeLowerIrql(oldirql); <--4
|
- KIRQL定義了用于保存IRQL值的數(shù)據(jù)類型。我們需要一個變量來保存當前IRQL。
- 這個ASSERT斷定了調(diào)用KeRaiseIrql的必要條件:新IRQL必須大于或等于當前IRQL。如果這個關系不成立,KeRaiseIrql將導致bug check。(即用死亡藍屏報告一個致命錯誤)
- KeRaiseIrql把當前的IRQL提升到第一個參數(shù)指定的IRQL級上。它同時還把當前的IRQL值保存到第二個參數(shù)指定的變量中。在這個例子中,我們把IRQL提升到DISPATCH_LEVEL級,并把原來的IRQL級保存到oldirql變量中。
- 執(zhí)行完任何需要在提升的IRQL上執(zhí)行的代碼后,我們調(diào)用KeLowerIrql把IRQL降低到調(diào)用KeRaiseIrql時的級別。
DDK文檔中提到,你必須用與你最近的KeRaiseIrql調(diào)用所返回的值調(diào)用KeLowerIrql。這在大的方面是對的,因為你提升了IRQL就必須再降低它。然而,由于你調(diào)用的代碼或者調(diào)用你的代碼所做的各種假設會使后面的決定變得不正確。所以,文檔中的這句話從嚴格意義上講是不正確的。應用到KeLowerIrql函數(shù)的唯一的規(guī)則就是新IRQL必須低于或等于當前IRQL。
當系統(tǒng)調(diào)用你的驅(qū)動程序例程時,你降低了IRQL(系統(tǒng)調(diào)用你的例程時使用的IRQL,或你的例程希望執(zhí)行的IRQL),這是一個錯誤,而且是嚴重錯誤,盡管你在例程返回前又提升了IRQL。這種打破同步的結(jié)果是,某些活動可以搶先你的例程,并能訪問你的調(diào)用者認為不能被共享的數(shù)據(jù)對象。
有一個函數(shù)專用于把IRQL提升到DISPATCH_LEVEL級:
KIRQL oldirql = KeRaiseIrqlToDpcLevel();
...
KeLowerIrql(oldirql)
|
注意:該函數(shù)僅在NTDDK.H中聲明,WDM.H中并沒有聲明該函數(shù),因此WDM驅(qū)動程序不應該使用該函數(shù)。