现在的驱动大多都是分层的,既然是分层驱动,那么调用下层驱动就无可避免。调用下层驱动可以分为同步和异步两种。
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
#include
#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。