前言:
再过几个月我就要硕士毕业了(惭愧,是工程硕士)
学校要求论文中必须包括一定数量的翻译材料,虽然我水平很菜,但实在不想去抄别人的东西,所以自己找了DDK中的一点内容翻译了一下。熬了5个多小时才完成,现在拿出来和广大初学者分享一下,希望能有一些用。
原文是:Windows 2000 ddk -> Kernel Mode Drivers-> design guide -> Part 2的前半部分关于IRP的内容。这是我第一次翻译这么长的东西,在文法和术语的处理上可能不大好,希望大家多多批评指正:)
2.1 Windows2000 I/O 模型:
所有的操作系统的内部都包含一个潜在或者明确的I/O处理机制,用于处理外部设备和主机的数据交换。在第一章中我们已经知道Windows2000 I/O处理机制具有支持异步I/O请求的特征,Windows2000的I/O处理机制还包含以下特点:
1. 所有的内核模式的驱动程序都由系统的I/O管理器(I/O Manager)来提供统一的调用界面,其中包括最底层(lowest-level)、内部介质(intermediate)和文件系统(file system)驱动程序。那些针对驱动程序的I/O请求都将被封装成为I/O请求包(I/O request packets,IRPs)。
2. I/O操作的处理是一个层次化模型(layered)。I/O管理器提供系统的I/O服务,应用程序通过用户模式下的保护子系统(user-mode protected subsystem)调用这些I/O服务来执行特定的I/O操作,当I/O管理器接收到I/O调用时,它会建立一个或者多个I/O请求包(IRPs),并经过多个层次的驱动程序最终将请求传递到设备。
3. I/O管理器定义了一系列的标准函数,驱动程序必须实现其中必须的实现的函数,或者可选择地实现其他函数。驱动程序通过选择实现不同的函数来驱动总线,功能设备,文件系统等。
4. 和操作系统一样,驱动程序是基于对象的。驱动程序、所驱动的设备和系统硬件都以对象的方式存在。I/O管理器和其他的操作系统组件会提供一些内核模式(kernel-mode)下的系统调用,让驱动程序管理这些对象。
另外,在处理即插即用和电源管理信息,I/O管理器协同即插即用(PnP)管理器和电源管理器(PnP and Power Managers)发送包含即插即用和电源信息的请求。请参考“安装、即插即用及电源管理设计指南”一章学习如何处理这类I/O请求包
2.2 用户I/O请求和Windows 2000 文件对象
内核模式驱动程序(Kernel-mode drivers)对用户是不可见的,用户可通过特定的文件句柄来访问设备。设备的文件句柄由I/O管理器提供,被置于保护子系统的保护之下。图2.1解释了用户、子系统和I/O管理器的关系
图2.1 文件对象代表文件,卷和设备
Windows2000保护子系统,如Win32子系统,将I/O请求通过I/O系统服务传递给内核模式下的驱动程序,在图2.1中标明的子系统(subsystem)依赖显示、视频、键盘和鼠标的设备驱动程序。
由于保护子系统的存在,最终用户和应用程序不需要关心内核中的任何组件(包括驱动程序),其中I/O管理器的职责就是使最终用户、应用程序和具体的机器硬件、驱动程序隔离开来。同时I/O管理器还将驱动程序和以下各项隔离开来:
1、 I/O请求具体来自于什么地方,如来源于Win32或POSIX子系统
2、 特定的子系统中是否包含对应的用户模式驱动程序
3、 子系统中采用什么样的I/O模型和设备访问方式
I/O管理器为驱动程序提供一个单一的I/O管理机制:一系列用于驱动程序执行I/O请求的函数,以及一个介于I/O请求发起者和驱动程序响应之间的统一界面。
如图2.1子系统及子系统下的程序只能通过文件句柄访问数据文件或者设备,子系统使用一个文件名通过调用I/O系统服务打开数据文件或者设备句柄,文件名可以是一个由子系统映射到内核设备名称的别名。
负责提供这些系统服务的I/O管理器有责任定位、创建代表设备或数据文件的文件句柄,并使用相应的驱动程序。图2.2演示了子系统打开数据文件句柄时的过程:
图2.2子系统打开数据文件句柄时的过程:
1、 子系统使用文件名调用系统I/O服务,请求打开一个文件
2、 I/O管理器调用对象管理器按文件名该文件对象的符号连接,同时调用安全模块(Security Reference Monitor)检查子系统是否具有打开这个文件对象的权限
3、 如果卷没有加载,I/O管理器暂停这个打开文件的请求并调用一个或者多个Windows 2000文件系统直到其中一个识别出这个文件对象处于自己所管理的块存储设备上,当文件系统挂载对应的卷之后,I/O管理器重新恢复打开文件的请求。
4、 I/O管理器初始化一个代表打开文件的I/O请求包(IRP)。对于驱动程序来说,一个打开请求等价于一个“创建”请求。
5、 I/O管理器将I/O请求包(IRP)传递给文件系统驱动程序,文件系统驱动程序访问这个I/O请求包的堆栈中对应该驱动程序的位置来取人自己应该执行什么样的操作、检查参数、检查当前文件是否在缓存中,如果文件不在缓存中,就在I/O请求包(IRP)的堆栈中用于指定下一个驱动程序的位置设置下一层驱动程序的指针。
6、 驱动程序在处理I/O请求包(IRP)和完成I/O请求的过程中都会调用I/O管理器或其他系统组件在内核模式中提供的函数(图中未标出调用其他系统组件的过程)。
7、 驱动程序设置I/O请求包(IRP)中的状态块(status block), 返回I/O请求包(IRP)给I/O管理器,告诉I/O管理器该操作已经成功或者该操作为什么失败。
8、 I/O管理器根据来自于I/O请求包(IRP)的状态信息将操作结果通过保护子系统返回给I/O请求最初的发起者。
9、 I/O管理器释放被完成的I/O请求包(IRP)。
10、 如果打开操作成功I/O管理器将代表文件对象的句柄返回给子系统,反之打开操作失败的话,I/O管理器将返回相应的错误信息给子系统。
当子系统成功打开一个代表数据文件、设备或卷的文件句柄之后,通常应用程序会通过子系统使用句柄进行一系列的I/O请求(诸如读、写以及设备控制ioctl操作)。这些I/O请求仍然会被子系统传递给I/O管理器,I/O管理器仍然将使用I/O请求包(IRPs)将请求传递给对应的设备驱动程序
2.2.1 处理用户I/O请求需要考虑的几点问题
设计一个驱动程序需要牢记以下几点:
1、 驱动程序体系是层次化的,一个I/O请求包(IRP)可能被多个驱动程序处理
2、 任何一个驱动程序都不能假定一定会有其他的驱动程序处于设备堆栈中。也就是说,每个驱动程序都应该处理来自于其他任何驱动程序的请求并处理各种潜在的错误。
3、 驱动程序是通过I/O请求包(IRP)的状态块(status block of the IRP)来说明一个I/O操作是否成功的;而I/O管理器(I/O Manager)负责直接向用户模式下的请求返回I/O操作成功或者失败信息。
4、 驱动程序部需要也不应该直接提供应用程序所期待的功能,由保护子系统或者子系统的模块以及用户模式下的驱动程序来提供这种支持。
2.3 I/O请求包和驱动程序的I/O堆栈位置
图2.2显示了一个拥有两个I/O堆栈位置,但是一个IRP实际上可能有多个数量的I/O堆栈位置,I/O堆栈位置的数量具体依赖于将有多少层驱动程序将处理这个请求。图2.3更加清晰地显示了图2.2中的驱动程序是如何使用I/O支持函数来处理一个读或写请求的IRP
图2.3 多层驱动程序如何处理I/O请求包
1、 I/O管理器将自己根据子系统的读写请求所分配的I/O请求包(IRP)传递给文件系统驱动程序(file system driver FSD)。文件系统驱动程序访问I/O请求包中属于他自己的堆栈位置来确定进行什么样的操作。
2、 由于有可能请求多个下级设备,文件系统驱动程序可以多次通过调用IoAllocateIrp函数将原有的请求分解成为多个更小的请求。这些新分配的I/O请求包的I/O堆栈位置的值均为0。也可以需要,文件系统驱动程序复用原有的I/O请求包(如图2.3所示),并设置I/O请求包的堆栈中关于下一个驱动程序指针的值,并将该报文传递给下个驱动程序。
3、 对于每个由驱动程序分配的I/O请求包(IRP),图2.3中,文件系统驱动程序调用IoSetCompletionRoutine在所分配的I/O请求包上登记自己的结束函数,这样文件系统驱动程序就能跟踪到底层驱动程序完成所请求的I/O请求的状况,并在请求完成之后释放自己分配的I/O请求包。因为不管这些请求完成是否成功,I/O管理器根据I/O请求包(IRP)中文件系统所登记的结束函数地址调用文件系统驱动程序提供的结束函数。高层次的驱动程序有责任释放自己分配的I/O请求包(IRP),当所有的驱动程序协作完成I/O管理器所分配的I/O请求包(IRP)时,I/O管理器负责释放它所分配的I/O请求包(IRP)。
下一步,文件驱动程序调用IoGetNextIrpStackLocation访问I/O请求包(IRP)中关于下一个驱动程序位置的指针变量,通过设置这个值并调用IoCallDriver可以将I/O请求包(IRP)发送到下一个驱动程序(图2.3中,下一个驱动程序正好是最底层的驱动程序)。
4、 当最底层的驱动程序接受到I/O请求包(IRP)时,他会检查I/O请求包的堆栈中属于他自己的位置,判断命令目标设备进行什么样的操作(通过IRP_MJ_XXX 代码值)。目标设备由他自己的I/O堆栈中的设备对象表示,并和I/O请求包(IRP)一块传递过来。作为最底层的驱动程序可以认为I/O请求包(IRP)均由I/O管理器传递到驱动程序注册的处理IRP_MJ_XXX消息的某一个入口(entry point)的I/O请求包(图中为IRP_MJ_READ或者IRP_MJ_WRITE),并认为上层驱动程序已经检查了I/O请求包中参数的合法性。
对于没有任何上级驱动程序的情况,最低层驱动程序应该在处理IRP_MJ_XXX消息的入口检查所有输入参数的合法性。如果合法,驱动程序通常调用I/O系统服务函数告诉I/O管理器现在这个I/O请求包(IRP)有一个悬而未决的设备操作并将这个I/O请求包存入队列或者将这个I/O请求包传递给驱动程序制定的其他用于控制设备进行工作的函数(可能是一个物理或逻辑设备:如磁盘或者磁盘上的分区)。
5、 如果I/O管理器发现设备正在处理其他的I/O请求,I/O管理器将会把这个I/O请求包排入队列并返回。否则,I/O管理器将把I/O请求包传送给驱动程序指定的用于在设备上启动对应操作的入口函数。(图2.3中,驱动程序和I/O管理器均直接返回)
6、 当设备发生中断,驱动程序的中断服务函数(Interrupt Service Routine ISR)只需要做一些能够立即停止设备的中断状态、保存必要信息的一些操作。然后中断服务函数ISR会调用IoRequestDpc函数,将I/O请求包排入驱动程序的延期函数(Deferred Procedure Call DPC)的队列中,延期函数在一个低于中断服务函数(ISR)优先级的优先权下完成整个请求
7、 当驱动程序的延期函数被调用时,他使用中断程序调用IoRequestDpc时提供的上下文(context passed in the ISR’s call to IoRequestDpc)来完成I/O 操作。如果此时请求队列仍然有请求需要处理延期函数将调用一个系统服务从请求队列中取出下一个I/O请求包,并传递给驱动程序用于在设备上启动I/O操作的函数。延期函数然后调用IoCompleteRequest函数将刚刚完成的I/O处理包返回给I/O管理器
8、 I/O管理器将I/O请求包的I/O堆栈中指向最低层驱动程序的指针清零,对于文件系统驱动程序分配的那些I/O请求包,将调用文件系统驱动程序在I/O请求包中注册的结束函数(参考步骤3)。这个结束函数将检查I/O请求包中I/O堆栈中的状态块(status block)以便知道是否重试该请求或者更新一些关于最初的I/O请求的完成状态,并释放那些由驱动程序分配的I/O请求包。通过这种方式文件系东驱动程序可以收集到所有由他分配的I/O请求包所返回的信息,当文件系统驱动程序完成了整个最初的I/O请求时,I/O管理器将返回一个恰当的NTSTATUS值给最初的I/O请求者。
图2.3中,最初的I/O请求包有两个I/O堆栈项目(I/O statck location),其原因是将有两个驱动程序:文件系统驱动程序和一个块存储设备驱动程序来完成这次I/O请求,I/O管理器在I/O请求包中为每个处于驱动程序链中的驱动程序分配一个I/O堆栈项目。相比之下由文件系统驱动程序分配的I/O请求包中并不包含文件系统驱动程序的I/O堆栈项目。任何高层次的驱动程序为低层次的驱动程序分配I/O请求包的时候都需要根据链表中下一个驱动程序的设备对象中的StackSize属性判断新分配的I/O请求包应该有多少个I/O堆栈项目。
图2.4显示了I/O请求包IRP更详细的信息
图2.4 IRP中I/O堆栈项目的内容(Contents of I/O Stack Location in an IRP)
如图2.4中所示,IRP中每个用于指定驱动程序的I/O堆栈项目都包含以下信息:
1、 主要函数代码(major function code IRP_MJ_XXX),表明驱动程序应该执行哪个主要处理函数(major function)
2、 对于文件驱动程序、高层次SCSI驱动程序和所有的即插即用设备的驱动程序的一些主要处理函数,次要函数代码(minor function code IRP_MN_XXX)说明了驱动程序处理基本主要函数代码时需要考虑的一些子状况。
3、 一系列依赖于具体操作的参数,如一个驱动程序用于传输数据的缓冲区的长度和起始位置
4、 一个指向由驱动程序创建的设备对象句柄,表征当前操作请求的目标设备(可以是一个物理设备,逻辑设备和虚拟设备)
5、 一个文件句柄指针,表征一个打开的文件,设备,目录,或者卷(文件系统驱动程序通常使用I/O请求包的I/O堆栈中的文件句柄,其他的一些驱动程序往往会忽略这个文件句柄)
在请求包中的主要和次要功能代码是可以由具体的驱动程序自定义的,但是最低层的驱动程序和介质驱动程序都应该处理如下基本功能请求:
1、 IRP_MJ_CREATE
2、 IRP_MJ_READ
3、 IRP_MJ_WRITE
4、 IRP_MJ_DEVICE_CONTROL
5、 IRP_MJ_CLOSE
6、 IRP_MJ_PNP
7、 IRP_MJ_POWER
具体的功能代码和设备I/O操作码的含义请参照《内核模式参考手册》和《安装,即插即用和电源管理参考手册》
一般来说,I/O管理器总是会发送一个具有两个I/O堆栈项目的I/O请求包,因为总是会有一个文件系统处于一个块存储设备之上的。对于那些没有上层驱动程序的驱动程序,I/O管理器只会发送一个只有一个I/O堆栈项目的I/O请求包。
当然,I/O管理器也允许新的驱动程序插入到任何一个现有的驱动程序链中。比方说一个用于备份数据用的介质镜像驱动程序就可以插入到一个现存的文件系统驱动程序和块驱动程序之间。当这样一个新的驱动程序插入之后,I/O管理器将调整所有的发送到文件系统驱动程序的I/O请求包,包中的I/O堆栈项目将增加到三个。
请注意:这种支持新驱动程序插入到现有驱动程序链的机制暗示着任何一个驱动程序访问I/O堆栈项目都需要注意的潜在规则:
1、在任何I/O请求包中位于链表中的高层次驱动程序只能安全地访问他自己和下一个驱动程序的I/O堆栈项,这类驱动程序都必须在I/O请求包中为下一个驱动程序设置正确的I/O堆栈项目。也就是说,当设计一个高层次驱动程序的时候,你不能预知到何时会有一个驱动程序会插入到当前驱动程序的下方。
因此,程序员必须假定任何一个插入的驱动程序都回处理向同的I/O请求包功能代码。
2.1 Windows2000 I/O 模型:
所有的操作系统的内部都包含一个潜在或者明确的I/O处理机制,用于处理外部设备和主机的数据交换。在第一章中我们已经知道Windows2000 I/O处理机制具有支持异步I/O请求的特征,Windows2000的I/O处理机制还包含以下特点:
1. 所有的内核模式的驱动程序都由系统的I/O管理器(I/O Manager)来提供统一的调用界面,其中包括最底层(lowest-level)、内部介质(intermediate)和文件系统(file system)驱动程序。那些针对驱动程序的I/O请求都将被封装成为I/O请求包(I/O request packets,IRPs)。
2. I/O操作的处理是一个层次化模型(layered)。I/O管理器提供系统的I/O服务,应用程序通过用户模式下的保护子系统(user-mode protected subsystem)调用这些I/O服务来执行特定的I/O操作,当I/O管理器接收到I/O调用时,它会建立一个或者多个I/O请求包(IRPs),并经过多个层次的驱动程序最终将请求传递到设备。
3. I/O管理器定义了一系列的标准函数,驱动程序必须实现其中必须的实现的函数,或者可选择地实现其他函数。驱动程序通过选择实现不同的函数来驱动总线,功能设备,文件系统等。
4. 和操作系统一样,驱动程序是基于对象的。驱动程序、所驱动的设备和系统硬件都以对象的方式存在。I/O管理器和其他的操作系统组件会提供一些内核模式(kernel-mode)下的系统调用,让驱动程序管理这些对象。
另外,在处理即插即用和电源管理信息,I/O管理器协同即插即用(PnP)管理器和电源管理器(PnP and Power Managers)发送包含即插即用和电源信息的请求。请参考“安装、即插即用及电源管理设计指南”一章学习如何处理这类I/O请求包
2.2 用户I/O请求和Windows 2000 文件对象
内核模式驱动程序(Kernel-mode drivers)对用户是不可见的,用户可通过特定的文件句柄来访问设备。设备的文件句柄由I/O管理器提供,被置于保护子系统的保护之下。图2.1解释了用户、子系统和I/O管理器的关系
图2.1 文件对象代表文件,卷和设备
Windows2000保护子系统,如Win32子系统,将I/O请求通过I/O系统服务传递给内核模式下的驱动程序,在图2.1中标明的子系统(subsystem)依赖显示、视频、键盘和鼠标的设备驱动程序。
由于保护子系统的存在,最终用户和应用程序不需要关心内核中的任何组件(包括驱动程序),其中I/O管理器的职责就是使最终用户、应用程序和具体的机器硬件、驱动程序隔离开来。同时I/O管理器还将驱动程序和以下各项隔离开来:
1、 I/O请求具体来自于什么地方,如来源于Win32或POSIX子系统
2、 特定的子系统中是否包含对应的用户模式驱动程序
3、 子系统中采用什么样的I/O模型和设备访问方式
I/O管理器为驱动程序提供一个单一的I/O管理机制:一系列用于驱动程序执行I/O请求的函数,以及一个介于I/O请求发起者和驱动程序响应之间的统一界面。
如图2.1子系统及子系统下的程序只能通过文件句柄访问数据文件或者设备,子系统使用一个文件名通过调用I/O系统服务打开数据文件或者设备句柄,文件名可以是一个由子系统映射到内核设备名称的别名。
负责提供这些系统服务的I/O管理器有责任定位、创建代表设备或数据文件的文件句柄,并使用相应的驱动程序。图2.2演示了子系统打开数据文件句柄时的过程:
图2.2子系统打开数据文件句柄时的过程:
1、 子系统使用文件名调用系统I/O服务,请求打开一个文件
2、 I/O管理器调用对象管理器按文件名该文件对象的符号连接,同时调用安全模块(Security Reference Monitor)检查子系统是否具有打开这个文件对象的权限
3、 如果卷没有加载,I/O管理器暂停这个打开文件的请求并调用一个或者多个Windows 2000文件系统直到其中一个识别出这个文件对象处于自己所管理的块存储设备上,当文件系统挂载对应的卷之后,I/O管理器重新恢复打开文件的请求。
4、 I/O管理器初始化一个代表打开文件的I/O请求包(IRP)。对于驱动程序来说,一个打开请求等价于一个“创建”请求。
5、 I/O管理器将I/O请求包(IRP)传递给文件系统驱动程序,文件系统驱动程序访问这个I/O请求包的堆栈中对应该驱动程序的位置来取人自己应该执行什么样的操作、检查参数、检查当前文件是否在缓存中,如果文件不在缓存中,就在I/O请求包(IRP)的堆栈中用于指定下一个驱动程序的位置设置下一层驱动程序的指针。
6、 驱动程序在处理I/O请求包(IRP)和完成I/O请求的过程中都会调用I/O管理器或其他系统组件在内核模式中提供的函数(图中未标出调用其他系统组件的过程)。
7、 驱动程序设置I/O请求包(IRP)中的状态块(status block), 返回I/O请求包(IRP)给I/O管理器,告诉I/O管理器该操作已经成功或者该操作为什么失败。
8、 I/O管理器根据来自于I/O请求包(IRP)的状态信息将操作结果通过保护子系统返回给I/O请求最初的发起者。
9、 I/O管理器释放被完成的I/O请求包(IRP)。
10、 如果打开操作成功I/O管理器将代表文件对象的句柄返回给子系统,反之打开操作失败的话,I/O管理器将返回相应的错误信息给子系统。
当子系统成功打开一个代表数据文件、设备或卷的文件句柄之后,通常应用程序会通过子系统使用句柄进行一系列的I/O请求(诸如读、写以及设备控制ioctl操作)。这些I/O请求仍然会被子系统传递给I/O管理器,I/O管理器仍然将使用I/O请求包(IRPs)将请求传递给对应的设备驱动程序
2.2.1 处理用户I/O请求需要考虑的几点问题
设计一个驱动程序需要牢记以下几点:
1、 驱动程序体系是层次化的,一个I/O请求包(IRP)可能被多个驱动程序处理
2、 任何一个驱动程序都不能假定一定会有其他的驱动程序处于设备堆栈中。也就是说,每个驱动程序都应该处理来自于其他任何驱动程序的请求并处理各种潜在的错误。
3、 驱动程序是通过I/O请求包(IRP)的状态块(status block of the IRP)来说明一个I/O操作是否成功的;而I/O管理器(I/O Manager)负责直接向用户模式下的请求返回I/O操作成功或者失败信息。
4、 驱动程序部需要也不应该直接提供应用程序所期待的功能,由保护子系统或者子系统的模块以及用户模式下的驱动程序来提供这种支持。
2.3 I/O请求包和驱动程序的I/O堆栈位置
图2.2显示了一个拥有两个I/O堆栈位置,但是一个IRP实际上可能有多个数量的I/O堆栈位置,I/O堆栈位置的数量具体依赖于将有多少层驱动程序将处理这个请求。图2.3更加清晰地显示了图2.2中的驱动程序是如何使用I/O支持函数来处理一个读或写请求的IRP
图2.3 多层驱动程序如何处理I/O请求包
1、 I/O管理器将自己根据子系统的读写请求所分配的I/O请求包(IRP)传递给文件系统驱动程序(file system driver FSD)。文件系统驱动程序访问I/O请求包中属于他自己的堆栈位置来确定进行什么样的操作。
2、 由于有可能请求多个下级设备,文件系统驱动程序可以多次通过调用IoAllocateIrp函数将原有的请求分解成为多个更小的请求。这些新分配的I/O请求包的I/O堆栈位置的值均为0。也可以需要,文件系统驱动程序复用原有的I/O请求包(如图2.3所示),并设置I/O请求包的堆栈中关于下一个驱动程序指针的值,并将该报文传递给下个驱动程序。
3、 对于每个由驱动程序分配的I/O请求包(IRP),图2.3中,文件系统驱动程序调用IoSetCompletionRoutine在所分配的I/O请求包上登记自己的结束函数,这样文件系统驱动程序就能跟踪到底层驱动程序完成所请求的I/O请求的状况,并在请求完成之后释放自己分配的I/O请求包。因为不管这些请求完成是否成功,I/O管理器根据I/O请求包(IRP)中文件系统所登记的结束函数地址调用文件系统驱动程序提供的结束函数。高层次的驱动程序有责任释放自己分配的I/O请求包(IRP),当所有的驱动程序协作完成I/O管理器所分配的I/O请求包(IRP)时,I/O管理器负责释放它所分配的I/O请求包(IRP)。
下一步,文件驱动程序调用IoGetNextIrpStackLocation访问I/O请求包(IRP)中关于下一个驱动程序位置的指针变量,通过设置这个值并调用IoCallDriver可以将I/O请求包(IRP)发送到下一个驱动程序(图2.3中,下一个驱动程序正好是最底层的驱动程序)。
4、 当最底层的驱动程序接受到I/O请求包(IRP)时,他会检查I/O请求包的堆栈中属于他自己的位置,判断命令目标设备进行什么样的操作(通过IRP_MJ_XXX 代码值)。目标设备由他自己的I/O堆栈中的设备对象表示,并和I/O请求包(IRP)一块传递过来。作为最底层的驱动程序可以认为I/O请求包(IRP)均由I/O管理器传递到驱动程序注册的处理IRP_MJ_XXX消息的某一个入口(entry point)的I/O请求包(图中为IRP_MJ_READ或者IRP_MJ_WRITE),并认为上层驱动程序已经检查了I/O请求包中参数的合法性。
对于没有任何上级驱动程序的情况,最低层驱动程序应该在处理IRP_MJ_XXX消息的入口检查所有输入参数的合法性。如果合法,驱动程序通常调用I/O系统服务函数告诉I/O管理器现在这个I/O请求包(IRP)有一个悬而未决的设备操作并将这个I/O请求包存入队列或者将这个I/O请求包传递给驱动程序制定的其他用于控制设备进行工作的函数(可能是一个物理或逻辑设备:如磁盘或者磁盘上的分区)。
5、 如果I/O管理器发现设备正在处理其他的I/O请求,I/O管理器将会把这个I/O请求包排入队列并返回。否则,I/O管理器将把I/O请求包传送给驱动程序指定的用于在设备上启动对应操作的入口函数。(图2.3中,驱动程序和I/O管理器均直接返回)
6、 当设备发生中断,驱动程序的中断服务函数(Interrupt Service Routine ISR)只需要做一些能够立即停止设备的中断状态、保存必要信息的一些操作。然后中断服务函数ISR会调用IoRequestDpc函数,将I/O请求包排入驱动程序的延期函数(Deferred Procedure Call DPC)的队列中,延期函数在一个低于中断服务函数(ISR)优先级的优先权下完成整个请求
7、 当驱动程序的延期函数被调用时,他使用中断程序调用IoRequestDpc时提供的上下文(context passed in the ISR’s call to IoRequestDpc)来完成I/O 操作。如果此时请求队列仍然有请求需要处理延期函数将调用一个系统服务从请求队列中取出下一个I/O请求包,并传递给驱动程序用于在设备上启动I/O操作的函数。延期函数然后调用IoCompleteRequest函数将刚刚完成的I/O处理包返回给I/O管理器
8、 I/O管理器将I/O请求包的I/O堆栈中指向最低层驱动程序的指针清零,对于文件系统驱动程序分配的那些I/O请求包,将调用文件系统驱动程序在I/O请求包中注册的结束函数(参考步骤3)。这个结束函数将检查I/O请求包中I/O堆栈中的状态块(status block)以便知道是否重试该请求或者更新一些关于最初的I/O请求的完成状态,并释放那些由驱动程序分配的I/O请求包。通过这种方式文件系东驱动程序可以收集到所有由他分配的I/O请求包所返回的信息,当文件系统驱动程序完成了整个最初的I/O请求时,I/O管理器将返回一个恰当的NTSTATUS值给最初的I/O请求者。
图2.3中,最初的I/O请求包有两个I/O堆栈项目(I/O statck location),其原因是将有两个驱动程序:文件系统驱动程序和一个块存储设备驱动程序来完成这次I/O请求,I/O管理器在I/O请求包中为每个处于驱动程序链中的驱动程序分配一个I/O堆栈项目。相比之下由文件系统驱动程序分配的I/O请求包中并不包含文件系统驱动程序的I/O堆栈项目。任何高层次的驱动程序为低层次的驱动程序分配I/O请求包的时候都需要根据链表中下一个驱动程序的设备对象中的StackSize属性判断新分配的I/O请求包应该有多少个I/O堆栈项目。
图2.4显示了I/O请求包IRP更详细的信息
图2.4 IRP中I/O堆栈项目的内容(Contents of I/O Stack Location in an IRP)
如图2.4中所示,IRP中每个用于指定驱动程序的I/O堆栈项目都包含以下信息:
1、 主要函数代码(major function code IRP_MJ_XXX),表明驱动程序应该执行哪个主要处理函数(major function)
2、 对于文件驱动程序、高层次SCSI驱动程序和所有的即插即用设备的驱动程序的一些主要处理函数,次要函数代码(minor function code IRP_MN_XXX)说明了驱动程序处理基本主要函数代码时需要考虑的一些子状况。
3、 一系列依赖于具体操作的参数,如一个驱动程序用于传输数据的缓冲区的长度和起始位置
4、 一个指向由驱动程序创建的设备对象句柄,表征当前操作请求的目标设备(可以是一个物理设备,逻辑设备和虚拟设备)
5、 一个文件句柄指针,表征一个打开的文件,设备,目录,或者卷(文件系统驱动程序通常使用I/O请求包的I/O堆栈中的文件句柄,其他的一些驱动程序往往会忽略这个文件句柄)
在请求包中的主要和次要功能代码是可以由具体的驱动程序自定义的,但是最低层的驱动程序和介质驱动程序都应该处理如下基本功能请求:
1、 IRP_MJ_CREATE
2、 IRP_MJ_READ
3、 IRP_MJ_WRITE
4、 IRP_MJ_DEVICE_CONTROL
5、 IRP_MJ_CLOSE
6、 IRP_MJ_PNP
7、 IRP_MJ_POWER
具体的功能代码和设备I/O操作码的含义请参照《内核模式参考手册》和《安装,即插即用和电源管理参考手册》
一般来说,I/O管理器总是会发送一个具有两个I/O堆栈项目的I/O请求包,因为总是会有一个文件系统处于一个块存储设备之上的。对于那些没有上层驱动程序的驱动程序,I/O管理器只会发送一个只有一个I/O堆栈项目的I/O请求包。
当然,I/O管理器也允许新的驱动程序插入到任何一个现有的驱动程序链中。比方说一个用于备份数据用的介质镜像驱动程序就可以插入到一个现存的文件系统驱动程序和块驱动程序之间。当这样一个新的驱动程序插入之后,I/O管理器将调整所有的发送到文件系统驱动程序的I/O请求包,包中的I/O堆栈项目将增加到三个。
请注意:这种支持新驱动程序插入到现有驱动程序链的机制暗示着任何一个驱动程序访问I/O堆栈项目都需要注意的潜在规则:
1、在任何I/O请求包中位于链表中的高层次驱动程序只能安全地访问他自己和下一个驱动程序的I/O堆栈项,这类驱动程序都必须在I/O请求包中为下一个驱动程序设置正确的I/O堆栈项目。也就是说,当设计一个高层次驱动程序的时候,你不能预知到何时会有一个驱动程序会插入到当前驱动程序的下方。
因此,程序员必须假定任何一个插入的驱动程序都回处理向同的I/O请求包功能代码。