1. 程式人生 > >Windows驅動開發技術詳解__驅動程式的同步處理

Windows驅動開發技術詳解__驅動程式的同步處理

如果驅動程式沒有很好地處理同步問題,作業系統的效能就會下降,甚至出現死鎖等現象。

基本概念

1.問題的引出

下面這段程式碼:

int number;
void Foo()
{
    number++;
    //做一些事情
    number--;
}

將其轉換成彙編程式碼:
; 將number++分解成如下
mov   eax , [number]
add   eax , 1
mov   [number] , eax
; 將number--分解成如下
mov    ecx , dword ptr [number]
sub    ecx , 1
mov    dword ptr [number] , ecx

在多執行緒環境下這些程式碼可能混作一團。

2.同步和非同步

執行在不同執行緒中的程式,各自沒有相互影響的執行,成為它們之間是非同步的。

當不同執行緒需要按照一定順序來執行時,稱為執行緒間的同步。

中斷請求級

1.中斷請求(IRQ)和可程式設計中斷控制器(PIC)

在設計Windows的時候,設計者將中斷請求劃分為軟體中斷和硬體中斷,並將這些中斷對映成不同級別的中斷請求級(IRQL)。

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

在傳統PC中,一般可以接收16箇中斷訊號,每個中斷訊號對應一箇中斷號。外部中斷分為不可遮蔽(NMI)和可遮蔽中斷,分別有CPU的兩根引腳NMI和INTR萊接收。

可遮蔽中斷是通過可程式設計中斷控制器(PIC)晶片想CPU傳送的。

2.高階可程式設計控制器

現在的X86計算機基本都用高階可程式設計控制器(APIC)

APIC相容PIC,但是將IRQ的數量增加到24個。

3.中斷請求級(IRQL)

在APIC中,IRQ的數量倍增加到了24個,每個IRQ有各自的優先級別,正在執行的執行緒可能隨時被中斷打斷,進入到中斷處理程式。當優先順序高的中斷來臨時,處於優先順序低的中斷處理程式,也會被打斷,進入到更高級別的中斷處理函式。

Windows將中斷的概念進行了擴充套件,提出了一箇中斷請求級(IRQL)的概念。其中規定了32箇中斷請求級別,分別0~2級別為軟體中斷,3~31級為硬體中斷(這裡包括APIC中的24箇中斷)。數字從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級別。驅動程式的StartIO函式和DPC函式也是執行在DISPATCH_LEVEL級別。

在核心模式下,可以通過KeGetCurrentIrql核心函式來得到當前的IRQL級別。

4執行緒排程和執行緒優先順序

在應用程式的程式設計中,經常會聽到執行緒優先順序的概念。執行緒優先順序和IRQL是兩個極易混淆的概念。所有應用程式都執行在PASSIVE_LEVEL級別上,它的優先順序最低,可以被其他IRQL級別的程式打斷。執行緒優先順序只針對應用程式而言,只有執行在PASSIVE_LEVEL級別才有意義。

5.IRQL的變化

執行緒執行在PASSIVIE_LEVEL級別,這個時候作業系統隨時可能將當前執行緒切換至別的執行緒。但是如果提升IRQL到DISPATCH_LEVEL級別,這個時候不會出現執行緒的切換,這是一種很常用的同步處理機制,但這種方法只能使用在單核CPU的系統。

6.IRQL和記憶體分頁

在使用分頁記憶體時,可能會導致頁故障。因為分頁記憶體隨時可能從實體記憶體交換到磁碟檔案。讀取不在實體記憶體中的分頁記憶體時,會引發一個頁故障,從而執行這個異常的處理函式。異常處理函式會將磁碟檔案的內容交換到實體記憶體中。

頁故障允許出現在PASSIVE_LEVEL級別的應用程式中,但是如果在DISPATCH_LEVEL或者更高級別的IRQL的程式中則會帶來系統崩潰。

7.控制IRQL提升與降低

首先驅動程式需要知道當前狀態是什麼IRQL級別,可以通過KeGetCurrentIrql核心函式獲取當前IRQL級別。

然後驅動程式使用核心函式KeRaiseIrql將IRQL提高。還可以通過核心函式KeLowerIrql恢復到以前的IRQL級別。

示例程式碼:

VOID RasieIRQL_Test()
{
	KIRQL oldIrql;
	//確保當前IRQL等於或者小於DISPATCH_LEVEL
	ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL);
    //提升IRQL至DISPATCH_LEVEL,並將先前的IRQL儲存
	KeRaiseIrql(DISPATCH_LEVEL,oldIrql);
	//...............
	//恢復到先前的IRQL
	KeLowerIrql(oldIrql);
}

自旋鎖

自旋鎖也是一種同步處理機制,他能保證某個資源只能被一個執行緒所有。

1.原理

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

如果自旋鎖已經被鎖住,這時程式申請“獲取”這個自旋鎖,程式則處於“自旋”狀態。所謂自旋狀態,就是不停地詢問是否可以“獲取”自旋鎖。

2.使用方法

自旋鎖的作用一般是為使各個派遣函式之間的同步。儘量不要將自旋鎖放在全域性變數中,而應該將自旋鎖放在裝置擴充套件裡。自旋鎖用KSPIN_LOCK資料結構表示。

typedef struct _DEVICE_EXTENSION
{
	.......
	KSPIN_LOCK My_SpinLock;//在裝置擴充套件中定義自旋鎖

}DEVICE_EXTENSION,*PDEVICE_EXTENSION;

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

申請自旋鎖可以使用核心函式KeAcquireSpinLock,它有兩個引數,第一個引數為自旋鎖指標,第二個引數為記錄獲得自旋鎖以前的IRQL級別。

PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
KIRQL oldIrql;
keAcquireSpinLock(&pdx->My_SpinLock,&oldIrql);

釋放自旋鎖使用KeReleaseSpinLock核心函式,它也有兩個引數,第一個引數為自旋鎖指標,第二個是釋放自旋鎖後應該恢復的IRQL級別。
KeReleaseSpinLock(&pdx->SpinLock,oldIrql);

如果在DISPATCH_LEVEL級別申請自旋鎖,不會改變IRQL級別。這是可以簡單的使用KeAcquireSpinLockAtDpcLevel和KeReleaseSpinLockFromDpcLevel核心函式。

使用者模式下的同步物件

在核心模式下可以使用很多種核心同步物件,這些核心同步物件和使用者模式下的核心同步物件非常類似。同步物件包括事件(Event),互斥體(Mutex),訊號燈(Semaphore)等。

1.使用者模式的等待

在應用程式中,可以使用WaitForSingleObject和WaitForMultipleObjects等待同步物件。其中WaitForSingleObject用於等待一個同步物件,而WaitForMultipleObjects用於等待多個同步物件。WaitForSingleObject函式宣告如下:

DWORD WaitForSingleObject( 
				HANDLE hHandle,       //同步物件控制代碼
				DWORD dwMilliseconds  //等待時間
		);

第二個引數dwMillseconds是等待時間(毫秒ms)。同步物件有兩種狀態,一種是激發狀態,一種是未激發狀態。

WaitForMultipleObjects函式宣告如下:

WaitForMultipleObjects(
			DWORD nCount,                  //同步物件陣列元素個數
			CONST HANDLE *lpHandles,       //同步物件陣列指標
			BOOL bWaitAll,                 //是否等待全部同步物件
			DWORD dwMilliseconds           //等待時間
	);


2.使用者模式開啟多執行緒

Win32 API CreateThread函式負責建立新執行緒

HANDLE CreateThread( 
		 LPSECURITY_ATTRIBUTES lpThreadAttributes,  //安全屬性
		 DWORD dwStackSize,                         //初始化堆疊大小
		 LPTHREAD_START_ROUTINE lpStartAddress,     //執行緒執行函式指標
		 LPVOID lpParameter,                        //傳入函式中的引數
		 DWORD dwCreationFlags,                     //開啟執行緒時的狀態
		 LPDWORD lpThreadId                         //返回執行緒ID
	);

建立多執行緒的時候最好不要用CreateThread函式,而使用_beginthreadex函式,它是對CreateThread函式的封裝,其引數與CreateThread完全一致。_beginthreadex函式的函式名前面有個下劃線,因為它不是標準C語言提供的執行時函式。

3.使用者模式的事件

事件是一種典型的同步物件。在使用之前,需要對事件進行初始化,使用CreateEvent API函式。

HANDLE CreateEvent( 
		  LPSECURITY_ATTRIBUTES lpEventAttributes,  //安全屬性
		  BOOL bManualReset,                        //是否設定為手動
		  BOOL bInitialState,                       //i初始化狀態
		  LPCSTR lpName                             //命名
	);

所有形如CreateXXX的Win32 API函式,如果他的第一個引數是LPSECURITY_ATTRIBUTES型別,那麼這種API內部都會建立一個相應的核心物件,這種API返回一個控制代碼,作業系統可以通過這個控制代碼找到具體的核心物件。下面例子綜合演示上面所述內容:
#include <windows.h>
#include <stdio.h>
#include <process.h>
#include <stddef.h>
#include <stdlib.h>
#include <conio.h>

UINT WINAPI Thread1(LPVOID para)
{
	printf("Enter Thread1\n");
	HANDLE *phEvent = (HANDLE*)para;
	//設定該事件激發
	SetEvent(*phEvent);
	printf("Leave Thread1\n");
	return 0;
}

int main()
{
	//建立同步事件
	HANDLE hEvent = CreateEvent(NULL,FALSE,FALSE,NULL);
	//開啟新執行緒,並將同步事件的控制代碼傳遞給新執行緒
	HANDLE hThread1 = (HANDLE)_beginthreadex(NULL,0,Thread1,&hEvent,0,NULL);
	//等待該事件激發
	WaitForSingleObject(hEvent,INFINITE);
	return 0;
}

4.使用者模式的訊號燈

訊號燈也是一種常用的同步物件,訊號燈也有兩種狀態,一種是激發態,另一種是未激發態。訊號燈內部有個計數器,可以理解為訊號燈內部有N個燈泡,如果有一個燈泡亮著,就代表訊號燈處於激發狀態,如果完全熄滅,則代表訊號燈處於未激發狀態。使用訊號燈前需要先建立訊號燈,CreateSemphore函式負責建立訊號燈,宣告如下:

HANDLE CreateSemaphore(
			LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, //安全屬性
			LONG lInitialCount,                          //初始化計數個數
			LONG lMaximumCount,                          //計數器最大個數
			LPCSTR lpName                                //命名
	);

另外可以使用ReleaseSemphore函式增加訊號燈,其函式宣告如下:
BOOL ReleaseSemaphore(
		HANDLE hSemaphore,        //訊號燈控制代碼
		LONG lReleaseCount,       //本次操作增加的計數
		LPLONG lpPreviousCount    //記錄以前的計數
  );

對訊號燈執行一次等待操作,就會減少一個計數,相當於滅一盞燈泡。當計數器為0時,也就是所有燈泡都熄滅時,當前執行緒就會進入休眠狀態,直到訊號燈變為激發狀態。

綜合程式碼如下:

#include <windows.h>
#include <process.h>
#include <stdio.h>

UINT WINAPI Thread1(LPVOID para)
{
	printf("Enter Thread\n");
	HANDLE* phSemaphore = (HANDLE*)para;
	//等待5s
	Sleep(5000);
	printf("Leave Thread1\n");
	//將訊號燈計數器加1,使之處於啟用狀態
	ReleaseSemaphore(*phSemaphore,1,NULL);
	return 0;
}

int main()
{
	//建立訊號燈
	HANDLE hSemaphore = CreateSemaphore(NULL,2,2,NULL);
	//此時的訊號燈計數器為2,處於激發狀態
	WaitForSingleObject(hSemaphore,INFINITE);
	//此時的訊號燈計數器為1,處於激發狀態
	WaitForSingleObject(hSemaphore,INFINITE);
	//此時的訊號燈計數器為0,處於未激發狀態
	//開啟新執行緒,並將同步事件控制代碼指標傳遞給新執行緒
	HANDLE hThread1 = (HANDLE)_beginthreadex(NULL,0,Thread1,&hSemaphore,0,NULL);
	//等待事件激發
	WaitForSingleObject(hSemaphore,INFINITE);
	return 0;
}

5.使用者模式的互斥體

互斥體也是一種常用的同步物件,互斥體可以避免多個執行緒爭奪同一資源。與事件不同,得到互斥體的執行緒可以遞迴呼叫該互斥體。

互斥體也有兩種狀態,激發態和未激發態。如果執行緒獲得互斥體時,此時狀態是未激發狀態,當釋放互斥體時,互斥體狀態是激發狀態。

初始化互斥體的函式是CreateMutex,其宣告函式如下:

HANDLE CreateMutex( 
		LPSECURITY_ATTRIBUTES lpMutexAttributes, //安全屬性
		BOOL bInitialOwner,                      //是否被佔有
		LPCSTR lpName                            //命名
	);


WaiForSingleObject獲得互斥體,ReleaseMutex釋放互斥體。

下面程式碼綜合互斥體的使用:

#include <windows.h>
#include <process.h>
#include <stdio.h>

UINT WINAPI Thread1(LPVOID para)
{
	HANDLE* phMutex = (HANDLE*)para;
	//得到互斥體
	WaitForSingleObject(*phMutex,INFINITE);
	//對於同一個互斥體,可以多次獲得
	WaitForSingleObject(*phMutex,INFINITE);
	printf("Enter Thread1\n");
	//強迫等待2s
	Sleep(2000);
	printf("Leave Thread1\n");
	//釋放互斥體
	ReleaseMutex(*phMutex);
	return 0;
}

UINT WINAPI Thread2(LPVOID para)
{
	HANDLE* phMutex = (HANDLE*)para;
	//得到互斥體
	WaitForSingleObject(*phMutex,INFINITE);
	printf("Enter Thread2\n");
	//強迫等待2s
	Sleep(2000);
	printf("Leave Thread2\n");
	//釋放互斥體
	ReleaseMutex(*phMutex);
	return 0;
}

int main()
{
	//建立同步事件
	HANDLE hMutex = CreateMutex(NULL,FALSE,NULL);
	//開啟新的執行緒,並將同步事件控制代碼指標傳遞給新執行緒
	HANDLE hThread1 = (HANDLE)_beginthreadex(NULL,0,Thread1,&hMutex,0,NULL);
	HANDLE hThread2 = (HANDLE)_beginthreadex(NULL,0,Thread2,&hMutex,0,NULL);
	//強迫等待6秒,讓兩個執行緒執行完畢
	Sleep(6000);
	return 0;
}


6.等待執行緒完成

還有一種同步物件,這就是執行緒物件。每個執行緒同樣有激發態和為激發態兩個狀態。

下面面例子綜合演示執行緒物件的同步:

#include <windows.h>
#include <process.h>
#include <stdio.h>

UINT WINAPI Thread1(LPVOID para)
{
	printf("Enter Thread\n");
	//等待5s
	Sleep(5000);
	return 0;
}

int main()
{
	HANDLE hThread[2];
	//開啟兩個執行緒
	hThread[0] = (HANDLE)_beginthreadex(NULL,0,Thread1,NULL,0,NULL);
	hThread[1] = (HANDLE)_beginthreadex(NULL,0,Thread1,NULL,0,NULL);
	//主執行緒等待兩個執行緒結束
	WaitForSingleObject(2,Thread1,TRUE,INFINITE);
}

核心模式下的同步物件


在核心模式下,有一系列的同步物件和使用者模式下的同步物件相對應。在使用者模式下,各個函式都是以控制代碼操作同步物件,而無法獲得真實同步物件的指標。在核心模式下,程式設計師可以獲得真實同步物件的指標。

1.核心模式下的等待

在核心模式下,同樣也有兩個函式負責等待核心同步物件,分別是KeWaitForSingleObject和KeWaitForMultipleObject函式。

KeWaitForSingleObject函式負責等待單個同步物件,其宣告如下:

NTSTATUS
  KeWaitForSingleObject(
                     IN PVOID Object,
		   IN KWAIT_REASON WaitReason, 
		   IN KPROCESSOR_MODE WaitMode, 
		   IN BOOLEAN Alertable, 
		   IN PLARGE_INTEGER Timeout OPTIONAL
		);


第一個引數Object是一個同步物件的指標,注意這裡不是控制代碼。

第二個引數WaitReason表示等待的原因,一般設定為Executive

第三個引數WaitMode是等待模式,說明這個函式是在使用者模式下還是在核心模式下等待。一般設定為KernelMode。

第四個引數Alertable指明等待是否“警惕”,一般設定為FALSE

最後一個引數是等待時間,如果設定為NULL,表示無限期等待。

KeWaitForMultipleObjects負責在核心模式下等待多個同步物件,其宣告如下:

NTSTATUS
  KeWaitForMultipleObjects(
				IN ULONG Count,
				IN PVOID Object[],
				IN WAIT_TYPE WaitType,
				IN KWAIT_REASON WaitReason,
				IN KPROCESSOR_MODE WaitMode, 
				IN BOOLEAN Alertable,
				IN PLARGE_INTEGER Timeout OPTIONAL,
				IN PKWAIT_BLOCK WaitBlockArray OPTIONAL
			);


第一個引數Count表示等待同步物件的個數

第二個引數Object是同步物件陣列

第三個引數WaitType指示等待任意一個同步物件還是等待所有同步物件

剩下的引數和KeWaitForSingle函式引數功能基本一致

2.核心模式下開啟多執行緒

在核心模式下,PsCreateSystemThread負責建立新執行緒。該函式可以建立兩種執行緒,一種是使用者執行緒,一種是系統執行緒。使用者執行緒屬於當前程序中的執行緒。當前程序是指當前I/O操作的發起者。如果在IRP_MJ_READ的派遣函式中呼叫PsCreateSystemThread函式建立使用者執行緒,新執行緒就屬於ReadFile的程序。

系統執行緒不屬於當前使用者程序,而是屬於系統程序,系統程序是作業系統中一個特殊的程序,程序ID一般為4。

驅動程式中的DriverEntry和AddDevice等函式都是被某個系統執行緒呼叫的。

PsCreateSystemThread函式宣告如下:

NTSTATUS
  PsCreateSystemThread(
                     OUT PHANDLE ThreadHandle,
		   IN ULONG DesiredAccess, 
		   IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
		   IN HANDLE ProcessHandle OPTIONAL,
		   OUT PCLIENT_ID ClientId OPTIONAL,
		   IN PKSTART_ROUTINE StartRoutine,
		   IN PVOID StartContext
		);

第一個引數ThreadHandle用於輸出,這個引數得到建立的執行緒控制代碼

第二個引數DesireAccess是建立的許可權

第三個引數ObjectAttributes是該執行緒的屬性,一般設定為NULL

第四個引數ProcessHandle指定建立的是使用者執行緒還是系統執行緒。如果為NULL,則是系統執行緒。如果改值是一個程序控制代碼,則建立的執行緒屬於這個指定的程序。DDK提供的巨集NtGetCurrentProcess函式可以得到當前程序控制代碼。

第六個引數StartRoutine為新執行緒的執行地址

第七個蠶食StartContext為新執行緒接受的引數

在核心模式下,建立執行緒必須使用函式PsTerminateSystemThread強制結束執行緒。否則執行緒無法自動退出。

下面程式碼演示如何在驅動程式中建立執行緒:

VOID SystemThread(IN PVOID pContext)
{
	KdPrint(("Enter SystemThread\n"));
	PEPROCESS pEProcess = GetCurrentProcess();
	PTSTR ProcessName = (PTSTR)((ULONG)pEProcess+0x174);
	KdPrint(("This thread run in %s process\n",ProcessName));
	KdPrint(("Leave SystemThread\n"));
	//結束執行緒
	PsTerminateSystemThread(STATUS_SUCCESS);
}

VOID MyProcessThread(IN PVOID pContext)
{
	KdPrint(("Enter MyProcessThread\n"));
	//得到當前程序
	PEPROCESS pEProcess = IoGetCurrentProcess();
	PTSTR ProcessName = (PTSTR)((ULONG)pEProcess+0x174);
	KdPrint(("This thread run in %s process!\n",ProcessName));

	KdPrint(("Leave MyProcessThread\n"));
	//結束執行緒
	PsTerminateSystemThread(STATUS_SUCCESS);
}

VOID CreateThread_Test()
{
	HANDLE hSystemThread , hMyThread;
	//建立系統執行緒,該執行緒是System程序的執行緒
	NTSTATUS status = PsCreateSystemThread(&SystemThread,0,NULL,NULL,NULL,SystemThread,NULL);
	//建立程序執行緒,該執行緒是使用者程序的執行緒
	status = PsCreateSystemThread(&hMyThread,0,NULL,NtCurrentProcess(),NULL,MyProcessThread,NULL);
}


3.核心模式下的事件物件

在應用程式中,程式設計師只能操作事件控制代碼,無法得到事件物件的指標。

在核心中,用KEVENT資料結構表示一個事件。在使用前需要初始化,核心函式KeInitializeEvent負責對事件物件初始化,其宣告如下:

VOID
 KeInitializeEvent(
                  IN PRKEVENT Event, 
		IN EVENT_TYPE Type, 
		IN BOOLEAN State
	);

第一個引數Event:這個引數是初始化事件物件指標

第二個引數Type:這個引數是事件的型別。事件型別分為兩類:一類是“通知事件”,對應引數是NotificationEvent,另一類是“同步事件”,相應的引數是SynchronizationEvent.

第三個引數State:這個引數如果為真,事件物件的初始化狀態為激發狀態,如果該引數為假,則事件物件的初始化狀態為未激發。

如果建立的事件物件是“通知事件”,當事件物件變為激發態時,程式設計師需要手動將其改回未激發態。如果建立的事件物件是“同步事件”,當事件物件為激發態時,如果遇到KeWaitForXXX等核心函式,事件物件則自動變回為未激發狀態。

下面程式碼演示如何在驅動程式中使用物件事件:

VOID MyProcessThread(IN PVOID pContext)
{
	//獲得時間指標
	PKEVENT pEvent = (PKEVENT)pContext;
	KdPrint(("Enter MyProcessThread\n"));
	//設定事件
	KeSetEvent(pEvent,IO_NO_INCREMENT,FALSE);
	KdPrint(("Leave MyProcessThread\n"));
	//結束執行緒
	PsTerminateSystemThread(STATUS_SUCCESS);
}

#pragma PAGEDCODE
VOID Test()
{
	HANDLE hMyThread;
	KEVENT kEvent;
	//初始化核心事件
	KeInitializeEvent(&kEvent,NotificationEvent,FALSE);
	//建立系統執行緒,該執行緒是System程序的執行緒
	NTSTATUS status = PsCreateSystemThread(&hMyThread,0,NULL,
		NtCurrentProcess(),NULL,MyProcessThread,&kEvent);
	KeWaitForSingleObject(&kEvent,Executive,KernelMode,FALSE,NULL);
}

4.驅動程式與應用程式互動事件物件

應用程式中建立的事件和核心模式中建立的事件物件,本質上是同一個東西。在使用者模式時,它用控制代碼代表,在核心模式時,它用KEVENT代表。

需要解決的第一個問題是如何將使用者模式下建立的事件傳遞給驅動程式。解決辦法是採用DeviceIOControl API函式。在使用者模式下建立一個同步事件,然後用DeviceIOControl把事件控制代碼傳遞給驅動程式。DDK提供了核心函式將控制代碼轉化為指標,該函式是ObReferenceObjectByHandle。

ObReferenceObjectByHandle函式在得到指標的同時,會為物件的指標維護一個計數。每次呼叫ObRefenrenceObjectByHandle會使計數加1。因此為了使計數平衡,在使用Ob後需要呼叫ObDereferenceObject函式,使得計數減一。

下面例子演示如何在應用程式和驅動程式中互動事件物件。

使用者模式程式碼:

int main()
{
	//開啟裝置
	HANDLE hDevice = 
		CreateFile("\\\\.\\HelloDDK",
		            GENERIC_READ | GENERIC_WRITE,
					0,
					NULL,
					OPEN_EXISTING,
					FILE_ATTRIBUTE_NORMAL,
					NULL);
	//判斷裝置是否成功開啟
	if (hDevice == INVALID_HANDLE_VALUE)
	{
		printf("Failed to obtain file handle to device: "
			"&s with Win32 error code: %d\n",
			"MyWDMDevice",GetLastError());
		return 1;
	}
	BOOL bRet;
	DWORD dwOutPut;
	//建立使用者模式同步事件
	HANDLE hEvent = CreateEvent(NULL,FALSE,FALSE,NULL);
	//建立輔助執行緒
	HANDLE hThread = (HANDLE)_beginthreadex(NULL,0,Thread1,&hEvent,0,NULL);
	//將使用者模式的事件控制代碼傳遞給驅動
	bRet = DeviceIoControl(hDevice,IOCTL_TRANSMIT_EVENT,&hEvent,
		sizeof(hEvent),NULL,0,&dwOutPut,NULL);
	//等待執行緒結束
	WaitForSingleObject(hThread1,INFINITE);
	//關閉各個控制代碼
	CloseHandle(hDevice);
	CloseHandle(hThread);
	CloseHandle(hEvent);
	return 0;
}


下面是核心模式的程式碼:

NTSTATUS HelloDDKDeviceIOControl(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp)
{
	NTSTATUS status = STATUS_SUCCESS;
	KdPrint(("Enter HelloDDKDeviceIOControl\n"));
	//獲得當前IO堆疊
	PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
	//獲得輸入引數大小
	ULONG cbin = stack->Parameters.DeviceIoControl.InputBufferLength;
	//獲得輸出引數大小
	ULONG cbout = stack->Parameters.DeviceIoControl.OutputBufferLength;
	//得到IOCTL嘛
	ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;
	ULONG info = 0;
	switch(code)
	{
	case IOCTL_TRANSMIT_EVENT:
		{
			KdPrint(("IOCTL_TEST1\n"));
			//得到應用程式傳遞進來的事件
			HANDLE hUserEvent = *(HANDLE)pIrp->AssociatedIrp.SystemBuffer;
			PKEVENT pEvent;
			//由事件控制代碼得到核心事件資料結構
			status = ObReferenceObjectByHandle(hUserEvent,EVENT_MODIFY_STATE,*ExEventObjectType,
				KernelMode,(PVOID*)&pEvent,NULL);
			//設定事件
			KeSetEvent(pEvent,IO_NO_INCREMENT,FALSE);
			//減小引用計數
			ObReferenceObject(pEvent);
			break;
		}
	default:
		status = STATUS_INVALID_VARIANT;
	}
	//設定IRP完成狀態
	pIrp->IoStatus.Status = status;
	//設定IRP操作位元組
	pIrp->IoStatus.Information = info;
	//結束IRP請求
	IoCompleteRequest(pIrp,IO_NO_INCREMENT);
	KdPrint(("Leave HelloDDKDeviceControl\n"));
	return status;
}

5.驅動程式與驅動程式互動事件物件

要讓驅動程式A獲取驅動程式B中建立的事件物件,最簡單的方法是驅動程式B建立一個有“名字”的事件物件,這樣驅動程式A就可以根據“名字”尋找到事件物件的指標。

建立有名的事件可以通過IoCreateNotificationEvent和IoCreateSynchronizationEvent核心函式,一個建立“通知物件”,一個建立“同步物件”。

6.核心模式下的訊號燈

和事件物件一樣,訊號燈在使用者模式和核心模式下是完全統一的,只不過操作方式不同。在使用者模式下,訊號燈通過控制代碼代表,在核心模式下,訊號燈物件用KSEMAPHORE資料結構表示。

在使用訊號燈前需要對其進行初始化,其函式宣告如下:

VOID 
  KeInitializeSemaphore(
                   IN PRKSEMAPHORE Semaphore, 
		 IN LONG Count, 
		 IN LONG Limit
	);


第一個引數Semaphore:這個引數獲得核心訊號燈物件指標

第二個引數Count:這個引數是初始化時的訊號燈計數

第三個Limit:這個引數指明訊號燈計數的上限值

KeReadStateSemaphore函式可以讀取訊號燈當前的計數

下面程式碼演示如何在驅動程式中使用訊號燈物件:

VOID MyProcessThread(IN PVOID pContext)
{
	//得到訊號燈
	PKSEMAPHORE pkSemaphore = (PKSEMAPHORE)pContext;
	KdPrint(("Enter MyProcessThread\n"));
	KeReleaseSemaphore(pkSemaphore,IO_NO_INCREMENT,1,FALSE);
	KdPrint(("Leave MyProcessThread\n"));
	//結束執行緒
	PsTerminateSystemThread(STATUS_SUCCESS);
}

#pragma PAGEDCODE
VOID Test()
{
	HANDLE hMyThread;
	KSEMAPHORE kSemaphore;
	//初始化核心訊號燈
	KeInitializeSemaphore(&kSemaphore,2,2);
	//讀取訊號燈狀態
	LONG count = KeReadStateSemaphore(&kSemaphore);
	KdPrint(("The Semaphore count is %d\n",count));
	//等待訊號燈
	KeWaitForSingleObject(&kSemaphore,Executive,KernelMode,FALSE,NULL);
	//讀取訊號燈的狀態
	count = KeReadStateSemaphore(&kSemaphore);
	KdPrint(("The Semaphore count is %d\n",count));
	KeWaitForSingleObject(&kSemaphore,Executive,KernelMode,FALSE,NULL);
	//讀取訊號燈的狀態
	count = KeReadStateSemaphore(&kSemaphore);
	KdPrint(("The Semaphore count is %d\n",count));
	//建立系統執行緒,該執行緒是System程序的執行緒
	NTSTATUS status = PsCreateSystemThread(*hMyThread,0,NULL,NtCurrentProcess(),
		NULL,MyProcessThread,&kSemaphore);

	WaitForSingleObject(&kSemaphore,Executive,KernelMode,FALSE,NULL);
	KdPrint(("After KeWaitForSingleObject\n"));
}

7.核心模式下的互斥體

互斥體在核心中的資料結構是KMUTEX,使用前需要初始化互斥體物件。可以使用KeInitializeMutex核心函式初始化互斥體物件,其宣告如下:

VOID 
   KeInitializeMutex( 
            IN PRKMUTEX Mutex, 
	   IN ULONG Level
	);

第一個引數Mutex:這個引數可以獲得核心互斥體物件的指標

第二個引數Level:保留至,一般設定為0

下面例子演示如何在驅動程式中使用互斥體物件:

VOID MyProcessThread1(IN PVOID pContext)
{
	PKMUTEX pkMutex = (PKMUTEX)pContext;
	//獲得互斥體
	KeWaitForSingleObject(pkMutex,Executive,KernelMode,FALSE,NULL);
	KdPrint(("Enter MyProcessThread1\n"));
	//強迫停止50ms,模擬執行一段程式碼,模擬執行某段費時
	KeStallExecutionProcessor(50);
	KdPrint(("Leave MyProcessThread1\n"));
	//釋放互斥體
	KeReleaseMutex(pkMutex,FALSE);
	//結束執行緒
	PsTerminateSystemThread(STATUS_SUCCESS);
}

VOID MyProcessThread2(IN PVOID pContext)
{
	PKMUTEX pkMutex = (PKMUTEX)pContext;
	//獲得互斥體
	KeWaitForSingleObject(pkMutex,Executive,KernelMode,FALSE,NULL);
	KdPrint(("Leave MyProcessThread2\n"));
	//強迫停止50ms,模擬執行一段程式碼,模擬執行某段費時
	KdPrint(("Leave MyProcessThread2\n"));
	//釋放互斥體
	KeReleaseMutex(pkMutex,FALSE);
	//結束執行緒
	PsTerminateSystemThread(STATUS_SUCCESS);
}

#pragma PAGEDCODE
VOID Test()
{
	HANDLE hMyThread1,hMyThread2;
	KMUTEX kMutex;
	//初始化核心互斥體
	KeInitializeMutex(&kMutex,0);
	//建立系統執行緒,該執行緒是程序的執行緒
	PsCreateSystemThread(&hMyThread1,0,NULL,NtCurrentProcess(),
		NULL,MyProcessThread1,&kMutex);
	PsCreateSystemThread(&hMyThread2,0,NULL,NtCurrentProcess(),
		NULL,MyProcessThread2,&kMutex);
	PVOID Pointer_Array[2];
	//得到物件指標
	ObReferenceObjectByHandle(hMyThread1,0,NULL,KernelMode,&Pointer_Array[0],NULL);
	ObReferenceObjectByHandle(hMyThread2,0,NULL,KernelMode,&Pointer_Array[1],NULL);
	//等待多個物件
	KeWaitForMultipleObjects(2,Pointer_Array,WaitAll,Executive,KernelMode,FALSE,NULL,NULL);
	//減小計數器
	ObDereferenceObject(Pointer_Array[0]);
	ObDereferenceObject(Pointer_Array[1]);
	KdPrint(("After KeWaitForMultipleObjects\n"));
}