Windows驱动开发WDM (9)- StartIO例程(串行化处理IRP)

来源:互联网 发布:微赞源码下载百度云 编辑:程序博客网 时间:2024/06/10 16:08

有时候,对设备的操作必须是串行化的而不能并行执行。如果是同步IRP的话,用同步对象就可以搞定了,比如在IRP处理函数的开始获取Mutex,结束前释放Mutex。如果是异步IRP的话,就复杂一些了,我们可以在驱动里面自己维护一个状态和一个列表,比如当状态会空闲的时候就可以处理IRP,并把状态置为“忙”,然后又有一个IRP过来的时候,假设状态是“忙”,那么就将IRP放入列表中,等一个IRP处理结束后,就去处理列表里面的IRP,然后依次处理所有IRP。这样完全可以搞定,但是得费时间维护列表和状态。幸运的是,DDK已经提供了一个内部队列,这样就大大简化了程序员的工作,我们只需要将IRP往这个内部队列丢就可以了,然后通过StartIO例程串行化处理这些IRP请求。

 

StartIO例程

DDK提供的内部队列用KDEVICE_QUEUE数据结构表示,如下(从wdm.h中copy的):

typedef struct _KDEVICE_QUEUE {    CSHORT Type;    CSHORT Size;    LIST_ENTRY DeviceListHead;    KSPIN_LOCK Lock;#if defined(_AMD64_)    union {        BOOLEAN Busy;        struct {            LONG64 Reserved : 8;            LONG64 Hint : 56;        };    };#else    BOOLEAN Busy;#endif} KDEVICE_QUEUE, *PKDEVICE_QUEUE, *PRKDEVICE_QUEUE;

这个队列的列头保存在设备对象的DeviceObject->DeviceQueue里面。插入和删除都是系统搞定,我们无需关心。使用这个队列需要设置StartIo例程,比如:

#pragma INITCODE extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject,IN PUNICODE_STRING pRegistryPath){KdPrint(("Enter DriverEntry\n"));pDriverObject->DriverExtension->AddDevice = HelloWDMAddDevice;pDriverObject->MajorFunction[IRP_MJ_PNP] = HelloWDMPnp;pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = pDriverObject->MajorFunction[IRP_MJ_CREATE] = pDriverObject->MajorFunction[IRP_MJ_READ] = pDriverObject->MajorFunction[IRP_MJ_QUERY_INFORMATION] =pDriverObject->MajorFunction[IRP_MJ_WRITE] = HelloWDMDispatchRoutine;pDriverObject->DriverUnload = HelloWDMUnload;pDriverObject->DriverStartIo = HelloWDMStartIO;KdPrint(("Leave DriverEntry\n"));return STATUS_SUCCESS;}

通过pDriverObject->DriverStartIo = HelloWDMStartIO;设置StartIo例程。给出StartIo例程的实现:

void HelloWDMStartIO(IN PDEVICE_OBJECT fdo, IN PIRP Irp){KdPrint(("++++Enter HelloWDMStartIO, IRP address: 0x%x\n", Irp));KIRQL oldirql;//自旋锁会将当前运行级别提高到DISPATCH_LEVEL,驱动程序如果想调用IoSetCancelRoutine,那么就得先获取这个lock,具体参考//http://msdn.microsoft.com/en-us/library/windows/hardware/ff548196(v=vs.85).aspxIoAcquireCancelSpinLock(&oldirql);//fdo->CurrentIrp就是驱动当前正在处理的IRP。if (Irp != fdo->CurrentIrp || Irp->Cancel){//如果Irp不是当前处理的IRP,或者这个Irp是想取消的,那么直接返回,啥也不做。IoReleaseCancelSpinLock(oldirql);KdPrint(("Do nothing\n"));return;}else{//正在处理该IRPKdPrint(("Forbit to use CancelRoutine\n"));IoSetCancelRoutine(Irp, NULL);//不允许调用取消例程IoReleaseCancelSpinLock(oldirql);}//可以根据需要处理IRP,这里只处理IOCTL_ENCODEEncoding(fdo, Irp);//在队列中读取下一个IRP,并且进行StartIO.IoStartNextPacket(fdo, TRUE);KdPrint(("++++Leave HelloWDMStartIO, IRP address: 0x%x\n", Irp));}

StartIo例程需要注意3个细节:

1. CancelRoutine的处理(下个章节再介绍);

2. 处理请求;

3. 调用IoStartNextPacket处理队列中的其他请求。
 

StartIo里面需要处理请求,比如这里调用Encoding函数。当然可以根据需要在StartIo里面处理IRP请求。Encoding函数的实现:

#pragma LOCKEDCODENTSTATUS Encoding(IN PDEVICE_OBJECT fdo, IN PIRP Irp){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;char temp[100] = {0};RtlCopyMemory(temp, inBuf, cbin);KdPrint(("----Start to Encode string: %s\n", temp));//假如需要将数据放到一个公共资源中,然后再进行操作,比如这里是亦或编码,那么就需要考虑同步的问题。//不然在多线程调用的时候,公共资源的访问将会有不可预测的问题。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 bufferKdPrint(("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);//完成irpIrp->IoStatus.Status = STATUS_SUCCESS;Irp->IoStatus.Information = cbin;IoCompleteRequest(Irp, IO_NO_INCREMENT);KdPrint(("----Encode thread finished, string: %s\n",temp));return Irp->IoStatus.Status;}

这里例子只是将输入的信息和m亦或,然后放到输出buffer。这里使用了直接方式。

需要注意的是IoStartNextPacket函数。专门起一个章节来介绍相关的知识。

 

StartIo例程的递归

使用StartIo例程会接触到2个函数:IoStartPacket和IoStartNextPacket。

IoStartPacket:如果驱动不是“忙”状态,那么会调用StartIo例程;如果是“忙”状态那么就放入队列,立刻返回。MSDN:http://msdn.microsoft.com/en-us/library/windows/hardware/ff550370(v=vs.85).aspx

If the driver is already busy processing a request for the target device object, then the packet is queued in the device queue. Otherwise, this routine calls the driver'sStartIo routine with the specified IRP.

IoStartNextPacket: 如果队列中有请求需要处理的话,IoStartNextPacket会取出一个请求,调用StartIo例程。如果没有请求了,那么这个函数就直接返回。

从StartIo例程的实现里面可以看到,StartIo例程的最后会调用IoStartNextPacket函数,那么当队列里面还有请求的话,StartIo将会被递归调用。为了搞清楚这个递归过程,我画了个流程图:

从上图可以看到StartIo是个递归调用,直到队列里面所有的请求都处理完才返回。比方说:有3个IRP请求,每个请求会花费3秒钟。驱动在1秒内收到了这3个请求(假设是A,B,C三个请求,先后顺序是A,B,C)。那么StartIo的调用过程就是:

A的IoStartPacket函数要等处理完A,B,C三个请求才返回。而B和C的IoStartPacket函数将把IRP放入队列,然后马上返回。看一下DeviceIoControl的派遣函数:

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 = STATUS_PENDING; Irp->IoStatus.Status = status; Irp->IoStatus.Information = info; IoMarkIrpPending(Irp);//将IRP设置为挂起状态(异步IRP) /*********************************************************************************************************  调用IoStartPacket,IoStartPacket会调用驱动的StartIo或者将IRP放入内部队列(假如驱动状态是“忙”).  当驱动不是“忙”状态的时候,IoStartPacket将会调用StartIo例程。StartIo例程其实是递归操作,那么假如StartIo例程还没有结束的时候,  而这个时候又有caller请求过来,那么StartIo例程将进入递归调用。也就是说需要等Device Queue里面所有的请求全部处理完,StartIo例程  才返回。假如有3个请求,第一个请求将直接被执行,第二个第三个请求在第一个请求执行期间到达,也就是说第二个第三个请求将会被放在queue  里面,当第一个请求执行完毕的时候,StartIo例程会调用IoStartNextPacket处理队列,也就是说StartIo例程会递归2次来处理#2,#3请求。  等队列里面所有请求处理完毕,StartIo例程才一个个返回。那么第一个请求的StartIo例程相当于需要3x时间,第二个请求对应的StartIo例程需要  2x时间,第三个请求的StartIo需要1x时间。***********************************************************************************************************/ KdPrint(("start to call IoStartPacket, IRP: 0x%x\n", Irp)); IoStartPacket(fdo, Irp, 0, HelloWDMOnCancelIRP); KdPrint(("end call IoStartPacket, IRP: 0x%x\n", 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请求,只是调用了IoStartPacket函数。

根据这个例子的代码,A的IoStartPacket将会花费9秒钟,而B和C的IoStartPacket将立刻返回,并且IRP的status是pengding。A的DeviceIoControl派遣函数先是将IRP设置成了pending,然后调用IoStartPacket,IoStartPacket又调用了StartIo例程,StartIo例程里面完成了这个IRP,所有当A的HelloWDMIOControl返回的时候IRP状态是完成。而B和C 则与A不同,因为当B和C的HelloWDMIOControl被调用的时候,驱动正在处理A的请求,那么B和C的IoStartPacket只是将IRP放入队列,马上返回了。所以B和C的DeviceIoControl立刻返回了,并且IRP状态是pending。

 

调用例子

写了个例子,总共起了5个线程(0-4),

// 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, FILE_SHARE_READ | FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,NULL);if (hDevice != INVALID_HANDLE_VALUE){char inbuf[100] = {0};sprintf(inbuf, "hello world %d", (int)pParam);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 returns %d, last error: %d, used: %d ms, input: %s\n", 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 result, outbuf: %s, used: %d ms\n", outbuf, dwEnd - dwStart);CloseHandle(hDevice);}elseprintf("CreateFile failed, err: %x\n", GetLastError());}int _tmain(int argc, _TCHAR* argv[]){HANDLE handles[5];for (int i = 0; i < 5; i++){handles[i] = (HANDLE)_beginthread(Test, 0, (void*)i);Sleep(100);}WaitForMultipleObjects(3, handles, TRUE, INFINITE);return 0;}

线程0的DeviceIoControl的派遣函数里面IoStartPacket将会直接调用StartIo例程(3秒钟)。线程1-4的请求将在驱动处理线程0的请求期间到达。那么线程1-4的DeviceIoControl将会立即返回,并且LastError是pending。而线程0的DeviceIoControl将会在5个线程的请求全部处理完才返回,而且状态是成功。5个IRP的完成时间点应该是:

3秒,6秒,9秒,12秒,15秒。

但是线程0的IRP虽说在3秒的时候已经完成了,但是因为DeviceIoControl还没有返回(它对应的派遣函数还在处理其他的请求),所以线程0一直被堵在那里,知道线程4完成后,StartIo例程才递归返回。那么线程0应该和线程4同时得到结果。看运行结果:

线程1-4立刻返回了,并且lasterror是997,997就是指:Overlapped I/O operation is in progress.而线程0的DeviceIoControl花了15秒才返回。同时可以看线程0和线程4的请求几乎同时完成,都是15秒。线程4还比线程0稍微快了一点点,因为线程4的StartIo完成后,才一个个递归返回,然后线程0的DeviceIoControl返回,接着WaitForSingleObject得到信号(其实这个信号3秒的时候就有了,只是因为堵在DeviceIoControl那里,没有去接收而已)。

 

总结

StartIo例程就是将IRP请求串行化,一个一个排队执行,避免了并发情况下的种种问题。注意上例中的DeviceIoControl派遣函数有可能是并发的,比如Caller创建几个线程同时调用DeviceIoControl。驱动的DeviceIoControl派遣函数可能是并发的,然后通过IoStartPacket函数可以将IRP串行化(如果驱动不忙,则直接调用StartIo例程,如果驱动忙则丢进队列,驱动会将这些IRP一个一个处理掉)。IoStartPacket内部有锁,IoStartPacket被并发调用的时候,锁会帮助IoStartPacket的内部实现被串行处理。从而使得StartIo可以串行处理所有IRP请求。另外需要注意两点:

1. StartIo例程运行在DISPATCH_LEVEL,那么就不可以使用分页内存;

2. StartIo的递归调用需要注意。

 

所有代码:http://download.csdn.net/detail/zj510/4877576

DDK 编译驱动

vs2008 编译调用例子