天天看點

Windows驅動開發WDM (8)- 核心同步對象

隻要寫過多線程應用的程式員都知道,多線程通路公共資源的時候需要同步。在使用者模式下,經常使用事件,互斥,信号量等對象來控制公共資源的通路。核心模式下,也是有相應的事件,互斥,信号量等核心對象,還有自旋鎖。如果不是用同步對象進行控制,那麼當多線程通路的時候就會産生一些不可預測的問題了。

不使用同步對象

看下面的代碼:

NTSTATUS Encoding(IN PDEVICE_OBJECT fdo, IN PIRP Irp)
{
	KdPrint(("Start to Encode\n"));
	
	PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);

	//得到輸入緩沖區大小
	ULONG cbin = stack->Parameters.DeviceIoControl.InputBufferLength;

	//得到輸出緩沖區大小
	ULONG cbout = stack->Parameters.DeviceIoControl.OutputBufferLength;

	//擷取輸入緩沖區,IRP_MJ_DEVICE_CONTROL的輸入都是通過buffered io的方式
	char* inBuf = (char*)Irp->AssociatedIrp.SystemBuffer;

	//假如需要将資料放到一個公共資源中,然後再進行操作,比如這裡是亦或編碼,那麼就需要考慮同步的問題。
	//不然在多線程調用的時候,公共資源的通路将會有不可預測的問題。
	PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)fdo->DeviceExtension;

	RtlCopyMemory(pdx->buffer, inBuf, cbin);

	//模拟延時3秒
	KdPrint(("Wait 3s\n"));
	KEVENT event;
	KeInitializeEvent(&event, NotificationEvent, FALSE);
	LARGE_INTEGER timeout;
	timeout.QuadPart = -3 * 1000 * 1000 * 10;//負數表示從現在開始計數,KeWaitForSingleObject的timeout是100ns為機關的。
	KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, &timeout);//等待3秒

	for (ULONG i = 0; i < cbin; i++)//将輸入緩沖區裡面的每個位元組和m亦或
	{
		pdx->buffer[i] = pdx->buffer[i] ^ 'm';
	}

	//擷取輸出緩沖區,這裡使用了直接方式,見CTL_CODE的定義,使用了METHOD_IN_DIRECT。是以需要通過直接方式擷取out buffer
	KdPrint(("user address: %x, this address should be same to user mode addess.\n", MmGetMdlVirtualAddress(Irp->MdlAddress)));
	//擷取核心模式下的位址,這個位址一定> 0x7FFFFFFF,這個位址和上面的使用者模式位址對應同一塊實體記憶體
	char* outBuf = (char*)MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);

	ASSERT(cbout >= cbin);
	RtlCopyMemory(outBuf, pdx->buffer, cbin);


	//完成irp
	Irp->IoStatus.Status = STATUS_SUCCESS;
	Irp->IoStatus.Information = cbin;
	IoCompleteRequest(Irp, IO_NO_INCREMENT);

	KdPrint(("Encode thread finished\n"));

	return Irp->IoStatus.Status;
}

NTSTATUS HelloWDMIOControl(IN PDEVICE_OBJECT fdo, IN PIRP Irp)
{
	KdPrint(("Enter HelloWDMIOControl\n"));

	PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)fdo->DeviceExtension;
	PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);


	//得到IOCTRL碼
	ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;

	NTSTATUS status;
	ULONG info = 0;
	switch (code)
	{
	case IOCTL_ENCODE:
		{
			 status = Encoding(fdo, Irp);//調用編碼函數
		}
		break;
	default:
		status = STATUS_INVALID_VARIANT;
		Irp->IoStatus.Status = status;
		Irp->IoStatus.Information = info;
		IoCompleteRequest(Irp, IO_NO_INCREMENT);
		break;
	}

	KdPrint(("Leave HelloWDMIOControl\n"));
	return status;
}
           

IOCTL_ENCODE的處理函數裡面調用了Encoding函數,這裡就有個問題,假如caller建立多個線程來調用DeviceIoControl函數,那麼Encoding函數就會被并發調用。而Encoding函數裡面使用了pdx->buffer這個公共資源,那麼當多線程并發的時候勢必會出問題。比如這麼caller這麼調用:

// TestWDMDriver.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <windows.h>
#include <process.h>

#define DEVICE_NAME L"\\\\.\\HelloWDM"

void Test(void* pParam)
{
	//設定overlapped标志,表示異步打開
	HANDLE hDevice = CreateFile(DEVICE_NAME,GENERIC_READ | GENERIC_WRITE,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

	if (hDevice != INVALID_HANDLE_VALUE)
	{
		char inbuf[100] = {0};
		sprintf(inbuf, "hello world %d", (int)pParam);
		char outbuf[100] = {0};
		DWORD dwBytes = 0;

		printf("input buffer: %s\n", inbuf);
		DWORD dwStart = GetTickCount();
		BOOL b = DeviceIoControl(hDevice, CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_IN_DIRECT, FILE_ANY_ACCESS), 
			inbuf, strlen(inbuf), outbuf, 100, &dwBytes, NULL);

		//将輸出buffer的資料和'm'亦或,看看是否能夠得到初始的字元串。
		for (int i = 0; i < strlen(inbuf); i++)
		{
			outbuf[i] = outbuf[i] ^ 'm';
		}

		printf("Verify result, outbuf: %s, operated: %d bytes, time used: %d ms\n", outbuf, dwBytes, GetTickCount() - dwStart);

		CloseHandle(hDevice);

	}
	else
		printf("CreateFile failed, err: %x\n", GetLastError());
}

int _tmain(int argc, _TCHAR* argv[])
{
	HANDLE handles[10];
	for (int i = 0; i < 10; i++)
	{
		handles[i] = (HANDLE)_beginthread(Test, 0, (void*)i);
	}
	
	WaitForMultipleObjects(10, handles, TRUE, INFINITE);
	return 0;
}

           

caller建立了10個線程,分别傳入hello world 0,hello world 2 ... hellow world 9。看實際輸出結果:

Windows驅動開發WDM (8)- 核心同步對象

輸入字元串正确,但是輸出就一塌糊塗了,原因就是驅動内部使用了一個公共資源,進而導緻多線程并發的時候出問題。要解決這個問題其實很簡單,就是使用同步對象進行控制。

使用同步對象

這裡以互斥為例子。使用互斥核心對象需要3個函數:

KeInitializeMutex 初始化互斥對象

KeWaitForSingleObject 等待擷取互斥對象,如果互斥對象已經被别的線程擷取了,那麼這個函數就将等待。

KeReleaseMutex 釋放互斥對象。

通常在裝置擴充裡面會加一個互斥對象,然後在AddDevice函數裡面初始化互斥對象。比如:

typedef struct _DEVICE_EXTENSION
{
    PDEVICE_OBJECT fdo;
    PDEVICE_OBJECT NextStackDevice;
	UNICODE_STRING ustrDeviceName;	// 裝置名
	UNICODE_STRING ustrSymLinkName;	// 符号連結名
	char* buffer;
	ULONG filelen;
	KMUTEX myMutex;
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
           
//建立一個互斥對象
	KeInitializeMutex(&pdx->myMutex, 0);
           

然後在通路公共資源前需要擷取互斥對象,通路結束釋放互斥對象,比如(紅色的代碼就是用來控制公共資源的):

NTSTATUS Encoding(IN PDEVICE_OBJECT fdo, IN PIRP Irp)
{
	KdPrint(("Start to Encode\n"));
	
	PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);

	//得到輸入緩沖區大小
	ULONG cbin = stack->Parameters.DeviceIoControl.InputBufferLength;

	//得到輸出緩沖區大小
	ULONG cbout = stack->Parameters.DeviceIoControl.OutputBufferLength;

	//擷取輸入緩沖區,IRP_MJ_DEVICE_CONTROL的輸入都是通過buffered io的方式
	char* inBuf = (char*)Irp->AssociatedIrp.SystemBuffer;

	//假如需要将資料放到一個公共資源中,然後再進行操作,比如這裡是亦或編碼,那麼就需要考慮同步的問題。
	//不然在多線程調用的時候,公共資源的通路将會有不可預測的問題。
	PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)fdo->DeviceExtension;

	KeWaitForSingleObject(&pdx->myMutex, Executive, KernelMode, FALSE, NULL);//等待擷取mutext

	RtlCopyMemory(pdx->buffer, inBuf, cbin);

	//模拟延時3秒
	KdPrint(("Wait 3s\n"));
	KEVENT event;
	KeInitializeEvent(&event, NotificationEvent, FALSE);
	LARGE_INTEGER timeout;
	timeout.QuadPart = -3 * 1000 * 1000 * 10;//負數表示從現在開始計數,KeWaitForSingleObject的timeout是100ns為機關的。
	KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, &timeout);//等待3秒

	for (ULONG i = 0; i < cbin; i++)//将輸入緩沖區裡面的每個位元組和m亦或
	{
		pdx->buffer[i] = pdx->buffer[i] ^ 'm';
	}

	//擷取輸出緩沖區,這裡使用了直接方式,見CTL_CODE的定義,使用了METHOD_IN_DIRECT。是以需要通過直接方式擷取out buffer
	KdPrint(("user address: %x, this address should be same to user mode addess.\n", MmGetMdlVirtualAddress(Irp->MdlAddress)));
	//擷取核心模式下的位址,這個位址一定> 0x7FFFFFFF,這個位址和上面的使用者模式位址對應同一塊實體記憶體
	char* outBuf = (char*)MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);

	ASSERT(cbout >= cbin);
	RtlCopyMemory(outBuf, pdx->buffer, cbin);

	KeReleaseMutex(&pdx->myMutex, FALSE);


	//完成irp
	Irp->IoStatus.Status = STATUS_SUCCESS;
	Irp->IoStatus.Information = cbin;
	IoCompleteRequest(Irp, IO_NO_INCREMENT);

	KdPrint(("Encode thread finished\n"));

	return Irp->IoStatus.Status;
}
           

ok,将pdx->buffer通過互斥量對象給控制起來了,意味着這個資源在某一時刻隻能被一個線程擁有并且使用,那麼也就不存在并發的問題了。看結果:

Windows驅動開發WDM (8)- 核心同步對象

可以正常輸出,搞定。

從上面的截圖可以看到第一個線程等了3秒,而最後一個線程等了30秒。這是因為驅動的處理函數裡面模拟了3秒延時。那3秒延時代碼處在mutex中。而mutex相當于将那段代碼給串行話運作了(一個線程一個線程的運作)。所有最後一個線程需要等待30秒,10 * 3 = 30.

核心模式下驅動的同步對象是作用于所有調用程序的,比方說有2個測試執行個體運作,那麼20個線程會排隊通路pdx->buffer.可以做這麼個實驗,同時運作2個調用例子,可以看到:

Windows驅動開發WDM (8)- 核心同步對象

第二個調用例子的第10個線程等待了差不多60秒。

在多線程環境下,同步對象是經常被用到的,核心模式同使用者模式一樣都需要同步對象。

繼續閱讀