《Windows驱动开发技术详解》之自定义StartIO
- 自定义StartIO
系统定义的StartIO队列只能使用一个队列(DDK提供的StartIO内部只有一个队列),这个队列将所有的IRP进行处理化。例如,读、写操作都会混在一起进行串行处理。然而,有时需要读、写分别进行串行化处理。这就需要自定义StartIO例程。当然,程序员需要自己去维护这个IRP队列。
当使用StartIO时,程序员不用关心队列的“入队”和“出队”操作,这些都由操作系统自动负责。但是如果使用自定义的StartIO,程序员需要自己负责“入队”和“出队”操作。
虽说队列的个数是由程序员自己定义的,但是队列的数据结构以及队列中元素的数据结构还要使用DDK提供的结构。其中,存储队列的结构如下:
1 typedef struct _KDEVICE_QUEUE { 2 CSHORT Type; 3 CSHORT Size; 4 LIST_ENTRY DeviceListHead; 5 KSPIN_LOCK Lock; 6 7 #if defined(_AMD64_) 8 9 union { 10 BOOLEAN Busy; 11 struct { 12 LONG64 Reserved : 8; 13 LONG64 Hint : 56; 14 }; 15 }; 16 17 #else 18 19 BOOLEAN Busy; 20 21 #endif 22 23 } KDEVICE_QUEUE, *PKDEVICE_QUEUE, *PRKDEVICE_QUEUE;
存储队列中每个元素的结构如下:
1 typedef struct _KDEVICE_QUEUE_ENTRY { 2 LIST_ENTRY DeviceListEntry; 3 ULONG SortKey; 4 BOOLEAN Inserted; 5 } KDEVICE_QUEUE_ENTRY, *PKDEVICE_QUEUE_ENTRY, *PRKDEVICE_QUEUE_ENTRY;
如果要插如一个IRP进队列或者从队列移除一个IRP,使用内核函数:KeInsertDeviceQueue和KeRemoveDeviceQueue。
这里值得注意的是KeInsertDeviceQueue的返回值。如果设备不忙,则可以直接处理IRP,因此这时候不需要插入队列,返回值为FALSE。如果设备正在处理,这时候需要将IRP插入队列,这时候会返回TRUE。
示例代码如下:
派遣函数代码如下:
1 NTSTATUS HelloDDKDispatchRead_MyStart(PDEVICE_OBJECT pDevObj, PIRP pIrp){ 2 DbgPrint("Enter HelloDDKDispatchRead_MyStart!\n"); 3 PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) 4 pDevObj->DeviceExtension; 5 IoMarkIrpPending(pIrp); 6 KIRQL oldirql; 7 KeRaiseIrql(DISPATCH_LEVEL, &oldirql); 8 if (!KeInsertDeviceQueue(&pDevExt->device_queue_read, &pIrp->Tail.Overlay.DeviceQueueEntry)) 9 MyStartIO(pDevObj, pIrp); 10 KeLowerIrql(oldirql); 11 DbgPrint("Leave HelloDDKDispatchRead_MyStart!\n"); 12 return STATUS_PENDING; 13 }
自定义StartIO代码如下:
1 VOID MyStartIO(PDEVICE_OBJECT DeviceObject, PIRP pFirstIrp){ 2 DbgPrint("Enter MyStartIO!\n"); 3 PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) 4 DeviceObject->DeviceExtension; 5 PKDEVICE_QUEUE_ENTRY device_entry; 6 PIRP pIrp = pFirstIrp; 7 do{ 8 KEVENT event; 9 KeInitializeEvent(&event, NotificationEvent, FALSE); 10 LARGE_INTEGER timeout; 11 timeout.QuadPart = -3 * 1000 * 1000 * 10; 12 KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, &timeout); 13 14 pIrp->IoStatus.Status = STATUS_SUCCESS; 15 pIrp->IoStatus.Information = 0; 16 IoCompleteRequest(pIrp, IO_NO_INCREMENT); 17 device_entry = KeRemoveDeviceQueue(&pDevExt->device_queue_read); 18 /*if (NULL == device_entry){ 19 break; 20 }*/ 21 pIrp = CONTAINING_RECORD(device_entry, IRP, Tail.Overlay.DeviceQueueEntry); 22 } while (NULL != device_entry); 23 DbgPrint("Leave MyStartIO!\n"); 24 }
DriverEntry中的队列初始化代码如下:
InitializeListHead(&pDevExt->device_queue_read.DeviceListHead);
第一次运行时,R3层代码如下:
CreateFile是异步,
输出结果如下:
我们看到每次都进入了MyStartIO。这里尽管是Pending住每个IRP,但是由于是异步完成,所以每个ReadFile都会先返回,然后将它的IRP留给底层去处理。此外,第一次ReadFile时,队列为空,这时KeInsertDeviceQueue返回FALSE,进入MyStartIO,MyStartIO结束掉这个挂起的IRP,然后才又进入下一个ReadFile。
如果把R3的代码改为并发:
输出结果为:
发现根本没有进入MyStartIO。因为并发,速度很快,一口气将十个IRP发给队列,所以队列很忙,没有进入MyStartIO。当然此时是异步完成,所以即使返回Pending也没有卡在原地。
问题:
第一个R3测试里,是先在MyStartIO中结束掉了这个IRP,而又返回STATUS_PENDING,那么是返回SUCCESS还是PENDING?
实验证明,这样可以成功结束掉IRP 即便返回Pending,也不会挂起。后来我问了下老师为什么,老师说,IoStatus.Status的设置的值才是返回给R3的值,而下边的return返回的值是返回给系统框架的。这是显而易见的,因为派遣函数的调用是从应用层一层一层向下调用的,所以这个派遣函数的返回值定是要返回给调用它的那个函数,也就是系统框架中的一个函数。而真正要返回给R3的值,当然要保存在一个结构体里。
如果底层代码改为:
因为没有结束掉IRP所以迟迟没有返回:
我们再排除下是否是CreateFile异步完成的原因。改成同步完成:
R0层仍旧是:
从结果上看:
显然是没有pending而是成功返回了。
第二个R3测试里,全部返回STATUS_PENDING,没有去结束掉这些IRP,会有什么影响?
只要没有WaitForMultiObject,那么这个程序就可以顺利运行到最后。因为是异步完成,所以派遣函数会先返回,上层运行上层的,底层去自己继续处理IRP。
- 注意1:
一开始我尝试自己去初始化设备扩展中的成员:
这样初始化会报错:
我就尝试把这个KDEVICE_QUEUE中的数据摘出来自己单独组织成一个结构体:
此时再去初始化这样的一个结构体就不会报错:
这说明DDK可能对系统提供的结构体有一种“保护”,它会防止你自己胡乱初始化。
- 注意2:
一开始我注册了一个这个ReadFile的派遣函数:
运行之后蓝屏:
仔细想想这里哪错了,很简单,没有在DriverEntry中初始化你操作的这个队列。
可以参考:http://www.yiiyee.cn/Blog/bsod-0x3b-1/
和https://msdn.microsoft.com/zh-cn/library/windows/hardware/ff558949(v=vs.85).aspx