创建框架请求对象
框架请求对象表示 I/O 管理器已发送到驱动程序的 I/O 请求。 基于框架的驱动程序通过调用 框架请求对象方法来处理每个 I/O 请求。
每个 I/O 请求都包含一个 WDM I/O 请求数据包 (IRP 结构) ,但基于框架的驱动程序通常不需要访问 IRP 结构。
大多数框架请求对象由框架创建,但驱动程序也可以创建请求对象。
由框架创建的请求对象
当基于框架的驱动程序从 I/O 管理器接收 I/O (IRP) 的 I/O 请求数据包时,框架会截获 IRP 并创建框架请求对象。 框架将请求对象放入 I/O 队列,如果驱动程序已注册队列 的请求处理程序 ,则调用相应的处理程序。
下图演示了框架为读取操作创建请求对象时发生的步骤:
以下步骤对应于上图中的数字:
- 用户模式应用程序通过调用 Microsoft Win32 ReadFile 函数读取文件;
- ReadFile 函数调用在内核模式下运行的 I/O 管理器;
- I/O 管理器分配 IRP 结构,并将 IRP_MJ_READ 函数代码存储在 结构中;
- I/O 管理器调用驱动程序 x 的 DispatchRead 标准驱动程序例程,传递指向 IRP 结构的指针。 由于驱动程序 x 是基于框架的驱动程序,因此框架提供驱动程序的 DispatchRead 例程;
- 框架创建一个表示 IRP 结构的请求对象。 框架将请求对象添加到驱动程序的队列对象之一;
- 框架调用驱动程序的 EvtIoRead 请求处理程序,传递队列对象句柄和请求对象句柄;
请求驱动程序创建的对象
基于框架的驱动程序还可以创建请求对象。 例如,如果驱动程序收到一个读取或写入请求,该请求量大于驱动程序的 I/O 目标 一次可以处理的数据量,则驱动程序可能会创建请求对象。 在这种情况下,驱动程序可以将数据划分为多个较小的请求,并使用其他请求对象将这些较小的请求发送到一个或多个 I/O 目标。
若要创建请求对象,驱动程序应调用 WdfRequestCreate ,后跟初始化请求的框架对象方法,例如 WdfUsbTargetPipeFormatRequestForRead。
如果驱动程序在 WDM 调度例程中接收 WDM IRP,然后使用框架对其进行服务或转发,则该驱动程序可以调用 WdfRequestCreateFromIrp。
使用请求对象上下文
每个框架请求对象(无论是由框架创建还是由驱动程序创建)都可以包含驱动程序定义的上下文空间。 当基于框架的驱动程序初始化框架设备对象时,驱动程序可以调用 WdfDeviceInitSetRequestAttributes 来指定描述设备请求对象的上下文空间的 WDF_OBJECT_ATTRIBUTES 结构。
框架为请求对象分配上下文空间,如下所示:
- 当框架为驱动程序创建请求对象时,它会使用驱动程序在调用 WdfDeviceInitSetRequestAttributes 时指定的大小来分配上下文空间;
- 如果驱动程序通过调用 WdfRequestCreate 创建其他请求对象,可以通过提供WDF_OBJECT_ATTRIBUTES结构来指定上下文大小;
请求所有权
当 I/O 管理器将 I/O 请求发送到基于框架的驱动程序时,框架会截获该请求并创建框架请求对象。 框架"拥有"请求对象,因为只有框架可以访问请求并对该对象执行操作。
框架创建请求对象后,会将该对象置于驱动程序的 I/O 队列之一中。 框架继续拥有请求对象,直到它从队列中删除请求并将其传递给驱动程序。
驱动程序 收到 请求对象后,它拥有该请求。 驱动程序可以通过句柄访问请求对象,并对该对象执行操作。 虽然驱动程序拥有请求对象,但它可以 重新排队、 完成、 取消或 转发 请求,之后它不再拥有请求对象,并且无法访问它。
当请求对象的所有权在驱动程序和框架之间传递时,对象句柄的值不会更改。 例如,如果驱动程序从 I/O 队列接收请求,将其重新排队到其他队列,然后再次接收请求,则句柄的值不会更改。 同样,如果驱动程序将请求转发到 I/O 目标,然后收到 I/O 目标已完成请求的通知,则驱动程序的通知回调函数将接收驱动程序提供给 I/O 目标的相同句柄值。
将 I/O 请求重新排队
驱动程序可以重新排队从 I/O 队列获取的 I/O 请求。 驱动程序可以将 I/O 请求重新排到驱动程序为同一设备创建的另一个 I/O 队列。 此外, 总线驱动程序 可以将 I/O 请求从子设备的 I/O 队列重新排队到父设备的 I/O 队列。
将 I/O 请求重新排队到设备的不同 I/O 队列
在驱动程序的请求处理程序从驱动程序的 I/O 队列收到 I/O 请求后,驱动程序可以调用 WdfRequestForwardToIoQueue 将请求重新排队到另一个队列。
例如,如果希望驱动程序在处理请求之前将资源分配给请求,驱动程序的 EvtIoDefault 回调函数可以接收所有请求,将资源信息存储在每个请求的上下文内存中,然后调用 WdfRequestForwardToIoQueue 将每个请求重新排队到其他队列。
如果驱动程序调用 WdfRequestForwardToIoQueue 来重新排队驱动程序从使用顺序 调度方法的 I/O 队列中获取的 I/O 请求,则框架会将下一个 I/O 请求从顺序队列传递到驱动程序,而无需等待重新排队的请求完成。
如果驱动程序使用手动调度方法,它可以调用 WdfRequestRequeue 方法,将 I/O 请求返回到驱动程序从中获取它的 I/O 队列的头。 调用 WdfRequestRequeue 后,驱动程序对 WdfIoQueueRetrieveNextRequest 的下一次调用将检索重新排队的请求。
将 I/O 请求重新排队到父设备的 I/O 队列
父设备的函数驱动程序可以充当 总线驱动程序 ,该驱动程序 枚举 父设备的子设备,并为子 设备创建物理设备对象 (PDO) 。 此类驱动程序有时可以接收父设备必须处理的子设备的 I/O 请求。
例如,协议总线 ((如 USB) )通常控制分配给每个已连接设备的硬件资源。 因此,父总线的函数驱动程序通常处理每个子设备的 I/O 操作。 当 I/O 管理器将 I/O 请求发送到其中一个子 设备的设备堆栈 时,总线的函数驱动程序会在子设备的某个 I/O 队列中接收 I/O 请求,因为该驱动程序创建了子设备的 PDO。 在驱动程序可以在父总线设备的上下文中处理 I/O 请求之前,它必须将子设备的 I/O 队列中的 I/O 请求重新排队到属于父设备的 I/O 队列。
但是,驱动程序无法调用 WdfRequestForwardToIoQueue 将请求从子队列移动到父队列。 由于 I/O 管理器为父设备和子设备创建单独的设备堆栈,因此必须先将基础 WDM 设备对象从表示子设备的对象更改为表示父设备的对象。
在 KMDF 版本 1.9 之前,驱动程序只能通过创建远程 I/O 目标、增加子设备的设备堆栈大小以及指定正确的 WDM 设备对象,将 I /O 请求从子设备发送到其父设备。
从 KMDF 版本 1.9 开始,驱动程序可以在创建子设备之前调用 WdfPdoInitAllowForwardingRequestToParent ,然后调用 WdfRequestForwardToParentDeviceIoQueue 以将请求从子级 I/O 队列重新排队到父队列。 如果驱动程序使用WdfPdoInitAllowForwardingRequestToParent 和 WdfRequestForwardToParentDeviceIoQueue,框架将增加子级的设备堆栈大小,并将正确的 WDM 设备对象分配给 I/O 请求。
完成 I/O 请求
每个基于框架的驱动程序最终都必须完成它从框架收到的每个 I/O 请求。 驱动程序通过调用请求对象的 WdfRequestComplete、 WdfRequestCompleteWithInformation 或 WdfRequestCompleteWithPriorityBoost 方法完成请求。
何时完成请求
驱动程序在确定以下情况之一为 true 时,必须完成请求:
- 请求的 I/O 操作已成功完成;
- 请求的 I/O 操作已启动,但在完成之前失败;
- 请求的 I/O 操作不受支持,或在收到操作时无效,无法启动;
- 请求的 I/O 操作已取消;
如果驱动程序通过在设备上创建 I/O 活动来为 I/O 请求提供服务,驱动程序通常会从其 EvtInterruptDpc 或 EvtDpcFunc 回调函数调用 WdfRequestComplete。
如果驱动程序收到不受支持的请求或无效请求,它通常从接收请求的请求处理程序调用 WdfRequestComplete。
如果 I/O 操作被取消,驱动程序通常会从其 EvtRequestCancel 回调函数调用 WdfRequestComplete。
如果驱动程序将 I/O 请求 转发 到 I/O 目标,驱动程序在 I/O 目标完成请求后完成请求,如下所示:
- 如果驱动程序将 I/O 请求 同步 转发到 I/O 目标,则驱动程序对 I/O 目标的调用仅在较低级别的驱动程序完成请求 后返回,除非发生错误。 I/O 目标返回后,驱动程序必须调用 WdfRequestComplete;
- 如果驱动程序 以异步方式转发 I/O 请求,你将希望在较低级别的驱动程序完成请求时收到通知。 如果驱动程序注册 CompletionRoutine 回调函数,框架会在 I/O 目标完成请求后调用此回调函数。 CompletionRoutine 回调函数通常调用 WdfRequestComplete;
若要注册 CompletionRoutine 回调函数,驱动程序必须先调用 WdfRequestSetCompletionRoutine ,然后才能将 I/O 请求转发到 I/O 目标。
如果在 I/O 目标完成异步转发的 I/O 请求时不需要通知驱动程序,则驱动程序不必注册 CompletionRoutine 回调函数。 相反,驱动程序可以在调用 WdfRequestSend 时设置WDF_REQUEST_SEND_OPTION_SEND_AND_FORGET标志。 在这种情况下,驱动程序不调用 WdfRequestComplete。
驱动程序无法完成它通过调用 WdfRequestCreate 或 WdfRequestCreateFromIrp 创建的 I/O 请求。 相反,驱动程序必须调用 WdfObjectDelete 来删除请求对象,通常是在 I/O 目标完成请求之后。
例如,驱动程序可能会收到读取或写入请求,该请求的数据量大于驱动程序的 I/O 目标一次可以处理的数据量。 驱动程序必须将数据划分为多个较小的请求,并将这些较小的请求发送到一个或多个 I/O 目标。 处理这种情况的方法包括:
- 调用 WdfRequestCreate 以创建表示较小请求的单个附加请求对象:驱动程序可以同步将此请求发送到 I/O 目标。 较小请求的 CompletionRoutine 回调函数可以调用 WdfRequestReuse ,以便驱动程序可以重用请求并再次将其发送到 I/O 目标。 在 I/O 目标完成最后一个较小的请求后, CompletionRoutine 回调函数可以调用 WdfObjectDelete 以删除驱动程序创建的请求对象,驱动程序可以调用 WdfRequestComplete 来完成原始请求;
- 调用 WdfRequestCreate 以创建表示较小请求的其他几个请求对象:驱动程序的 I/O 目标可以异步处理这些多个较小的请求。 驱动程序可以为每个较小的请求注册 CompletionRoutine 回调函数。 每次调用 CompletionRoutine 回调函数时,它都可以调用 WdfObjectDelete 来删除驱动程序创建的请求对象。 I/O 目标完成所有较小的请求后,驱动程序可以调用 WdfRequestComplete 来完成原始请求;
提供完成信息
驱动程序完成请求后,可以选择提供其他驱动程序可以访问的一些附加信息。 例如,驱动程序可以提供为读取或写入请求传输的字节数。 若要提供此信息,驱动程序可以执行以下任一操作:
- 在调用 WdfRequestComplete 之前调用 WdfRequestSetInformation;
- 调用 WdfRequestCompleteWithInformation;
获取完成信息
若要获取有关另一个驱动程序已完成的 I/O 请求的信息,驱动程序可以:
- 调用 WdfRequestGetStatus 以获取较低级别驱动程序在调用 WdfRequestComplete 时指定的完成状态值;
- 调用 WdfRequestGetCompletionParams 以获取包含有关已完成请求的其他信息的 WDF_REQUEST_COMPLETION_PARAMS 结构,例如表示请求缓冲区的内存对象的句柄或特定于总线的信息。驱动程序只能在调用 WdfRequestSend 以同步或异步方式将 I/O 请求发送到 I/O 目标之后调用 WdfRequestGetCompletionParams。 驱动程序调用将 I/O 请求仅 同步发送到 I/O 目标的方法之一(例如 WdfIoTargetSendReadSynchronously) 后,不得调用 WdfRequestGetCompletionParams;
- 调用 WdfRequestGetInformation 以获取较低级别驱动程序在调用 WdfRequestSetInformation 或 WdfRequestCompleteWithInformation 时指定的附加 I/O 完成信息,如果驱动程序堆栈中的驱动程序提供此类信息;
如果驱动程序以同步方式发送 I/O 请求,则它通常在同步调用返回后调用 WdfRequestGetStatus、 WdfRequestGetCompletionParams 和 WdfRequestGetInformation 。 如果驱动程序异步发送 I/O 请求,它通常从 CompletionRoutine 回调函数中调用这些方法。