获取有关 I/O 请求的信息
在处理 I/O 请求之前,驱动程序必须确定请求类型。 当基于框架的驱动程序为设备 创建 I/O 队列 时,它通常会设置 I/O 队列和请求处理程序,以便每个队列或请求处理程序接收特定类型的请求, 包括读取、写入或设备 I/O 控制。
确定请求类型后,驱动程序必须获取请求的输入和输出缓冲区(如果需要)。
为了提供有关驱动程序已接收的 I/O 请求的其他信息,框架请求对象定义了以下方法:
- WdfRequestGetIoQueue,它返回从中传递 I/O 请求的 I/O 队列的句柄;
- WdfRequestGetRequestorMode,它 请求发起方的用户或内核返回处理器访问模式;
- WdfRequestGetFileObject,它返回与请求关联的框架文件对象的句柄;
- WdfRequestWdmGetIrp,它返回与请求关联的 WDM IRP 结构;
- WdfRequestGetParameters,用于检索 WDM 格式的非 IRP 请求参数;
驱动程序完成 I/O 请求后,驱动程序堆栈中的其他驱动程序可以调用其他请求对象方法来获取请求完成信息。
指定缓冲区访问方法
KMDF 驱动程序
对于读取和写入请求,驱动程序堆栈中的所有驱动程序都必须使用相同的方法来访问设备的缓冲区,但最高级别驱动程序除外,该驱动程序可以使用“neither”方法,无论较低级别的驱动程序使用哪种方法。
从版本 1.13 开始,KMDF 驱动程序通过为每个设备调用 WdfDeviceInitSetIoTypeEx 来指定设备的所有读取和写入请求的访问方法。 例如,如果驱动程序为其设备之一指定缓冲 I/O 方法,则 I/O 管理器在向该设备的驱动程序传递读取和写入请求时使用缓冲 I/O 方法。
对于设备 I/O 控制请求,IOCTL) (I/O 控制代码包含指定缓冲区访问方法的位。 因此,KMDF 驱动程序无需执行任何操作即可为 IOCTL 选择缓冲方法。 与读取和写入请求不同,设备的所有 IOCTL 不必指定相同的访问方法。
UMDF 驱动程序
UMDF 驱动程序指定框架用于读取和写入请求以及设备 I/O 控制请求的访问方法的 首选项 。 UMDF 驱动程序提供的值只是首选项,不保证框架会使用。
UMDF 驱动程序通过为每个设备调用 WdfDeviceInitSetIoTypeEx 来指定设备的所有读取、写入和 IOCTL 请求的访问方法。 例如,如果驱动程序为其设备之一指定缓冲 I/O 方法,则框架在向该设备的驱动程序传递读取、写入和 IOCTL 请求时使用缓冲 I/O 方法。
请注意 KMDF 和 UMDF 之间 IOCTL 的缓冲区访问技术的差异。 KMDF 驱动程序不为 IOCTL 指定缓冲区访问方法,而 UMDF 驱动程序指定 IOCTL 的缓冲区访问方法。
如果 WDF 驱动程序通过使用对 I/O 目标使用的 I/O 方法不正确的技术描述 I/O 请求的缓冲区,则框架会更正缓冲区说明。 例如,如果驱动程序使用 MDL 来描述它传递给 WdfIoTargetSendReadhronously 的缓冲区,并且 I/O 目标使用缓冲的 I/O (这要求使用虚拟地址而不是 MDL) 指定缓冲区,则框架会将缓冲区说明从 MDL 转换为虚拟地址和长度。 但是,如果驱动程序以正确的格式指定缓冲区,则效率会更高。
访问缓冲 I/O 的数据缓冲区
如果驱动程序使用缓冲 I/O,则其行为会根据数据请求的类型以及它使用的是 KMDF 还是 UMDF 而更改。
KMDF 驱动程序
当 KMDF 驱动程序使用缓冲 I/O 时,I/O 管理器会创建一个中间缓冲区,驱动程序可以针对每种请求类型访问该缓冲区。 下面是发生的具体情况:
- 写入请求: I/O 管理器在调用驱动程序堆栈之前从调用应用的输入缓冲区传输输入信息。 然后,KMDF 驱动程序从中间缓冲区读取输入信息并将其写入设备;
- 读取请求: KMDF 驱动程序从设备读取信息并将其存储在中间缓冲区中。 然后,I/O 管理器将输出数据从中间缓冲区复制到应用的输出缓冲区;
- 设备 I/O 控制请求: KMDF 驱动程序向或从中间缓冲区读取或写入该请求的数据;
UMDF 驱动程序
当 UMDF 驱动程序使用缓冲 I/O 时,驱动程序主机进程会创建一个或两个中间缓冲区,具体取决于请求的类型。 下面是发生的具体情况:
写入请求。 框架创建一个缓冲区,从调用应用的输入缓冲区传输输入信息,然后调用驱动程序堆栈。 UMDF 驱动程序从中间缓冲区读取输入信息并将其写入设备。
读取请求。 UMDF 驱动程序从设备读取信息,并将其存储在框架创建的缓冲区中。 驱动程序主机进程将输出数据从中间缓冲区复制到应用的输出缓冲区。
设备 I/O 控制请求。 框架创建两个缓冲区,对应于驱动程序可以访问的 IOCTL 的输入和输出缓冲区。 框架将输入信息从 IOCTL 复制到新的中间缓冲区,并使其可供驱动程序使用。 框架不会复制输出缓冲区的内容,因此驱动程序不应尝试从中读取 ,否则最终会读取垃圾数据。 驱动程序写入输出缓冲区的任何数据将复制回原始 IOCTL 缓冲区,并在成功完成 I/O 请求后返回到应用。 请注意,驱动程序写入输入缓冲区的任何数据将被丢弃,不会返回到调用应用。
若要检索表示缓冲区的框架内存对象的句柄,KMDF 和 UMDF 驱动程序调用 WdfRequestRetrieveInputMemory 或 WdfRequestRetrieveOutputMemory,具体取决于这是读取请求还是写入请求。 然后,驱动程序可以通过调用 WdfMemoryGetBuffer 来检索指向缓冲区的指针。 若要读取和写入缓冲区,驱动程序会调用 WdfMemoryCopyFromBuffer 或 WdfMemoryCopyToBuffer。
若要检索缓冲区的虚拟地址和长度,驱动程序会调用 WdfRequestRetrieveInputBuffer 或 WdfRequestRetrieveOutputBuffer。
若要为缓冲区分配和生成内存描述符列表 (MDL) ,KMDF 驱动程序调用 WdfRequestRetrieveInputWdmMdl 或 WdfRequestRetrieveOutputWdmMdl。
访问直接 I/O 的数据缓冲区
KMDF 驱动程序
如果驱动程序使用直接 I/O,则 I/O 管理器验证 I/O 请求的发起方通常 (指定的用户模式应用程序) 缓冲区空间的可访问性,将缓冲区空间锁定到物理内存中,然后为驱动程序提供对缓冲区空间的直接访问权限。
UMDF 驱动程序
如果驱动程序已指定了直接 I/O 的首选项,并且已满足直接 I/O 的所有 UMDF 要求 (请参阅 管理 UMDF 驱动程序) 中的缓冲区访问方法 ,框架会将其从 I/O 管理器接收的内存缓冲区直接映射到驱动程序的主机进程地址空间,从而为驱动程序提供对缓冲区空间的直接访问。
若要检索表示缓冲区空间的框架内存对象的句柄,驱动程序会调用 WdfRequestRetrieveInputMemory 或 WdfRequestRetrieveOutputMemory。 然后,驱动程序可以通过调用 WdfMemoryGetBuffer 来检索指向缓冲区的指针。 若要读取和写入缓冲区,驱动程序会调用 WdfMemoryCopyFromBuffer 或 WdfMemoryCopyToBuffer。
若要检索缓冲区空间的虚拟地址和长度,驱动程序会调用 WdfRequestRetrieveInputBuffer 或 WdfRequestRetrieveOutputBuffer。
如果设备的驱动程序使用直接 I/O,则 I/O 管理器使用 MDL 描述缓冲区。 为了检索指向缓冲区 MDL 的指针,KMDF 驱动程序调用 WdfRequestRetrieveInputWdmMdl 或 WdfRequestRetrieveOutputWdmMdl。 UMDF 驱动程序无法访问 MDL。
访问非缓冲 I/O 和直接 I/O 的数据缓冲区
KMDF 驱动程序
如果驱动程序使用称为 非缓冲 I/O 或直接 I/O 方法 的缓冲区访问方法 (或“neither”方法,则) ,则 I/O 管理器只是为驱动程序提供为请求的缓冲区空间指定的 I/O 请求发起方虚拟地址。 I/O 管理器不会验证 I/O 请求的缓冲区空间,因此驱动程序必须验证缓冲区空间是否可访问,并将缓冲区空间锁定到物理内存中。
只能在 I/O 请求发起者的进程上下文中访问 I/O 管理器提供的虚拟地址。 只有驱动程序堆栈中的最高级别驱动程序保证在发起方的进程上下文中执行。
若要获取对 I/O 请求缓冲区空间的访问权限,最高级别的驱动程序必须提供 EvtIoInCallerContext 回调函数。 每次收到驱动程序的 I/O 请求时,框架都会调用此回调函数。
如果请求的缓冲区访问方法“neither”,则 KMDF 驱动程序必须为每个缓冲区执行以下操作:
- 调用 WdfRequestRetrieveUnsafeUserInputBuffer 或 WdfRequestRetrieveUnsafeUserOutputBuffer 以获取缓冲区的虚拟地址;
- 调用 WdfRequestProbeAndLockUserBufferForRead 或 WdfRequestProbeAndLockUserBufferForWrite 来探测和锁定缓冲区,并获取缓冲区的框架内存对象的句柄;
- 将内存对象句柄保存在请求的 上下文空间中;
- 调用 WdfDeviceEnqueueRequest,它将请求返回到框架;
框架随后将请求添加到驱动程序的 I/O 队列之一。 如果驱动程序提供了 请求处理程序,框架最终将调用相应的请求处理程序。
请求处理程序可以从请求的上下文空间检索请求的内存对象句柄。 驱动程序可以将句柄传递给 WdfMemoryGetBuffer 以获取缓冲区的地址。
有时,最高级别驱动程序必须使用上述步骤来访问用户模式缓冲区,即使驱动程序未使用“neither”访问方法也是如此。 例如,假设驱动程序使用缓冲 I/O。 使用缓冲访问方法的 I/O 控制代码可能会传递包含指向用户模式缓冲区的嵌入指针的结构。 在这种情况下,驱动程序必须提供 EvtIoInCallerContext 回调函数,该函数从 结构中提取指针,然后使用前面的步骤 2 到 4。
UMDF 驱动程序
UMDF 既不支持缓冲缓冲区也不支持直接 I/O 类型缓冲区,因此 UMDF 驱动程序永远不需要直接处理此类缓冲区。
但是,如果框架从 I/O 管理器接收此类缓冲区进行读取或写入,则会将其作为缓冲 I/O 或直接 I/O 提供给 UMDF 驱动程序,具体取决于驱动程序选择的访问方法。 如果框架收到指定“neither”缓冲区方法的 IOCTL,则它可以根据 INF 指令的存在,选择性地将 IOCTL 请求的缓冲区访问方法转换为缓冲 I/O 或直接 I/O。
管理 UMDF 驱动程序中的缓冲区访问方法
如果要编写 UMDF 驱动程序,则可以指定框架用于读取和写入请求的缓冲区访问方法的首选项,以及设备 I/O 控制请求。 UMDF 驱动程序提供的值只是首选项,不保证框架使用。
指定首选缓冲区访问方法
从 UMDF 版本 2.0 开始,UMDF 驱动程序调用 WdfDeviceInitSetIoTypeEx 来注册读取/写入请求和设备 I/O 控制请求的首选访问方法。
如果驱动程序不调用 WdfDeviceInitSetIoTypeEx,UMDF 将使用缓冲方法向此设备发出 I/O 请求。
框架使用以下规则来确定要使用的访问方法:
- 驱动程序堆栈中的所有 UMDF 驱动程序都必须使用相同的方法来访问设备的缓冲区,并且框架优先于缓冲 I/O;
- 如果 UMDF 确定某些驱动程序更喜欢对设备使用缓冲 I/O 或直接 I/O,而其他驱动程序则只对设备使用缓冲 I/O,则 UMDF 对所有驱动程序使用缓冲 I/O。 如果一个或多个堆栈的驱动程序只喜欢缓冲 I/O,而其他驱动程序则只喜欢直接 I/O,则 UMDF 会将事件记录到系统事件日志中,并且不会启动驱动程序堆栈;
- 驱动程序可以调用 WdfDeviceGetDeviceStackIoType 来确定 UMDF 已分配给设备的读/写请求和 I/O 控制请求的缓冲区访问方法;
- 在某些情况下,UMDF 会将直接 I/O 分配给设备,但为了获得最佳性能,对设备的一个或多个请求使用缓冲 I/O。 例如,如果 UMDF 将数据复制到驱动程序缓冲区的速度比映射缓冲区以便直接访问的快,则 UMDF 对小型缓冲区使用缓冲 I/O。(可选)驱动程序可以在调用 WdfDeviceInitSetIoTypeEx 时提供 DirectTransferThreshold 值。 框架使用此值来确定框架将使用直接 I/O 的最小缓冲区大小。 通常,不需要提供此值,因为框架使用提供最佳性能的设置;
- UMDF 仅对在内存页边界上开始和结束的缓冲区空间使用直接 I/O。 如果缓冲区的开头或末尾不位于页面边界上,则 UMDF 对缓冲区的这一部分使用缓冲 I/O。 换句话说,UMDF 可能同时使用缓冲 I/O 和直接 I/O 进行由多个 I/O 请求组成的大型数据传输;
- 对于设备 I/O 控制请求,仅当 IOCTL) (I/O 控制代码指定直接 I/O 时,UMDF 才使用直接 I/O,并且仅当该设备的所有 UMDF 驱动程序都调用 WdfDeviceInitSetIoTypeEx 来指定直接访问方法时;
检索 I/O 请求的访问方法
驱动程序使用相同的请求对象方法集来访问数据缓冲区,而不考虑缓冲区访问方法。 因此,大多数驱动程序通常不需要知道 UMDF 是对 I/O 请求使用缓冲 I/O 还是直接 I/O。
在某些情况下,如果知道 I/O 请求的访问方法,则可以提高驱动程序的性能。 例如,请考虑通常使用直接 I/O 的高吞吐量设备。 当驱动程序收到 I/O 请求时,它会将数据从共享缓冲区空间复制到本地驱动程序内存中进行验证。
但是,驱动程序偶尔可能会收到使用缓冲 I/O 的缓冲区。 由于 I/O 管理器已将此数据复制到中间缓冲区,因此驱动程序无需在本地复制参数。 通过避免复制操作,驱动程序可以提高性能。
UMDF 驱动程序调用 WdfRequestGetEffectiveIoType 以获取 I/O 请求的缓冲区访问方法。 如上所述,特定请求的 I/O 类型可能与设备的框架分配 I/O 类型设置不同。
从缓冲 I/O 和直接 I/O 转换
UMDF 驱动程序不能使用“neither”方法。
但是,某些设备 I/O 控制代码 的定义 (IOCTL) 指定请求使用“neither”方法。 (可选)UMDF 驱动程序可以将此类设备 I/O 控制请求的缓冲区访问方法转换为缓冲 I/O 或直接 I/O。 请使用以下步骤:
- 将 UmdfMethodNeitherAction 指令包含在驱动程序的 INF 文件的 INF DDInstall 节 中。 可以设置 指令的值,以指示 UMDF 应将使用“neither”访问方法的设备 I/O 控制请求传递给驱动程序。 否则,UMDF 会以错误状态值完成这些 I/O 请求l
- 使用 UMDF 为缓冲 I/O 或直接 I/O 提供的对象方法访问 I/O 请求的缓冲区l
仅当确定 UMDF 可以将访问方法转换为缓冲 I/O 或直接 I/O 时,才应启用对使用“neither”方法的 IOCTL 请求的支持。 例如,如果 IOCTL 指定的自定义请求不符合 I/O 控制代码的缓冲区说明中所述的缓冲区规范规则,则 UMDF 无法转换缓冲区。
重复使用框架请求对象
为了提高性能,创建大量几乎相同的异步请求并将其发送到 I/O 目标的基于框架的驱动程序可以重复使用请求对象,而不是为每个请求创建新的请求对象。 完成请求后,驱动程序可以重复使用请求对象。
如果驱动程序已通过调用 WdfRequestCreate 或 WdfRequestCreateFromIrp 创建了请求对象,则可以通过调用 WdfRequestReuse 来重复使用该请求。 驱动程序还可以在其 I/O 队列中重复使用从框架收到的请求对象,但无法更改收到的请求对象包含的 IRP。
如果小心避免导致 WdfRequestReuse 中所述的返回值失败的情况,驱动程序可以从 CompletionRoutine 回调函数中调用 WdfRequestReuse。 CompletionRoutine 回调函数具有 VOID 返回值,因此无法报告 errors.
如果驱动程序为它重用的请求对象提供 CompletionRoutine 回调函数,则驱动程序必须在调用 WdfRequestReuse 后调用 WdfRequestSetCompletionRoutine。