windows驱动开发-I/O请求(三)

发布于:2024-04-27 ⋅ 阅读:(14) ⋅ 点赞:(0)

之前的两篇文章已经将I/O请求的使用说清楚了,接下来试着探索一下I/O请求的其它方面。

I/O请求原理

如果对IRP结构有印象的话,会发现IRP结构中有一个DeviceObject成员以及FileObject成员,这里已经隐含了IRP是如何传递的。

在DriverEntry中,我们一开始会收到一个PDRIVER_OBJECT的参数,后续我们会在IoCreateDevice中创建设备对象DeviceObject,并使用IoAttachDeviceToDeviceStack建立设备堆栈,这个过程隐含了以下信息:

这个过程中DriverObject其实就对应FileObject,每个驱动文件是一个DLL,这个DLL在系统重仅有一份,也就只有一个DriverObject,但是DeviceObject可以有很多个,这里是类定义和类对象的关系;

设备堆栈从上往下是一定的,一条总线上可能有N个设备,但一个设备上只有一条总线;故我们调用IoCallDriver 传递的时候,只要传递DeviceObject即可,I/O管理器解析DeviceObject就能找到它的下级驱动对象;

值得指出的是,IRP总是从非分页内存池中分配的,故任何驱动都能直接访问它而不引起违规!同时,I.O管理器是依赖于IRP中的信息来管理IRP和驱动的交互的,它派发IRP和完成IRP的时候,并没有太多的依赖它自身的数据结构,故完全可以利用这个特征来实现远程I/O。


创建IRP

我们同样在驱动中创建IRP,相关函数如下:

IoAllocateIrp: 用于分配 IRP 和多个零初始化的 I/O 堆栈位置。Dispatch例程必须为新分配的 IRP 设置下一个较低驱动程序的 I/O 堆栈位置,通常是从原始 IRP 中自己的堆栈位置复制 (可能修改) 信息。 如果更高级别的驱动程序为新分配的 IRP 分配自己的 I/O 堆栈位置,则Dispatch例程可以在其中设置每个请求的上下文信息,供 IoCompletion 例程使用。

IoBuildAsynchronousFsdRequest:根据调用方指定的参数为调用方设置下一个较低驱动程序的 I/O 堆栈位置,高级别的驱动程序可以调用此例程,为 IRP_MJ_READ、 IRP_MJ_WRITE、 IRP_MJ_FLUSH_BUFFERS和IRP_MJ_SHUTDOWN请求分配 IRP 。

为此类 IRP 调用 IoCompletion 例程时,它可以检查 I/O 状态块,并在必要时 (或可能) 再次在 IRP 中设置下一个较低驱动程序的 I/O 堆栈位置,然后重试请求或重复使用它。 但是, IoCompletion 例程在 IRP 中本身没有本地上下文存储,因此驱动程序必须在驻留内存中的其他位置维护有关原始请求的上下文。

IoMakeAssociatedIrp:用于分配 IRP 和多个零初始化的 I/O 堆栈位置,并将 IRP 与 主 IRP 相关联,中间驱动程序无法调用 IoMakeAssociatedIrp 来创建较低驱动程序的 IRP。

调用 IoMakeAssociatedIrp 为较低驱动程序创建 IRP 的任何最高级别驱动程序,在发送其关联的 IRP 并为原始主 IRP 调用 IoMarkIrpPending 后,可以将控制权返回到 I/O 管理器。 当所有关联的 IRP 都由较低驱动程序完成时,最高级别的驱动程序可以依赖 I/O 管理器来完成主 IRP。

驱动程序很少为关联的 IRP 设置 IoCompletion 例程。 如果高级别驱动程序为其创建的关联 IRP 调用 IoSetCompletionRoutine ,则如果驱动程序从其 IoCompletion 例程返回STATUS_MORE_PROCESSING_REQUIRED,则 I/O 管理器不会完成主 IRP。 在这些情况下,驱动程序的 IoCompletion 例程必须使用 IoCompleteRequest 显式完成主 IRP。

如果驱动程序在新 IRP 中分配自己的 I/O 堆栈位置,则调度例程必须先调用 IoSetNextIrpStackLocation ,然后才能调用 IoGetCurrentIrpStackLocation ,以便在 IoCompletion 例程的自己的 I/O 堆栈位置中设置上下文。 

Dispatch例程必须使用原始 IRP 调用 IoMarkIrpPending ,但不能调用任何驱动程序分配的 IRP,因为 IoCompletion 例程将释放它们。

如果Dispatch例程正在为某个传输分配 IRP(文件系统中很常见),并且基础设备驱动程序可能控制可移动媒体设备,则调度例程必须从原始 IRP 中 Tail.Overlay.Thread 的值在其新分配的 IRP 中设置线程上下文。

可移动媒体设备的基础驱动程序可能会为驱动程序分配的 IRP 调用 IoSetHardErrorOrVerifyDevice,它引用 Irp-Tail.Overlay.Thread> 上的指针。 如果驱动程序调用此支持例程,则文件系统驱动程序可以向相应的用户线程发送一个对话框,提示用户取消、重试或失败驱动程序无法满足的操作。 

将驱动程序分配的所有 IRP 发送到较低驱动程序后,调度例程必须返回STATUS_PENDING。

驱动程序的 IoCompletion 例程应在调用原始 IRP 的 IoCompleteRequest 之前,使用 IoFreeIrp 释放所有驱动程序分配的 IRP。 完成原始 IRP 后, IoCompletion 例程必须释放所有驱动程序分配的 IRP,然后才能返回控制权。

每个高级别的驱动程序都会为低驱动程序设置可重用的IRP,这样,无论给定的请求来自中间驱动程序还是来自任何其他源(例如文件系统或用户模式应用程序),对基础设备驱动程序都无关紧要。

最高级别的驱动程序可以调用 IoMakeAssociatedIrp 来分配 IRP 并为较低级别的驱动程序链设置它们。 只要驱动程序不调用 IoSetCompletionRoutine 与原始 IRP 或其分配的任何关联 IRP,I/O 管理器会自动完成原始 IRP。 但是,最高级别的驱动程序不得为请求缓冲 I/O 操作的任何 IRP 分配关联的 IRP。

中间级别驱动程序无法通过调用 IoMakeAssociatedIrp 为低级别驱动程序分配 IRP。 中间驱动程序接收的任何 IRP 可能已经是关联的 IRP,并且驱动程序无法将另一个 IRP 与此类 IRP 相关联。

相反,如果中间驱动程序为较低驱动程序创建 IRP,它应调用 IoAllocateIrp、 IoBuildDeviceIoControlRequest、 IoBuildSynchronousFsdRequest 或 IoBuildAsynchronousFsdRequest。 但是, IoBuildSynchronousFsdRequest 只能在以下情况下调用:

由驱动程序创建的线程为读取或写入请求生成 IRP,因为此类线程可以在调度程序对象(如传递给 IoBuildSynchronousFsdRequest 的驱动程序初始化事件)的线程上下文中等待

  • 在初始化期间或在卸载时在系统线程上下文中
  • 为固有同步操作(例如创建、刷新、关闭、关闭和设备控制请求)生成 IRP

但是,与 IoBuildSynchronousFsdRequest 相比,驱动程序更可能调用 IoBuildDeviceIoControlRequest 来分配设备控制 IRP。

I/O请求的生存期

和我们想象的不一样,每个IRP在我们收到直到调用IRP处理函数IoCancelIrp、IoCallDriver 、IoCompleteRequest之前都是有效的,但是不要这么做,这么做是有可能带来问题的。

按照内核编程的风格,会有几种情况非常特殊,会让我们不得不维护自己的IRP请求队列,但是维护队列并不代表我们一定随时随地访问它们,按照安全性的描述,我们尽可能在Dispatch例程以及IoComplete完成例程、CancelIrp例程这几个明确的例程中访问它们。

第一种情况是一个IRP被分为几个IRP去执行,这种情况下,驱动需要将IRP存入自己的队列中,然后在完成的时候调用IoCompleteRequest完成它,但是这种情况下,可能导致性能下降;

第二种情况是异步I/O,在异步I/O中,往往会设置PEEDING标志以及通知事件,这样上层可以将控制流转向,直到通知事件被触发。

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 请求之前,不一定处理每个 IRP 以完成。

当驱动程序收到 IRP 时,它会通过尽可能多地执行特定于 IRP 的处理来做出响应。 如果驱动程序支持异步 IRP 处理,它可以根据需要将 IRP 发送到下一个驱动程序,并开始处理下一个 IRP,而无需等待第一个 IRP 完成。 驱动程序可以注册IoComplete例程,当另一个驱动程序处理完 IRP 时,I/O 管理器会调用该例程。 驱动程序在 IRP 的 I/O 状态块中提供状态值,其他驱动程序可以访问该值来确定 I/O 请求的状态。

驱动程序可以在设备对象的 设备扩展中维护有关其当前 I/O 操作的状态信息。

取消I/O请求

IRP 可能保持无限期排队 (以便用户可以取消以前提交的 I/O 请求的驱动程序) 必须具有一个或多个 Cancel 例程才能完成用户取消的 I/O 请求。 例如,键盘、鼠标、并行、串行和声音设备驱动程序 (或分层) 和文件系统驱动程序应具有 Cancel 例程。

适用于 Microsoft Windows XP 和更高版本的操作系统的驱动程序可以使用保证取消安全的 IRP 队列, 而不是实现自己的 Cancel 例程。

“取消 IRP”意味着在保持系统完整性的同时尽快完成 IRP。 

取消过程在系统或驱动程序调用 IoCancelIrp 时开始。 对于与尚未完全完成的线程关联的每个 IRP,都会调用此例程。 如果启动 I/O 请求的线程退出,系统将取消未处理的 IRP。 驱动程序只能取消已创建的 IRP, (请参阅 为 Lower-Level Drivers 创建 IRP。)

如果取消的 IRP 未在 5 分钟内完成,I/O 管理器将认为 IRP 超时。此类 IRP 与线程取消关联,并且会为当前拥有 IRP 的设备记录错误。 应确保驱动程序中可能需要很长时间才能完成的任何请求都是可取消的。

I/O 管理器调用驱动程序提供的 Cancel 例程,其中包含要取消的输入 IRP 和表示 I/O 请求的目标设备的 DeviceObject 指针。

IRP 可能是驱动程序的 DispatchReadWrite 例程在用户关闭当前 Win32 应用程序时已排队的 IRP。 IRP 也可能是更高级别驱动程序显式取消的 IRP,具体取决于基础设备的性质。

调用 Cancel 例程时,如果驱动程序具有 StartIo 例程,则输入 IRP 可能已经是目标设备对象中的 CurrentIrp,或者可能已在与目标设备对象关联的设备队列中。 如果驱动程序没有 StartIo 例程,则调用其 Cancel 例程时,IRP 可能位于驱动程序管理的 IRP 内部队列中。 在任何情况下,在 I/O 管理器为传入 IRP 调用 Cancel 例程之前,I/O 管理器会将此 IRP 中的 Cancel 成员设置为 TRUE ,并将 IRP 中的 CancelRoutine 成员设置为 NULL。具有关联 IRP 的主 IRP 的 Cancel 例程负责调用 IoCancelIrp 来取消这些关联的 IRP。

所有 Cancel 例程必须遵循以下准则:

  • 调用 IoReleaseCancelSpinLock 以释放系统的取消旋转锁;
  • 将 I/O 状态块的 Status 成员设置为 STATUS_CANCELLED,并将其 信息 成员设置为零。
  • 通过调用 IoCompleteRequest 完成指定的 IRP;
  • 由于 始终调用 Cancel 例程并保留系统取消旋转锁,因此此例程不得调用 IoAcquireCancelSpinLock ,除非它先调用 IoReleaseCancelSpinLock ;
  • 当系统返回控件时,Cancel 例程不能持有系统取消旋转锁。 也就是说,每个 Cancel 例程必须至少调用 IoReleaseCancelSpinLock 一次,然后才能返回控制权;
  • 如果调用 IoAcquireCancelSpinLock, 则 Cancel 例程必须尽快对 IoReleaseCancelSpinLock 进行倒数调用;
  • 切勿在按住旋转锁时使用 IRP 调用 IoCompleteRequest 。 尝试在按住旋转锁时完成 IRP 可能会导致死锁;
远程I/O请求

一般的,每个驱动导出的例程中,至少包含1个DeviceObject,就是它自身,在这种情况下,IRP的下一级驱动非常明确,调用IoAttachDeviceToDeviceStack建立设备栈的时候,明确了下一级的驱动对象和设备对象。

但是问题在于总会有这样的需求,就是向非设备栈下一级的设备对象发送I/O请求,这种情况一般不会是上层发过来的I/O请求,而是自身的创建的I/O请求。

在前面的已经说过,I/O管理器是依赖于IRP本身来挂你IRP的,故远程I/O其实只需要创建IRP的时候,将一对应的结构设置好,就可以实现控制了。

最关键的一步就是,我们需要找到调用IoCallDriver需要的参数,它的参数既可以是自身对应的设备对象,也可以是系统中其它的设备对象,而系统中的设备对象是可以打开的。