现在的驱动大多都是分层的,既然是分层驱动,那么调用下层驱动就无可避免。调用下层驱动可以分为同步和异步两种。
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。
看看输出结果:
第一个线程花了6秒才返回(这是因为第一个线程的IoStartPacket是同步的),第二个线程马上返回。看看驱动的输出:
从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。