【驱动笔记10】再谈IRP

时间:2022-10-18 17:44:01

文章作者:grayfox
作者主页:http://nokyo.blogbus.com
原始出处:http://www.blogbus.com/nokyo-logs/34010655.html

这一节会对IRP进行稍微详细的讲解,这是在看了王艳平的《Windows网络与通信程序设计》之后理解的。
      在Windows中几乎所有的I/O都是通过包(packet)驱动的,每个单独的I/O由一个工作命令描述,此命令将会告诉驱动程序需要一些什么操作,并通过I/O子系统跟踪处理过程。这些工作命令就表现为一个个被称为IRP的数据结构。
      IRP是从未分页内存申请的大小可变的结构,它由两部分组成:
      1,包含一般簿记信息的头区域,即IRP头。
      2,若干个成为I/O堆栈位置的内存块,即I/O堆栈。

IRP头部是一个固定长度的结构,它包含了关于整个I/O请求的各种信息。其中的一部分信息可以通过驱动程序访问,另一部分仅供I/O管理器使用,下面列出了允许驱动程序访问的域:

I/O_STATUS_BLOCK    I/OStatus;          // 包含I/O请求的状态
PVOID    AssociatedIrp.SystemBuffer;    // 指向系统缓冲区空间(使用缓冲区I/O)
PMDL    MdlAddress;         // 指向用户缓冲区空间的内核描述表(使用直接I/O)
PVOID    UserBuffer;         // I/O缓冲区的用户空间地址
BOOLEAN    Cancel;          // 指明此IRP已经被取消

I/OStatus的成员包含I/O操作的最终状态,当驱动程序将要完成IRP处理时,它将IRP的Status状态域设置为STATUS_XXX,同时应该将它的InformatI/On域设为0(如果有错误发生)或者一个功能代码指定的值(例如传输的字节数)。

紧跟在IRP头部之后的是I/O堆栈,这是一个I/O_STACK_LOCATI/ON结构的数组,这个数组中元素的个数是根据不同的实际情况而定的。
      I/O堆栈的主要目的是保存功能代码和I/O请求的参数,至于MajorFunctI/On域的作用就不用废话了,Parameters这个联合体需要根据不同情况作出不同的解释,它有Read、Write、DeviceI/OControl三种可能的解释,分别对应三个Win32 API函数,下面列出了I/O堆栈中一些常用的成员域:

UCHAR MajorFunctI/On;               // IRP_MJ_XXX功能代码
struct Read;                               // IRP_MJ_READ的参数
struct Write;                              // IRP_MJ_WRITE的参数
struct DeviceI/OControl;              // IRP_MJ_DEVICE_CONTROL的参数
PDEVICE_OBJECT DeviceObject;   // 该I/O请求的目标设备
PFILE_OBJECT FileObject;            // 该I/O请求的文件对象(如果有的话)

其中FileObject对应着用户模式下CreateFile函数创建的文件对象,FILE_OBJECT结构中的FsContext和FsContext2域是Per-Handle变量,即文件句柄唯一变量,驱动程序可在这里指定要为每个文件对象关联的数据。

Windows操作系统家族支持三种数据传输机制: 
      缓存 I/O(Buffered I/O)在内核模式上操作对用户数据的拷贝 
      直接 I/O(Direct I/O)通过内存描述元列表(MDL, Memory Descriptor List)以及内核模式的指针直接访问用户数据 
      其他方式 I/O(Method neither I/O,既非缓存,也非直接 I/O)通过用户模式的指针访问用户数据

对于标准的 I/O请求,例如IRP_MJ_READ和 IRP_MJ_WRITE,应该在驱动刚刚创建设备后,马上通过修改DeviceObject->Flags域的值来指定支持哪一种传输机制。

缓存 I/O 
 
      为了以缓存 I/O的方式接收读、写的请求,驱动会在初始化时在DeviceObject->Flags域上设置DO_BUFFERED_I/O标志。当驱动收到了一个缓存I/O 的请求,在特定的Irp->AssociatedIrp.SystemBuffer 域中会放有驱动应该操作的内核模式缓冲区的地址。 I/O 管理器,在进行读请求时将数据由内核模式缓冲区拷贝到用户模式缓冲区,或者在进行写请求时从用户模式缓冲区向内核模式缓冲区拷贝数据。 
 
直接 I/O 
 
      为了以直接 I/O的方式接收读、写请求,驱动会在初始化时在DeviceObject->Flags域上设置DO_DIRECT_I/O标志。当驱动接收到一个直接 I/O请求,特定的Irp->MdlAddress域中会放有一个用来描述请求缓冲区的MDL的地址。这个MDL列出了缓冲区的虚拟地址和尺寸,连同相应缓冲区中的物理页表(physical pages)。I/O 管理器会在将请求发送给驱动之前锁定这些物理页,并在(请求)完成的过程中解锁。驱动千万不能使用 MDL中列举的用户模式缓冲区地址,而必须通过调用 MmGetSystemAddressForMdlSafe宏来得到一个内核模式的地址。 
 
其他方式 I/O 
 
      为了接收非缓存非直接 I/O 的方式的请求,驱动初始化时在 DeviceObject->Flags域上既不设置 DO_BUFFERED_I/O 标志,也不设置 DO_DIRECT_I/O 标志。当驱动接收到这样的请求,相应的 Irp->UserBuffer 域会放有附属于这个请求的数据地址。因为这个缓冲区在用户地址空间上,驱动程序必须在用之前使相应的地址合法化。驱动程序在 try/except块里调用ProbeForRead或者 ProbeForWrite函数来合法化特定的指针。驱动还必须完全在 try/except块里处理所有对这一缓冲区的访问。 
另外,驱动还必须在应用(manipulating)数据之前将它拷贝到池(the pool)或堆栈里一个安全的内核模式地址。将数据拷贝到内核模式缓冲区确保了用户模式的调用者不会在驱动已经合法化数据之后再修改它。