天天看點

驅動程式的同步處理驅動程式的同步處理

Windows是個多任務的作業系統,每個任務對應一個運作的程序。每個運作的程序中可以包含多個線程。如果沒有同步機制的控制,所有的線程會任意運作。然而,多個線程可能會要求操作同一個資源,這時就需要同步處理。

1、基本概念

1.1、問題的引出

在支援多線程的作業系統下,有些函數會出現不可重入現象。所謂“可重入”,是指函數的執行結果和執行順序無關。反之,如果執行結果和執行順序有關,則稱這個函數是“不可重入”的。

“不可重入”的函數會對多線程作業系統下的程式帶來錯誤,“不可重入”的根本原因就是線程之間的切換導緻的。

1.2、同步與異步

多線程運作的基本原理:如果PC中隻有一個CPU,CPU将時間分成一個個時間片段,然後CPU将這些時間片段配置設定給各個線程,目前的線程消耗完這個時間片段後,CPU會轉而執行其他的線程。由于CPU運作速度非常快,每個線程仿佛是在同時運作一樣。

這時候,各個線程之間的關系成為異步的。每個線程的運作不受其他線程的影響。

2、中斷請求級

在設計windows的時候,設計者将中斷請求劃分為軟體中斷和硬體中斷,并将這些中斷都映射成不同級别的中斷請求級(IRQL)。同步處理機制很大程度上依賴于中斷請求級。

2.1、中斷請求(IRQ)與可程式設計中斷控制器(PIC)

中斷請求(IRQ)一般有兩種,一種是外部中斷,也就是硬體産生的中斷,另一種由軟體指令int n産生的中斷。

在傳統PC中,一般可以接收16個中斷信号,每個中斷信号對應一個中斷号,外部中斷分為不可屏蔽(NMI)和可屏蔽中斷,分别由CPU的兩根引腳NMI和INTR來接收。

2.2、進階可程式設計控制器(APIC)

2.3、中斷請求級(IRQL)

在APIC中,IRQ的數量被增加到24個,每個IRQ有各自的優先級别,正在運作的線程随時可以被中斷打斷,進入到中斷處理程式,當優先級高的中斷來臨時,處在優先級低的中斷處理程式,也會被打斷,進入到更進階别的中斷處理函數。

Windows将中斷的概念進行了擴充,,提出一個中斷請求級(IRQL)的概念。其中規定了32個中斷請求級别,分别是0~2級别為軟體中斷,3~31級為硬體中斷,數字從0~31,優先級遞增

Windows将24個IRQ映射到了從DISPATCH_LEVEL到PROFILE_LEVEL之間,不同硬體的中斷處理程式運作在不同的IRQL級别中。硬體的IRQL稱為裝置中斷請求級别,簡稱DIRQL。Windows大部分時間運作在軟體中斷級别中,當裝置中斷來臨時,作業系統提升IRQL至DIRQL級别,并且運作中斷處理函數。當中斷處理函數結束後,作業系統把IRQL降到原來的級别

使用者模式的代碼是運作在最低優先級的PASSIVE_LEVEL級别。驅動程式的DriverEntry函數、派遣函數、AddDevice等函數一般都運作在PASSIVE_LEVEL級别,它們在必要時可以申請進入DISPATCH_LEVEL級别。

Windows負責線程排程的元件是運作在DISPATCH_LEVEL級别,目前的線程運作時間片後,系統自動從PASSIVE_LEVEL級别提升到DISPATCH_LEVEL級别。當線程切換完畢後,作業系統又從DISPATCH_LEVEL級别降到PASSIVE_LEVEL級别。

在核心模式下,可以通過調用KeGetCurrentIrql核心函數來得到目前IRQL級别。

8.2.4線程排程與線程優先級

線程優先級和IRQL是兩個容易混淆的概念。所有應用程式都運作在PASSIVE_LEVEL級别上,它的優先級别最低,可以被其他IRQL級别的程式打斷。線程優先級隻針對應用程式而言,隻有程式運作在PASSIVE_LEVEL級别才有意義。

線程的優先級别是指某線程是否有更多的機會運作在CPU上,線程優先級高的線程有更多的機會被核心排程。負責排程線程的核心元件運作在DISPATCH_LEVEL級别的IRQL上,這時候所有應用程式的線程都停止,等待着被排程。

ReadFile内部建立IRP_MJ_READ,然後這個IRP被傳遞到驅動程式的派遣函數中。這時候派遣函數運作于ReadFile所在的線程中,或者說ReadFile和派遣函數位于同一個線程上下文中。

2.4 IRQL的變化

以下描述一個線程的運作過程:

① 一個普通線程A正在運作

② 這個時刻有一個中斷發生,它的IRQL為0xD。CPU中斷目前運作的線程A,将IRQL提升至0xD級别。

③ 這個時候有一個更高優先級的中斷發生,它的IRQL是0x1A。這時候CPU将IRQL提升至0x1A級别

④ 這個時候又有一個中斷發生,但它的IRQL為0x18,低于上一個中斷優先級。CPU不會理睬這個中斷

⑤ 這時候IRQL為0x1A的中斷結束,作業系統進入IRQL為0x18的中斷服務。

⑥ 這時候IRQL為0x18的中斷結束,于是進入IRQL為0xD的中斷服務

⑦ 最後IRQL為0xD的終端結束,作業系統恢複線程A

線程運作在PASSIVE_LEVEL級别,這個時候作業系統随時可能将目前切換到别的線程。但是如果提升IRQL到DISPATCH_LEVEL級别,這時候,這時候不會出現線程的切換。這是一種很常用的同步處理機制,但這種方法隻能使用于單CPU的系統。對于CPU的系統,需要采用别的同步處理機制。

2.6 IRQL與記憶體分頁

在使用分頁記憶體時,可能後導緻頁故障。因為分頁記憶體随時可能從實體記憶體交換到磁盤檔案。讀取不在實體記憶體中的分頁記憶體時,會引發一個頁故障,進而執行這個異常的處理函數。異常處理函數會重新将磁盤檔案的内容交換到實體記憶體中。

頁故障允許出現在PASSIVE_LEVEL級别的程式中,但如果在DISPATCH_LEVEL或更進階别IRQL的程式中會帶來系統崩潰。

2.7 控制IRQL提升與降低

驅動程式使用核心函數KeRaiseIrql将IRQL提高

VOID 

  KeRaiseIrql(

    IN KIRQL  ,//提升後的IRQL級别

    OUT PKIRQL  //儲存提升前的IRQL級别

    );

驅動程式使用核心函數KeLowerIrql将IRQL恢複到以前IRQL級别

  KeLowerIrql(

    IN KIRQL  

3、自旋鎖

自旋鎖也是一種同步處理機制。他能保證某個資源隻能被一個線程所擁有。這種保護被形象稱為“上鎖”。

3.1、原理

在Windows核心中,有一種被稱為自旋鎖(Spin Lock)的鎖,它可以用于驅動程式中的同步處理。初始化自旋鎖時,處于解鎖狀态,這時它可以被程式“擷取”。“擷取”後的自旋鎖處于鎖住狀态,不能被再次“擷取”。鎖住的自旋鎖必須被“釋放”後才能被再次“擷取”。

如果自旋鎖已經被鎖住,這時有程式申請“擷取”這個自旋鎖,程式則處于“自旋”狀态,所謂自旋狀态,就是不停地詢問是否可以“擷取”自旋鎖,自旋鎖也是以得名。

在單個CPU的系統中,“擷取”自旋鎖僅僅是将目前的IRQL從PASSIVE_LEVEL級别提升到DISPATCH_LEVEL級别,但是在多CPU系統中,自旋鎖的實作方法會複雜的多。驅動程式必須在低于或者等于DISPATCH_LEVEL的IRQL級别中使用自旋鎖。

3.2 使用方法

自旋鎖的作用一般是為使各派遣函數之間同步。盡量不要将自旋鎖放在全局變量中,而應該将自旋鎖放在裝置擴充裡。自旋鎖用KSPIN_LOCK資料結構表示:

Type struct _DEVICE_EXTENSION{

.....

KSPIN_LOCK My_SpinLock;//在裝置擴充中定義自旋鎖

}DEVICE_EXTENSION,*PDEVICE_EXTENSION;

使用自旋鎖前,需要先對其進行初始化,可以使用KeInitializeSpinLock核心函數,一般在驅動程式的DriverEntry或者AddDevice函數中初始化自旋鎖。

申請自旋鎖可以使用核心函數KeAcquireSpinLock

釋放自旋鎖使用核心函數KeReleaseSpinLock

4、使用者模式下的同步對象

在核心模式下可以使用很多種核心同步對象,這些核心同步對象和使用者模式下的同步對象非常類似。同步對象包括事件(Event)、互斥體(Mutex)、信号燈(Semaphore)等。使用者模式下的同步對象其實是核心模式下同步對象的再次封裝。

4.1 使用者模式的等待

在應用程式中,可以使用WaitForSingleObject(用于等待一個同步對象)和WaitForMultipleObjects(用于等待多個同步對象)等待同步對象。

DWORD WaitForSingleObject{

HANDLE hHandle, //同步對象句柄

DWORD dwMilliseconds //等待時間ms,值為INFINITE表示無限等待,值為0表示強迫作業系統将目前線程切換到其他線程

};

WaitForMultipleObjects函數聲明:

DWORD WaitForMultipleObjects{

DWORD nCount, //同步對象數組元素個數

CONST HANDLE *lpHandles, //同步對象數組

BOOL bWaitAll, //是否等待全部同步對象

DWORD dwMillseconds //等待時間

4.2 使用者模式開啟多線程

等待同步對象一般出現在多線程的程式設計中,是以這裡介紹一下應用程式如何建立新線程。Win32 API CreateThread函數負責建立新線程。

HANDLE CreateThread{

LPSECURITY_ATTRIBUTES lpThreadAttributes, //安全屬性

SIZE_T dwStackSize, //初始化堆棧大小

LPTHREAD_START_ROUTINE lpStartAddress, //線程運作的函數指針

LPVOID lpParameter, //傳入函數中的參數

DWORD dwCreationFlags, //開啟線程時的狀态

LPDWORD lpThreadId //傳回線程ID

}

_beginthreadex函數對CreateThread函數進行了封裝,其參數與CreateThread完全一緻。

4.3、使用者模式的事件

事件是一種典型的同步對象。使用者模式下的事件和核心模式的事件對象緊密相連。在使用事件之前,需要對事件進行初始化,使用CreateEvent API函數。

主線程開啟新的輔助線程,主線程把一個事件的句柄傳遞給子線程。同時,主線程等待該事件激發,輔助線程所做的事情就是現實一些資訊,并設定該事件。如果主線程不等待事件,也是以異步的方式共同的和輔線程執行,這時很有可能主線程都退出來了,輔助線程還在繼續運作。

4.4 使用者模式的信号燈

信号燈也是一種常用的同步對象,信号燈也有兩種狀态,一種是激發狀态,另一種是未激發狀态。信号燈内部有個計數器,可以了解信号燈内部有N個燈泡。如果一個燈泡亮着,就代表信号處于激發狀态,如果全部熄滅,則代表信号燈處于未激發狀态。使用信号燈錢需要先建立信号燈。CreateSemaphore函數負責建立信号燈。

4.5 使用者模式的互斥體

互斥體也是一種常用的同步對象。互斥體可以避免多個線程争奪同一個資源。例如:多線程環境中,隻能有一個線程占有互斥體,獲得互斥體的線程如果不釋放互斥體,其他線程永遠不會獲得這個互斥體。互斥體的概念類似于同步事件,所不同的是同一個線程可以遞歸獲得互斥體:即得到互斥體的線程還可以再次獲得這個互斥體,或者說互斥體對于已經獲得互斥體的線程不産生“互斥”關系。而同步事件不能遞歸擷取。

互斥體也有兩種狀态:激發态和未激發态。如果線程獲得互斥體時,此時的狀态時未激發态,當釋放互斥體時,互斥體的狀态為激發态。初始化互斥體的函數是CreateMutex。

4.6 等待線程完成

還有一種同步對象,就是線程對象,每個線程同樣有兩個狀态,激發狀态和未激發狀态。當線程處于運作之中的時候,是未激發狀态。當線程終止後,線程處于激發狀态。

以上内容參考自張帆 史彩成等編著的《Windows 驅動開發技術詳解》第8章