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"));
}



 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章