Windows驱动开发WDM (11)- 多个设备对象(同一个驱动)

通常在WDM驱动的AddDevice里面只会调用一次IoCreateDevice创建一个设备对象。其实我们也可以调用多次IoCreateDevice来创建多个设备对象。当驱动调用IoCreateDevice成功后,驱动对象DriverObject的DeviceObject指针会指向新创建的设备对象,这个设备对象的NextDevice=NULL。如果再调用一次IoCreateDevice,那么DriverObject::DeviceObject指向新创建的设备对象,然后新创建的设备对象的NextDevice指向前面创建的设备对象。总体来讲,DriverObject::DeviceObject永远指向IoCreateDevice新创建的设备对象,而新创建的设备对象(DeviceObject::NextDevice)会指向前面一次创建的设备对象(如果没有,那么就是NULL)。

 

写个例子试试看,首先增加一个创建设备对象的函数:

NTSTATUS CreateFDO(IN PDRIVER_OBJECT DriverObject, IN PDEVICE_OBJECT pdo, 
				   IN const wchar_t* pszDeviceName, IN const wchar_t* pszSymbolicLink)
{
	NTSTATUS status;
	PDEVICE_OBJECT fdo;
	UNICODE_STRING devName;
	RtlInitUnicodeString(&devName,pszDeviceName);//设备名称,设备名称只能被内核模式下的其他驱动所识别。

	//创建FDO(Function Device Object)
	status = IoCreateDevice(
		DriverObject,
		sizeof(DEVICE_EXTENSION),
		&(UNICODE_STRING)devName,
		FILE_DEVICE_UNKNOWN,
		0,
		FALSE,
		&fdo);
	if( !NT_SUCCESS(status))
		return status;

	KdPrint(("CreateFDO\n"));

	PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)fdo->DeviceExtension;
	pdx->fdo = fdo;
	//将FDO附加在PDO上面,并且将Extension中的NextStackDevice指向FDO的下层设备。如果PDO上面有过滤驱动的话,NextStackDevice就是过滤驱动,如果没有就是PDO。
	pdx->NextStackDevice = IoAttachDeviceToDeviceStack(fdo, pdo);
	//创建一个缓冲,用来模拟文件。
	pdx->buffer = (char*)ExAllocatePool(NonPagedPool, MAX_FILE_LEN);
	pdx->filelen = 0;
	//创建一个互斥对象
	KeInitializeMutex(&pdx->myMutex, 0);

	UNICODE_STRING symLinkName;
	//创建链接符号,这样用户模式的应用就可以访问此设备。内核模式下,符号链接是以\??\开头的(或者\DosDevices\)。用户模式下则是\\.\开头。
	//这里就可以在用户模式下用\\.\HelloWDM来访问本设备。
	RtlInitUnicodeString(&symLinkName,pszSymbolicLink);

	pdx->ustrDeviceName = devName;
	pdx->ustrSymLinkName = symLinkName;
	status = IoCreateSymbolicLink(&(UNICODE_STRING)symLinkName,&(UNICODE_STRING)devName);

	if( !NT_SUCCESS(status))
	{
		IoDeleteSymbolicLink(&pdx->ustrSymLinkName);
		status = IoCreateSymbolicLink(&symLinkName,&devName);
		if( !NT_SUCCESS(status))
		{
			return status;
		}
	}

	fdo->Flags |= DO_BUFFERED_IO | DO_POWER_PAGABLE;//DO_BUFFERED_IO,定义为“缓冲内存设备”
	fdo->Flags &= ~DO_DEVICE_INITIALIZING;//将Flag上的DO_DEVICE_INITIALIZING位清零,保证设备初始化完毕,必须的。

	return STATUS_SUCCESS;
}

这个函数会创建一个新的设备对象。看看AddDevice函数:

NTSTATUS HelloWDMAddDevice(IN PDRIVER_OBJECT DriverObject,
                           IN PDEVICE_OBJECT PhysicalDeviceObject)
//DriverObject就是指向本驱动程序的一个对象,是由PNP管理器传递进来的。
//PhysicalDeviceObject是PNP管理器传递进来的底层驱动设备对象,这个东西在NT驱动中是没有的。通常称之为PDO,确切的说是由总线驱动创建的。
{ 
	PAGED_CODE();
	KdPrint(("Enter HelloWDMAddDevice\n"));

	NTSTATUS status = CreateFDO(DriverObject, PhysicalDeviceObject, L"\\Device\\MyWDMDevice", L"\\DosDevices\\HelloWDM");
	if (status == STATUS_SUCCESS)
	{
		status = CreateFDO(DriverObject, PhysicalDeviceObject, L"\\Device\\MyWDMDevice2", L"\\DosDevices\\HelloWDM2");

		if (status == STATUS_SUCCESS)
		{
			status = CreateFDO(DriverObject, PhysicalDeviceObject, L"\\Device\\MyWDMDevice3", L"\\DosDevices\\HelloWDM3");
		}
	}
	
	KdPrint(("Leave HelloWDMAddDevice\n"));
	return status;
}

很简单,就是创建3个不同名字的设备对象。然后在DeviceIoControl派遣函数那里打印一下,比如:

PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)fdo->DeviceExtension;
			 KdPrint(("start to call IoStartPacket, IRP: 0x%x, fdo: %x, NextStackDevice: %x, NextDevice: %x\n", Irp, fdo, pdx->NextStackDevice, fdo->NextDevice));

			 PDEVICE_OBJECT pFdo = fdo->DriverObject->DeviceObject;
			 do 
			 {
				pdx = (PDEVICE_EXTENSION)pFdo->DeviceExtension;
				KdPrint(("Device: %x, Device name: %ws\n", pFdo, pdx->ustrDeviceName.Buffer));
				pFdo = pFdo->NextDevice;
			 } while (pFdo != NULL);

首先打印DeviceIoControl派遣函数传进来的设备对象,然后根据DriverObject::DevicObject和NextDevice打印整个设备链。看看测试代码:

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

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

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

void Test(void* pParam)
{
	int index = (int)pParam;
	//设置overlapped标志,表示异步打开
	HANDLE hDevice;
	if (index == 1)
	{
		hDevice = CreateFile(DEVICE_NAME2,GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,NULL,OPEN_EXISTING,
							FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,NULL);
		wprintf(L"CreateFile, name: %s, ret: %x\n", DEVICE_NAME2, hDevice);
	}
	else
	{
		hDevice = CreateFile(DEVICE_NAME,GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,NULL,OPEN_EXISTING,
			FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,NULL);

		wprintf(L"CreateFile, name: %s, ret: %x\n", DEVICE_NAME, hDevice);
	}
	

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

		DWORD dwStart = GetTickCount();

		printf("input buffer: %s\n", inbuf);
		
		OVERLAPPED ol = {0};
		ol.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);

		BOOL b = DeviceIoControl(hDevice, CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_IN_DIRECT, FILE_ANY_ACCESS), 
			inbuf, strlen(inbuf), outbuf, 100, &dwBytes, &ol);

		printf("DeviceIoControl thread %d, returns %d, last error: %d, used: %d ms, input: %s\n", index, b, GetLastError(), GetTickCount() - dwStart, inbuf);


		WaitForSingleObject(ol.hEvent, INFINITE);

		DWORD dwEnd = GetTickCount();
		//将输出buffer的数据和'm'亦或,看看是否能够得到初始的字符串。
		for (int i = 0; i < strlen(inbuf); i++)
		{
			outbuf[i] = outbuf[i] ^ 'm';
		}

		printf("Verify thread %d, outbuf: %s, used: %d ms\n", index, outbuf, dwEnd - dwStart);

		CloseHandle(hDevice);

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

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

	printf("Test ends\n");
	return 0;
}

就是创建2个线程,然后打开2个设备。看看结果:
 测试代码打开了设备1和设备2,然后从log里面可以看到DeviceIoControl派遣函数传进来的设备对象是设备3,挺奇怪的,不知道为什么,有待研究。

然后从DriverObject::DeviceObject可以打印整个设备链,从log里面可以看到3个设备。画图描述一下设备链:


Windows驱动开发WDM (11)- 多个设备对象(同一个驱动)_第1张图片

 小结:一个驱动可以创建多个设备对象(有没有必要那是另外一码事),然后通过DriverObject::DeviceObject和DeviceObject::NextDevice可以遍历整个设备链。

代码:http://download.csdn.net/detail/zj510/4887669

DDK编译驱动,VS2008编译测试代码。

 提高:

又做了个测试,打印IoAttachDeviceToDeviceStack的返回值。

pdx->NextStackDevice = IoAttachDeviceToDeviceStack(fdo, pdo);

每次attach设备的时候,都是attach到pdo上,那么三次attach返回值一样吗?打印代码:

			 PDEVICE_OBJECT pFdo = fdo->DriverObject->DeviceObject;
			 do 
			 {
				pdx = (PDEVICE_EXTENSION)pFdo->DeviceExtension;
				KdPrint(("Device: %x, Device name: %ws, PDX::NextStackDevice: %x\n", pFdo, pdx->ustrDeviceName.Buffer, pdx->NextStackDevice));
				pFdo = pFdo->NextDevice;
			 } while (pFdo != NULL);

就是从设备扩展里面把NextStackDevice打印出来,看看结果:
Windows驱动开发WDM (11)- 多个设备对象(同一个驱动)_第2张图片 从不同的颜色下划线可以看到,设备3的NextStackDevice是设备2,设备2的NextStackDevice是设备1, 那么设备1的NextStackDevice是什么呢?应该是PDO吧。

这个例子里面设备对象的创建顺序是设备1,设备2,设备3.当创建设备1的时候,PDO上面没有任何东西,那么设备1的NextStackDevice就指向PDO,当创建设备2的时候,pdo上面已经附加了设备1,那么IoAttachDeviceToDeviceStack就将设备2附加到了设备1上,设备3就附加到了设备2上面。我觉得这个地方应该是个垂直结构,这个设备栈的结构是:设备3-》设备2-》设备1-》PDO,其中设备3是栈顶。那么现在也可以解释上面的一个问题了(黑体字),为什么测试代码打开了设备1和设备2,结果派遣函数里面的设备对象居然是设备3.原因就是I/O管理器会把IRP请求送给栈顶的设备,也就是设备3,然后各个设备对象可以依次往下传递。

 这个例子只是为了测试IoCreateDevice和IoAttachDeviceToDeviceStack,真正的工作中,应该很少会在AddDevice里面创建多个设备对象的吧。

附:

用DriverTree来验证一下,截了几个图以供参考:

从上面的图,可以清楚地看到PnpManager创建了一个虚拟的PDO(\Device\0000003c),因为这个驱动是个虚拟的设备,那么虚拟总线驱动就会创建一个虚拟的PDO。然后PDO的Attached Device会指向第一个附在它上面的设备对象,这里是MyWDEDevice,因为这个设备是第一个被驱动程序创建的。

看看MyWDMDevice的截图:

Windows驱动开发WDM (11)- 多个设备对象(同一个驱动)_第3张图片

NextDevice指向0,因为这是驱动创建的第一个设备。然后AttachedDevice是0x82256030,其实他指向MyWDMDevice2,这是驱动创建的第二个设备。看MyWDMDevice2的截图:

Windows驱动开发WDM (11)- 多个设备对象(同一个驱动)_第4张图片

看MyWDMDevice2的NextDevice指向MyWDEDevice,AttachedDevice指向MyWDMDevice3.看看MyWDMDevice3,正确的情况应该是NextDevice指向MyWDMDevice2(0x82256030),然后AttachedDevice指向0.看结果:

完全正确。再复习一下:

1. DeviceObject::NextDevie指向同一个驱动的前一次IoCreateDevice创建的设备对象。

2. DriverObject::DeviveObject指向最后一次IoCreateDevice创建的设备对象。通过DriverObject::DeviceObject和DeviceObject::NextDevice可以找到同一个驱动创建的对象(一个对象链)

3. DeviceObject::AttachedObject指向附在当前设备对象上面的设备对象。

你可能感兴趣的:(Windows驱动开发WDM (11)- 多个设备对象(同一个驱动))