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 编译调用例子
- Windows驱动开发WDM (9)- StartIO例程(串行化处理IRP)
- Windows驱动开发WDM (10)- StartIo取消例程
- Windows驱动开发WDM (16)- 完成例程 (重新获得IRP控制权)
- Windows驱动开发WDM (7)- 异步IRP
- Windows驱动开发WDM (15)- 完成例程
- WDM驱动之IRP处理:取消IRP
- WDM驱动之IRP处理:取消IRP
- Windows驱动开发WDM (13)- 过滤驱动
- Windows驱动开发WDM (13)- 过滤驱动
- Windows驱动开发(9) - IRP结构体
- Windows 7驱动开发系列(四)--WDM模型介绍
- Windows驱动开发WDM (1) - 基本结构
- Windows驱动开发WDM (3)- 设备内存读写方式
- Windows驱动开发WDM (4)- 缓冲区方式例子
- Windows驱动开发WDM (6)- 中断请求级别
- Windows驱动开发WDM (8)- 内核同步对象
- Windows驱动开发WDM (2)- 一个简单的WDM驱动程序
- WINDOWS WDM驱动开发基础
- 网卡的工作模式及iwconfig 使用手册(内容很丰富,有用,转自Openwrt论坛)
- jquery-ajax样例
- C++对象的内存分布和虚函数表
- 搭建版本控制工具SVN
- 对webconfig文件中的数据库连接字符串加密,解密以及常见问题解决方案[打不开 RSA 密钥容器]
- Windows驱动开发WDM (9)- StartIO例程(串行化处理IRP)
- 换分币问题
- iOS开发之自定义pageControl
- java中的int类型和byte数组的相互转换
- JdbcTemplate的用法
- 如何在oracle中查询所有用户表的表名、主键名称、索引、外键等
- dns_util
- hibernate中对象的三种状态
- Sudoku backtracking with one dimension array