内核模式驱动程序框架 (KMDF)处理I/O请求的情况
在内核模式驱动程序框架 (KMDF) 中,一个 WDFREQUEST 对象代表一个 I/O 请求。每个 WDFREQUEST 对象都与一个或多个 WDFMEMORY 对象关联,每个这种 WDFMEMORY 对象都代表一个用于请求中的输入或输出的缓冲区。
当 KMDF 创建 WDFREQUEST 和 WDFMEMORY 对象来表示一个传入的 I/O 请求时,它将 WDFREQUEST 对象设置为关联的 WDFMEMORY 对象的父对象。因此,WDFMEMORY 对象可以持续的时间不会比 WDFREQUEST 对象的生存期长。当 KMDF 驱动程序完成 I/O 请求时,框架删除 WDFREQUEST 对象和 WDFMEMORY 对象,因此这两个对象的句柄变得无效。
但是,底层的缓冲区不同。根据哪个组件创建了缓冲区以及如何创建缓冲区,缓冲区可能有一个引用计数并且可能“属于” WDFMEMORY 对象(或者可能不属于)。如果 WDFMEMORY 对象拥有缓冲区,那么缓冲区拥有一个引用计数并且其生存期被限制为 WDFMEMORY 对象的生存期。如果一些其他组件创建了缓冲区,那么缓冲区的生存期和 WDFMEMORY 对象不相关。
KMDF 驱动程序也可以创建自己的 WDFREQUEST 对象来发送到 I/O 目标。驱动程序创建的请求可以重用驱动程序在一个 I/O 请求中接收到的现有 WDFMEMORY 对象。频繁发送请求到 I/O 目标的驱动程序可以重用其创建的 WDFREQUEST 对象。但是,它不能重用从 KMDF 接收到的 WDFREQUEST 对象。
理解 WDFREQUEST 对象、WDFMEMORY 对象和底层缓冲区的生存期非常重要。这可以确保您的驱动程序不会试图引用无效的句柄或缓冲区指针。
考虑可能的使用场景:
? 场景 1:驱动程序从 KMDF 接收到一个 I/O 请求、处理它并完成它。
? 场景 2:驱动程序从 KMDF 接收到一个 I/O 请求并将其转发给一个 I/O 目标。
? 场景 3:驱动程序发出一个使用新 WDFMEMORY 对象的 I/O 请求。
? 场景 4:驱动程序发出一个使用现有 WDFMEMORY 对象的 I/O 请求。
? 场景 5:驱动程序重用其创建的 WDFREQUEST 对象。
场景 1:驱动程序从 KMDF 接收到一个 I/O 请求、处理它并完成它
在最简单的场景中,KMDF 将请求分发给驱动程序,驱动程序执行 I/O 并完成请求。在这种情况中,底层缓冲区可能已经被用户模式应用程序、另一个驱动程序或操作系统本身创建。如果驱动程序执行带缓冲的 I/O 或直接 I/O,那么它可以通过下列方法之一访问缓冲区:
? 通过调用 WdfRequestRetrieveInputMemory 或 WdfRequestRetrieveOutputMemory(取决于这是写请求还是读请求)来获取与 WDFREQUEST 关联的 WDFMEMORY 对象的句柄,然后通过调用 WdfMemoryGetBuffer 来获取缓冲区的指针。要读写缓冲区,驱动程序调用 WdfMemoryCopyFromBuffer 或 WdfMemoryCopyToBuffer。
? 通过调用 WdfRequestRetrieveInputBuffer 或 WdfRequestRetrieveOutputBuffer(取决于这是读请求还是写请求)来获取缓冲区的指针。
当驱动程序调用 WdfRequestComplete 来完成 I/O 请求时,框架删除 WDFMEMORY 对象。然后缓冲区指针无效。
场景 2:驱动程序从 KMDF 接收到一个 I/O 请求并将其转发给一个 I/O 目标
如果驱动程序将请求转发给一个 I/O 目标而不自己执行 I/O,那么会发生什么?在这种情况中,驱动程序将进行如下处理:
1.
调用 WdfDeviceGetIoTarget 来获取一个 I/O 目标对象的句柄。
2.
调用 WdfRequestFormatUsingCurrentType 来为 I/O 目标格式化请求,或者调用 WdfRequestRetrieveXxxMemory 来获取内存对象和调用 WdfIoTargetFormatRequestForXxx 来为 I/O 目标格式化请求。
3.
调用 WdfRequestSend 将请求发送到 I/O 目标。
驱动程序不得以任何方式改动 WDFREQUEST 对象或 WDFMEMORY 对象。如果驱动程序为请求设置一个 I/O 完成回调,那么 KMDF 在 I/O 目标完成请求之后调用该回调。缓冲区指针保持有效,直到驱动程序(不是 I/O 目标)调用 WdfRequestComplete。
下面的示例代码显示驱动程序如何从传入的 WDFREQUEST 对象检索一个 WDFMEMORY 对象的句柄,格式化请求要发送到 I/O 目标的请求,然后发送请求:
VOID EvtIoRead( IN WDFQUEUE Queue, IN WDFREQUEST Request, IN size_t Length ) { NTSTATUS status; WDFMEMORY memory; WDFIOTARGET ioTarget; BOOLEAN ret; ioTarget = WdfDeviceGetIoTarget(WdfIoQueueGetDevice(Queue));
status = WdfRequestRetrieveOutputMemory(Request, &memory); if (!NT_SUCCESS(status)) { goto End; }
status = WdfIoTargetFormatRequestForRead(ioTarget, Request, memory, NULL, NULL); if (!NT_SUCCESS(status)) { goto End; }
WdfRequestSetCompletionRoutine(Request, RequestCompletionRoutine, WDF_NO_CONTEXT);
ret = WdfRequestSend (Request, ioTarget, WDF_NO_SEND_OPTIONS); if (!ret) { status = WdfRequestGetStatus (Request); goto End; }
return;
End:WdfRequestComplete(Request, status); return;
}
当 I/O 目标完成请求时,KMDF 调用驱动程序为请求设置的完成回调。下面是这个例程的代码:
VOID RequestCompletionRoutine( IN WDFREQUEST Request, IN WDFIOTARGET Target, PWDF_REQUEST_COMPLETION_PARAMS CompletionParams, IN WDFCONTEXT Context ) { UNREFERENCED_PARAMETER(Target); UNREFERENCED_PARAMETER(Context);
WdfRequestComplete(Request, CompletionParams->IoStatus.Status);
return;
}
当驱动程序在其完成回调中调用 WdfCompleteRequest 时,KMDF 销毁 WDFMEMORY 对象。驱动程序在 EvtIoRead 函数中检索到的 WDFMEMORY 对象句柄现在无效。
场景 3:驱动程序发出一个使用现有 WDFMEMORY 对象的请求
一些驱动程序发出它们自己的 I/O 请求并将它们发送到 I/O 目标(由 WDFIOTARGET 对象表示)。这种请求必须使用驱动程序创建的 WDFREQUEST 对象;驱动程序不能重用从 KMDF 接收到的 WDFREQUEST 对象。但是,驱动程序可以从传入的 WDFREQUEST 中获取一个 WDFMEMORY 对象,然后创建一个使用该 WDFMEMORY 对象的新请求。驱动程序不得更改底层的缓冲区,但是它可以在格式化新的 I/O 请求时传递一个缓冲区偏移。要格式化一个使用现有 WDFMEMORY 对象的新 I/O 请求,驱动程序调用 WdfIoTargetFormatRequestXxx 或 WdfIoTargetSendXxxSynchronously 方法之一。如果驱动程序异步发送请求,那么它应该注册一个 I/O 完成回调例程,从而使 KMDF 可以在请求完成时通知它。
当 KMDF 格式化发送到 I/O 目标的请求时,它代表 I/O 目标对象清除回收的 WDFMEMORY 对象上的一个引用。I/O 目标对象保留这个引用,直到发生下列情况之一:
? 请求已经完成。
? 驱动程序通过调用 WdfIoTargetFormatRequestXxx 或 WdfIoTargetSendXxxSynchronously 方法之一重新格式化 WDFREQUEST 对象。
? 驱动程序调用 WdfRequestReuse。
当新的 I/O 请求完成时,WDF 调用驱动程序为此请求设置的 I/O 完成回调。这时候,WDFIOTARGET 对象仍然持有 WDFMEMORY 对象上的一个引用。因此,在 I/O 完成回调中,驱动程序必须在完成原始请求(它从其中获取 WDFMEMORY 对象)之前在驱动程序创建的 WDFREQUEST 对象上调用 WdfRequestReuse。如果驱动程序不调用 WdfRequestReuse,那么会由于额外的引用而发生错误检查。
场景 4:驱动程序发出一个使用新 WDFMEMORY 对象的请求
根据底层缓冲区的来源,KMDF 为驱动程序提供三种方式来创建新的 WDFMEMORY 对象。
? 为了让 KMDF 分配缓冲区,驱动程序调用 WdfMemoryCreate。根据驱动程序的规范,可以从分页池或未分页池分配缓冲区。
? 要从前面定义的后备链表分配缓冲区,驱动程序调用 WdfMemoryCreateFromLookaside。
? 要将先前分配的缓冲区指定给一个新的 WDFMEMORY 对象,驱动程序调用 WdfMemoryCreatePreallocated。驱动程序可以在以后通过调用 WdfMemoryAssignBuffer 将不同的缓冲区指定给相同的 WDFMEMORY 对象。
如果缓冲区由 KMDF 分配或从驱动程序创建的 WDFLOOKASIDE 列表分配,那么 WDFMEMORY 对象拥有缓冲区,因此只要 WDFMEMORY 对象存在,缓冲区指针就会保持有效。发出异步 I/O 请求的驱动程序应该始终使用 WDFMEMORY 对象拥有的缓冲区,从而 KMDF 可以确保缓冲区持续存在,直到 I/O 请求已经完成并返回到发出请求的驱动程序。因此,驱动程序应该使用 WdfMemoryCreate 或 WdfMemoryCreateFromLookaside 为异步 I/O 请求来创建 WDFMEMORY 对象。
如果驱动程序通过调用 WdfMemoryCreatePreallocated 将先前分配的缓冲区指定给新的 WDFMEMORY 对象,那么 WDFMEMORY 对象不会拥有该缓冲区。在这种情况中,WDFMEMORY 对象的生存期和底层缓冲区的生存期无关。驱动程序必须管理缓冲区的生存期,并且不得试图使用一个无效的缓冲区指针。
驱动程序不能对在 I/O 请求中接收到的 WDFMEMORY 对象使用 WdfMemoryAssignBuffer;它只能对驱动程序创建的新 WDFMEMORY 对象使用这种方法。
场景 5:驱动程序重用其创建的 WDFREQUEST 对象
驱动程序可以重用其创建的 WDFREQUEST 对象,但是它必须在每次重用之前通过调用 WdfRequestReuse 来重新初始化每个这种对象。关于重新初始化一个 WDFREQUEST 对象的示例代码,请参见 KMDF 发布版提供的 Toastmon 和 NdisEdge 例子。
您应该做什么?
? 不要试图在完成关联的 WDFREQUEST 对象之后引用 WDFMEMORY 对象下层的缓冲区。
? 当发送异步 I/O 请求时,始终使用由 WdfMemoryCreate 或 WdfMemoryCreateFromLookaside 分配的缓冲区。
? 如果您的驱动程序发送一个包含重用的 WDFMEMORY 对象的 I/O 请求,那么它必须始终在新请求完成之后但在完成从其中检索 WDFMEMORY 对象的请求之前调用 WdfRequestReuse。
? 始终在重用新的 I/O 请求中的对象之前调用 WdfRequestReuse 来重新初始化 WDFREQUEST 对象。