Windows驱动开发WDM (15)- 完成例程

现在的驱动大多都是分层的,既然是分层驱动,那么调用下层驱动就无可避免。调用下层驱动可以分为同步和异步两种。

1. 下层驱动同步完成irp,那么IoCallDriver返回的时候irp已经完成了。

2. 下层驱动异步完成irp,那么IoCallDriver返回的时候irp还没有完成,irp的状态是pending。

针对第二种情况,因为IoCallDriver直接返回了,那么过滤驱动怎么知道下层驱动什么时候完成了irp呢?哈哈,该轮到完成例程出场了。

 

使用完成例程需要2个步骤:

1. 增加一个完成例程,原型如下:

NTSTATUS MyIoCompletion(IN PDEVICE_OBJECT fdo, IN PIRP irp, IN PVOID context)

2. 在调用下层驱动前,调用IoSetCompletionRoutine,如:

IoSetCompletionRoutine(Irp, MyIoCompletion, NULL, TRUE, TRUE, TRUE);

 

将前面的例子http://blog.csdn.net/zj510/article/details/8332658,稍作改动,改动的是过滤驱动。

增加一个完成例程

NTSTATUS MyIoCompletion(IN PDEVICE_OBJECT fdo, IN PIRP irp, IN PVOID context)
{
	KdPrint(("Enter MyIoCompletion\n"));

	if(irp->PendingReturned)
	{
		IoMarkIrpPending(irp);
	}

	KdPrint(("Leave MyIoCompletion\n"));

	return STATUS_SUCCESS;
}

这个函数啥也没做,就打2个log。

 

设置完成例程

NTSTATUS HelloWDMIOControl(IN PDEVICE_OBJECT fdo, IN PIRP Irp)
{
	KdPrint(("EX 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:
		{			 
			PDEVICE_OBJECT pFdo = fdo->DriverObject->DeviceObject;
			do 
			{
				pdx = (PDEVICE_EXTENSION)pFdo->DeviceExtension;
				KdPrint(("EX Device: %x, PDX::NextStackDevice: %x\n", pFdo, pdx->NextStackDevice));
				pFdo = pFdo->NextDevice;
			} while (pFdo != NULL);

			//过滤驱动里面修改一个输入缓冲的数据。
			char* inBuf = (char*)Irp->AssociatedIrp.SystemBuffer;
			inBuf[0] = 'x';

			KdPrint(("Try to call lower driver Irp: %x\n", Irp));
			pdx = (PDEVICE_EXTENSION)fdo->DeviceExtension;
			IoCopyCurrentIrpStackLocationToNext(Irp);

			IoSetCompletionRoutine(Irp, MyIoCompletion, NULL, TRUE, TRUE, TRUE);

			status = IoCallDriver(pdx->NextStackDevice, Irp);

			KdPrint(("Finished calling lower driver, Irp: %x", Irp));
		}
		break;
	default:
		IoSkipCurrentIrpStackLocation(Irp);
		status = IoCallDriver(pdx->NextStackDevice, Irp);
		break;
	}

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

其它的代码跟http://blog.csdn.net/zj510/article/details/8332658里面的例子一模一样,只是增加一行代码:

IoSetCompletionRoutine(Irp, MyIoCompletion, NULL, TRUE, TRUE, TRUE);

这行代码的作用就是给该irp设置完成例程。

 

测试代码

// 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"
#define DEVICE_EX L"\\\\.\\HelloWDM_EX"

void Test(void* pParam)
{
	int index = (int)pParam;
	//设置overlapped标志,表示异步打开
	HANDLE hDevice;
	
	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 t1 = (HANDLE)_beginthread(Test, 0, (void*)0);
	_beginthread(Test, 0, (void*)1);

	WaitForSingleObject(t1, INFINITE);

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

测试代码就起2个线程,每个线程发送一个irp。

看看输出结果:

Windows驱动开发WDM (15)- 完成例程_第1张图片

第一个线程花了6秒才返回(这是因为第一个线程的IoStartPacket是同步的),第二个线程马上返回。看看驱动的输出:

Windows驱动开发WDM (15)- 完成例程_第2张图片
从log里面可以看出请求1的IoCallDriver花了6秒,这是因为请求1的IoStartPacket花了6秒才返回,请求2的IoCallDriver马上返回,因为请求2的IoStartPacket只是将irp放入StartIo的队列,马上返回的。那么完成例程是什么时候被调用的呢?请求1的完成例程是在3s时间点被调用的,因为请求1在3s的时候就完成了。请求2的完成例程是在6s时间点完成的。对于请求2,IoCallDriver马上返回,这个时候irp并没有完成还是pending的,那么我们就可以从完成例程知道什么时候irp被完成了。完成例程被调用的时候就说明下层驱动已经完成了irp。

IRP的控制权

当驱动调用IoCallDriver之后,当前驱动就失去了对irp的控制权。如果再去设置irp的属性,将会崩溃。完成例程有2个返回值:STATUS_SUCCESS和STATUS_MORE_PROCESSING_REQUIRED。如果返回STATUS_SUCCESS,那么当IoCallDriver返回后,当前驱动不会再获得irp的控制权。如果返回STATUS_MORE_PROCESSING_REQUIRED,驱动将重新获得irp的控制权,当前驱动可以对irp进行再次操作。而且当前驱动必须对irp调用IoCompleteRequest或者继续发给下层驱动。这个例子我们返回了STATUS_SUCCESS,其实这个时候完成例程只是起一个通知的作用,因为驱动无法更改irp的属性。

 

传播Pending位

每当低级驱动完成IRP后,将IRP的堆栈向上回卷时,底层IO堆栈中Control域的SL_PENDING_RETURNED位必须被传播到上一层。如果没有完成例程,这种传播是自动的,我们无需关心;如果有完成例程,则这种传播需要我们实现(从《Windows驱动开发技术详解》抄来的)。怎么实现呢?就是完成例程里面的这段代码:

	if(irp->PendingReturned)
	{
		IoMarkIrpPending(irp);
	}

其实我不是很懂这段代码,但是看起来好像每个完成例程里面都有这段代码。

OK,一个简单的完成例程的例子,这里只是作为一个通知用(返回STATUS_SUCCESS)。下次再尝试IoCallDriver返回后再改变IRP的某些属性。

 

注意:如果使用完成例程的话,那么就不能跳过本层I/O堆栈了,不可以使用IoSkipCurrentIrpStackLocation,而需要使用IoCopyCurrentIrpStackLocationToNext将I/O堆栈复制到下一层。如果跳过的话,当IoCompleteRequest被下层驱动调用的时候,IRP回卷的时候,恐怕就找不到当前驱动的I/O堆栈了。不知道会发生什么事,起码完成例程不会被调用了吧,搞不好还会crash。

你可能感兴趣的:(Windows驱动开发WDM (15)- 完成例程)