文章作者:grayfox
作者主页:http://nokyo.blogbus.com
原始出处:http://nokyo.blogbus.com/logs/34005738.html
此前我们可能曾经多次听说过IRP这个名词,那么它究竟是什么呢?
IRP的全名是I/O Request Package,即输入输出请求包,它是Windows内核中的一种非常重要的数据结构。上层应用程序与底层驱动程序通信时,应用程序会发出I/O请求,操作系统将相应的I/O请求转换成相应的IRP,不同的IRP会根据类型被分派到不同的派遣例程中进行处理。
IRP有两个基本的属性,即MajorFunction和MinorFunction,分别记录IRP的主类型和子类型。操作系统根据MajorFunction决定将IRP分发到哪个派遣例程,然后派遣例程根据MinorFunction进行细分处理。
IRP的概念类似于Windows应用程序中“消息”的概念。在Win32编程中,程序由“消息”驱动,不同的消息被分发到不同的处理函数中,否则由系统默认处理。
文件I/O的相关函数例如CreateFile、ReadFile、WriteFile、CloseHandle等分别会引发操作系统产生IRP_MJ_CREATE、IRP_MJ_READ、IRP_MJ_WRITE、IRP_MJ_CLOSE等不同的IRP,这些IRP会被传送到驱动程序的相应派遣例程中。
在派遣例程中处理IRP最简单做法就是将IRP的状态设置为成功,然后结束IRP请求并返回成功,同时还要记得设置这个IRP请求操作了多少字节。
我们在派遣函数中设置IRP的完成状态为STATUS_SUCCESS,发起I/O请求的Win32 API才能返回TRUE,否则Win32 API将返回FALSE,在这个时候可以通过GetLastError获得错误代码,这个错误代码会和此时IRP被设置的状态一致。
下面我们首先在驱动程序中添加一个IRP_MJ_CLEANUP的例程(照抄IRP_MJ_CLOSE的即可),然后编写下面的应用层程序(控制台程序):
01#include "windows.h"
02#include "stdio.h"
03
04int main()
05{
06 // 打开设备句柄,它会触发IRP_MJ_CREATE
07 HANDLE hDevice = ::CreateFile("\\\\.\\Test", // 符号链接
08 GENERIC_READ | GENERIC_WRITE,
09 0,
10 NULL,
11 OPEN_EXISTING,
12 FILE_ATTRIBUTE_NORMAL,
13 NULL);
14 if (hDevice == INVALID_HANDLE_VALUE)
15 {
16 printf("Try to Open Device %s Error : %d!\n", "\\\\.\\Test",::GetLastError());
17 return -1;
18 }
19
20 // 关闭设备句柄,它会触发IRP_MJ_CLEANUP和IRP_MJ_CLOSE
21 CloseHandle(hDevice);
22 return 0;
23}
在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)或堆栈里一个安全的内核模式地址。将数据拷贝到内核模式缓冲区确保了用户模式的调用者不会在驱动已经合法化数据之后再修改它。