插斷要求級

來源:互聯網
上載者:User

[返回] [上一頁] [下一頁]

插斷要求級

Windows NT為每個硬體中斷和少數軟體事件賦予了一個優先順序,即插斷要求級(interrupt request level - IRQL)。IRQL為單CPU上的活動提供了同步方法,它基於下面規則:

一旦某CPU執行在高於PASSIVE_LEVEL的IRQL上時,該CPU上的活動僅能被擁有更高IRQL的活動搶先。

圖4-1顯示了x86平台上的IRQL值範圍。(通常,這個IRQL數值要取決於你所面對的平台) 使用者模式程式執行在PASSIVE_LEVEL上,可以被任何執行在高於該IRQL上的活動搶先。許多裝置驅動程式常式也執行在PASSIVE_LEVEL上。第二章中討論的DriverEntryAddDevice常式就屬於這類,大部分IRP派遣常式也屬於這類。

某些公用驅動程式常式執行在DISPATCH_LEVEL上,而DISPATCH_LEVEL級要比PASSIVE_LEVEL級高。這些公用常式包括StartIo常式,DPC(延遲程序呼叫)常式,和其它一些常式。這些常式的共同特點是,它們都需要訪問裝置對象和裝置擴充中的某些域,它們都不受派遣常式的幹擾或互相干擾。當任何一個這樣的常式運行時,上面陳述的規則可以保證它們不被任何驅動程式的派遣常式搶先,因為派遣常式本身執行在更低級的IRQL上。另外,它們也不會被同類常式搶先,因為那些常式啟動並執行IRQL與它們自己的相同。只有擁有更高IRQL的活動才能搶先它們。

注意
派遣常式(Dispatch routine)和DISPATCH_LEVEL級名稱類似。之所以稱做派遣常式是因為I/O管理器向這些函數派遣I/O請求。而存在派遣級(DISPATCH_LEVEL)這個名稱是因為核心線程派遣器運行在這個IRQL上,它決定下一次該執行哪個線程。(現在,線程發送器通常運行在SYNCH_LEVEL級上)

圖4-1. 插斷要求級

在DISPATCH_LEVEL級和PROFILE_LEVEL級之間是各種硬體中斷級。通常,每個有中斷能力的裝置都有一個IRQL,它定義了該裝置的中斷優先順序別。WDM驅動程式只有在收到一個副功能碼為IRP_MN_START_DEVICE的IRP_MJ_PNP請求後,才能確定其裝置的IRQL。裝置的配置資訊作為參數傳遞給該請求,而裝置的IRQL就包含在這個配置資訊中。我們通常把裝置的中斷級稱為裝置IRQL,或DIRQL。

其它IRQL級的含義有時需要依靠具體的CPU結構。這些IRQL通常僅被Windows NT核心內部使用,因此它們的含義與裝置驅動程式的編寫不是特別密切相關。例如,我將要在本章後面詳細討論的APC_LEVEL,當系統在該級上為某線程調度APC(非同步程序呼叫)常式時不會被同一CPU上的其它線程所幹擾。在HIGH_LEVEL級上系統可以執行一些特殊操作,如系統休眠前的記憶體快照、處理bug check、處理假中斷,等等。

IRQL的變化

為了示範IRQL的重要性,參見圖4-2,該圖顯示了發生在單CPU上的一系列事件。在時間序列的開始處,CPU執行在PASSIVE_LEVEL級上。在t1時刻,一個中斷到達,它的服務常式執行在DIRQL1上,該級是在DISPATCH_LEVEL和PROFILE_LEVEL之間的某個DIRQL。在t2時刻,另一個中斷到達,它的服務常式執行在DIRQL2上,比DIRQL1低一級。我們討論過搶先規則,所以CPU將繼續服務於第一個中斷。當第一個插斷服務常式在t3時刻完成時,該中斷服務程式可能會請求一個DPC。而DPC常式是執行在DISPATCH_LEVEL上。所以當前存在的未執行的最高優先順序的活動就是第二個中斷的服務常式,所以系統接著執行第二個中斷的服務常式。這個常式在t4時刻結束,假設這之後再沒有其它中斷髮生,CPU將降到DISPATCH_LEVEL級上執行第一個中斷的DPC常式。當DPC常式在t5時刻完成後,IRQL又落回到原來的PASSIVE_LEVEL級。

圖4-2. 變化中的中斷優先順序

 

基本同步規則

遵循下面規則,你可以利用IRQL的同步效果:

所有對共用資料的訪問都應該在同一(提升的)IRQL上進行。

換句話說,不論何時何地,如果你的代碼訪問的資料對象被其它代碼共用,那麼你應該使你的代碼執行在高於PASSIVE_LEVEL的級上。一旦越過PASSIVE_LEVEL級,作業系統將不允許同IRQL的活動相互搶先,從而防止了潛在的衝突。然而這個規則不足以保護多處理器機器上的資料,在多處理器機器中你還需要另外的防護措施——自旋鎖(spin lock)。如果你僅關心單CPU上的操作,那麼使用IRQL就可以解決所有同步問題。但事實上,所有WDM驅動程式都必須設計成能夠運行在多處理器的系統上。

IRQL與線程優先順序

線程優先順序是與IRQL非常不同的概念。線程優先順序控制著線程調度器的調度動作,決定何時搶先運行線程以及下一次運行什麼線程。然而,當IRQL級高於或等於DISPATCH_LEVEL級時線程切換停止,無論當前活動的是什麼線程都將保持活動狀態直到IRQL降到DISPATCH_LEVEL級之下。而此時的“優先順序”僅指IRQL本身,由它控制到底哪個活動該執行,而不是該切換到哪個線程的上下文。

IRQL和分頁

執行在提升的IRQL級上的一個後果是,系統將不能處理頁故障(系統在APC級處理頁故障)。這意味著:

執行在高於或等於DISPATCH_LEVEL級上的代碼絕對不能造成頁故障。

這也意味著執行在高於或等於DISPATCH_LEVEL級上的代碼必須存在於非分頁式記憶體中。此外,所有這些代碼要訪問的資料也必須存在於非分頁式記憶體中。最後,隨著IRQL的提升,你能使用的核心模式支援常式將會越來越少。

DDK文檔中明確指出支援常式的IRQL限定。例如,KeWaitForSingleObject常式有兩個限定:

  • 調用者必須運行在低於或等於DISPATCH_LEVEL級上。
  • 如果調用中指定了非0的逾時,那麼調用者必須嚴格地運行在低於DISPATCH_LEVEL的IRQL上。

上面這兩行想要說明的是:如果KeWaitForSingleObject真的被阻塞了指定長的時間(你指定的非0逾時),那麼你必定運行在低於DISPATCH_LEVEL的IRQL上,因為只有在這樣的IRQL上線程阻塞才是允許的。如果你所做的一切就是為了檢測事件是否進入訊號態,則可以執行在DISPATCH_LEVEL級上。但你不能在ISR或其它運行在高於DISPATCH_LEVEL級上的常式中調用KeWaitForSingleObject常式。

IRQL的隱含控制

在大部分時間裡,系統都是在正確的IRQL上調用驅動程式中的常式。雖然我們還沒有詳細地討論過這些常式,但我希望舉一個例子來表達這句話的含義。你首先遇到的I/O請求就是I/O管理器調用你的某個派遣常式來處理一個IRP。這個調用發生在PASSIVE_LEVEL級上,因為你需要阻塞調用者線程,還需要調用其它支援常式。當然,你不能在更高的IRQL級上阻塞一個線程,而PASSIVE_LEVEL也是唯一能讓你無限制地調用任何支援常式的IRQL級。

如果你的派遣常式通過調用IoStartPacket來排隊IRP,那麼你第一個遇到的請求將發生在I/O管理器調用你的StartIo常式時。這個調用發生在DISPATCH_LEVEL級,因為系統需要在沒有其它常式(這些常式能在隊列中插入或刪除IRP)幹擾的情況下訪問I/O隊列。回想一下前面提到的規則:所有對共用資料的訪問都應該在同一(提升的)IRQL級上進行。因為每個能訪問IRP隊列的常式都執行在DISPATCH_LEVEL級上,所以任何常式在操作隊列期間都不可能被打斷(僅指在單CPU系統)。

之後,裝置可能產生一個中斷,而該中斷的服務常式(ISR)將在DIRQL級上被調用。裝置上的某些寄存器也許不能被安全地共用。但是,如果你僅在DIRQL上訪問那些寄存器,可以保證在單CPU電腦上沒人能妨礙你的ISR執行。如果驅動程式的其它代碼需要訪問這些關鍵的硬體寄存器,你應該讓這些代碼僅執行在DIRQL級上。KeSynchronizeExecution服務函數可以協助你強制執行這個規則,我將在第七章的“與中斷處理串連”段中討論這個函數。

再往後,你應該安排一個DPC調用。DPC常式執行在DISPATCH_LEVEL級上,它們需要訪問你的IRP隊列,並取出隊列中的下一個請求,然後把這個請求發送給StartIo常式。你可以調用IoStartNextPacket服務函數從隊列中提取下一個請求,但必須在DISPATCH_LEVEL級上調用。該函數在返回前將調用你的StartIo常式。注意,這裡的IRQL吻合得相當巧妙:隊列訪問,調用IoStartNextPacket,和調用StartIo都需要發生在DISPATCH_LEVEL級上,並且系統也是在這個IRQL級上調用DPC常式的。

儘管明確地控制IRQL也是可能的,但幾乎沒有理由這樣做,因為你需要的IRQL和系統調用你時使用的IRQL總是相應的。所以不必不時地提高IRQL,常式希望的IRQL和系統使用的IRQL幾乎總是正確對應的。

IRQL的明確控制

如果必要,你還可以在當前處理器上臨時提升IRQL,然後再降回到原來的IRQL,使用KeRaiseIrqlKeLowerIrql函數。下面代碼運行在PASSIVE_LEVEL級上:

KIRQL oldirql;<--1ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL);<--2KeRaiseIrql(DISPATCH_LEVEL, &oldirql);<--3...KeLowerIrql(oldirql);<--4
  1. KIRQL定義了用於儲存IRQL值的資料類型。我們需要一個變數來儲存當前IRQL。
  2. 這個ASSERT斷定了調用KeRaiseIrql的必要條件:新IRQL必須大於或等於當前IRQL。如果這個關係不成立,KeRaiseIrql將導致bug check。(即用死亡藍屏報告一個致命錯誤)
  3. KeRaiseIrql把當前的IRQL提升到第一個參數指定的IRQL級上。它同時還把當前的IRQL值儲存到第二個參數指定的變數中。在這個例子中,我們把IRQL提升到DISPATCH_LEVEL級,並把原來的IRQL級儲存到oldirql變數中。
  4. 執行完任何需要在提升的IRQL上執行的代碼後,我們調用KeLowerIrql把IRQL降低到調用KeRaiseIrql時的層級。

DDK文檔中提到,你必須用與你最近的KeRaiseIrql調用所返回的值調用KeLowerIrql。這在大的方面是對的,因為你提升了IRQL就必須再降低它。然而,由於你調用的代碼或者調用你的代碼所做的各種假設會使後面的決定變得不正確。所以,文檔中的這句話從嚴格意義上講是不正確的。應用到KeLowerIrql函數的唯一的規則就是新IRQL必須低於或等於當前IRQL。

當系統調用你的驅動程式常式時,你降低了IRQL(系統調用你的常式時使用的IRQL,或你的常式希望執行的IRQL),這是一個錯誤,而且是嚴重錯誤,儘管你在常式返回前又提升了IRQL。這種打破同步的結果是,某些活動可以搶先你的常式,並能訪問你的調用者認為不能被共用的資料對象。

有一個函數專用於把IRQL提升到DISPATCH_LEVEL級:

KIRQL oldirql = KeRaiseIrqlToDpcLevel();...KeLowerIrql(oldirql)

注意:該函數僅在NTDDK.H中聲明,WDM.H中並沒有聲明該函數,因此WDM驅動程式不應該使用該函數。

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.