内核模式驱动程序设计指南 第一部分 一般内核模式 一般内核模式 第1章Windows 2000和WDM驱动程序 第2章 分层的I/O、IRP和I/O对象 第3章 系统定义的对象和对驱动程序的支持 第4章 驱动程序基本结构 第5章DriverEntry 和 Reinitialize 例程 第6章Dispatch例程 第7章StartIo和队列管理例程 第8章 中断服务例程 第9章 DpcForIsr例程和CustomDpc例程 第10章SynchCritSection例程 第11章AdapterControl和ControllerControl例程 第12章Cancel例程 第13章IoCompletion例程 第14章IoTimer和CustomTimerDpc例程 第15章Unload例程 第16章 常见的驱动程序设计问题 Windows 2000和WDM驱动程序 本章叙述了什么是Windows 2000内核模式驱动程序,介绍了一些重要的驱动程序名词,并总结了内核模式驱动程序的设计目标,解释为了满足这些设计目标,Windows 2000(和WDM)的内核模式驱动程序如何不同于其他操作系统的驱动程序。 本章内容组织如下: Windows 2000组件一览 Windows 2000中的驱动程序种类 Windows 2000和WDM驱动程序设计目标 1.3.1移植性 1.3.2可配置性 1.3.3永远抢占优先和永远中断 1.3.4多处理器安全 1.3.5基于对象 1.3.6带有可复用IRP的包驱动I/O 1.3.7支持异步I/O 关于Windows NT/Windows 2000操作系统环境的更多细节,可以参看David Solomon的《Inside Windows NT》(微软出版社,1998)。 Windows 2000组件一览 图1.1显示了Microsoft Windows 2000操作系统环境的主要组件。 图1.1 Windows 2000组件一览 在Windows 2000操作系统环境中,一部分组件运行在用户模式下,其他的则运行在内核模式下。文件系统、中间层和最低层驱动程序被显示在图的左下方,其中包含了内核模式I/O管理器。 如图1.1所示,Windows 2000操作系统包括了许多内核模式组件,它们被精心地定义为功能相互独立的组件。对内核模式驱动程序设计者来说,最感兴趣的就是内核I/O管理、即插即用管理(Plug and Play Manager)、电源管理(Power Manager)、硬件抽象层(Hardware Abstraction Layer,HAL)、配置管理(Configuration Manager)、存储管理(Memory Manager)、运行支持(Executive Support)和过程结构(Process Structure)组件。对另一些设计者来说,感兴趣的其他组件可能包括对象管理(Object Manager)和安全引用监控器(Security Reference Monitor)。 即插即用(PnP)管理和电源管理是Windows 2000中的新组件。他们仅仅支持Windows 2000驱动程序和WDM驱动程序。有关Windows 2000和WDM驱动程序使用这些Microsoft操作系统新特性的更详细资料,请参见《Windows 2000驱动程序开发参考》的第1卷《即插即用、电源管理和安装设计指南》。 Windows 2000中的驱动程序种类 在Windows 2000操作系统中,有两个基本的驱动程序种类: 用户模式驱动程序(例如Win32 VDD,它是为MS-DOS应用程序开发的专用设备)或另一个被保护的子系统的驱动程序 用户模式驱动程序是与子系统细节相关的,我们不在这里讨论。更多VDD的细节,参考在线DDK中的“Virtual Device Drivers(虚拟设备驱动程序)”文档。 针对逻辑、虚拟或物理设备的内核模式驱动程序 这些驱动程序作为Windows NT执行体部分来运行:Windows NT是基础的、基于微内核的操作系统,它支持一个或更多的保护子系统。 一些Windows 2000内核驱动程序也是WDM驱动程序,它们符合Windows驱动程序模型(WDM)。所有的WDM驱动程序是PnP驱动程序,并支持电源管理。WDM驱动程序对Microsoft Windows 98和Windows 2000操作系统来说,是源代码兼容的(不是二进制兼容的)。 在这里,术语“Windows 2000驱动程序”通常指代任何运行于Windows 2000操作系统之上的内核模式驱动程序(是WDM或不是)。必要的时候,本书会特别指出哪些是WDM驱动程序,哪些只是Windows 2000驱动程序。名词“早期驱动程序”指的是那些为Windows NT以前版本写的驱动程序,它们是不支持PnP的。 像操作系统自己,内核模式驱动程序是依据仔细定义的需求功能集,作为单独的、模块化的组件来实现的。 所有的Windows 2000内核模式驱动程序,包括WDM驱动程序,都包含一组系统定义的标准驱动程序例程和一些依赖于个别设备需求的内部例程。 图1.2 内核模式驱动程序类型 如图1.2所示,有三种基本类型的内核模式驱动程序。每一种都有稍微不同的结构和完全不同的功能: 最高层驱动程序,例如系统支持的FAT、NTFS和CDFS文件系统驱动程序(FSD)。最高层驱动程序通常依赖于较低层的驱动程序支持。 虽然特定的文件系统驱动程序可能、或不可能从一个或多个中间层驱动程序获得支持,但是每个文件系统驱动程序最终依赖一个或多个下层外围设备(可能是PnP硬件总线)驱动程序的支持。 中间层驱动程序,例如虚拟磁盘、镜像、或指定设备类型的类驱动程序。中间层驱动程序也需要较低层驱动程序的支持。 PnP功能驱动程序是中间层驱动程序,它们在一个I/O总线上控制特定的外围设备,这个I/O总线是由PnP硬件总线驱动程序来控制的。同样还有PnP过滤器驱动程序(PnP filter driver),它们在驱动程序栈(driver stack)内将它们自己插入到PnP功能驱动程序中去,以支持任何特定的外围设备。PnP功能和过滤器驱动程序的子集也是WDM功能和过滤器驱动程序。 任何系统支持的向外输出一个系统定义的WDM类/微类接口的类驱动程序,实际上是一个带有一个或多个链接的WDM微类驱动程序(有时称为WDM微驱动程序)的中间层驱动程序。每一对相互链接的WDM类/微类,提供与WDM功能驱动程序或WDM“软件”总线驱动程序相同的功能。 PnP软件总线驱动程序表示一组子设备,较高层的类、功能和/或过滤器驱动程序能连接它们自己到这些子设备上,PnP软件总线驱动程序也是中间层驱动程序。例如,控制多功能适配器的驱动程序是一个PnP软件总线驱动程序,这样的多功能适配器是用于不同的设备的一个便携集合。根据便携设备的特性和它的驱动程序使用的接口,这样的驱动程序也可能是WDM软件总线驱动程序。它似乎使得较高层驱动程序成为一个PnP/WDM总线驱动程序。对于操作系统来说,驱动程序是以PnP/WDM驱动程序的面目出现的,它服务于它自己的适配器,将它自己插入底层PnP硬件总线驱动程序之上的驱动程序堆栈中。 最低层驱动程序,例如PnP硬件总线驱动程序,它控制一个I/O总线,这个总线连了一些外围设备在上面。最低层驱动程序不依赖于较低层驱动程序,而是控制物理外围设备,例如总线。 早期的NT设备驱动程序直接地控制物理外围设备,它们是最低层的驱动程序,同样的驱动程序例如SCSI HBA(Host Bus Adapter,宿主总线适配器)驱动程序。 PnP硬件总线驱动程序是系统提供的,并且通常动态控制可配置的I/O总线。这个驱动程序通过与即插即用管理协作,为所有连到它所控制的I/O总线上的子设备,配置和重新配置系统硬件资源(例如设备存储的映射和IRQ)。对其I/O总线来说,PnP硬件总线驱动程序包含了一部分Windows NT早期版本中由HAL组件提供的功能。 Windows 2000网络驱动程序也能被划分为上述的基本类型之一。例如,NT服务器或转向器是一个特定的文件系统驱动程序;传输堆栈中的任何驱动程序都是中间层驱动程序;物理网卡(有时称为网络接口控制器或NIC)驱动程序则是最低层设备驱动程序。当然,Windows 2000为网络设备提供了明确的接口和支持,例如NDIS(Network Device Interface Specification,网络设备接口规范)库在网络传输堆栈的下界提供了NIC驱动程序和中间层驱动程序。 本书为文件系统和网络驱动程序设计者提供了一些有用的综述和背景信息,对于内核模式设备和中间层驱动程序,以及包括WDM驱动程序的设计者来说,本书主要是一个设计指南。网络中间层和设备驱动程序的设计者也应该参考在线DDK中的“Network Driver Design Guide”和“Reference”。文件系统驱动程序设计者和最高层网络驱动程序设计者应当参考IFS工具包。 Windows 2000和WDM驱动程序的设计目标 内核模式驱动程序与很多Windows 2000的设计目标相符合,特别是系统I/O管理器部分。这些设计目标包括: 平台之间的移植性 硬件和软件的可配置性 永远抢占优先和永远中断 多处理器平台上的多处理器安全 基于对象 带可重用I/O请求包(IRP)的包驱动I/O 异步I/O支持 后面的小节中将向所有的Windows 2000内核模式和WDM驱动程序设计者介绍这些系统设计目标。 移植性 Windows 2000输出两个系统提供的运行时库,它们包含了以Rtl为前缀的函数。内核模式驱动程序可以使用相同的内核模式RtlXxx例程作为执行组件,但是不能调用用户模式RtlXxx例程。 每个NT执行组件输出一组内核模式支持的例程,驱动程序和所有其他的内核模式组件可以调用这些例程。如果支持例程的底层实现随着时间的推移发生了变化,它的调用者依然可以保持不变,因为定义组件的接口没有发生变化。 大多数Windows 2000组件完全是用C写成的,仅仅少部分的HAL和内核是用汇编语言写成的。所以操作系统对于硬件平台来说是容易移植的。内核模式驱动程序应该也使用C来写,从而使得它能被系统兼容的C编译器重新编译、链接,并且运行在不同的Windows 2000平台上。如果是一个WDM驱动程序,不用重新书写驱动程序部分的代码,也不用替换驱动程序中的模块,就可以在Windows 98平台上运行。 如果相同的特性不能保证被其他系统兼容的编译器所支持,驱动程序则不应该依赖于任何特定系统兼容的C编译器或C支持库的特性。通常,驱动程序代码应该符合ANSI C标准,要避免使用任何这个标准中描述为“实现定义的”的部分。 特别地,编写可移植的Windows 2000驱动程序,最好避免以下的情况: 对数据类型的依赖,因为它可能随着平台的不同而在大小和表现形式上有所不同 调用任何保持状态的标准C运行时库函数 调用任何标准C运行时库函数,而操作系统已经提供了一个可以替换被调用函数的支持例程 DDK提供了一组包含文件,这些文件中定义了系统特定的数据类型和常数,驱动程序(以及所有其他内核模式组件)可以使用它们来保证平台之间的可移植性。大多数驱动程序包含了主DDK内核模式的包含文件之一,这些文件是:ntddk.h和wdm.h。这些主包含文件不仅收录了系统提供的头文件,它们定义了基本的内核模式类型;而且,还收录了针对不同处理器构架特定的头文件的合适选择,从而驱动程序能被使用相应的编译器指令编译。 对于WDM驱动程序,DDK提供了一组包含文件,它们输出Windows 2000内核模式支持例程、宏、常数以及类型的一个子集。WDM驱动程序使用主DDK包含文件wdm.h而不是ntddk.h。包含wdm.h以及只使用它所输出的支持例程、宏、常数以及类型,使得WDM驱动程序可以象在Windows 2000上一样在Windows 98上编译运行。 如果驱动程序需要与平台有关的定义,最好是将这些定义用#ifdef语句单独分割开,这样就能使驱动程序针对合适的硬件平台被编译和连接。当然,使用DDK主头文件提供的支持例程、宏、常数和类型,你几乎可以避免实现任何与平台有关的条件编译代码。 某些驱动程序包含系统提供的其他头文件来确保Windows平台上的可移植性,特别是,SCSI、NDIS、以及视频微端口驱动程序。关于这些类型的驱动程序的头文件的详细信息分别参看第3部分,“存储驱动程序”;以及在线DDK中的“Graphics Driver Design Guide(图形驱动程序设计指南)”和“Reference(参考)”, “Network Driver Guide (网络驱动程序设计指南)”和“Reference(参考)”。 可配置性 因为Windows NT/Windows 2000是一个可移植的操作系统,所以,设备及其驱动程序必须是硬件可配置和软件可配置的。 例如,在一个热插拔的RAID(廉价磁盘冗余阵列)配置中,用户可以在运行的时候替换磁盘。这样的设备被称为是硬件可配置的。它的驱动程序不能假定给定的磁盘设备会在某个特定的位置固定。因此,为了保持可移植性,它的驱动程序一定不能包含硬编码的与机器有关的值。 这类磁盘设备的驱动程序可以在任何给定的机器上对一个或多个文件系统提供硬件级支持,当然这取决于用户如何对这些磁盘分区。另外,中间层驱动程序能为高层文件系统驱动程序提供灾难恢复(镜像分区、带集、和/或卷集)支持,这样的机器通常具有充足的存储能力。换句话说,同样的物理磁盘驱动程序能为一些机器提供对多种高层驱动程序的支持,而对其他的机器则不能。这样的磁盘驱动程序是软件可配置的,它们都是中间层和文件系统驱动程序,并且它们最终依赖于底层设备和/或即插即用(PnP)硬件总线驱动程序。 Windows NT/2000 HAL组件被实现成一个动态连接库,它负责所有的硬件层、平台特定支持,包括内核模式驱动程序在内,系统中的其他任何组件都需要它的支持。HAL输出的例程提供了平台硬件与包括所有驱动程序在内的系统软件组件之间的一个接口,这些例程隐藏了平台特定的硬件细节,例如高速缓存、I/O总线、中断等等。 系统支持的PnP硬件总线驱动程序,在PnP管理器的合作下对HAL组件提供了类似的功能。这就是说,PnP管理器与每一个PnP硬件总线驱动程序合作,提供特定类型I/O总线的平台硬件和系统软件之间的接口。 PnP管理器创建一个设备树,它的节点代表系统中所有的设备,包括总线和连在总线上的设备。对于每一个设备,PnP管理器维护两个列表:一个是设备可以使用的硬件资源表,另一个是设备实际占用的硬件资源表。设备驱动程序辅助PnP管理器创建这些列表,这些列表被保存在注册表中。一旦在系统中增添或删除设备,PnP管理器有必要重新分配资源,并更新列表。 永远抢占优先和永远中断 内核组件确定什么时候依照下列优先级调度标准之一来运行一段特定的代码: 针对线程的内核定义的运行时优先级 系统中每一个线程都被分配了一个优先级属性。通常,系统中的大多数线程拥有可变的优先级属性:它们是总是抢占优先的,并且当前在同一优先级上的线程被轮询调度。系统中的一些线程拥有实时优先属性:这些对时间要求苛刻的线程可以被拥有更高实时优先级的线程抢占,直到它们释放控制。 无论拥有什么优先级属性,系统中的任何线程在硬件和某种软件中断发生时被抢占。 内核定义的中断请求级(IRQL),在给定平台上被赋予特定的中断向量 内核也区分硬件和软件中断的优先次序,使得一些内核模式代码可以在更高的IRQL上运行。从而使得这些代码,包括大多数驱动程序,比系统中所有的线程拥有更高的优先级。 一段内核模式代码在特定的IRQL上执行,这个IRQL定义了它的硬件优先级。内核模式代码是总是可中断的:一个带有更高IRQL值的中断可能在任何时候发生,从而导致拥有更高IRQL的另一段内核模式代码被处理器立即执行。换句话说,当一段代码在给定的IRQL上运行时,内核屏蔽微处理器上所有小于等于当前IRQL值的中断向量。 通常,线程运行在PASSIVW_LEVEL IRQL上:没有中断向量被屏蔽。软件中断被赋予相对较低的IRQL值(APC_LEVEL、DISPATCH_LEVEL、或针对内核调试WAKE_LEVEL)。设备中断拥有较高的IRQL值,内核则为系统关键的中断保留最高的IRQL值,例如系统时钟或总线错误中断。 一些系统支持例程运行在IRQL_PASSIVE_LEVEL,原因有两个方面:一是因为一些内核模式组件创建它们自己的线程,另一是因为一些支持例程被作为可分页代码实现,并且/或者访问可分页数据。 同样的,某种标准驱动程序例程通常运行于IRQL_PASSIVE_LEVEL。然而,几个标准的驱动程序例程要么运行于IRQL DISPATCH_LEVEL,要么运行于设备IRQL(也称为DIRQL),后一种是针对最低层的驱动程序来说的。参看《内核模式驱动程序设计指南》的第16章中的“管理硬件优先级”,可以得到关于IRQL的细节。 通常,如果线程正在请求驱动程序的当前I/O操作,则在这些线程的环境中,仅仅最高层的驱动程序被调用。如果线程已经请求了它的当前I/O操作,中间层或最低层驱动程序从不假定它正在这些线程的环境中执行。 由于性能的原因(避免环境交换),几乎没有驱动程序创建它们自己的线程。因此,当一个标准的驱动程序例程被调用做某些工作时,驱动程序例程通常恰恰在当前线程环境中执行。也就是说,它们在专用线程环境中执行。 任何内核模式例程都运行在比PASSIVE_LEVEL高的IRQL上,这些例程拥有比系统中所有线程都高的优先级。驱动程序中的任何例程都是可中断的:运行在特定IRQL上的任何内核模式例程一直保留处理器的控制权,除非例程运行时有更高的内核指定的IRQL发生。 甚至一个最底层的驱动程序中断服务例程(ISR)也能被其他运行在较高IRQL上的例程中断(例如,被另一个驱动程序的ISR中断)。不像一些旧的PC操作系统中的驱动程序,Windows NT/Windows 2000驱动程序的ISR几乎不做任何驱动程序的I/O处理,因为驱动程序的ISR一直到它返回,也不必保留它正在其上运行的CPU的控制。 相反,最低层的驱动程序必须在较低的IRQL上完成它的I/O操作的大部分,而不是它的ISR的DIRQL。为了获得好的系统整体性能,所有的运行在高级IRQL上的内核模式例程必须迅速释放CPU的控制权。这样的例程不仅仅做它们在高级IRQL上必须作的,通常将一个延迟过程调用(DPC)排队,以完成任何能在较低IRQL(DISPATCH_LEVEL)上完成的操作。 操作系统的可中断、可抢占优先的设计目标是最大化平均性能。任何驱动程序的ISR能被运行在较高IRQL上的例程中断,并且任何线程都能被具有较高优先级的线程中断。虽然一些线程拥有实时优先级属性,它们仍然能被具有更高优先级的线程抢占。然而Windows NT/Windows 2000构架并没有提供一个真正的实时系统。 如果想得到内核模式驱动程序中系统定义的标准例程的介绍,可以参看第2章,“分层的I/O、IRP和I/O对象”。参看第4章,“基本驱动程序结构”可以得到这些例程的概述。 多处理器安全 在任何Windows NT/Windows 2000多处理器平台中,需要满足下列条件: 所有的CPU是相同的,并且它们要么都有协处理器,要么都没有。 所有的CPU共享内存,并且一致地访问内存。 在对称平台中,每一个CPU能访问内存、处理中断、访问I/O控制寄存器。(相对来说,在不对称多处理器机器中,一个CPU可能为一组从属CPU处理所有中断。) Windows NT/Windows 2000被设计成一律运行在单一或对称多处理器平台上,内核模式的Windows 2000和WDM驱动程序应该被同样的设计。 为了能在对称多处理器(SMP)平台上安全运行,任何操作系统都必须解决这样一个问题:即,怎样保证正在某个处理器上运行的代码,不会同时访问和修改运行在另一个处理器上的代码正在访问和修改的数据。例如,正在一个处理器上处理设备中断的最低层驱动程序的ISR排斥其他对临界的、驱动程序定义的数据(或设备寄存器)的访问,以防它的设备同时在另一个处理器上中断。 此外,在单一处理器的机器中被序列化的驱动程序I/O操作在SMP机器中能被同时执行。这就是说,当一个处理输入I/O操作的驱动程序例程在一个处理器上被执行的时候,另一个与设备通讯的例程能同时在另一个处理器上被执行。不论是运行在单处理器机器上,还是SMP机器上,都要求包括WDM驱动程序在内的所有内核模式驱动程序能同步访问系统定义的数据或系统提供给驱动程序例程共享的资源,并且在任何时候要同步对物理设备的访问。 NT内核组件输出了一个机制,被称为自旋锁(spin lock),对称多处理器平台中一个或多个正在运行的例程同时访问时,它被用来保护共享数据(或设备寄存器)。内核对自旋锁的应用使用了两个策略: 在任何时刻,一个且仅一个例程能拥有一个特定的自旋锁;仅仅自旋锁的拥有者才能访问它所保护的数据。另一个例程必须取得自旋锁后,才能访问相同的数据,并且,除非自旋锁的当前拥有者主动释放了它,否则其他任何例程都不能得到它。 象硬件和软件中断向量一样,内核为系统中的每一个自旋锁制定一个相关的IRQL值。内核模式例程只有运行在自旋锁指定的IRQL上时,才能到特定的自旋锁。 这些策略用来保护某个驱动程序例程被更高优先级的驱动程序例程所抢占,前者通常是在较低的IRQL上运行,但是现在却拥有某个自旋锁,后者则希望获取同样的自旋锁,这通常会造成死锁。 分配给某个自旋锁的IRQL通常是能获得这个自旋锁的最高层IRQL例程的IRQL。一个最低层驱动程序的ISR频繁地与驱动程序的DPC例程共享一个状态区,后者靠调用驱动程序提供的临界区例程来访问共享区域。在这个例子中,保护共享区域的自旋锁拥有与DIRQL相同的IRQL,设备中断通常发生在DIRQL。当临界区例程拥有自旋锁并且在DIRQL上访问共享区域,ISR不能在单处理器机器中运行,这是因为设备中断被屏蔽,正如前面提到的那样。在一个对程多处理器机器中,当临界区例程拥有自旋锁并且在DIRQL访问共享数据时,ISR还不能获得自旋锁以保护共享数据。 通过等待内核的调度者对象之一,一组内核模式线程能同步访问共享数据或资源。这些调度对象有:事件、互斥体、信号量、定时器、或另一个线程。然而,大多数驱动程序不创建它们自己的线程,因为当它们避免线程环境交换的同时,获得了更好的性能。对时间要求比较苛刻的内核模式支持例程和驱动程序无论何时运行在IRQL DISPATCH_LEVEL或DIRQL,都必须使用内核的自旋锁,从而来同步对共享数据和资源的访问。 想要得到更详细的信息,可以参看第16章“使用自旋锁和管理硬件优先级”,第3章“内核调度对象”。 基于对象 Windows NT/Windows 2000是基于对象的系统。在执行体中,各种各样的组件定义一个或多个对象类型。每个组件输出仅内核模式具有的支持例程,当这些例程被调用时,它们操纵它的对象类型的实例。没有组件被允许直接访问另一个组件的对象类型的任何实例。每个组件要想使用其他的组件对象,必须调用输出的支持例程。 对这些约定的严格遵守使得Window NT/Windows 2000具有移植性和伸缩性。例如,操作系统将来的某个版本能包含一个完全或部分被重写的内核组件,这个组件定义了相同的对象类型,但是可能使用了完全不同的内部结构,却可以输出一组具有与现存组件所输出的例程相同名字和参数的支持例程。这个假定的被重写的内核版本将对现存系统中任何其他的执行组件的移植性没有任何影响。换句话说,操作系统组件没有使用后门通讯,并且驱动程序也必须避免这种使用,以保持系统的移植性和可配置性。 与操作系统相似,驱动程序及其设备也是基于对象的。包括用户模式代码在内,对于系统中所有其他的组件,对某个设备的连接被描述成I/O管理器的文件对象之一的打开操作。在I/O系统中,每一个驱动程序的逻辑、虚拟、和/或物理设备被描述为设备对象。在I/O管理器中,每一个驱动程序的加载映像被描述成一个驱动程序对象。I/O管理器为文件对象、设备对象和驱动程序对象定义对象类型。 像任何其他的可执行组件,驱动程序通过调用内核模式支持例程来使用对象,这些支持例程是由I/O管理器和其他系统组件输出的。内核模式支持例程通常拥有明确的名称,指明了每次操纵的特定对象和在这个对象上执行的操作。这些支持例程名称的形式如下: PrefixOperationObject 这里 Prefix 指明了输出支持例程的内核模式组件,并且还通常指明了定义对象类型的组件。大多数前缀有两个字母。 Operation 描述对这个对象作什么。 Object 指明对象类型。 例如,IoCreateDevice,在设备初始化过程中每个内核模式驱动程序都要调用这个例程一次或多次,从名字可以看出,这个例程创建一个设备对象,以描述一个物理、逻辑、或虚拟设备,作为I/O请求的目标。 为了方便起见,一个系统组件能输出调用其他组件的支持例程。特别是,I/O管理器输出特定例程,以简化驱动程序开发。例如,IoConnectInterrupt是一个最低层的驱动程序调用,被用来注册它们的中断服务例程(ISR),它调用针对中断的内核的支持例程。 在线DDK中的术语表包括了一组系统对象的定义,它们对驱动程序开发者非常有用。关于对最低层和中间层驱动程序特别有用的支持例程的详细信息,参见《Windows 2000驱动程序开发参考》的卷1和卷2,以及在线DDK。 带有可复用IRP的包驱动I/O I/O管理器的主要工作是接收I/O请求(通常来自用户模式应用程序)、创建IRP以描述它们、发送IRP到合适的驱动程序、跟踪它们直到它们被完成,还有返回状态到每个I/O操作的初始请求者。I/O管理器、即插即用管理器和电源管理器使用IRP与包括WDM驱动程序在内的内核模式驱动程序通信,并且允许驱动程序之间相互通讯。 注意,一些IRP可以被发送到多个驱动程序。例如,一个在某个磁碟上打开文件的请求可能首先到达文件系统驱动程序,通过中间层镜像驱动程序,最终到达磁盘驱动程序,可能是一个PnP硬件总线驱动程序。 因此,每个IRP有一个固定的部分和一个或多个驱动程序特定的I/O栈位置(stack location): 在固定部分(或头)中,I/O管理器维护与原始请求有关的信息,例如调用者的线程ID和参数,还有在其上被打开文件的设备对象的地址,等等。固定部分也包含一个I/O状态块,驱动程序在其中设置请求的I/O操作的状态信息。 在最高层驱动程序的I/O栈位置中,I/O管理器、即插即用管理器、或电源管理器设置驱动程序特定的参数,例如由相应驱动程序请求的特定操作(描述成一个函数代码)和它使用的环境,以确定的它将要做些什么。如果需要的话,按照顺序,每一个较高层的驱动程序创建相邻的较低层驱动程序的I/O栈位置。 因为给定的IRP通过每个驱动程序的标准例程集被处理,每一个标准例程能在IRP中访问那个驱动程序的I/O栈位置,从而在驱动程序操作的每个阶段重用IRP。另外,更高层的驱动程序能创建(或重用)IRP以发送请求到低层的驱动程序。 参见第2章,“分层的I/O、IRP、和I/O对象”可以得到IRP通过分层的驱动程序的处理过程的介绍。如果希望得到关于IRP设备类型特定的信息,参看《Windows 2000驱动程序开发参考》的卷2;对于PnP和电源管理IRP,参看《即插即用、电源管理和安装设计指南》和《Windows 2000驱动程序开发参考》的卷1。 支持异步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.3.4,“多处理器安全”一节中提到的那样。 还有,对内核模式驱动程序的单独I/O请求的处理不必要序列化。这就是说,在驱动程序开始处理下一个进来的I/O请求之前,它不必处理完每个IRP,就像在单处理器机器中,驱动程序在被写成支持同步I/O的单任务操作系统所作的那样。 IRP被传递给驱动程序的标准例程,通过完成任何例程指定的操作以最终满足当前请求,驱动程序响应当前的IRP。当然,最低层的驱动程序被要求帮助I/O管理器和任何高层的驱动程序处理同样的IRP,并通过设置IRP中的I/O状态块来跟踪每个请求的状态。 驱动程序也能在其设备对象的特定部分维护当前I/O操作的信息,这个部分被称为设备扩展。 若想得到IRP处理和设备对象如何表示驱动程序的物理和逻辑设备的概述,可以参看第2章“分层的I/O、IRP和I/O对象”。关于设备对象和设备扩展更详细的信息,参见第3章,“针对驱动程序系统定义的对象和支持”。 分层的I/O、IRP和I/O对象 本章介绍Microsoft Windows 2000的I/O模型,内容包括:内核模式驱动程序如何适合系统,驱动程序如何处理IRP,系统定义的标准驱动程序例程,通用设备配置和相应的分层驱动程序,以及描述设备和驱动程序的对象。 本章的组织如下: 2.1 Windows 2000 I/O模型概述 2.2 终端用户I/O请求和Windows 2000文件对象 2.2.1 用户 I/O请求的注意事项 2.3 IRP和驱动程序特定I/O栈位置 2.3.1 处理IRP的注意事项 2.4 驱动程序对象和标准驱动程序例程 2.4.1 对象不透明性 2.4.2 标准驱动程序对象入口点 2.4.3 其他的标准驱动程序例程 2.4.4 标准驱动程序例程的注意事项 2.5 设备配置和分层驱动程序 2.5.1 简单设备和驱动程序配置 2.5.2 添加驱动程序的注意事项 2.6 设备对象和分层的驱动程序 2.6.1 针对简单配置的设备对象 2.6.2 设备对象的注意事项 注意除了第一节,每节都包含了一个“注意事项”列表。这些小节总结对驱动程序的基本要求和限制,就像本节从整体上介绍一样。 Windows 2000 I/O模型概述 每一个操作系统都有一个隐含的或明确的I/O模型,以处理与外围设备之间的数据流。Microsoft Windows 2000 I/O模型的最大不同之处是它支持异步I/O,就如第1章中描述的那样。另外,I/O模型有下列特性: I/O管理器提供一致的接口给所有的内核模式驱动程序,包括最低层、中间层和文件驱动程序。所有的对驱动程序的I/O请求被作为I/O请求包(IRP)发送。 I/O操作是分层的。I/O管理器输出I/O系统服务,用户模式保护子系统代表它们的应用程序和/或终端用户,调用它们完成I/O操作。I/O管理器解释这些调用,创建一个或多个IRP,并且通过可能的分层驱动程序发送它们到物理设备。 I/O管理器定义一组驱动程序支持的标准例程,一些是必需的,一些是可选的。所有的驱动程序遵从一个相对一致的实现模型,允许外围设备之间的差别,并且总线、功能、过滤器和文件系统驱动程序被要求的不同功能。 就如操作系统本身,驱动程序是基于对象的。驱动程序、它们的设备和系统硬件被表示为对象。I/O管理器和其他操作系统组件输出内核模式支持例程,驱动程序通过操纵合适的对象调用这些例程来完成工作。 除了使用IRP传送传统的I/O请求之外,I/O管理器与PnP和电源管理器一起工作发送包含PnP和电源管理请求的IRP。如果希望了解驱动程序是如何操纵这些IRP的,可参看《安装、即插即用和电源管理设计指南》。 终端用户I/O请求和Windows 2000文件对象 内核模式驱动程序被Microsoft Windows 2000保护子系统向终端用户隐藏,这个子系统实现一个已经很友善的编程接口,例如Windows或POSIX。设备对用户模式代码是可见的,它包括保护子系统,仅仅作为命名的文件对象被I/O管理器控制。 图2.1说明了终端用户和I/O管理器之间的这种关系。 图2.1 代表文件、卷和设备的文件对象 Windows 2000保护子系统,例如Win32子系统,通过I/O系统服务传送I/O请求到合适的内核模式驱动程序。图2.1中显示的子系统依赖于显示、视频适配器、键盘和鼠标设备驱动程序的支持。 保护子系统隔离它的终端用户和应用程序,使得它们不必知道关于内核模式组件(包括驱动程序)的任何细节。同样,I/O管理器隔离保护子系统,使得它们不必知道关于机器特定的设备配置或驱动程序实现的任何细节。 I/O管理器的分层方法也使得大多数驱动程序不必知道任何下列的细节: 是否一个I/O请求在任何特定的保护子系统中被生成,如Win32或POSIX 是否一个给定的保护子系统拥有特定种类的用户模式驱动程序 什么是保护子系统的I/O模型和什么是对驱动程序的接口 I/O管理器提供给驱动程序一个单独的I/O模型、一组驱动程序能用来完成I/O操作的内核模式支持例程,和I/O请求发出者与响应这个请求的驱动程序之间的一个一致接口。 如图2.1所示,子系统和它的本地应用程序仅仅通过I/O管理器提供的文件对象句柄,能访问驱动程序设备或海量存储设备上的文件。为了打开这样一个文件对象或获得一个句柄以实现与设备或数据文件的I/O,文件子系统使用一个请求调用I/O系统服务以打开一个命名的文件。这个命名的文件可以拥有一个子系统指定的别名(符号连接),以将文件对象联系到内核模式名字。 I/O管理器,输出这些系统服务,负责寻找或创建文件对象,这个文件对象用来描述设备或数据文件;I/O管理器还要寻找合适的驱动程序。图2.2描述了当子系统代表应用程序打开描述数据文件的文件对象时发生的事情。 子系统调用一个I/O系统服务以打开一个命名文件。 I/O管理器调用对象管理器以寻找命名的文件,并帮助它决定到文件对象的符号连接。它也调用安全引用监视器以检查子系统是否拥有正确的权限打开那个文件对象。 如果卷还没有被安装(mount),I/O管理器暂停打开请求,并且调用一个或多个Windows 2000文件系统,直到它们中间之一认可这个文件对象存储在文件系统使用的海量存储设备之一上面。当文件系统已经安装了卷,I/O管理器继续处理这个请求。 I/O管理器为这个打开请求分配内存并初始化一个IRP。对于驱动程序来说,打开等同于一个“创建”请求。 I/O管理器调用文件系统驱动程序,传递它给IRP。文件系统驱动程序访问它的在IRP中的I/O栈位置,以确定什么操作必须被完成、检查参数、确定是否请求的文件在缓存中,并且如果没有的话,在IRP中设置相邻的较低层的驱动程序的I/O栈位置。 驱动程序处理IRP,并完成请求的I/O操作,调用I/O管理器和其他系统组件(图2.2种没有显示)提供的内核模式支持例程。 驱动程序将IRP连同I/O状态块集返回到I/O管理器,以指明请求的操作是成功被执行了,还是失败了。 I/O管理器从IRP获得I/O状态,所以它能通过保护子系统返回状态信息到原始调用者。 I/O管理器释放完成的IRP。 如果打开操作成功,I/O管理器返回文件对象的句柄到子系统。如果出现错误,它返回合适的状态到子系统。 子系统成功打开一个描述数据文件、设备或卷的文件对象后,它使用返回的句柄为设备I/O操作(通常是读、写、或设备I/O控制请求)在后续的请求中指明文件对象。为了创建这样的请求,子系统调用I/O系统设备。I/O管理器将这些请求作为IRP,发送它们到合适的驱动程序。 用户I/O请求的注意事项 当设计一个内核模式的驱动程序时,切记以下几点: 驱动程序可以被分层,并且多个驱动程序能处理单独的I/O请求(IRP)。 驱动程序不能做任何有关其他驱动程序将会在设备栈位置中的假设。因此,每个驱动程序应该被准备用于接收来自任何其他驱动程序的请求,并能处理所有潜在的错误。 驱动程序在IRP的I/O状态块中获得请求的I/O操作成功或失败的消息。I/O管理器将请求的I/O操作成功或失败的消息发送给用户模式的请求者。 驱动程序不必,也不应该被设计成能提供应用程序特定的支持。保护子系统或它的子系统特定的用户模式驱动程序提供这种支持。对这条规则有一个例外:依赖于一个应用专用设备的MS-DOS应用程序能请求Windows 2000驱动程序以控制这个设备,还能请求一个紧密结合的Wind32用户模式的虚拟设备驱动程序(VDD)。有关VDD的详细信息参看在线DDK中的“Virtual Device Drivers”(虚拟设备驱动程序)文档。 IRP和驱动程序指定的I/O栈位置 图2.2 带有两个I/O栈位置的IRP,但实际上一个IRP可以拥有任意多个I/O栈位置,这依赖于有多少分层的驱动程序处理给定的请求。 图2.3 在分层的驱动程序中处理IRP 图2.3更详细的说明了图2.2中的驱动程序是怎样使用I/O支持例程(IoXxx)为一个读/写请求处理IRP。 I/O管理器用一个IRP调用文件系统驱动程序(FSD),这个IRP是为子系统读/写请求分配的。FSD访问IRP中它的I/O栈位置,以决定它完成什么操作。 FSD能将原始请求分成更小的请求(可能是针对几个设备驱动程序),这项工作是通过调用一个I/O支持例程(IoAllocateIrp)一次或多次以分配额外的IRP。这些额外的IRP连同为更低层驱动程序创建的I/O栈位置(被填充为全零)被返回给FSD。由它随意决定,FSD能在原始IRP中安装相邻的较低层驱动程序的I/O栈位置,并传递它给更低层的驱动程序,通过这种方法FSD能重用原来的IRP,而不是象图2.3种所示的那样分配额外的IRP。 对于每个驱动程序分配的IRP,图2.3中的FSD调用一个I/O支持例程以注册一个FSD支持的完成例程。在这个完成例程中,FSD能确定低层驱动程序是否满足请求,并能在低层驱动程序完成任务后释放每个驱动程序分配的IRP。不论每个驱动程序分配的IRP是被成功完成,还是返回一个错误状态,或者是被放弃,I/O管理器都将调用FSD支持的完成例程。更高层驱动程序负责释放IRP,这个IRP是它根据自己的需求为较低层驱动程序分配并安装的。I/O管理器在所有驱动程序完成任务后释放它分配的IRP。 下一步,FSD调用一个I/O支持例程(IoGetIrpStackLocation)访问相邻的较低层的驱动程序的I/O栈位置,从而为临近低层驱动程序设置请求。(图2.3种,相邻的较低层驱动程序碰巧是最低层的驱动程序。)FSD然后调用I/O支持例程(IoCallDriver)将那个IRP传递给相邻的较低层驱动程序。 当最低层驱动程序被使用IRP调用时,它检查它的I/O栈位置以确定它将在目标设备上完成什么操作(通过IRP_MJ_XXX函数代码指定)。目标设备在它的被指定的I/O栈位置中被设备对象表示,并且连带IRP被传送给驱动程序。最低层驱动程序假定I/O管理器已经发送IRP到一个入口点,这个入口点是驱动程序为IRP_MJ_XXX(这里是IRP_MJ_READ或IRP_MJ_WRITE)操作定义的,并且假定高层驱动程序已经为请求检查过其他参数的有效性。 如果没有较高层驱动程序,最低层驱动程序将检查IRP_MJ_XXX的输入参数是否是有效的。如果它们是有效的,驱动程序通常调用I/O支持例程以通知I/O管理器设备操作正在IRP上等待决定,并且要么将IRP放入队列,要么传递它给另一个驱动程序提供的例程,这些例程可以访问目标设备(这里是物理设备或逻辑设备:磁盘或磁盘上的一个分区)。 I/O管理器确定目标设备的驱动程序是否正在处理另一个IRP,如果是这样的话,将IRP放入队列,并返回。否则,I/O管理器发送IRP到驱动程序提供的例程,这个例程在它的设备上开始I/O操作。(在这个阶段,图2.3中的驱动程序和I/O管理器返回控制。) 当设备中断时,驱动程序的中断服务例程(ISR)所作的仅仅是阻止设备中断,并保存必要的操作环境。然后,ISR用IRP调用I/O支持例程(IoRequestDpc),等待一个驱动程序提供的DPC(延迟过程调用)例程以在比ISR更低的硬件优先级上完成请求的操作。 当驱动程序的DPC获得控制,它使用环境(在ISR的调用中被传递给IoRequestDpc)以完成I/O操作。DPC调用支持例程将下一个IRP(如果有)取出队列,然后传递这个IRP到驱动程序提供的例程上,这个例程在设备上开始I/O操作(见第5步)。DPC然后在IRP的I/O状态块中设置刚刚完成的操作的状态,并使用IoCompleteRequest将它返回给I/O管理器。 I/O管理器将IRP中最低层的驱动程序的I/O栈位置赋零,并用FSD分配的IRP调用文件系统的已注册的完成例程(见第3步)。这个完成例程检查I/O状态块,以确定是否重试请求或更新任何保存原始请求的内部状态,以及是否释放它的驱动程序分配的IRP。文件系统能收集所有驱动程序分配的发送给低层驱动程序的IRP的状态信息,所以它能设置I/O状态,并能完成原始IRP。当文件系统完成原始IRP,I/O管理器返回Windows NT状态给I/O操作的原始请求者(子系统的本地函数)。 图2.3也显示了原始IRP中的两个I/O栈位置,因为它显示了两个驱动程序,一个文件系统驱动程序和一个海量存储设备驱动程序。I/O管理器给分层的驱动程序链中的每个驱动程序一个它自己的I/O栈位置,这个栈位置是在它安装的每个IRP中的。图2.3中,驱动程序分配的IRP没有FSD创建的栈位置。任何分配IRP给低层驱动程序的高层驱动程序通过设置相邻的较低层驱动程序的设备对象的StackSize值,来决定新IRP拥有多少I/O栈位置。 图2.4更详细描述了IRP环境。 图2.4 IRP中I/O栈位置的环境 如图2.4所示,IRP中每个驱动程序指定的I/O栈位置包含了下列通用的信息: 主功能代码(IRP_MJ_XXX),指定驱动程序应该完成的基本操作 对于一些被FSD操纵的主功能代码、更高层SCSI驱动程序、和所有的PnP驱动程序,一个次功能代码(IRP_MN_XXX),指明驱动程序应该完成的基本操作的子项。 一组操作特定的参数,例如驱动程序用来传递数据的缓冲区的大小和起始位置。 指向驱动程序创建的设备对象的指针,为请求操作描述目标设备(物理的、逻辑的或虚拟的) 指向文件对象的指针,描述打开的文件、设备、目录或卷 文件系统驱动程序通过IRP中它的I/O栈位置访问文件对象。其他的驱动程序通常忽略这个文件对象。 特定驱动程序操作的IRP主要和次功能代码集可以是设备类型特定的。当然,更低层驱动程序(包括PnP函数和过滤器驱动程序)和中间层驱动程序通常操作下列基本请求集: IRP_MJ_CREATE——打开目标设备对象,指明它对I/O操作是现存的和可用的; IRP_MJ_READ——从设备传输数据; IRP_MJ_WRITE——传输数据到设备; IRP_MJ_DEVICE_CONTROL——设置(或重设)设备,通过系统定义的、设备类型特定的I/O控制代码(IOCTL); IRP_MJ_CLOSE——关闭目标设备对象 IRP_MJ_PNP——在设备上执行即插即用操作,IRP_MJ_PNP请求由PnP管理器通过I/O管理器发送; IRP_MJ_POWER——在设备上执行电源操作,IRP_MJ_POWER请求被电源管理器通过I/O管理器发送。 特定种类设备的驱动程序被请求操作的主IRP函数代码和设备I/O控制代码的详细信息,可以参见《Windows 2000驱动程序开发参考》的卷1和卷2。 通常,I/O管理器至少使用两个I/O栈位置发送IRP给海量存储设备驱动程序,因为文件系统在海量存储设备的其他驱动程序上被分层。I/O使用单个栈位置发送IRP给那些其上没有其他分层驱动程序的驱动程序。 当然,I/O管理器为增添新驱动程序到系统内任何现存驱动程序链中提供支持。例如,一个在给定磁盘分区上完成数据备份的中间层镜像驱动程序可以被插入到一对驱动程序之间,例如图2.3中所示的文件系统驱动程序和最低层驱动程序。当这个新驱动程序将它自己连到(attach)设备栈,I/O管理器调整所有IRP中的I/O栈空间数目,它发送这些IRP给文件系统、镜像和最低层驱动程序。图2.3中文件系统分配的每个IRP也包含另一个针对新镜像驱动程序的I/O栈空间。 注意,在任何特定驱动程序对IRP中I/O栈空间的访问方面,这个对增添新设备到现存链的支持包含某种约束: 分层的驱动程序链内的更高层驱动程序能安全的访问任何IRP中它自己的和相邻较低层驱动程序的I/O栈位置。这样的驱动程序必须在IRP中为相邻的较低层驱动程序创建I/O栈位置。然而,当设计这样一个较高层驱动程序时,你不能预知什么时候新驱动程序将被添加到现存的链中,并且恰好在你的驱动程序之下。 因此,你应该假定任何后来添加的驱动程序将处理IRP主功能代码(IRP_MJ_XXX),就像替换的相邻的较低层驱动程序作的那样。 分层的驱动程序链内的最低层驱动程序能在任何IRP中安全地访问仅仅它自己的I/O栈位置。当设计这样的驱动程序时,你不能预见什么时候(或者是否)新驱动程序将被添加到现存链中你的驱动程序上面。 在设计最低层驱动程序期间,要假定驱动程序能使用传递到它自己的I/O栈位置的信息继续处理IRP,而不管给定IRP的开始源,以及很多驱动程序如何位于它之上。 与图2.3中所示的文件系统驱动程序类似,被添加到现存驱动程序链的任何新驱动程序能作下列事情: 设置它自己的完成例程到IRP中。IoCompletion例程检查I/O状态块,以决定较低的驱动程序是否成功完成IRP、取消IRP、和/或带有一个错误完成它。在完成IRP之前,完成例程也能更新任何IRP特定的驱动程已经保存的状态,释放任何操作特定的驱动程序已经分配的资源,等等。另外,完成例程能推迟IRP完成(通过通知I/O管理器在IRP上更多处理被请求),并且在允许IRP完成之前,能发送另一个请求到相邻的较低层驱动程序。 在它分配和发送请求到相邻的较低层驱动程序的IRP中,设置相邻的较低层驱动程序的I/O栈位置。 通过在每个IRP中设置相邻的较低层驱动程序的I/O栈位置和调用IoCallDriver来传递任何到达的请求到较低层驱动程序。(注意,对于带有主要功能代码IRP_MJ_POWER的IRP,驱动程序必须使用PoCallDriver。) 希望得到中间层和最低层驱动程序调用的支持例程的特定信息,以及这些驱动程序必须处理的设备类型特定请求的信息,参见《Windows 2000驱动程序开发参考》卷2。也可参考《Windows 2000驱动程序开发参考》的卷1得到关于PnP和电源支持例程的信息。 如图2.3所示,Windows 2000文件系统是一个两部分驱动程序: 文件系统驱动程序(FSD),它在用户模式线程的环境中执行,这个线程调用I/O系统服务 I/O管理器把相应的IRP发送到FSD。 如果FSD为IRP创建完成例程,则这个完成例程不必在原来用户模式线程的环境中被调用。 一组文件系统线程,和一个可能的FSP(文件系统处理) FSD能创建一组驱动程序专用系统线程,但是为了不阻碍作I/O请求的用户模式线程,大多数FSD使用系统工作者线程。任何FSD可以创建自己的过程地址空间,它的驱动程序专用线程在其中执行,但是系统提供的FSD避免这种做法,以节省系统存储空间。 Windows 2000文件系统一般使用系统工作者线程创建并且管理IRP的内部工作队列,它们发送IRP到一个或更多低层驱动程序,有可能对于不同的设备。 如图2.3中所示的最低层驱动程序通过一组离散的驱动程序提供的例程去处理每个IRP时,与文件系统不同,它不使用系统线程。最低层驱动程序不需要它自己的线程环境,除非为I/O安装它自己的设备是一个非常延时的过程,以至对系统性能有显著的影响。 几乎没有最低层或者中间层的驱动程序需要创建它们自己的驱动程序专用的或者设备专用的系统线程,并且这确实要付出性能上的代价,这通常是由向它们的线程作环境交换引起的。 大多数内核模式驱动程序,像图2.3的物理设备驱动程序一样,在一个专用线程环境中执行:当它们被调用处理IRP时,任何线程的环境恰巧是当前的。因而,驱动程序通常维护它们的I/O操作和服务的设备的状态信息,这些信息被存放在它们的设备对象中驱动程序定义的部分,被称为设备扩展。 每一驱动程序创建的设备对象代表一种物理、逻辑、或者虚拟设备,一个特定的驱动程序为它处理I/O请求。有关不同种类的设备驱动程序如何使用设备对象代表它们各自的设备的指导方针,参见本章后面的“设备对象和分层的驱动程序”。有关创建和安装设备对象的详尽信息,参见第3章。也可参见《即插即用、电源管理和安装设计指南》,提供了一个PnP驱动程序创建的设备对象的类型的讨论。 也如图2.3所示,大多数驱动程序通过驱动程序提供的一组系统定义的标准例程,来处理各阶段中的每一IRP,但是链中的不同层的驱动程序必然有不同的标准例程。例如,只有最低层驱动程序处理来自物理设备的中断,因此只有最低层驱动程序拥有ISR和DPC,它完成中断驱动的I/O操作。另一方面,因为当这样的驱动程序收到来自它的设备的中断,它知道I/O是完成的,它不需要一个完成例程。只有较高层的驱动程序拥有一个或更多个完成例程,像图2.3中所示的FSD一样。参见2.4节可以得到对驱动程序必须或者可以拥有的系统定义的例程的简要介绍。第4章提供了这些驱动程序例程的一个概述,后续的章节提供了例程特定的请求。 IRP处理的注意事项 设计内核模式驱动程序时,注意下列的事项: 一个新的驱动程序必须与它所替换的任何系统提供的驱动程序一样,能处理相同的一组IRP_MJ_XXX。如果其驱动程序不为那个IRP_MJ_XXX定义入口点,I/O管理器将针对一个给定的I/O请求返回STATUS_INVALID_DEVICE_REQUEST到目标设备。设备驱动程序也必须与任何系统提供的驱动程序一样,能为IRP_MJ_DEVICE_CONTROL请求处理同样的I/O控制代码。 换句话说,通过实现比现存驱动程序为相同类型设备提供的功能少的功能,新的设备驱动程序一定不能“中断应用”。 被插入现存驱动程序链中的新的中间层驱动程序应该象它所替换的驱动程序一样,能识别同样的一组IRP_MJ_XXX。新的驱动程序能简单地传递那些请求的IRP,而不为较低层驱动程序处理它们。然而,一个新的中间层驱动程序忽略为IRP_MJ_XXX请求(新替换的更低层的驱动程序处理这个请求)定义入口点,但不必为它上层和下层的驱动程序“打破这个链”。 最低层驱动程序仅仅能访问被发送的任何IRP中它自己的I/O栈位置。一个较高层驱动程序仅仅访问被发送的任何IRP中它自己的和较低层驱动程序的I/O栈位置。 每个驱动程序仅仅在IRP的I/O状态块中向较高层的驱动程序(并且最终通过I/O管理器向用户模式的应用程序)交换信息,因为当链中的每个驱动程序完成IRP时,I/O管理器将相应的I/O栈位置赋零。从一种Windows NT/Windows 2000到更高的平台或版本,试图与一个特殊的较高(或者低)的驱动程序实现后门通信的任何新驱动程序将在可移植性和互操作性方面与其他驱动程序做出妥协。 一对驱动程序能为IRP_MJ_INTERNAL_DEVICEC_CONTROL请求定义一组设备特定的(也称为私有的)I/O控制编码,这个请求由比这对驱动程序高的驱动程序发送到较低的驱动程序。 然而,如果希望它们从一种Windows NT和Windows 2000到下一个平台或版本,保持与其他驱动程序的可移植性和互操作性,这样的一对驱动程序必须符合前面的所有方针。如果你用一个私有的接口设计一对驱动程序,要注意仔细定义I/O控制代码集。使它们尽可能通用,并且按照前述的方针设计你的一对驱动程序,从而使得当它们从一种Windows NT和Windows 2000平台或者版本向另一个迁移时,你(或者其他的人)能容易地重用、替换、或者移植两个新驱动程序或其中之一。 驱动程序对象和标准驱动程序例程 图2.5说明驱动程序对象,描述一个驱动程序,以及最低层和较高层驱动程序可以,或者必须拥有的一组系统定义的(或者标准的)例程。 图2.5 驱动程序对象 每个名称旁边带有星号的标准例程在被调用时,被传递一个IRP。在入口处,每个被传递IRP的标准例程也被传递一个I/O请求的目标设备对象的指针。 I/O管理器定义驱动程序对象类型,并且使用驱动程序对象来注册和跟踪驱动程序的加载的映像的信息。注意,驱动程序对象中Dispatch入口点(从DDDispatchXxx到DDDispatchYyy) 对应于被传递到IRP的I/O栈位置中的主要功能代码(IRP_MJ_XXX)。 如前面的图2.3所示,I/O管理器首先把每个IRP发送到驱动程序提供的Dispatch例程。最低层驱动程序的Dispatch例序通常调用I/O支持例程(IoStartPacket),从而排队(或者传递)每个IRP,这个IRP拥有驱动程序的StartIo例程的有效参数。StartIo例程在一个特定的设备上开始请求的I/O操作。较高层的驱动程序通常没有StartIo例程,但是它们可以有。 当驱动程序被装载时,其DriverEntry例程连带一个指向这个驱动程序对象的指针被调用。 DriverEntry例程在输入驱动程序对象中设置一个或多个Dispatch入口点,这样I/O管理器就能把IRP发送到适当的驱动程序提供的Dispatch例程。DriverEntry例程也在驱动程序对象中设置驱动程序的StartIo和Unload入口点,并且在DriverExtension中设置AddDevice例程。 DriverEntry或者可选的Reinitialize例程也能使用驱动程序对象(没有在图2.5中显示)中的域在配置管理器的数据库中获得信息和/或设置信息。更多信息,可参见《即插即用、电源管理和安装设计指南》的第4部分,第2章中的“注册表中的驱动程序信息”。 对象的不透明性 像所有系统定义对象一样,驱动程序对象是不透明的:只有定义的系统组件(这里是I/O管理器)“知道”对象类型的内部结构,并能直接访问对象包含的所有数据。定义的系统组件通常输出支持例程,驱动程序和其他内核模式组件能调用它们以操纵那些组件的对象。例如,Windows NT 内核输出支持例程,在最低层驱动程序注册它的中断服务程序(ISR)时,I/O管理器调用它们以初始化并且连接中断对象,如图2.5中所示的DDInterruptService。 为了保持驱动程序的可移植性,考虑下面这个有关系统定义的对象的实现方针: 驱动程序必须使用系统提供的支持例程来操纵系统定义对象。定义的系统组件能随时改变其对象类型的内部结构。 然而,I/O管理器不输出支持例程来操纵驱动程序对象。驱动程序对象被I/O管理器仅仅用来跟踪当前加载的驱动程序。一些在驱动程序对象之内的域是不透明的:只有I/O管理器“知道”并且只有I/O管理器使用。其他是部分地不透明的;例如,你必须知道某个域名称,以定义AddDevice、Dispatch、StartIo,和Unload入口点。然而,你既不应该试图使用驱动程序对象内未发表的域,也不应该做有关本文中命名的任何驱动程序域的位置的假定。否则,你不能保证从一种Windows NT和Windows 2000个平台到另一个平台上的移植性。 标准驱动程序对象入口点 内核模式驱动程序必须在它的驱动程序对象中定义下列的入口点: 至少一个Dispatch入口点,以得到请求PnP、电源、和I/O操作的IRP 其AddDevice例程的入口点,在DriverObject->DriverExtension->AddDevice 其StartIo例程的入口点,如果它管理它自己的IRP队列 如果驱动程序能动态地被装载和/或者替换,还需要一个Unload入口点,从而可以释放任何系统资源,诸如驱动程序已分配的系统对象或者内存 当系统运行时不能被替换的驱动程序,诸如键盘驱动程序,不必提供Unload例程。 这些请求不适用于一些微端口驱动程序,相应的类别或者端口驱动程序在驱动程序对象中定义入口点。细节参见设备类型特定的文档。 支持PnP的任何驱动程序必须拥有AddDevice例程。AddDevice例程创建一个或多个设备对象,它们代表物理、逻辑、或者虚拟的设备,驱动程序则为它们完成I/O请求。如图2.5所示,I/O管理器在相应的驱动程序对象中维护驱动程序创建的设备对象的信息。 如果最低层驱动程序被设计成创建并且管理它自己的IRP队列,它不必定义StartIo入口点。然而,大多数这样驱动程序在它们的驱动程序对象中定义StartIo入口点,并且依靠I/O管理器为发往它们的StartIo例程的IRP排队。事实上,几乎没有最低层的系统提供的驱动程序被设计成没有StartIo例程,即使它们为IRP创建并且保持它们自己的辅助队列。 要不是为了更好的性能,较高层的的驱动程序(包括FSD和PnP功能,以及过滤器驱动程序)都可能有StartIo例程,但实际上很少这样做。相反,大多数Windows 2000文件系统驱动程序创建并且保持内部的IRP队列。其他较高层的驱动程序要么拥有内部的IRP队列,要么从它们的Dispatch例程简单的传递IRP到低层的驱动程序,这些都是在每个IRP中为更低层驱动程序安装I/O栈位置,并且可能为给定的IRP安装较高层驱动程序的IoCompletion例程之后发生的。 当驱动程序的DriverEntry例程被调用,它直接在驱动程序对象中设置Dispatch、StartIo(如果可能)、和Unload(如果可能)入口点,如下所示: DriverObject->MajorFunction[IRP_NJ_xxx]=DDDispatchXxx; : : DriverObject->MajorFunction[IRP_NJ_yyy]=DDDispatchYyy; : : DriverObject->DriverStartIo=DDStartIo; DriverObject->DriverUnload=DDUnload; : : 它也在它的驱动程序对象内的DriverExtension中,设置它的AddDevice例程的入口点,如下: DriverObject->DriverExtension->AddDevice=DDAddDevice; 驱动程序能定义若干Dispatch入口点,但是它只能在其驱动程序对象中定义一个StartIo入口点、一个AddDevice入口点,和一个Unload入口点。 其他标准驱动程序例程 如图2.5所示,与它们在各自驱动程序对象中为其设置入口点的那些例程一起,内核模式驱动程序还拥有其他的标准例程。大多数标准驱动程序例程及其使用的一些配置相关的对象是由I/O管理器定义的。ISR、SynchCritSection例程、以及图2.5中所示的名字包含“custom”的那些例程,是由Windows NT 内核定义的。 大多数驱动程序使用每个设备对象的设备扩展,它们创建这些设备对象以保持有关它们的I/O操作的设备特定状态,并且存储一些指针,这些指针指向那些为了其他标准例程它们必须分配的任何系统资源。例如,在图2.5中所显示的DDCustomTimerDpc例程请求驱动程序提供对内核定义的定时器和DPC对象的存储。 如2.2节,“终端用户I/O请求和Windows 2000文件对象”中提到那样,针对在图2.5中左边所示的最低层驱动程序的一组标准的驱动程序例程,必然不同于针对较高层的驱动程序的。在图2.5中所示的一些例程是设备相关或者配置相关的请求。其他的是可选的:你能选择实现这样一个例程,这取决于驱动程序的设备的性质或者配置、驱动程序的设计和驱动程序在分层驱动程序的链中所处的位置。 除了Dispatch、AddDevice、StartIo、和Unload例程之外,系统定义的标准驱动程序例程还包括: InterruptService(ISR) 能产生中断的物理设备的驱动程序必须拥有ISR。ISR必须根据中断停止设备。然后,它所应该做的仅仅是保存状态,并且排队一个DPC,以在较低硬件优先级上完成中断驱动的I/O操作,而不是在ISR执行的优先级上完成。 DpcForIsr 拥有ISR的驱动程序也应该拥有DPC,要么是一个DpcForIsr,要么是一个或者多个CustomDpc例程,以完成中断驱动的I/O操作。 SynchCritSection 任何带有与它的ISR共享数据或设备寄存器的例程的驱动程序必须拥有一个或多个SynchCritSection程序,以便以多处理器安全方式访问共享的数据或者寄存器。 Cancel 驱动程序中的IRP可能为一个不确定的间隔保持排队状态(所以用户可以取消先前提交的I/O请求),这样的驱动程序必须有一个或多个Cancel例程让用户取消I/O请求。应该拥有Cancel例程的驱动程序的例子是键盘、鼠标、并口、串口、和声音设备驱动程序(或位于它们之上的驱动程序),以及文件系统驱动程序。 ControllerControl 必须通过一个物理控制器使到一个简单设备的操作同步的最低层设备驱动程序,诸如“AT”盘控制器,可以拥有ControllerControl例程。WDM驱动程序不能有ControllerControl例程。 IoCompletion 在IRP特定基础上监控较低层驱动程序如何完成特定请求的较高层驱动程序可能拥有一个或多个IoCompletion例程。分配IRP把请求发送到较低层驱动程序的较高层驱动程序必须有IoCompletion程序。关于较高层驱动程序如何使用IoCompletion例程的描述,参见“IRP和驱动程序特定的I/O栈位置”。 IoTimer 为了确定驱动程序设备操作是否有超时,或更新某种驱动程序定义的变量(诸如计数器),或者出于某种原因需要被周期性地调用的驱动程序,可能拥有IoTimer例程。IoTimer例程实际上是一个与设备对象有关的DPC例程,I/O管理器每秒调用一次。对驱动程序创建的每个设备对象,它可能有一个IoTimer例程。 CustomTimerDpc 需要周期性地以精细的间隔(不是每秒一次)或者可变的间隔被调用的驱动程序可以拥有一个CustomTimerDpc例程,而非IoTimer例程。除了它的IoTimer例程之外,驱动程序也能有一个或多个CustomTimerDpc例程。 CustomDpc 需要在较低的硬件优先级(IRQL)上延迟完成中断驱动的I/O操作的驱动程序可能有CustomDpc例程。几乎没有最低层驱动程序拥有CustomDpc而让它们的ISR排队,除非它们的设备要求多于一个DpcForIsr例程来完成一个不同组的中断驱动的I/O操作。 注意,图2.5中所示的CustomTimerDpc和IoTimer例程实际上是系统定义的CustomDpc例程在系统时钟中断发生之后执行。 Reinitialize 需要各阶段中初始化自己的任何驱动程序可能有Reinitialize例程。Reinitialize例程在DriverEntry例程已返回控制并且其他驱动程序已初始化了它们自己之后被调用。 AdapterControl 使用系统DMA的任何设备的驱动程序可能有AdapterControl例程,这个例程通过系统DMA控制器在它的设备和系统物理内存之间完成传递操作。非SCSI总线控制器的设备的驱动程序通常必须拥有AdapterContro1例程。作为选择,执行基于包的DMA(要么系统,要么总线控制器)的驱动程序可能拥有AdapterListControl例程,使用系统提供分散/收集支持。细节参见第3章。 出于惯例,除DriverEntry之外的所有的标准例程的名称中被加入一个标识性的、驱动程序或设备特定的前缀。作为例子,本文使用“DD”,如图2.5所示。遵循这个惯例,可以使调试和维护驱动程序变得更容易。 标准驱动程序例程的注意事项 当设计内核模式驱动程序时,注意下列事项: 所有的驱动程序必须有至少一个Dispatch例程,并且必须在其驱动程序对象中为驱动程序处理的每个IRP_MJ_XXX定义发送入口点。驱动程序能拥有与它所能处理的IRP_MJ_XXX功能同样数量的Dispatch例程。 如果在系统运行时驱动程序能被替换,驱动程序必须有Unload例程,并且在其驱动程序对象中定义一个Unload入口点。Unload例程负责释放任何系统资源,诸如Windows 2000对象或者驱动程序分配的内存,这些资源通常在驱动程序本身被从系统卸载之前使用。 所有的PnP驱动程序必须有AddDevice例程,并且在驱动程序对象的驱动程序扩展中定义其入口点。AddDevice例程负责为驱动程序控制的每一个PnP设备创建和初始化设备对象。 驱动程序可以有StartIo例程,并且能在其驱动程序对象中定义一个Startlo入口点。没有StartIo例程的任何最低层驱动程序必须创建并且管理发送到其Dispatch例程的内部IRP队列,除非它能在它的Dispatch例程中完成它得到的所有IRP。较高层驱动程序可以有StartIo例程,但是很少这样做,这是因为较高层驱动程序通常把IRP从它们的Dispatch例程直接传递到低层驱动程序。 微端口驱动程序对于前面的要求是个例外。可以参见本DDK中的设备类型特定的文档中关于微端口驱动程序的要求。 驱动程序是否有任何其他种类的标准例程取决于其功能和那个驱动程序如何适应系统(例如,它是否与系统提供的驱动程序互操作)。细节参见这个DDK中设备类型特定的文档。 也参见下列的文档: 第2.6节,“设备对象和分层的驱动程序”,阐述不同种类的设备驱动程序如何使用设备对象描述物理、逻辑、和虚拟的设备 第3章阐述驱动程序能使用的系统定义的对象的详细信息 第4章是标准驱动程序例程的综述,第5章到第15章阐述例程特定的需求 第16章是关于驱动程序如何管理系统定义的硬件优先级,标准驱动程序例程在其上执行 在线DDK中的术语表,是与编写驱动程序有关的术语和缩写词 《即插即用、电源管理、和安装设计指南》和《Windows 2000驱动程序开发参考》卷1,阐述了PnP的需求 设备配置和分层驱动程序 对于大多数普通设备,Windows 2000 DDK提供一组全功能的系统驱动程序的样例集。当为类似的设备开发新驱动程序时,单独的样例驱动程序可以被用作模型。然而,系统的驱动程序有个附加的设计请求:要使开发新的设备驱动程序是容易的。因而,许多系统的驱动程序具有分层结构,这样使得特定的驱动程序能被重用,以支持为类似的设备开发的新驱动程序。 在大多数情况下,DDK提供的可重用的驱动程序是WDM驱动程序,它是支持PnP的,并且能为系统提供的、设备特定的最低层(PnP总线)驱动程序提供处理硬件独立性的操作。在某些情况下,诸如并行端口和SCSI端口驱动程序,这些可重用的驱动程序为高层的、设备类型特定的类驱动程序提供支持。注意,几乎所有系统的可重用驱动程序进行新中间层驱动程序的开发,这些中间层驱动程序将被加入到现存的驱动程序链中,正如“IRP和驱动程序特定的I/O栈位置”中已经描述的那样。 一个新(或者替换)的驱动程序在设备的驱动程序链中适合位置,部分取决于一个给定的Windows平台中设备的硬件配置,也部分取决于新驱动程序能从现存的系统驱动程序获得多少支持。 样例设备和驱动程序配置 本节介绍硬件和驱动程序配置之间的关系,用键盘和鼠标设备作为例子。配置因设备不同而不同。任何设备配置的完成信息,参看本DDK中的设备特定的文档。 图2.6描述了键盘和鼠标这两个设备的可能硬件配置: 它们都直接的连到系统总线的某个位置 它们都通过一个键盘和辅助设备控制器被连结 图2.7描述了为图2.6中设备上的I/O操作所提供的相应的分层驱动程序。 图2.6 键盘和鼠标硬件配置 图2.7 键盘和鼠标驱动程序层 注意,无论硬件配置如何,键盘和鼠标设备能使用系统的键盘类和鼠标类驱动程序,以处理硬件无关的操作。这些被称为类驱动程序是因为每个都为特定的一类设备提供系统要求的、但却与硬件无关的支持。 相应的端口驱动程序实现设备特定的支持,以在每个物理设备上完成要求的I/O操作。x86平台的系统(i8042)的键盘和辅助设备端口驱动程序,为鼠标和键盘管理设备特定的操作。在每个设备被分别连结的硬件配置中,如图2.6所示,每个系统类驱动程序能在分开的设备特定的端口驱动程序之上被分层,或者每个设备的单独驱动程序能作为一个分开的、单一的(最低层)驱动程序被实现。 新的中间层驱动程序,例如PnP过滤器驱动程序,能被添加到图2.7中的配置中。例如,被增加到上述的键盘类驱动程序的过滤器驱动程序,可以在键盘输入通过I/O服务到达要求它的子系统前,以平台特定的习惯过滤这个键盘输入。这样的过滤器驱动程序象键盘类驱动程序一样,必须识别相同的IRP和IOCTL。 添加驱动程序的注意事项 设计内核模式驱动程序时,要注意下列事项: 系统提供的SCSI和视频端口驱动程序不能被替换。 替换最低层驱动程序必须实现被替换的驱动程序的同样功能。例如,替换键盘或者鼠标端口驱动程序必须使用系统定义它自己和它所重用的系统提供的类驱动程序之间的接口,反之亦然。 将要插入到任何系统提供的一对驱动程序之间的新中间层驱动程序,必须能与那些驱动程序或操作。换句话说,这样的新驱动程序必须遵照“处理IRP的注意事项”和“标准驱动程序的注意事项”这些节中提供的事项。 设备对象和分层驱动程序 除了某些微端口驱动程序,所有的内核模式驱动程序创建至少一种设备对象。(系统提供的端口或者类驱动程序按照它们自己的需求创建和管理所有必要的系统定义的对象。) 一些驱动程序必须创建多个设备对象:无论分层驱动程序链中特定驱动程序处于哪一层,单独的设备对象描述一个物理、逻辑、和/或虚拟的设备,驱动程序为这个设备处理I/O请求。 换句话说,图2.5中显示的驱动程序对象描述一个单独的驱动程序,这个驱动程序可以创建多个设备对象。I/O管理器维护被每个驱动程序创建的设备对象的列表,并且在驱动程序对象中存储指向这个列表的指针。 当每个标准驱动程序例程被调用的时候,被传递IRP的例程也被传递一个指向目标设备的指针。大多数驱动程序使用目标设备对象的设备扩展来维护必要的设备状态信息或者驱动程序决定的有关它们当前正在处理的I/O请求的环境数据。 因为大多数总线、功能、和过滤器驱动程序(最低层和中间层驱动程序)在一个专用的线程环境中执行,设备扩展是每个驱动程序维护设备状态和这个驱动程序需求的所有其他设备特定的数据的主要地方。例如,在图2.5中所示的任何实现CustomTimerDpc或者CustomDpc例程的驱动程序通常在设备扩展中为必需的内核定义的定时器和/或DPC对象提供存储空间。 每个拥有ISR的驱动程序必须为指向一组内核定义的中断对象的指针提供存储空间,并且大多数设备驱动程序将这个指针存储在设备扩展中。当每个驱动程序创建设备对象,它确定设备扩展的大小,并且每个驱动程序定义它自己的设备扩展的内容和结构。 下一节通过说明这些对象和代表性硬件配置之间的对应关系来介绍设备对象。第3章较详细地分别描述设备对象,连同适配器和控制器对象。它也覆盖了所有其他系统定义的驱动程序可能或必须使用的对象,例如中断对象,这个对象是为那些产生中断的物理设备的最低层驱动程序而创建的。更多关于特定驱动程序必须创建的设备对象的信息,参见DDK中的设备类型特定文档。 针对样本配置的设备对象 图2.8说明代表键盘和鼠标设备的设备对象,这些设备在图2.6中显示。在图2.7中所示的键盘和鼠标驱动程序通过调用I/O支持例程(IoCreateDevice)来创建这些设备对象。 图2.8 键盘和鼠标设备对象 对于键盘和鼠标设备来说,它们各自的端口和类驱动程序分别创建设备对象。端口驱动程序创建物理设备对象(PDO)以代表物理的端口。每个类驱动程序创建它自己的功能设备对象(FDO)以代表键盘或者鼠标设备,作为I/O请求的目标。 每个类驱动程序调用I/O支持例程得到指向相邻的较低层驱动程序的设备对象的指针,因此类驱动程序能将自己链在端口驱动程序之上,并且每个类驱动程序能为代表其物理设备的目标PDO,把I/O请求发送到端口驱动程序。 增加到配置的可选的过滤器驱动程序,将创建过滤器设备对象(filter DO)。像类驱动程序一样,一个可选的过滤器驱动程序能链自己到设备栈中它的相邻的较低层驱动程序,并且为目标PDO发送I/O请求到相邻的较低层驱动程序。 如图2.7中所示,每个端口驱动程序是总线(最低层的)驱动程序,所以产生中断的设备的所有端口驱动程序必须创建中断对象,并且注册ISR。 双重设备端口驱动程序,如图2.6中所示的键盘和辅助设备控制器的(i8042)驱动程序,如果每个设备使用不同的中断向量,必须创建设备特定的中断对象。写这样的驱动程序时,要么为每个设备实现单独的ISR,要么两种设备实现单一的ISR。 设备对象的注意事项 设计内核模式驱动程序,应当遵从下列的事项: 除某些微端口驱动程序之外,所有的驱动程序必须创建命名的设备对象,以代表每个物理、逻辑、或者虚拟设备,这些设备可能是I/O请求的目标。 最低层驱动程序,例如PnP硬件总线驱动程序,为每个它控制的设备创建一个物理设备对象(PDO)。中间驱动程序,例如PnP功能驱动程序,创建一种功能设备对象(FDO)。 例如,替换的并行端口或者串行驱动程序应该为每个端口创建一个物理设备对象。相比之下,描述机器中的每个硬盘为大硬盘的新虚拟硬盘驱动程序应该创建一个单独功能的设备对象和一定数量的额外功能的设备对象,前者描述它的硬盘,后者为位于这个中间层驱动程序之上的文件系统描述磁盘分区。 驱动程序在其AddDevice例程中创建设备对象,这个例程在设备列举之后被PnP管理器调用。 对于大多数最低层和中间层驱动程序,每个设备对象的设备扩展是每一驱动程序的主要的(并且只是经常地)全局数据存储区域。许多驱动程序在驱动程序定义的设备扩展(属于一个或多个驱动程序创建的设备对象)中维护设备状态和驱动程序需要的所有其他设备特定的数据和资源。 对于某些种类的数据,IRP中驱动程序特定的I/O栈位置被认为是操作特定的本地存储区域。 系统定义的对象和对驱动程序的支持 本章包含下列的信息: 3.1 系统组成部分和分层内核模式驱动程序 3.1.1 操纵系统定义的对象的支持例程 3.1.2 存储系统定义的对象 3.2 设备对象和设备扩展 3.2.1 定义设备扩展 3.2.2 创建设备对象和设备扩展 3.2.3 初始化驱动程序特定的设备对象和设备扩展 3.2.4 设置用户缓冲区的访问 3.2.4.1 使用缓冲I/O 3.2.4.2 使用直接I/O 3.2.4.3 使用非直接也非缓冲I/O 3.3 适配器对象和DMA 3.3.1 映射寄存器 3.3.2 获取适配器对象 3.3.3 拆分传输请求 3.3.4 使用系统DMA 3.3.4.1 基于包的系统DMA 3.3.4.2 公用缓冲区的系统DMA 3.3.5 使用总线控制器DMA 3.3.5.1 基于包的总线控制器DMA 3.3.5.2 公用缓冲区的总线控制器DMA 3.3.6 执行分散/收集DMA 3.4 控制器对象 3.4.1 使用控制器扩展创建控制器对象 3.4.2 为I/O操作分配控制器对象 3.5 中断对象 3.5.1 获得系统指定中断向量、DIRQL、和处理器掩码 3.5.2 注册ISR 3.6 DPC对象 3.6.1 注册和排队DpcForIsr例程 3.6.2 注册和排队CustomDpc例程 3.7 带有相关DPC的定时器对象 3.7.1 注册和激活IoTimer例程 3.7.2 注册和排队CustomTimerDpc例程 3.8 设备队列对象和互锁队列 3.8.1 设置设备队列对象和排队IRP 3.8.2 设置一个互锁队列和排队IRP 3.9 内核调度者对象 3.9.1 定时器对象 3.9.2 事件对象 3.9.3 信号量对象 3.9.4 互斥体对象 3.9.5 调度者对象上线程等待的Alert和APC的处理 3.10 回调对象 3.10.1 定义回调对象 3.10.2 使用驱动程序定义的回调对象 3.10.3 使用系统定义的回调对象 所有内核模式驱动程序使用某些系统定义的对象。例如,所有的驱动程序(除某些微端口驱动程序之外)必须创建并安装设备对象,以代表它为其处理IRP的每个逻辑、虚拟、和/或物理的设备。 仅仅某些种类的驱动程序被要求使用特殊的系统定义的对象。例如,一个给定的驱动程序是否使用适配器对象取决于其设备是否使用DMA。 这章描述内核模式驱动程序使用的系统定义的对象。除了表明的以外,这个信息适用于Windows 2000和WDM驱动程序。哪个对象适合于特定设备类型的细节,参见DDK中设备特定的文档。 系统组成部分和分层内核模式驱动程序 图3.l总结了分层的内核模式驱动程序链和系统组件之间的联系,这些系统组件定义了这些驱动程序能调用哪些支持例程。 图3.l 对驱动程序的可执行组件支持 也可参看《Windows 2000驱动程序开发参考》卷2,得到关于对最低层和中间层驱动程序有用的支持例程的细节。 分层的驱动程序链中的驱动程序的数目有些依赖于设备的性质。例如,SCSI海量存储设备的系统驱动程序形成至少三个驱动程序的链:SCSI类、对应于中间层类的端口驱动程序和图3.1中所示的最低层物理设备驱动程序,并且至少一个文件系统驱动程序在每个类驱动程序上被分层。特定的HBA(宿主总线适配器)的SCSI微端口驱动程序被认为是SCSI端口驱动程序的一部分。 此外,如果用户能够分区镜像,或者创建带或卷集,系统提供的灾难恢复的磁盘驱动程序,ftdisk,可以在SCSI磁盘类驱动程序和文件系统驱动程序之间被分层。ftdisk驱动程序是可选的,但是它符合图3.1中显示的逻辑/虚拟的中间层设备驱动程序。 操纵系统定义的对象的支持例程 最低层驱动程序有与较高层驱动程序不同的标准驱动程序例程,所以图3.1中显示了与硬件和能使用这些对象的最低层驱动程序接近的某些对象(中断、控制器、和适配器)。硬件抽象层(HAL)为I/O管理器、内核、电源管理器和许多驱动程序提供平台特定的支持。 所有驱动程序也能调用由I/O管理器提供的支持例程,以获取和提供有关它们的设备的信息,并在它们的命名的设备对象与子系统特定的名字(针对设备)之间创建符号联接,保存在注册表中。最低层驱动程序也能调用IoXxx和/或RtlXxx支持例程,以获得和设置注册表中的配置信息。更多的信息参见《既插即用、电源管理和安装设计指南》的第4部分,第2章“注册表中的驱动程序信息”。 与I/O管理器类似,安全参考监视器(Security Reference Monitor)、配置管理器(Configuration Manager)、对象管理器、过程结构、内存管理器、运行支持和内核组件,每个都定义一组不透明的对象类型和数据结构。驱动程序只能通过调用适当的内核模式支持例程来使用这些对象和数据结构。 如图3.1所示,最低层(设备)和中间层驱动程序的作者可以忽略本地过程调用(LPC)组件。只有某些最高层的驱动程序调用由安全组件提供的支持例程。在IRP被送到较低层的驱动程序之前,I/O管理器或者文件系统驱动程序执行任何必要的进程内通信和安全访问检查。I/O管理器和对象管理器也在把IRP发送到低层驱动程序之前解析符号联接。 如图3.1所示,I/O管理器为操纵由其他组件定义的某些对象和数据结构提供支持例程,包括符号链接对象、中断对象、DPC对象、定时器对象和内存描述符表(MDL)。这些IoXxx支持例程帮助实现某些标准驱动程序例程,并且映射缓冲区,这可以与驱动程序定制的被发往较低层驱动程序的IRP相关。大多数驱动程序仅仅要求IoXxx支持例程做下列事情中的一些: 为设备注册标准ISR(IoConnectInterrupt),以及当设备被移走的时候注销ISR(IoDisconnectInterrupt)。 注册标准DpcForIsr例程(IoInitializeDpcRequest),并且从它的ISR中向驱动程序的DpcForlsr例程请求一个调用(IoRequestDpc)。 注册标准IoTimer例程(IoInitializeTimer),并且能够启用或禁止使每秒对IoTimer例程的调用(分别是IoStartTimer和IoStopTimer)。 将一个已经被与某个输入IRP相关的MDL映射的过大的缓冲区拆分成较小的映射的缓冲区,从而使的驱动程序能在具有有限数据传输能力的设备上进行DMA操作。 除了文件系统驱动程序,当它们调用IoCreateDevice时,I/O管理器也为驱动程序创建的每个设备对象安装一个相关的设备队列。它也提供这样的支持例程,驱动程序调用这样的支持例程以拥有发往它们的StartIo例程的IRP。IoStartPacket和IoStartNextPacket例程根据驱动程序的需要调用内核的设备队列支持例程。 然而,驱动程序如果必要的话能调用许多I/O管理器调用的相同的内核支持例程。取决于驱动程序的设计和它的设备,驱动程序能创建: 定时器对象和能明确地被排队作为CustomTimerDpc例程的相关DPC对象。例如,如果驱动程序的设备要求可变的超时间隔或者比每秒一次更精细的间隔,驱动程序可能拥有CustomTimerDpc,而非IoTimer例程。 一个或多个DPC对象和来自ISR明确地排队驱动程序提供的CustomDpc例程。例如,一个串口驱动程序可以排队一个CustomDpc,它可以在它的ISR检测到设备错误的时候取消未决的I/O请求。 设备专用线程和工作者线程在其上回调例程的定时器对象能等待驱动程序指定的时间间隔。创建它们自己设备专用系统线程的驱动程序,诸如系统软盘驱动程序,也能有它们的线程来等待内核定义事件、信号量,或者可能有互斥体,就向最高层驱动程序的工作线程能回调例程那样。驱动程序能通过调用PsCreateSystemThread来创建设备专用线程,并能使用PsTerminateSystemThread释放这样的线程。 附加的设备队列对象。例如,SCSI端口驱动程序(参见在线DDK中“Storage Drivers and Device Objects(存储驱动程序和设备对象)”) 创建单一的设备对象代表HBA,而I/O管理器通过(或者从)适当的SCSI类驱动程序把IRP发送到与那个HBA设备对象有关的设备队列中。然而,SCSI端口驱动程序为HBA特定的SCSI总线上的每个逻辑单元创建一个额外的设备队列对象,这个SCSI总是先由SCSI类驱动程序声明的。SCSI端口驱动程序使用这些额外的设备队列来排序进入的IRP到逻辑单元特定的设备队列。 因为所有的DriverEntry和AddDevice例程在系统线程的环境中被调用,在较高层驱动程序已经链自己到相邻的较低层驱动程序上以后,当它初始化的时候,它也可以等待内核事件对象。例如,类驱动程序的AddDevice例程可以调用IoBuildSynchronousFsdRequest使用一个相关的事件创建IRP,并且使用IoCallDriver传递它的驱动程序分配的IRP到下层的端口驱动程序。当位于这样的类驱动程序之下的端口驱动程序收集(或设置)设备状态信息(类驱动程序需要这些信息以完成它的设备的初始化)的时候,这样的类驱动程序可以通过调用KeWaitForSingleObject来等待事件。 运行支持(Executive Support)组件,图3.l中的内核和内存管理器组件之间,提供要求初始化的自旋锁的特定支持例程,调用者必须为这些自选锁提供存储。包含词“互锁”(interlocked)的任何执行体支持例程要求调用者提供的自旋锁作为参数。执行体支持组件也为分配系统空间(存储池)内存的驱动程序和使用系统工作者线程的最高层驱动程序(例如FSD)提供例程。 驱动程序是否使用内存管理器的例程来操纵MDL部分取决于其设备的性质。当每个驱动程序安装它的设备对象的时候,它决定它是否使用MDL去访问用户缓冲区。 存储系统定义的对象 使用I/O管理器的中断、DPC和定时器支持例程的驱动程序能依赖于I/O管理器为任何必要和有关的内核定义的对象提供存储。然而,内核并不代表调用者为支持例程分配内存。因而,任何调用KeXxx例程的系统组件,包括任何驱动程序,必须为它使用的系统定义的对象提供存储。例如,任何拥有标准CustomTimerDpc例程的驱动程序为了调用操作它所需要的定时器和DPC对象的内核支持例程,必须为这些对象提供存储。 有中断服务程序(ISR)的驱动程序必须为由IoConnectInterrupt返回的中断对象指针提供存储。I/O管理器为驱动程序的中断对象(在SMP机器中,每个处理器一个)提供存储,并且初始化它们。 内核实际上使用相关的中断自旋锁定义中断对象类型,并且提供KeSynchronizeExecution,驱动程序使用由IoConnectInterrupt返回的中断对象指针来调用它,并使用驱动程序提供SynchCritSection例程来保证对另一个驱动程序例程和驱动程序的ISR之间的共享数据的多处理器安全访问。 驱动程序也能提供存储、初始化、并使用执行体自旋锁,调用KeAcquireSpinLock和KeReleaseSpinLock来管理对数据的多处理器安全访问,这些数据是在驱动程序例程中间共享的,但是不包括ISR。在IRQ DISPATCH_LEVEL上运行并共享状态的驱动程序例程能调用KeAcquireSpinLockAtDpcLevel和KeReleaseSpinLockFromDpcLevel,它们比KeAcquireSpinLock和KeReleaseSpinLock运行的更快。当然,注意自旋锁不是对象,它是一个不透明的、内核定义的使用内存的同步机制。标准驱动程序例程被调用的IRQL的总述和用法参见第l6章的“使用自旋锁”。 驱动程序通常在它们创建的设备对象的设备扩展内为它们的系统定义的对象和自旋锁(如果有)分配存储空间。如果驱动程序使用控制器对象,并且有ControllerControl例程,它们在控制器扩展中分配存储空间,像系统“AT”盘驱动程序那样。 大多数中间层和最低层驱动程序仅仅使用设备或者控制器扩展来维护必要的设备状态,并为其他驱动程序确定的数据,诸如系统定义对象、自旋锁、互锁队列,以及其他驱动程序定义的数据提供存储空间。然而,如果没有这样的缓冲,驱动程序的设备不能充分地被服务,驱动程序能在设备开始时使用内存管理器的支持例程分配邻接的或者非高速缓存的内部缓冲区。更多信息参见第16章中“管理内存使用”。 设备对象和设备扩展 设备对象,像驱动程序对象一样,对驱动程序是部分地不透明的。因为它们的驱动程序必须通过由IoCreateDevice返回的DeviceObject指针访问这些领域,驱动程序作者必须知道某个域名称和与设备对象有关的系统定义的符号常量,因为它们的驱动程序必须通过DeviceObject指针访问这些域,这个DeviceObject指针是由IoCreateDevice返回的,并且被传递给大多数标准的驱动程序例程。 注意,任何通过一种返回的DeviceObject指针而可以访问的域的位置能从一种Windows平台或版本改变成为另一个。此外,在任何对象之内的“未发表过的”域实际上是难以访问的:即,驱动程序不应该访问它们。已经依赖于系统定义对象中任何域的位置,或访问对象中未公开的域的驱动程序,会削减它自己未来的可移植性和与其他驱动程序的互操作性。 图3.2说明一个设备对象,它在系统之内代表一种物理、逻辑、或者虚拟设备。驱动程序通过从它的AddDevice例程调用IoCreateDevice来创建一个或多个设备对象。 图3.2 设备对象 图3.2 显示了对最低层和中间层驱动程序的作者特别重要的域名称和常量。以下小节中将被更详细描述这些域。 定义设备扩展 对于大多数中间层和最低层驱动程序,设备扩展是与设备对象有关的最重要的数据结构。 其内部的结构是驱动程序定义的,并且它被典型地用于: 保持设备状态信息。 为任何内核定义的对象或者被驱动程序使用的其他系统资源提供存储,如自旋锁。 保持驱动程序必须拥有的任何数据是常驻的,并且在系统空间内,以便完成它的I/O操作。 I/O管理器从常驻的系统空间内存中分配设备对象内存和扩展:即,IoCreateDevice从非页式存储池中为设备对象分配内存。 所有被赋予一个IRP的标准驱动程序例程也被赋予一个指向代表请求I/O操作的目标设备的设备对象的指针,如在第2章中所述的那样。因而,任何给定IRP的驱动程序例程能通过这个指向合适设备对象的指针访问相应的DeviceExtension。通常,对最低层驱动程序的ISR来说,DeviceObject指针也是一个输入参数。 图3.3针对最低层驱动程序设备对象的扩展显示了驱动程序定义的数据的一个代表集。较高层的驱动程序将不再为中断对象指针提供存储,这里的指针由IoConnectInterrupt返回,并到KcSynchronizeExecution和IoDisconnectInterrupt。然而,如果驱动程序有CustomTimerDpc例程,较高层驱动程序将为图3.3中显示的定时器和DPC对象提供存储。较高层驱动程序也可以为执行的自旋锁和互锁的工作队列提供存储。 除为中断对象指针提供存储之外,如果最低层设备驱动程序为两个或更多的设备以不同的向量处理中断,或者它拥有多个ISR,那么它必须为中断自旋锁提供存储。更多的关于注册ISR的信息,参见本章中随后的“中断对象”。 图3.3 最低层驱动程序的样例设备扩展 如图3.3所示,大多数驱动程序发现在它们的设备扩展中存储指向它们设备对象的指针是方便的。驱动程序也可以在扩展中为设备保持一份资源表的拷贝。 较高层驱动程序典型地将指向相邻的较低层驱动程序的设备对象指针存储到其设备扩展中。在较高层驱动程序已经在IRP中创建相邻的较低层驱动程序的I/O栈位置之后,它必须把指向较低层驱动程序的设备对象的指针传递到IoCallDriver,如第2章中所述。 也要注意到,任何为低层驱动程序分配IRP的较高层驱动程序必须指定新的IRP应该有多少栈位置。尤其是,如果较高层驱动程序调用IoMakeAssociatedIrp(仅Windows 2000有)、IoAllocateIrp、或者IoInitializeIrp,为了给这些支持例程提供正确的StackSize作为参数,它必须访问相邻的较低层驱动程序的目标设备对象以读取其栈大小的值。 当较高层驱动程序能通过由IoAttachDeviceToDeviceStack返回的指针从相邻的较低层驱动程序的设备对象中读取数据时,这样的驱动程序必须遵循这些实现方针: 从不试图把数据写到较低层驱动程序的设备对象中。 前面方针的唯一例外情况是文件系统,它在较低层可拆除媒介驱动程序的设备对象的Flags中设置和清除DO_VERlFY_VOLUME。 从不试图因下列原因访问较低层驱动程序的设备扩展: 在两个驱动程序之间,没有同步访问单个设备扩展的安全方法。 实现这样一种后门通信方案的一对驱动程序不能单个地被升级,不改变现存的驱动程序资源不能在它们之间插入中间驱动程序,并且从一个Windows 平台到下一个不能被容易地重编辑和移植。 为了保有从一种Windows平台或者版本到下一个版本与低层驱动程序的互操作性能,较高层驱动程序必须重用给它们的IRP,或者必须创建新的IRP,并且它们必须使用IoCallDriver把请求传送到较低层驱动程序。 创建设备对象和设备扩展 当每个驱动程序调用IoCreateDevicc创建设备对象时,驱动程序在DeviceExtensionSize参数中指定了设备扩展的大小。 大多数驱动程序仅仅从AddDevice例程调用IoCreateDevice。对于一些驱动程序,例如必须对驱动器布局IOCTL作出响应的磁盘驱动程序,这个调用也能由Dispatch例程来作。 除了某些为驱动程序(它与创建设备对象的类驱动程序成对)之外,所有的驱动程序必须从其AddDevice例程调用IoCreateDevicc一次或多次,以创建设备对象,这个设备对象代表它为其处理I/O请求的每一种物理、逻辑、或者虚拟设备。否则,对于驱动程序没有为其创建设备对象的任何目标设备,驱动程序将不能为这些设备获取IRP。 除设备扩展的大小之外,IoCreateDevice要求确定以下参数: 系统定义常量,表明设备对象代表的DeviceType。对于系统定义的FILE_DEVICE_XXX常量列表,分别参见DDK或者WDM控制头文件,ntddk.h或者wdm.h。 一个或多个作过“或”(OR)操作的、系统定义的常量,表明一定类型的设备(软盘、CD-ROM、和WORM可拆卸媒介设备)的DeviceCharacteristics。否则,被传递到IoCreateDevice的DeviceCharacteristics值必须是零。 指明设备对象的Flags中的一位是否被置为DO_EXCLUSIVE的布尔值(Exclusive,专有权),表明驱动程序服务于一个专有设备,例如视频、串行、并行、或声音设备。WDM程序必须设置Exclusize为FALSE。 指向DriverObject的指针,它被输入到驱动程序的AddDevice例程,将驱动程序对象与调用者为其处理IRP的物理、逻辑、或者虚拟设备相联系。 一个可选的指向零结尾的Unicode串(DeviceName)的指针,这个Unicode串为设备命名。PnP驱动程序不应该提供设备名称;这样做绕过了PnP管理器的安全特性。PnP驱动程序能通过调用IoAttachDeviceToDeviceStack把自己链到一个没有名称的设备的驱动程序。 如果用户模式组件需要一个到设备的符号连接,注册设备接口(参见IoRegisterDeviccInterface)。如果内核模式组件需要传统的设备名称,驱动程序必须命名设备对象,但是命名是不被推荐的。 对于除文件系统驱动程序(FSD)之外的所有驱动程序,I/O管理器也为每个对IoCreateDevice的成功调用创建一个相关的设备队列对象,如图3.2。与设备对象相关的设备队列对象在驱动程序被装载之后,描述一个发往StartIo例程的IRP队列。管理它们自己内部IRP队列的驱动程序,例如系统软盘控制器驱动程序(参见第3部分),不使用与它们的设备对象有关的设备队列。 驱动程序也能创建附加的设备队列对象。更多的信息参见“设备队列对象和互锁队列”。 如果对IoCreateDevice的调用成功,I/O管理器为设备对象自己和与设备对象有关的所有其他数据结构提供存储,包括驱动程序设备扩展,它用零初始化。 如果调用者创建多个设备对象,I/O管理器通过在设备对象中维护NextDevice指针,连接其后其创建的设备对象到输入驱动程序对象。 初始化驱动程序特定的设备对象和设备扩展 IoCreateDevice返回后,给调用者一个指向DeviceObject的指针,DeviceObject中包含了指向DeviceExtension的指针,驱动程序必须为它们各自的物理,逻辑,和/或者虚拟设备在设备对象中创建一定的域。 IoCreateDcvice设置新创建的设备对象的StackSize域为一。最低层驱动程序可以忽略这个域。当一个较高层驱动程序调用IoAttachDeviceToDeviceStack把自己附加到相邻的较低层的驱动程序上时,程序自动地设置设备对象中的域StackSize为相邻的较低层驱动程序的那个值加一。然而,对于某些设备类型,较高层驱动程序可能需要设置域StackSize为较大的值,就像在设备特定文档中说明的那样。设置栈大小保证发送到较高层驱动程序的IRP将包含驱动程序特定的I/O栈位置,加上当前I/O栈位置的数得到链中所有较低层驱动程序。 IoCreateDevice设置新创建的设备对象的AlignmentRequiremcnt域为处理器的数据高速缓存线的大小减一,以保证用于直接I/O的缓冲区被正确地排列。IoCreateDevice返回之后,最低层物理设备驱动程序必须做如下步骤: 1. 从设备的排列请求中减一。 2. 把步骤1的结果与设备对象AlignmentRequirement的当前值相比较。 3. 如果设备排列请求较大,设置AlignmentRequirement为步骤1的结果。否则,将AlignmentRequirement值作为IoCreateDevice的设置值。 在任何较高层驱动程序通过调用IoGetDeviceObjectPointer把自己链到另一个驱动程序之后,较高层驱动程序必须设置它的新创建的设备对象的AlignmentRequirement域为相邻的较低层驱动程序的设备对象的那个值。作为一般规则,一个较高层驱动程序不应该改变这个值。如果较高层驱动程序调用IoAttachDevice或者IoAttachDeviceToDevice,那些例程自动的设置设备对象中的AlignmentRequirement域为较低层驱动程序的设备对象的那个值。 IoGetDeviceObjectPointer返回指向较低层驱动程序的设备对象和指向相关的文件对象的两个指针。只有FSD(或者可能是另一个最高层驱动程序)能使用返回的文件对象指针。调用IoGetDeviceObjectPointer的中间驱动程序保存这个文件对象指针,从而使得当驱动程序被卸载时,它能通过调用ObDereferenceObject被间接引用。 在FSD安装包含文件对象(它代表一较低层驱动程序的设备对象)的卷后,中间驱动程序不能通过调用IoAttachDevice或者IoAttachDeviceToDeviceStack把自己链到文件系统和较低层驱动程序之间。 中间层或者最低层驱动程序也在设备对象的Flags中设置一个位,方法是将它与DO_DIRECT_IO或者是与DO_BUFFERED_IO作“或”操作。如果驱动程序作者决定有关的附加工作将以较好的驱动程序性能为代价,对于缓冲或者直接I/O的,逻辑或者虚拟的设备的最高层驱动程序能避免设置Flags。中间驱动程序必须创建其设备对象的Flags域以匹配相邻的较低层驱动程序的设备对象。 设置设备对象的Flags域为DO_DIRECT_IO或DO_BUFFERED_IO,决定I/O管理器如何在所有的后来发送到驱动程序的数据传输请求中传递访问到用户缓冲区。 然后驱动程序能在设备对象中设置任何其他的依赖设备的值。例如,可拆卸媒介设备的非WDM驱动程序在I/O操作期间检测到(或怀疑)媒介中的变化,它必须使用DO_VERIFY_VOLUME与设备对象的Flags成员作“或”操作。(更多信息参见第16章中“处理可删除的媒介”。)要求突发(inrush)电源的设备的驱动程序必须用DO_POWER_INRUSH与Flags成员做“或”操作,并且不在系统页路径上的设备的驱动程序必须用DO_POWER_PAGABLE与Flags成员做或操作。PnP功能和过滤器驱动程序必须清除DO_DEVICE_INITIALIZING标记。 在驱动程序初始化设备对象之后,它也能初始化任何内核定义的对象和它在设备扩展中为其提供存储的其他系统定义的数据结构。驱动程序具体什么时候执行这些任务依赖于其设备、对象的类型、和/或数据的性质。简言之,能持续经过PnP开始和停止请求的任何对象或者数据结构能在AddDevice例程中被初始化。那些要求以PnP IRP_MN_START_DEVICE请求予以提供的资源信息的对象和数据结构,或者那些当设备被停止和/或重新启动时可能要求变化的对象和数据结构,应该在驱动程序处理IRP_MN_START_DEVlCE请求时被初始化。 设置用户缓冲区的访问 大多数最低层和所有中间层驱动程序在DeviceObject->Flags中设置一个位,如图3.2所示,方法是使用DO_BUFFERED_IO或者DO_DIRECT_IO与他们创建的每个设备对象中的Flags做或操作。对于数据传递,驱动程序必须使用下列的三种方法之一来访问用户缓冲区: 服务于一种交互式(缓慢)的设备或一次通常传输比较少的数据的驱动程序应该创建它的设备对象,以请求缓冲I/O。为小而交互式的传递使用缓冲I/O会全面地改进物理内存的使用,因为内存管理器不必为每个传输锁定整个物理页,而直接I/O通常是这样做的。一般来说,视频、键盘、鼠标、串口,和并口的驱动程序请求缓冲I/O。 任何在这样一种设备驱动程序之上的中间层驱动程序必须创建它的DeviceObject->Flags,以匹配相邻的较低层驱动程序的那个值。 服务于一次能传递大量数据的设备的驱动程序应该创建它的设备对象,以请求直接I/O。通过减少常规的中断和最小化缓冲I/O固有的内存分配和拷贝操作,对大批量数据传递使用直接I/O将提高驱动程序性能。一般地,海量存储设备驱动程序对传输请求要求直接I/O,包括使用DMA或者PIO的最低层驱动程序,还有任何链在它们之上的中间层驱动程序。 甚至请求直接I/O的驱动程序使用缓冲I/O满足一定的IRP。尤其是,驱动程序能对某种系统定义的、设备类型特定的I/O控制代码使用缓冲I/O,这些控制代码是对要求数据传输的RP_MJ_DEVICE_CONTROL请求的,而不论驱动程序是否已经用DO_DIRECT_IO与它的设备对象的Flags做或操作。设备控制请求的缓冲方法由设备控制代码自己确定。 任何在这样一种设备驱动程序之上的中间层驱动程序层必须创建它的DeviceObject->Flags,以匹配相邻的较低层驱动程序的那个值。 在创建一个设备对象时,一直在原始的、用户模式的线程(它请求一个I/O操作)的环境中被调用的驱动程序既不要求直接I/O也不要求缓冲I/O。这样一种驱动程序必须是最高层的驱动程序。 下面的小节描述如何使用DO_BUFFERED_IO、DO_DIRECT_IO与设备对象的Flags做“或”操作,或者也不影响发往驱动程序的数据传输请求,包括数据在物理内存中如何被存储,以及驱动程序如何访问那个内存。 使用缓冲I/O 图3.4说明I/O管理器如何创建一个对驱动程序请求传输操作的IRP,这个驱动程序使用DO_BUFFERED_IO与它们的设备对象的Flags做“或”操作。 图3.4 对用户缓冲区的缓冲I/O 图3.4显示驱动程序已经用DO_BUFFERED_IO与设备对象的Flags做“或”操作时,驱动程序如何为读请求使用IRP中的SystemBuffer指针以传递数据。循环中的序号解释如下: 用户空间的虚拟地址的某些范围代表当前线程的缓冲区,并且缓冲区的内容可以存储在基于页的物理地址(图3.4中黑暗部分)的范围之内的某一地方。 I/O管理器为当前线程的读请求服务,线程传递给它一个代表缓冲区的用户空间虚拟地址的范围。 I/O管理器检查用户提供的缓冲区的可访问性,并且调用ExAllocatePool以创建一个常驻的系统空间缓冲区(SystemBuffer),大小为等于用户提供缓冲区。 I/O管理器提供机会访问IRP中的新分配的SystemBuffer,这个IRP是它发送给驱动程序的。 如果图3.4显示一个写请求,I/O管理器将在它发送IRP到驱动程序之前,把从用户缓冲区拷贝数据到系统缓冲区。 对于图3.4中所示的读请求,驱动程序将数据从属设备读到系统空间缓冲区。当读的请求被满足,驱动程序使用IRP调用IoCompleteRequest。 当原来的线程再一次成为当前的线程,I/O管理器从系统缓冲区将读入数据拷贝到用户缓冲区。它也调用ExFreePool以释放系统缓冲区。 在I/O管理器已经为驱动程序创建系统空间缓冲区之后,请求的用户模式线程可能被交换出去,而其物理内存可能被另一个线程所重用,并且这个线程可能是属于另一个过程的。 然而,直到驱动程序使用IRP调用IoCompleteRequest,在IRP中所提供的系统空间虚拟地址范围会一直保持有效。 一次不传输大量数据的设备(例如交互式的设备)的驱动程序能使用缓冲I/O。 一次传输大量数据的驱动程序,尤其是做多页传输的驱动程序,不应该试图使用缓冲I/O。当系统运行时,非页式存储池可能变成碎片,这样I/O管理器不能分配大而连续的系统空间缓冲区给这样的驱动程序来发送IRP。 注意,对于某种IRP_MJ_XXX,所有驱动程序使用缓冲I/O。甚至,为直接I/O而创建它们自己的设备对象的驱动程序会为大多数请求使用缓冲I/O,这些请求不包括IRP_MJ_READ、IRP_MJ_WRITE,还可能不包括驱动程序定义的IRP_MJ_INTERNAL_DEVICE_CONTROL请求,这个请求要求大数据传输。 设备控制请求的缓冲方法由这个设备控制编码自己来确定。 使用直接I/O 图3.5说明I/O管理器如何创建一个向驱动程序请求传输操作的IRP,这个驱动程序使用DO_DIRECT_IO与它们的设备对象的Flags作“或”操作。 图3.5 使用DMA的设备的用户缓冲区上的直接I/O 图3.5说明驱动程序如何使用IRP的MdlAddress来为读请求传递数据。图中的驱动程序使用基于包的系统或者总线控制器DMA,并且已经使用DO_DIRECT_IO对设备对象的Flags作“或”操作。 用户空间的虚拟地址的某些范围代表当前线程的缓冲区,并且缓冲区内容实际上可以被存储在一些物理上不连续的页上(图3.5中黑暗部分)。I/O管理器创建MDL描述这个缓冲区。MDL是一个不透明的由内存管理器定义的数据结构,这个数据结构映射一块特殊的虚拟地址范围到一个或多个基于页的物理地址范围。 I/O管理器为当前线程的读请求服务,线程为它传递代表缓冲区的用户空间虚拟地址的范围。 I/O管理器或者FSD检查用户提供的缓冲区的可访问性,并且调用使用前面创建的MDL调用MmProbeAndLockPages。MmProbeAndLockPages也填充MDL中相应的物理地址范围。 如图3.5所示,虚拟范围的MDL可能有几个相应的基于页的物理地址入口和虚拟范围,因为缓冲区可以在距MDL中描述的首页和末页的开始一些字节偏移的地方开始和结束。 I/O管理器在IRP中提供一个指向MDL(MdlAddress)的指针。直到驱动程序完成IRP后I/O管理器或者文件系统调用MmUnlockPages,在MDL中所描述的物理页仍然锁定并被分配到缓冲区。然而,这样的MDL中的虚拟地址能变得不可见(和无效),即使是在IRP被发送到设备驱动程序或者发送到可能在设备驱动程序之上的任何中间层驱动程序之前。 如果驱动程序使用基于包的系统或者总线控制器DMA,它使用IRP的MdIAddress指针调用MmGetMdlVirtualAddress,以得到MDL基于页的入口的基本虚拟地址。 当驱动程序的AdapterControl例程拥有到DMA通道的访问和/或映射驱动器,为了从设备中直接读取数据到物理内存,驱动程序使用由MmGetMdlVirtualAddress返回的基地址调用MapTransfer。 驱动程序应该总是检查缓冲区长度。注意,I/O管理器不能为零长度的缓冲创建MDL。 在DMA和PIO传输期间,驱动程序必须拥有措施保持高速缓存的一致性。使用DMA的驱动程序也必须使用适配器对象。 使用可编程I/O(PlO)而不是DMA的设备必须两次映射用户空间缓冲区到系统空间地址范围,如图3.6所示。 图3.6 使用PIO的设备的直接I/O 图3.6显示使用PIO的设备如何处理同样的任务。 用户空间虚拟地址的某些范围代表当前线程的缓冲区,并且缓冲区的内容实际上可以被存储在一些物理上不连续的页上。如果缓冲区长度是零,I/O管理器创建MDL描述这个缓冲区。 I/O管理器为当前线程的读请求服务,线程为它传递代表缓冲区的用户空间虚拟地址的范围。 I/O管理器或者FSD检查用户提供的缓冲区的可访问性。如果I/O管理器已创建MDL,它使用MDL调用MmProbeAndLockPages,这个MDL为用户缓冲区指定了虚拟地址的范围。 MmProbeAndLockPages也填充MDL中相应的物理地址范围。 I/O管理器在请求传输操作的IRP中提供一个指向MDL(MdlAddress)的指针。直到驱动程序完成IRP后I/O管理器或者文件系统调用MmUnlockPages,在MDL中所描述的物理页仍然保持锁定并被分配到缓冲区。然而,这样的MDL中的虚拟地址能变得不可见(和无效),即使是在IRP被发送到设备驱动程序或者发送到可能在设备驱动程序之上的任何中间层驱动程序之前。 如果驱动程序请求系统(虚拟的)地址,驱动程序使用IRP的MdlAddress指针调用MmGetSystemAddressForMdlSafe(Windows 2000)或MmGetSystemAddressForMdl(WDM),以两次映射MDL中的用户空间虚拟地址到系统空间地址范围。在图3.6中,AliasBuff代表描述双重映射的地址的MDL。 驱动程序使用来自双重映射的MDL的系统空间虚拟的地址范围(AliasBuff),将数据读到内存中。 当驱动程序通过调用IoCompleteRequest完成IRP,如果驱动程序调用MmGetSystemAddressForMdlSafe(Windows 2000)或者MmGetSystemAddressForMdl(WDM)的话,I/O管理器或者文件系统释放MDL的双重映射的系统空间范围。 I/O管理器或者文件系统解锁在MDL中描述的页,并且根据驱动程序的需要处置MDL和IRP。要想得到较好的性能,驱动程序应该避免双重映射MDL物理地址到系统空间,如在步骤3中描述的,除非它们必须使用虚拟的地址。这样做没有必要使用系统页入口,并能降低驱动程序性能和伸缩性。此外,如果它运行出页表入口,系统可能崩溃,因为大多数较老的驱动程序不能处理这种情况。 仅仅在那个线程是当前的时候,当前用户线程的缓冲区和线程本身被保证常驻在物理内存中。对于在图3.6中所显示的线程,当另一个过程的线程被运行时,其用户缓冲区的内容可能被换到二级存储中。当另一个过程的线程被运行时,请求的线程的缓冲区的系统物理内存能被写,除非内存管理器已经锁定并且保存包含原来线程的缓冲区的相应物理页。 然而,当另一个线程当前正在运行的时候,其缓冲区的原始线程的虚拟地址并不保持是可见的,即使内存管理器保存缓冲区的物理页。因而,驱动程序不能使用由MmGetMdlVirtualAddress返回的一个虚拟地址来访问内存。为了使用基于包的系统或者总线控制器DMA传输数据,这个例程的调用者必须把其结果传递到MapTransfer(与IRP的MdlAddress指针一同)。 使用非直接也非缓冲的I/O 如果设备对象没有被创建使用直接和缓冲I/O,那么直接I/O管理器将传递原始用户空间虚拟地址,这个地址在被发送到驱动程序的IRP中。因为这样一种驱动程序必须在调用的线程的上下文中执行,以安全地访问其缓冲区,仅仅最高层驱动程序(例如FSD)能创建它们的设备对象,而不用使用DO_DIRECT_IO或者DO_BUFFERED_IO与每个设备对象的Flags作“或”操作。 中间或者最低层驱动程序不能总满足这个条件。例如,如果一个正在请求的线程等待一个I/O请求的完成,或者如果一个较高层驱动程序(特别是文件系统)位于中间层或者最低层驱动程序之上,那么较低层驱动程序的例程是不可能在请求的线程的上下文中被调用的。 当I/O管理器为缓冲区发送带有当前线程的用户空间虚拟地址的IRP时,没有使用DO_BUFFERED_IO或者DO_DIRECT_IO与设备对象的Flags作“或”操作的驱动程序必须作下列事情: 检查用户缓冲区的地址范围的有效性,并检查是否适当的读或写被许可,这个可以用ProbeForRead和ProbeForWrite支持例程。驱动程序必须将它对缓冲区的地址范围的访问封入驱动程序提供的例外情况处理者中,从而使得在这个驱动程序访问内存时用户线程不能改变对缓冲区的访问权限。如果检测到发生了例外情况,驱动程序将返回错误。驱动程序必须调用作I/O请求的线程的上下文之内的例程;因此,只有较高层驱动程序能执行这种任务。 以下列的方法之一管理缓冲区和内存操作: 完成它自己的双缓冲区操作,就像I/O管理器为使用缓冲I/O的驱动程序做的那样。 创建它自己MDL,并且通过调用内存管理器的支持例程锁定缓冲区,就像I/O管理器为使用直接I/O的驱动程序做的那样。 在调用的线程的上下文中的用户缓冲区上直接执行所有必要的操作。驱动程序必须在驱动程序提供的例外处理器中封装它对缓冲区的访问,以防在驱动程序访问内存时,用户线程改变访问缓冲区的权限,或者改变缓冲区中的数据。 实际上,驱动程序必须在每IRP基础上选择是否做缓冲I/O、直接I/O、或者在调用线程的上下文中的I/O,并且它必须处理可能在用户模式线程上下文中发生的任何例外情况。驱动程序必须管理它自己的用户缓冲区访问、双缓冲区操作、和内存映射,当必要时,让I/O管理器代替为驱动程序处理这些操作。 适配器对象和DMA 如果驱动程序为直接I/O创建它的设备对象,并且它的设备使用DMA,则它必须使用系统创建的适配器对象,如在3.2.4.2中论述的那样。适配器对象代表DMA控制器通道或者端口,或者总线控制器设备。 两种最低层驱动程序必须使用适配器对象: 使用系统DMA控制器(也称为从属设备)的设备的驱动程序。这样的设备被称为“使用系统DMA” 。 使用系统DMA 控制器的从属DMA设备的驱动程序必须有AdapterControl例程,并且调用系统提供的支持例程,这些支持例程为了完成DMA传输而操作适配器对象。细节参见“使用系统DMA”。 是总线控制器适配器的设备的驱动程序。这样的设备为I/O总线的使用进行系统仲裁,然后使用总线控制器DMA。细节参见“使用总线控制器DMA”。 非SCSI的总线控制器DMA设备的驱动程序通常必须有AdapterControl例程,并且必须调用系统提供的支持例程,这些支持例程为了完成DMA传输而操纵适配器对象。 SCSI端口驱动程序为HBA特定的SCSI微端口驱动程序创建所有必要的适配器对象。当微端口驱动程序的HwScsiFindAdapter例程执行时,微端口驱动程序为SCSI端口驱动程序提供必要的数据,以创建相应的适配器对象。 简短地讲,控制总线控制器设备或连接到系统DMA控制器的从属设备的任何最低层驱动程序,必须明确地使用适配器对象(参见“获取适配器对象”)关联它的设备对象。驱动程序为指向适配器对象的指针提供存储,通常是在设备扩展中。此外,驱动程序拥有一个AdapterControl例程,这个例程调用系统提供的适配器对象支持例程以执行DMA传输。 作为设备启动的一部分,这些驱动程序调用I/O管理器,I/O管理器轮流调用平台特定的HAL来创建一组适配器对象。这样,在任何Windows 平台上,适配器对象组通常为下述东西包括一个适配器对象: 每个系统DMA控制器通道或者端口,从属设备被附加到这个端口。 机器中的每个总线控制器DMA设备。 除使用适配器对象之外,执行DMA的驱动程序使用三种不同的地址空间,如图3.7中所示。 图3.7 物理、逻辑、和虚拟的地址映射 在任何Windows 平台上,驱动程序拥有对处理器支持全部虚拟地址空间的访问。在32位处理器上,虚拟地址空间可以描述四个G(gigabyte)的空间。CPU用页表的方法将虚拟地址空间中的地址转换为系统物理地址空间中的地址。每个页表项(PTE)映射虚拟内存的一页到物理内存的一页,必要时作翻页操作。MDL(内存描述符列表)为与驱动程序DMA操作有关的缓冲区提供类似的映射。 设备根据它们的能力改变它们对系统所有虚拟地址空间的访问。设备在逻辑(设备)地址空间中使用地址。每个HAL使用映射寄存器把设备或者逻辑地址转换成为一个物理地址(物理RAM中的位置)。对于设备硬件,映射寄存器执行与MDL(和页表)为软件(驱动程序)执行的相同的功能:它们把地址转换成为物理的内存。 因为这些地址被分别编址,所以驱动程序不能在虚拟地址空间中使用指针在物理内存中的定位一个位置,反之亦然。驱动程序必须首先把虚拟的地址转换成为一个物理的地址。 同样地,驱动程序不能使用一个逻辑地址来访问物理内存; 它必须首先转换地址。 映射寄存器 HAL必须创建支持不同机器上的各种各样的DMA设备和I/O总线DMA的适配器对象。例如,大多数ISA DMA控制器、从属设备、和总线控制器设备没有足够地址线以访问32位处理器的全部四G系统物理地址空间。相比之下,EISA DMA设备一般有多于足够数量的地址线在32位处理器中访问全部的系统物理地址空间。因而,所有的HAL都提供映射,这些映射将DMA设备能访问的逻辑地址范围映射到每台单独机器的物理地址范围。 每个适配器对象与一个或多个映射寄存器有关,依赖于将要传递的数据的数量和可供使用的内存的数量。在DMA传输期间,HAL使用每个映射寄存器用设备可访问的逻辑页作为CPU中物理内存页的别名。实际上,映射寄存器为使用DMA的驱动程序提供发散/收集支持,而不管它们的设备是否有发散/收集能力。 图3.8说明这样一种物理-逻辑地址的映射,它是没有分散和收集能力的ISA DMA设备的驱动程序的。 图3.8 样本ISA DMA设备的地址映射 图3.8显示了下列类型的映射: 每个映射寄存器为ISA DMA设备映射物理地址(实线所指的)的范围到低级逻辑地址(虚线)。 在这里,三个映射寄存器被用于将系统物理内存中的三个分页的范围别称为ISA DMA设备的低级逻辑地址。 在DMA操作期间,ISA设备使用绘制映射的逻辑地址来访问系统内存。 对于一种可比较的EISA DMA设备,三个映射寄存器也将用于数据的三个页大小的范围。然而,映射的逻辑地址范围将没有必要与相应的物理地址范围相同,所以EISA设备也将使用逻辑地址来访问系统内存。 每个MDL中的项映射虚拟地址空间中的位置到物理地址。 注意映射寄存器与MDL中虚拟-物理项之间的对应: 对一个DMA传输操作,每个映射寄存器和MDL中的每个虚拟项映射至少一个完整的数据物理页。 每个映射寄存器和MDL中的每个虚拟项可以映射小于一个完整的数据物理页。例如,MDL中的初始虚拟项可能从物理页边界映射一个偏移量,如图3.5所示。 每个映射寄存器和MDL中的每个虚拟项映射最小是一个字节。 在IRP中请求读或写操作,在Irp->MdlAddress的对驱动程序不透明的MDL中的每个虚拟项代表用户缓冲区的物理内存中的一个页边界。同样地,单独的DMA传输需要的每个额外映射寄存器代表设备可访问的逻辑地址范围中的页边界,这个逻辑地址是对系统物理地址的别称。 在所有的Windows 平台上,每个适配器对象拥有一个相关的集合,这个集合由一个或多个位于平台特定的(和对驱动程序不透明的)基地址上的映射寄存器组成。从驱动程序的观点来说,在图3.9中所显示的MapRegisterBase是一组映射寄存器的句柄,这些映射寄存器可以是芯片中的硬件寄存器,或者是在系统DMA控制器中,或者在总线控制器适配器中,或者甚至是系统内存中HAL创建的虚拟寄存器。 然而,可供适配器对象使用的映射寄存器的数目可以根据不同的设备和Windows平台而变化。例如,在某些平台上,HAL能创建更多的使用系统DMA的驱动程序可用的映射寄存器,因为不同Windows平台上的DMA控制器有不同的能力。 获取适配器对象 在设备启动时,驱动程序可以使用系统或者总线控制器DMA调用IoGetDmaAdapter以获得指向一个适配器对象的指针,并且确定可供每个传输操作使用的映射寄存器的最大数目。当驱动程序调用IoGetDmaAdapter时,I/O管理器依次调用HAL以获得必要的平台信息。 在驱动程序调用IoGetDmaAdapter时,它必须在系统定义的DEVICE_DESCRIPTION结构中提供一定的信息。在给DEVICE_DESCRIPTION结构赋值前,所有驱动程序必须使用RtlZeroMemory以用零来初始化这个结构。 需要的数据包括关于驱动程序的设备的特性信息,诸如如果设备有分散/收集能力,那么它是否是总线控制器,还有设备一次能传输的数据量是多少字节(MaximumLength)。 需要的设备描述数据也包括平台特定信息,诸如总线的平台特定的和系统指定的总线数量,这些总线由总线控制器的驱动程序来控制。驱动程序能通过调用IoGetDeviceProperty来获得这个信息。 DEVICE_DESCRIPTION结构包括一些域,这些域可以与一些DMA设备或驱动程序不相干。例如,WDM驱动程序中不使用BusNumber域。每个驱动程序应该为相关的结构成员提供值,并且应该为所有其他成员赋零。 从属设备的驱动程序不应该在ScatterGather领域中传递TRUE,除非当请求必须被拆分成两个或多个DMA操作时,这个设备能等待系统DMA控制器以重新被组织。 IoGetDmaAdapter返回一个指向适配器对象的指针和一个平台特定或设备特定的值,这个值指明对每个DMA传输操作,有多少映射寄存器是可供适配器对象使用的。 返回的适配器对象包含驱动程序可以访问的三个域: 版本号(Version) 大小(Size), 指向DMA_OPERATION结构的指针(DmaOperations) DMA_OPERATION结构组成一个指向函数的指针表,程序必须使用这些函数在其设备上执行DMA操作。函数只能通过这个数据结构中的指针被访问;驱动程序不能通过名称直接调用它们。(这些程序替换了在Windows NT的以前版本中支持的HalXxx。为了确保早期驱动程序的兼容性,wdm.h和ntddk.h头提供了使用过时名称的宏命令,但是新的驱动程序应该总是通过数据结构调用这些函数。) 映射寄存器的数目可以是因设备不同而不同,也可以是因平台不同而不同。通常,HAL根据下列标准分配一定数量的映射寄存器: 如果可能,HAL返回一个值,这个值比传输MaximumLength字节所需要的映射寄存器的数目大一,MaximumLength在对IoGetDmaAdapter的驱动程序的调用中被指定。 否则,HAL返回一个较小的值,这个值对特定的平台则是尽可能的大。 换句话说,HAL通常给每个驱动程序足够的映射寄存器,从而使得它的设备最大化DMA容许能力,但是HAL能在一些Windows 平台上返回较小的值。不能保证驱动程序将得到它所要求数量的映射寄存器,因此驱动程序应该总是检查返回值。 任何DMA设备驱动程序必须为IoGetDmaAdapter返回的适配器对象指针和NumberOfMapRegisters值提供存储。这个指针是系统提供的用于DMA的支持例程所要求的参数。因为许多这样的支持例程必须在IRQL DISPATCH_LEVEL上被调用,驱动程序分配的存储空间必须是常驻的。大多数DMA驱动程序在设备扩展中提供必要的存储。当然,如果驱动程序也使用控制器对象,存储可以在控制器扩展中,或者在驱动程序分配的非页式存储池中。更多信息参见第16章中的“分配系统空间内存和管理硬件优先级”。 当驱动程序已经完成了所有的DMA操作,它调用PutDmaAdapter释放适配器对象。 拆分传输请求 任何驱动程序可能需要拆分传输请求,并完成多个DMA操作,以满足给定的IRP,这依赖于下述条件: IoGetDmaAdapter返回的NumberOfMapRegisters 当前IRP中驱动程序的I/O栈位置的Length值(将要被传递的数据字节数) 缓冲区的系统物理内存中页边界的数目,这个缓冲区是驱动程序用来传递数据的 对驱动程序的DMA操作的任何设备特定约束 驱动程序能使用宏ADDRESS_AND_SIZE_TO_SPAN_PAGE来确定需要多少映射寄存器来传输所有在IRP中请求的数据,如下: 调用MmGetMdlVirtualAddress为缓冲区得到起始虚拟地址,把指针传递到在Irp->MdlAddress的MDL。注意,驱动程序不可试图使用这个虚拟的地址访问内存。 被MmGetMdlVirtualAddress返回的值是进入MDL的一个索引,不必是一个有效的地址。 传递IRP的驱动程序的I/O栈位置中返回的索引和Length值到ADDRESS_AND_SIZE_TO_SPAN_PAGE。 如果由ADDRESS_AND_SIZE_TO_SPAN_PAGE返回的值大于由IoGetDmaAdapter 返回的NumbertwRegisters,驱动程序不能为这种单一的DMA操作中的IRP传输所有被请求的数据。相反,它必须做下列事情: 将缓冲区拆分成一定尺寸的片,以适合可用的映射寄存器的数目和任何设备特定DMA的约束。 完成它能满足当前传输请求的同样多的DMA操作。 例如,假设ADDRESS_AND_SIZE_TO_SPAN_PAGE表明需要十二个映射寄存器来满足给定的传输请求,但是在这个平台上驱动程序只有五个可供使用的映射寄存器,并且没有设备特定的DMA约束。在这种情况下,驱动程序必须完成(l2/5+1=3)DMA传输操作,至少调用MapTransfer(通过DMA_OPERATION结构中返回的函数指针)三次,以传输被这个IRP请求的所有数据。 当没有足够的映射寄存器用单一的DMA传输操作满足给定的IRP时,系统DMA设备驱动程序使用各种各样的技术来拆分一个DMA传输。例如,存储类驱动程序被请求为位于下面的SCSI端口/微端口驱动程序拆分大的传输请求。系统存储类驱动程序为每片这样的IRP请求分配额外的IRP,并使用每个IRP注册它的IoCompletion例程,以跟踪整个传输请求的状态和释放驱动程序分配的IRP。然后,它把使用IoCallDriver发送IRP到端口驱动程序上。 然而,仅仅在类驱动程序能确定多少映射寄存器可供端口驱动程序使用的情况下,其他类/端口驱动程序才能使用这种技术。换句话说,端口驱动程序必须为成对的类驱动程序在注册表中存储这个配置信息,或者成对的驱动程序必须定义一个私有接口,使用内部设备I/O控制请求,从端口驱动程序到类驱动程序传递有关可供使用的映射寄存器的数目的配置信息。 其他类/端口驱动程序不必使用这种技术,而整体式DMA设备驱动程序必须为自己拆分大的传输请求。这样驱动程序通常拆分大的请求成若干块,并完成一系列DMA操作以满足当前IRP。 随后的小节(“使用系统DMA”和“使用总线控制器DMA”)描述DMA设备的整体式驱动程序如何使用支持例程以满足传输请求。这些节假定驱动程序拥有以下部分: 一个标准的StartIo例程,而不是设置和管理IRP的一个内部队列。 用于因为没有充足数量的可用映射寄存器而拆分传输请求的一个内部例程。 没有设备特定的DMA约束。 换句话说,这些节为驱动程序的DMA操作描述了最简单的可能技术,但是单独的驱动程序不必使用完全相同的技术。对于DMA设备的任何驱动程序,那些驱动程序例程应该拆分较大的DMA传输请求取决于驱动程序模型(类/端口,整体式的,如此等等),依赖于设备的特性,以及依赖于驱动程序必须处理的任何设备特定的DMA约束。 使用系统DMA 从属DMA设备的驱动程序使用下列类型的系统提供的DMA支持中的一个: 基于包的DMA,如果驱动程序不必使用系统DMA控制器的自动初始化模式的话。参见“基于包的系统DMA”。 公用缓冲区DMA,如果驱动程序确实使用自动初始化模式的话。参见“公用缓冲区的系统DMA”。 此外,任何使用基于包的DMA的驱动程序能使用预期是流线型分散/收集DMA的支持例程,而不管其设备是否拥有内部的分散/收集支持。细节参见“执行分散/收集DMA”。 基于包的系统DMA 当它们处理一个请求DMA传输的IRP时候,使用基于包的DMA的任何从属设备调用下列的支持例程的普通序列: KeFlushIoBuffers,仅仅在试图分配系统DMA控制器之前调用 更多关于在DMA期间使用KcFlushIoBuffers和FlushAdapterBuffers以保持高速缓存一致性的信息参见第16章。 AllocateAdapterChannel,当驱动程序准备好为DMA编程它的设备,并且需要系统DMA控制器的时候调用 反过来,AllocateAdapterChannel调用驱动程序的AdapterControl例程。 得到进入MDL的索引的MmGetMdlVirtualAddress,它被要求作为对MapTransfer初始调用中的一个参数 MapTransfer,它为传输操作编程系统DMA控制器 驱动程序可能需要调用MapTransfer多次,以传输所有请求的数据,如“拆分传输请求”中所述的那样。 FlushAdapterBuffers,恰好在每个DMA传输操作之后被调用 如果驱动程序必须调用MapTransfer多次以传输所有请求的数据,它必须调用FlushAdapterBuffers与调用MapTransfer同样多的次数。 FreeAdapterChannel,要么在所有请求的数据被传输完毕就调用,要么在由于设备I/O错误造成驱动程序处理IRP失败时调用 被IoGetDmaAdapter返回的AdaPterObject指针被每个这样的例程(除了KeFlushIoBuffers和MmGetMdlVirtualAddress)要求作为一个参数,KeFlushIoBuffers和MmGetMdlVirtualAddress需要指向在Irp->MdlAddress传递的MDL的指针。 单独的驱动程序在不同的点上调用这个支持例程的序列,这依赖于每一驱动程序如何被实现服务其设备。例如,一种驱动程序的StartIo例程可以对AllocateAdapterChannel作调用,另一个驱动程序可以从一个例程(这个例程从驱动程序创建的互锁队列中移出IRP)中做这个调用,并且还可以在另一个驱动程序的奴隶DMA设备表明它准备好传输数据的时候作这个调用。 为基于包的DMA分配适配器通道 为了为基于包的系统DMA作准备,驱动程序在收到IRP_MJ_READ或者IRP_MJ_WRITE之后调用KeFlushIoBuffers和AllocateAdapterChannel。 在驱动程序之前把这些程序称为,对于IRP_MJ_READ和/或IRP_MJ_WRITE请求,或者对于请求DMA传输的任何其他请求,它的Dispatch例程应该已经检查IRP参数的有效性(如果必要的话)。Dispatch例程也可以为将来的处理已经将IRP排到另一个驱动程序例程。传输请求是请求设备I/O操作的当前IRP。 调用AllocateAdapterChannel的驱动程序例程在这个调用发生时必须在IRQL DISPATCH_LEVEL上执行。更多关于驱动程序标准例程在其上执行的IRQL的信息参见第5到15章,它们描述了每种标准例程。此外,在每个支持例程必须在其上被调用的IRQL,在《Windows 2000驱动程序开发参考》卷2它的描述中被特别指明。 与指向被IoGetDmaAdapter返回的适配器对象的指针一同,驱动程序在它调用AIIocateAdapterChannel的时候必须提供下列: 指向目标设备对象的指针 其AdapterControl例程的入口点 指向AdapterControl例程将使用的任何驱动程序确定的上下文信息的指针 AllocateAdapterChannel排队驱动程序的AdapterControl例程,这个例程在系统DMA控制器被分配到这个驱动程序并且一组映射寄存器已经为驱动程序的DMA操作被分配的时候运行。 在入口处,AdapterControl例程被给DeviceObject和Context指针,它们在这个调用中被传递给AllocateAdapterChannel,与对分配的映射寄存器的操作(MapRegisterBase)一样。 如果驱动程序有StartIo例程,AdapterControl例程也被给一个指向DeviceObject->CurrentIrp的指针。如果驱动程序管理它自己的IRP(代替StartIo例程)队列,驱动程序将包括指向当前IRP的指针,把它作为在它调用AllocateAdapterChannel时传递的上下文的一部分。 AdapterControl例程典型地做下列事情: 保存或初始化驱动程序维护的关于DMA操作的所有上下文。上下文可以包括输入MapRegisterBase柄,驱动程序必须传递它到MapTransfer和FlushAdapterBuffers,可能还包括请求的传输(从其IRP中的I/O栈位置)的Length。 在MapTransfer之后调用MmGetMdlVirtuaIAddress。参见“为基于包的DMA安装系统DMA控制器”。 创建从属设备以开始传输操作。 返回值KeepObject。 所有的AdapterControl例程必须返回一个类型为IO_ALLOCATION_ACTION的系统定义的值。 对于使用系统DMA的驱动程序,AdapterControl例程必须返回值KeepObject。这允许驱动程序保持系统DMA控制器的“所有权”,并且分配映射寄存器,直到它已传输完所有请求的数据。 因为AdapterControl例程不能等待从属设备完成DMA操作,所以每个AdapterControl例程最少必须做如下事情: 保存驱动程序设备扩展、控制器扩展、或者其他驱动程序可访问的常驻存储区域(驱动程序分配的非页式存储池)中的上下文信息,特别是MaPRegisterBase句柄。 返回KeepObject。 当每个DMA传输操作完成时,另一个驱动程序例程(有可能是DpcForIsr)必须调用FlushAdapterBuffers。如果创建多次DMA控制器以满足当前IRP的传输请求是必要的,这个程序也必须再一次调用MapTransfer和FlushAdapterBuffers。 当驱动程序已经满足当前IRP请求,它必须调用FreeAdapterChannel。这个支持例程应该在为当前IRP最后一次调用FlushAdapterBuffers之后立即被调用,从而使得系统DMA控制器立即可供使用,以满足其他传输请求(任何驱动程序)。 具有发散/收集能力的从属设备的驱动程序也应该从其AdapterControl例程返回KeepObject。当驱动程序必须拆分给定的DMA请求时,设备必须能在系统DMA控制器被重新编程的期间(DMA操作之间)有等待能力。在一些Windows平台上,这样设备对每个DMA操作至多能传输一页的数据,因为HAL只能把一个单一的映射寄存器分配到那个设备的驱动程序。 为基于包的DMA设置系统DMA控制器 当AllocateAdapterChannel传输控制到驱动程序的AdapterControl例程,这个驱动程序“拥有”系统DMA控制器和一组映射寄存器。然后,驱动程序必须为传输操作创建DMA控制器,如图3.9所示。 如果驱动程序有StartIo例程,AllocateAdapterChannel把指向PIrp参数中的DeviceObject->CurrentIrP的指针传递到AdapterControl例程。然而,如果驱动程序管理它自己的IRP队列,驱动程序应该包括指向当前IRP的指针,将其作为它传递到AdapterControl的上下文中的一部分。 图3.9 编程系统DMA控制器 如图3.9所示,驱动程序的AdapterControl例程创建DMA传输,如下: AdapterControl例程在开始传输处得到地址。对于被要求满足IRP的初始传输请求,AdapterControl例程调用MmGetMdlVirtualAddress,在Irp->MdlAddress传递指向MDL的指针,它为这个DMA传输描述缓冲区。 MmGetMdlVirtuaIAddress返回一个虚拟地址,驱动程序能使用这个虚拟地址作为传输应该在其处开始的系统物理地址的索引。 如果IRP请求多个传输操作,驱动程序计算一个更新的起始地址,如本节中后面所述的。 AdapterControl例程保存第一步中由MmGetMdlVirtualAddress返回的或计算的地址。这个地址是MapTransfer要求的一个参数(CurrentVa)。 AdapterControl例程调用MapTransfer以创建系统DMA控制器,提供下列的参数: 被IoGetDmaAdapter返回的AdaPterObject指针 在当前IRP的Irp->MdlAddress中的指向MDL的指针 被AllocateAdapterChannel传递到驱动程序的AdapterControl例程的MapRegisterBase句柄 被MmGetMdlVirtualAddress返回的值(CurrentVa),如果这是对IRP的MapTransfer第一次调用的话 否则,驱动程序提供更新的CurrentVa值,表明下一个传输操作中应该在缓冲区的什么地方开始。 指向一个变量(Length)的指针,这个变量表明这次传输的字节数 如果驱动程序能使用一个单一的对MapTransfer的调用传输所有请求的数据,并且对其DMA操作没有设备特定的约束,Length能被设置成在IRP的驱动程序的I/O栈位置中的Length的值。至多,以字节为单位的长度可以是IoGetDma适配器返回的(PAGE_SIZE *)NumberOfMapRegisters。否则,驱动程序必须拆分请求,如“拆分传输请求”中所述,并且必须为当前IRP把后来调用MapTransfer中的Length的值予以更新。 一个布尔的值(WriteToDevice),表明传输操作(TRUE表示从系统内存到设备的数据传输)的方向 MapTransfer返回一个逻辑地址。使用系统DMA的驱动程序必须忽略这个值。 AdapterControl例程为DMA操作创建设备。 AdapterControl例程返回KeepObject。 当设备表明其当前DMA操作已经完成,驱动程序应该调用FlushAdapterBuffers,通常从驱动程序的DpcForIsr例程调用。 完成DMA操作的DpcForIsr或者另一个驱动程序例程调用FlushAdapterBuffers,以保证任何在系统DMA控制器中高速缓存的数据被读到系统内存中或者写到设备。如果为当前IRP再编程系统DMA控制器以传输更多数据是必要的,相同的例程也必须再一次调用MapTransfer。同样地,它在每次传输操作后必须再一次调用FlushAdapterBuffers。 如果驱动程序必须为当前IRP多次调用MapTransfer,它在所有的呼叫中提供同样的AdaPterObject指针、Mdl指针、MopRegisterBase句柄、和传输方向。然而,在它作第二次和任何后来的对MapTransfer调用之前,驱动程序必须更新CurrentVa和Length参数。为这些参数中的每一个计算更新的值,使用下列的公式: CurrentVa=CurrentVa+(在前面对MapTransfer的调用中被请求的Length) Length=Minimum(剩余的将要传输的长度,(IoGetDmaAdapter返回的PAGE_SIZE * NumberOfMapRegisters)) 每个驱动程序应该维护的有关其DMA传输的上下文信息依赖于其特殊设备的需要。典型地上下文可以在MDL(CurrentVa)中包括当前的虚拟地址、到当前为止被传输的字节数、传输剩余的字节数,还可能有指向当前IRP的指针,和驱动程序作者认为有用的任何其他信息。 当请求的传输完成,或者如果驱动程序必须为IRP返回一个错误状态,驱动程序必须迅速调用FreeAdapterChannel,从而释放系统DMA控制器以供其他驱动程序或本驱动程序使用。 公用缓冲区系统DMA 使用系统DMA控制器的自动初始化方式的驱动程序必须为一个缓冲区分配内存,DMA传输使用这个缓冲区完成操作。驱动程序调用AllocateCommonBuffer以得到这个缓冲区,典型地从处理IRP_MN_START_DEVICE 请求的DispatchPnP例程中获得。图3.l0显示了驱动程序如何分配缓冲区,并且映射它的虚拟地址范围到系统物理内存。 图3.l0 为系统DMA分配一个公用缓冲区 如图显示,驱动程序使用以下步骤为系统DMA分配缓冲区: 驱动程序调用AllocateCommonBuffer,传递指向适配器对象(由IoGetDmaAdapter返回)的指针,连同它的缓冲区请求的长度。为了经济地使用内存,输入缓冲区长度值要么小于等于PAGE_SIZE,要么应该是PAGE_SIZE的倍数。 如果AllocateCommonBuffer返回NULL指针,驱动程序应该释放任何它已经请求的系统资源,并且返回STATUS_INSUFFICIENT_RESOURCE以响应IRP_MN_START_DEVICE请求。 否则,AllocateCommonBuffer在系统虚拟地址空间中分配请求数量的内存,并返回指向那个缓冲区的两种不同类型的指针: 缓冲区(图3.10的BufferLogicalAddress)的LogicalAddress,驱动程序必须为其提供存储,但是此后应该忽略它 缓冲区(图3.l0的BufferVirtualAddress)的虚拟地址,这驱动程序也必须存储它,这样它能为DMA操作创建一个MDL,这个MDL描述它的缓冲区 驱动程序应该将这些指针存储在设备扩展中或者存储在其他驱动程序分配的常驻内存中。 驱动程序调用IoAllocateMdl,从而为缓冲区分配MDL。驱动程序传递由AllocateCommonBuffer返回的其缓冲区的VirtualAddress和用来分配MDL的缓冲区的Length。 驱动程序使用由IoAllocateMdl返回的指针调用MmBuildMdlForNonPagedPool,从而为它的常驻内存映射虚拟地址范围到系统物理内存 在分配一个公用缓冲区和映射它的虚拟地址范围之后,从属设备的驱动程序可以开始处理一个请求DMA传输的IRP。为了这样做,驱动程序调用下列支持例程: RtlMoveMemory,它从锁定的用户缓冲区拷贝数据到驱动程序分配的公用缓冲区(为到设备的传输而分配的) AllocateAdapterChannel,当驱动程序准备好为DMA编程它的设备,并且需要系统DMA控制器时被调用 MapTransfer,使用描述驱动程序分配的公用缓冲区为传输操作安装系统DMA控制器 注意,驱动程序仅仅调用MapTransfer一次,以创建系统DMA控制器来使用其公用缓冲区。 在传输期间,驱动程序能调用ReadDmaCounter以确定仍然有多少字节被传输,并且如果必要的话,调用RtlMoveMemory向或从用户缓冲区拷贝更多的数据。 FlushAdapterBuffers,当驱动程序已经完成它到/从从属设备的DMA传输时被调用 FreeAdapterChannel,一旦所有被请求的数据被传输,或者因为设备I/O错误驱动程序必须失败 被IoGetDmaAdapter返回的AdapterObject指针是除了RtlMoveMemory之外所有这些支持例程所需要的一个参数。 单独的驱动程序在不同的点调用这些一系列支持例程,这依赖于每个驱动程序如何被实现服务它的设备。例如,一种驱动程序的StartIo例程调用AllocateAdapterChannel,另一个驱动程序可以从一个例程中调用,这个例程从驱动程序创建的互锁队列中移走IRP,并且另一个驱动程序可以在其从属DMA设备表明它已经准备好传输数据的时候作这个调用。 为公用缓冲区系统DMA分配适配器通道 驱动程序在其针对IRP_MJ_READ和/或IRP_MJ_WRITE请求(或者请求DMA传输的任何其他请求)的Dispatch例程已经检查IRP的参数(如果必要的话)的有效性之后调用AllocateAdapterChannel,可能为将来的处理排队一个或多个IRP到另一个驱动程序,并且如果适当的话,可能连带被传输的数据加载它的普通缓冲区。 调用AllocateAdapterChannel的驱动程序例程在这个调用发生时,必须在IRQL DISPATCH_LEVEL上执行。更多关于驱动程序标准例程在其上执行的IRP的信息参见第5章到第15章,它们描述了所有这些例程。更多关于支持例程特定的IRQL请求的信息,参见《Windows 2000驱动程序开发参考》卷2中的支持例程的参考页。 AllocateAdapterChannel排队驱动程序的AdapterControl例程,这个例程在系统DMA控制器已经被分配到这个驱动程序和一组映射寄存器已经被分配驱动程序的DMA操作时运行。 在入口处,AdapterControl例程被给指针,这个指针指向设备对象和在AllocateAdapterChannel调用中被传递的上下文,被给的还有分配的映射寄存器的句柄。如果驱动程序有StartIo例程,AdapterControl例程也被给一个指向DeviceObject->CurrentIrp的指针。如果驱动程序管理它自己的IRP队列而不是拥有一个StartIo例程,驱动程序应该包括指向当前IRP的指针,并在调用AllocateAdapterChannel时作为它传递的上下文数据的一部分。 AdapterControl例程典型地做下列事情: 保存或初始化任何驱动程序维护的关于DMA操作的上下文。这个上下文可以包括输入MapRegisterBase句柄,驱动程序必须传递这个句柄到MapTransfer和FlushAdapterBuffers,以及可能还包括请求的传输(从其IRP中的I/O栈位置)的长度。 创建从属设备以开始传输操作 返回值KeepObject 对于使用系统DMA控制器的自动初始化模式的驱动程序,AdapterControl例程必须返回值KeepObject。这允许驱动程序保持系统DMA控制器和分配的映射寄存器的“所有权”,直到它已传输完所有数据。 因为AdapterControl例程不能等候从属设备完成DMA操作,AdapterControl例程至少必须做下列事情: 保存上下文信息,特别是MapRegisterBase句柄,到驱动程序设备扩展、控制器扩展、或者其他驱动程序可访问的常驻存储区域(驱动程序分配的非页式存储池)。 返回KeepObject。 当DMA传输操作被完成,另一个驱动程序例程(有可能时DpcForIsr)必须调用FlushAdapterBuffers和FreeAdapterChannel。 为公用缓冲区DMA设置系统DMA控制器 当AllocateAdapterChannel传输控制到驱动程序的AdapterControl例程时,驱动程序“拥有”系统DMA控制器和一组映射寄存器。然后,在驱动程序为传输操作创建其设备之前,驱动程序必须调用MapTransfer以创建系统DMA控制器,来使用驱动程序分配的公用缓冲区。 驱动程序向MapTransfer提供下列的参数; AdaPterObject指针,被IoGetDmaAdapter(参见3.3.2)返回 指向MDL的指针,MDL描述驱动程序分配的公用缓冲区(参见3.3.4.2) MapRegisterBase句柄,被AllocateAdapterChannel传递到驱动程序的AdaPteroontrol例程 指向变量(Length)的指针,它指出在驱动程序分配的公用缓冲区的大小,以字节为单位 布尔值,表明传输操作(TRUE表示从系统内存到设备的传输请求)的方向 MapTransfer返回一个逻辑地址,使用系统DMA的驱动程序必须忽略它。当MapTransfer返回控制,驱动程序应该为DMA操作创建其设备。驱动程序仅仅调用MapTransfer一次,但是继续在其公用缓冲区和锁定的用户缓冲区之间拷贝数据,直到完成请求的传输。 驱动程序能调用ReadDmaCounter以确定当前仍然有多少字节在公用缓冲区中被传输;驱动程序能继续用用户数据填充它的公用缓冲区,或者从其公用缓冲区拷贝数据到用户缓冲区,这依赖于DMA操作的方向。 当传输被完成,或者如果驱动程序必须为IRP返回错误状态,驱动程序调用FlushAdapterBuffers以保证任何被高速缓存在系统DMA控制器中的数据被读到系统内存中或被写到设备上。然后,驱动程序应该立即调用FreeAdapterChannel以释放系统DMA控制器,从而保证任何驱动程序(包括自己)的使用。 使用总线控制器DMA 总线控制器DMA设备的驱动程序能使用下列种类的系统提供的DMA支持: 基于包的DMA,如果总线控制器适配器允许驱动程序确定什么时候做一个DMA传输操作,和/或什么时候为给定的IRP开始另一个传输操作。细节参见“基于包的总线控制器DMA”。 公用缓冲区的DMA(也称为连续的DMA),如果总线控制器适配器不为驱动程序提供一个方法以容易确定什么时候一个传输操作将开始,或者什么时候传输完成的,或者如果一个单一的缓冲区区域被连续地或重复地用于DMA传输。细节参见“公用缓冲区总线控制器DMA”。 依赖于总线控制器适配器的性质,一些驱动程序只使用基于包的DMA,一些只使用公用缓冲区DMA,而一些则两者都使用。例如,使用邮箱策略(mailbox scheme)来通信状态信息和命令的总线控制器适配器的驱动程序与基于包的DMA一道,可以使用邮箱的一个公用缓冲区,这些邮箱被驱动程序和它的适配器共享。 设置一个公用缓冲区能束缚一些(或者所有,这依赖于请求的缓冲区的大小)与适配器对象有关的映射寄存器,这些对象代表总线控制器适配器,所以考虑下列设计方针: 经济地使用公用缓冲区DMA。 经济地设置公用缓冲区区域,诸如以PAGE_SlZE块的方式或以单独分配的方式,为任何基于包的DMA操作留下更多的可供使用的映射寄存器。它也为其他目的留下更多自由的系统内存,然而这样做降低驱动程序和系统的整体性能。 基于包的总线控制器DMA 为了使用基于包的DMA,当总线控制器DMA设备的驱动程序处理一个请求DMA传输的IRP时,它们调用下列支持例程: KeFlushIoBuffers,恰好在试图为传输请求分配映射寄存器之前调用 更多关于在DMA期间用KeFlushloBuffers和FlushAdapterBuffers保持高速缓存一致性的信息参见第16章。 AllocateAdapterChannel,当驱动程序为DMA准备好编程总线控制器适配器时调用 MmGetMdlVirtualAddress,调用它以得到进入MDL的索引,被要求作为MapTransfer的一个初始参数;MapTransfer,调用它使得返还到IRP的缓冲区的系统物理内存变得设备可访问 注意,任何驱动程序可能需要完成多于一个的传输操作,以满足当前IRP,如“拆分传输请求”中所述。没有发散/收集能力的设备的驱动程序能在每次传输操作时调用MapTransfer一次。有发散/收集能力的设备的驱动程序能调用多次MapTransfer以创建每个传输操作。作为选择,这些驱动程序能使用系统内部的发散/收集支持,这个在“执行发散/收集DMA”中描述。 FlushAdapterBuffers,在每个到/从目标设备的DMA传输操作末尾调用,用以确定是否所有请求的数据被完全传输 FreeMapRegisters,一旦或者当前IRP的所有DMA被完成就被调用,因为所有的请求数据已经被完全传输,或者因为设备或总线I/O错误导致驱动程序必须失败这个IRP 被IoGetDmaAdapter返回的AdapterObject指针是AllocateAdapterChannel、MapTransfer、FlushAdapterBuffers、和FreeMapRegisters所需的一个参数。注意,在Windows NT的以前的版本中,总线控制器设备可以把NULL的AdaPterObject指针传递到MapTransfer和FIushAdapterBuffers。在Windows 2000中,驱动程序不能再这样做。 KeFlushIoBuffers和MmGetMdlVirtualAddress在Irp->MdlAddress要求一个指向MDL的指针。 单独的驱动程序在不同的点调用这一系列的支持例程,这依赖于每一驱动程序如何被实现服务其设备。例如,一种驱动程序的StartIo例程可以调用AllocateAdapterChannel,而另一个驱动程序可以从一个例程做这个调用,这个例程从驱动程序创建的互锁队列或者设备队列中移去IRP。 代替本节中描述的如何使用例程,使用基于包的DMA的任何驱动程序能使用预计流水线发散/收集DMA的支持例程,而不管其设备是否拥有内部的发散/收集支持。细节参见“执行发散/收集DMA”。 分配总线控制器适配器对象 为准备基于包的总线控制器DMA,驱动程序在收到IRP_MJ_READ或者IRP_MJ_WRITE之后调用KeFlushIoBuffers和AllocateAdapterChannel。在驱动程序调用这些程序之前,它的Dispatch例程应该检查IRP参数(如果必要的话)的有效性。它也可为进一步处理将IRP排队到另一个驱动程序例程。传输请求是请求设备I/O操作的当前IRP。 调用AllocateAdapterChannel的驱动程序例程在调用发生时,必须在IRQL DISPATCH_LEVEL上执行。更多关于驱动程序标准例程在其上执行的IRQL的信息参见第5章到第15章,它们描述了标准例程。更多关于支持例程特定的IRQL请求的信息,参见《Windows 2000驱动程序开发参考》卷2中的支持例程的参考页。 与指向适配器对象(被IoGetDmaAdapter返回)的指针一同,当驱动程序调用AllocateAdapterChannel时,它必须提供下列: 指向当前IRP的目标设备对象的指针 其AdapterControl例程的入口点 指向AdapterControl例程将使用的任何任何驱动程序确定的上下文信息的指针 AllocateAdapterChannel排队驱动程序的AdapterControl例程,这个例程在下述时候运行:适配器对象被释放以及一组映射寄存器为驱动程序的DMA操作(到或从目标设备)已经被分配。 在入口处,Adapteroontrol例程被给DeviceObject和Context指针,它们被传递到AllocateAdapterChannel调用中,还被给分配的映射寄存器的句柄(MapRegisterBase)。 如果驱动程序有StartIo例程,AdapterControl例程也被给指向DeviceObject->CurrentIrp的指针。如果驱动程序管理它自己IRP队列,而不是拥有一个StartIo例程,驱动程序将包括一个指向当前IRP的指针,将它作为它调用AllocateAdapterChannel时传递的上下文的一部分。 对于没有发散/收集能力的总线控制器DMA设备的驱动程序,AdapterControl例程通常做下列事情: 保存或者初始化驱动程序维护的关于DMA操作的任何上下文。上下文可以包括输入MopRegisterBase句柄,驱动程序必须传递这个句柄到MapTransfer和FlushAdapterBuffers,还包括其IRP中的I/O栈位置请求的传输的字节的Length,如此等等 在MapTransfer(在“设置描述传输操作”中描述)之后调用MmGetMdlVirtuaIAddress,以得到其设备能使用的逻辑地址,从而开始传输操作 创建总线控制器适配器以开始传输操作 返回值DeallocateObjectKeepRegisters 对于带有发散/收集能力的总线控制器设备的驱动程序,AdapterControl例程通常做下列事情: 保存或者初始化驱动程序维护的关于DMA操作的任何上下文。上下文可以包括输入MopRegisterBase句柄,驱动程序必须传递这个句柄到MapTransfer和FlushAdapterBuffers,还包括其IRP中的I/O栈位置请求的传输的字节的Length,如此等等 在MapTransfer(在“设置描述传输操作”中描述)之后调用MmGetMdlVirtuaIAddress,以得到其设备能使用的逻辑地址,从而开始传输操作 AdapterControl例程反复地调用MapTransfer,直到它已使用所有可供使用的映射寄存器为总线控制器适配器建造发散/收集表。 创建总线控制器适配器以开始传输操作 返回值DeallocateObjectKeepRegisters 注意,执行总线控制器DMA的驱动程序能使用GetScatterGatherList和PutScatterGatherList例程,而不管它们的设备是否支持发散/收集DMA。使用这些程序改变对驱动程序的AdapterControl例程的请求;细节参见“执行发散/收集DMA”。 AdapterControl例程必须返回系统定义的类型为IO_ALLOCATION_ACTION的值。对于使用总线控制器DMA的驱动程序,AdapterControl例程应该典型地返回值DeallocateObjectKeepRegisters,这允许驱动程序为目标设备对象保持分配的映射寄存器,直到它已为当前IRP传输所有请求的数据。在传输完成之后,DPC程序应该调用FreeMapRegisters以释放分配的映射寄存器。然而,以防设备不支持命令排队,在当前IRP的传输完成时,AdapterControl例程能返回KeepObject,此时DPC例程调用FreeAdapterChannel。 AdapterControl例程不能等候总线控制器适配器完成DMA操作。 无论总线控制器适配器是否支持发散/收集,AdapterControl例程至少必须做下列事情: 保存必要的上下文信息——特别是MapRegisterBase句柄——在驱动程序设备扩展、控制器扩展、或者其他驱动程序能访问的常驻存储区域(非页式存储池,被驱动程序分配)内。当驱动程序调用MapTransfer和FlushAdapterBuffers,它必须传递这个句柄。 返回DeallocateObjectKeepRegisters。 当每个DMA传输操作被完成时,另一个驱动程序例程(有可能是DpcForIsr)必须调用FlushAdapterBuffers。这种程序也必须创建任何必要的附加DMA操作,以满足当前IRP。 当驱动程序已满足当前IRP传输请求或者因为设备或者总线I/O错误必须失败IRP,它必须调用FreeMapRegistcrs。这个调用应该在为当前IRP的最后一个对FlushAdapterBuffers的调用之后立即发生,这样使得驱动程序能服务其他DMA请求,有可能是总线上的其他设备。 设置传输操作 当AllocateAdapterChannel传输控制到驱动程序的AdapterControl例程的时候,它已分配一组映射寄存器。然而,驱动程序必须将当前IRP的传输请求的系统物理内存映射到总线控制器适配器的逻辑地址范围,如下: 使用在Irp->MdlAddress的MDL调用MmGetMdlVirtualAddress,从而为传输应该开始的系统物理地址得到索引。 返回值是MapTransfer要求的参数(CurrentVa)。 调用MapTransfer以映射IRP缓冲区的系统物理地址范围到总线控制器适配器的逻辑地址。 驱动程序能为传输操作创建适配器。图3.11显示与设置传输有关的步骤。 图3.11 设置DMA的逻辑范围 如图3.11所示,驱动程序的AdapterControl例程创建总线控制器DMA操作,如下: AdapterControl例程得到开始传输的地址。对于被要求满足IRP的初始传输,AdapterControl例程调用MmGetMdlVirtualAddress,传递指向在Irp->MdlAddress的MDL的指针,它描述这个DMA传输的缓冲区。 MmGetMdlVirtualAddress返回一个虚拟地址,驱动程序能使用它作为传输应该开始的系统物理地址的一个索引。 如果IRP请求多个传输操作,驱动程序计算一个更新的起始地址,这在本节后面描述。 AdapterContro1例程保存由MmGetMdlVirtualAddress返回的或在步骤1中计算的地址。 这个地址是MapTransfer需要的参数(CurrentVa)。 AdapterControl例程调用MapTransfer,它返回一个驱动程序编程总线控制器适配器以开始传输操作的逻辑地址。在对MapTransfer的调用中,驱动程序提供下列的参数: AdapterObject指针,被IoGetDmaAdapter(参见3.3.2)返回 指向在当前IRP的Irp->MdlAddress的MDL的指针 MapRegisterBase句柄,通过AllocateAdapterChannel(参见“分配总线控制器适配器对象”)传递到驱动程序的AdapterControl例程 被MmGetMdlVirtualAddress返回的值,如果这是对当前IRP的MapTransfer的第一次调用 否则,驱动程序提供更新的CurrentVa值,表明下一个物理-逻辑映射被做。(如何计算更新的CurrentVa在本节中后面叙述。) 指向变量(Length)的指针,它表明这个传输的字节数 如果驱动程序有足够映射寄存器,可以在单一的DMA操作中传输所有请求的数据,并且没有对其DMA操作的设备特定约束,Length能被设置成IRP的驱动程序I/O栈位置中Length的值。至多,输入长度可以是(PAGE_SIZE *)NumberOfMapRegisters(由IoGetDmaAdapter返回)。否则,驱动程序必须拆分请求,如3.3.3中所述,并且必须为当前IRP在后来的MapTransfer调用中更新Length值。 布尔值(WriteToDevice),表明传输操作(TRUE表示从内存到设备传输数据)的方向 AdapterControl例程为DMA操作创建设备。 AdapterControl例程返回DeallocateObjectKeepRegisters。 如果驱动程序必须调用MapTransfer多次以满足当前IRP,它在所有的MapTransfer中提供相同的AdapterObject指针、Mdl指针、MapRegisterBase句柄、和传输方向。然而,驱动程序必须在对MapTransfer的第二次以及后续调用中提供更新的CurrentVa和Length值。使用下列的公式来计算这些值: CurrentVa=CurrentVa+(在前面对MapTransfer中请求的Length) Length=Minimum(剩余的将要传输的长度,(被IoGetDmaAdapter返回的PAGE_SIZE*NumberOfMapRegisters)) 每个驱动程序应该维护的有关其DMA传输的上下文信息依赖于其特殊设备的需要。典型地上下文可以包括MDL(CurrentVa)中的当前虚拟地址、到目前为止传输的字节数、可能还有指向当前IRP的指针。 对于带有发散/收集能力的设备的驱动程序,Length参数对于MapTransfer既是输入参数又是输出参数。在从MapTransfer返回时,它表明系统已映射数据的字节数。长度的返回值,与返回的逻辑地址结合起来,表明逻辑地址的范围,这里的逻辑地址范围是总线控制器适配器能为这个DMA操作中的一块的传输所使用的。 由于Length被MapTransfer重写,符合下面这个实现方针: 如果设备支持发散/收集,从不把指向IRP的驱动程序I/O栈位置中的Length作为MapTransfer的参数。 这样做可能会毁坏当前IRP中的值,使得不可能确定驱动程序是否已传输所有请求的数据。 在每个DMA操作末尾,驱动程序必须用一个有效的AdapterObject指针和MapRegisterBase句柄,调用FlushAdapterBuffers以确信所有数据已经被传输(见第16章),并且释放当前DMA操作的物理-逻辑映射。如果驱动程序必须创建附加的DMA操作以满足当前IRP,在每一种传输操作完成之后,它必须调用FlushAdapterBuffers。 当所有请求的传输被完成,或驱动程序必须为IRP返回错误状态,驱动程序应该在其最后一次调用FlushAdapterBuffers之后立即调用FreeMapRegisters,从而最大化总线控制器适配器的吞吐量。在其调用FreeMapRegisters中,驱动程序必须传递它在前面对AllocateAdapterChannel的调用中传递的AdapterObject指针。 公用缓冲区的总线控制器DMA 为了为总线控制器DMA创建一个公用缓冲区,总线控制器DMA设备驱动程序必须使用由IoGetDmaAdapter返回的适配器对象指针调用AllocateCommonBuffer。典型地,驱动程序在其为PnP IRP_MN_START_DEVICE请求的Dispatch例程中调用它。当驱动程序仍然是加载期间,驱动程序仅仅在它将为其DMA操作重复地使用缓冲区的时候,应该分配一个公用缓冲区。图3.12说明了这样一个对AllocateCommomBuffer的调用。 图3.12 为总线控制器DMA分配一个公用缓冲区 缓冲区请求的大小(在图3.12中是LengthForBuffer)确定多少映射寄存器必须被使用从而为公用缓冲区提供虚拟-逻辑映射。映射寄存器数至多可以是(BYTES_TO_PAGE(LengthForBuffer))的值。这个值不能大于IoGetDmaAdapter返回的NumberofMapRegister。更多关于驱动程序能使用的系统提供的宏命令的信息,诸如BYTES_TO_PAGE,参见《Windows 2000个驱动程序开发参考》的卷2。 此外,调用者必须提供下列信息: 一个布尔值,表明高速缓存是否应该可用 指向驱动程序定义的变量的指针,它将包含AllocateCommonBuffer返回的缓冲区(图3.l2中的BufferLogicalAddress)的设备可访问的基础逻辑地址 如果调用成功,AllocateCommonBuffer为缓冲区(图3.l2的BufferVirtualAddress)返回一个驱动程序可访问的基础虚拟地址,驱动程序必须保存它在其设备扩展、控制器扩展、或者其他驱动程序可访问的的常驻存储区域(被驱动程序分配的非页式存储池)中。 如果AllocateCommonBuffer不能为缓冲区分配内存,它返回NULL。如果返回的基础虚拟地址是NULL,驱动程序必须专门使用系统的基于包的DMA支持,或者驱动程序必须使IRP_MN_START_DEVICE请求失败,返回STATUS_INSUFFICIENT_RESOURCE。 否则,驱动程序能使用分配的公用缓冲区作为驱动程序的或适配器的可访问的DMA传输存储区域。 当PnP管理器发送一个IRP以停止或者移去设备,驱动程序必须调用FreeCommonBuffer以释放它已分配的每个公用缓冲区。更多的关于处理PnP的信息停止和移去IRP的信息,参见《即插即用、电源管理、和安装设计指南》。 执行发散/收集DMA 执行系统或者总线控制器基于包的DMA的Windows 2000和WDM驱动程序能使用特别是为发散/收集DMA设计的支持例程。代替在“基于包的系统DMA”和“基于包的总线控制器”中调用一系列例程,驱动程序能使用GetScatterGatherList和PutScatterGatherList。 设备不需要为它的驱动程序拥有内部的发散/收集支持,就可以使用这些例程。 为发散/收集操作使用基于包的DMA的驱动程序调用下列支持例程: KeFlushIoBuffers,在调用GetScatterGatherList之前调用 更多信息参见第l6章中的“为DMA和PIO保持高速缓存一致性”。 MmGetMdlVirtualAddress,调用它得到进入MDL的索引,在对GetScatterGatherList的调用中被要求作为参数 GetScatterGatherList,当驱动程序准备好为DMA编程它的设备,并且需要系统DMA控制器或者总线控制器适配器 GetScatterGatherList分配系统DMA控制器或者总线控制器适配器,确定多少映射寄存器被请求,并且分配它们,填写发散/收集列表,并且在DMA控制器或适配器和映射寄存器可用时调用驱动程序的AdapterListControl例程。 PutScatterGatherList,一旦所有请求的数据被传输或者由于设备I/O差错造成驱动程序使IRP失败时被调用 PutScatterGatherList刷新适配器缓冲区,释放映射寄存器,并且释放发散/收集表。 在它能在缓冲区中访问数据之前,驱动程序必须调用PutScatterGatherList。 被IoGetDmaAdapter返回的AdapterObject指针是除KeFIushIoBuffers和MmGetMdIVirtuaIAddress之外每个这样的例程所需要的参数,KeFIushIoBuffers和MmGetMdIVirtuaIAddress请求一个指向在Irp->MdlAddress的MDL的指针。 GetScatterGatherList组合了AllocateAdapterChannel和MapTransfer例程的特性。它需要下列参数: 指向由IoGetDmaAdapter返回的DMA_ADAPTER结构的指针 指向DMA操作的目标设备对象的指针 指向在Irp->MdlAddress中描述缓冲区的MDL的指针 指向被Mdl所描述的缓冲区中的当前虚拟地址的指针 将被映射的字节数 指向执行传输的AdapterListControl例程的指针 指向被传递到AdapterListControl例程的驱动程序定义的上下文区域的指针 布尔值;到设备的传输是TRUE;否则FALSE 在确定要求的映射寄存器数量、分配适配器通道和映射寄存器、填写发散/收集表并准备传输之后,GetScatterGatherList调用系统提供的AdaPterListControl例程。AdapterListControl例程在一个专用线程环境中在IRQL DISPATCH_LEVEL上被运行。 在调用GetScatterGatherList时,驱动程序提供的AdapterLstControl例程不同于传递到AllocateAdapterChannel的AdapterControl例程,主要有以下几个方面: AdapterListControl例程没有返回值,而AdapterControl例程返回IO_ALLOCATION_ACTION。 不是指向系统分配映射寄存器的MapRegisterBase的指针,AdapterListControl例程的第三个参数用SCATTER_GATHER_LIST结构替换,驱动程序通过这个结构能执行DMA。 AdapterListControl例程执行在适配器控制程序中要求的任务的子集。 AdapterListContol程序不调用AllocateAdapterChannel或者MapTransfer。其唯一的责任是保存输入发散/收集表指针,创建其设备,并且使用发散/收集表来执行DMA。 发散/收集表结构包括一个SCATTER_GATHER_ELEMENT的数组和数组中的单元数。每个数组的单元提供长度以及物理的连续区域的开始物理地址。驱动程序在数据传输中使用长度和地址。 驱动程序能使用GetScatterGatherList,而不管其设备是否支持发散/收集DMA。对于不支持发散/收集DMA的设备,发散/收集表将包含仅仅一个单元。 使用发散/收集程序比调用AllocateAdapterChannel(以前在3.3.4.1和3.3.5.1中描述) 能改进性能。不同于AllocateAdapterChannel,多次对GetScatterGathcrList的调用能在任何一次为设备对象被排队。在驱动程序的AdapterListControl例程已经完成执行之前,驱动程序能为另一个相同驱动程序对象上的DMA操作再一次调用GetScatterGatherList。 当从驱动程序提供AdapterListControl例程返回时,GetScatterGatherList保持映射寄存器,但是释放DMA适配器结构。 当驱动程序已满足当前IRP传输请求,或者因为设备或者总线I/O错误必须失败IRP,在它能在缓冲区中访问传输的数据之前,它必须调用PutScatterGatherList。PutScatterGatherList刷新适配器缓冲区,并且释放映射寄存器和发散/收集表。 控制器对象 如其名称暗示的那样,控制器对象通常代表一个附加了设备的物理设备控制器。物理控制器所协同的一组相似设备的最低层Windows 2000驱动程序可以创建控制器对象,并且使用它来使附加设备之间的I/O操作同步。驱动程序执行ControllerControl例程,并且调用I/O管理器的控制器对象支持例程。在WDM驱动程序中,对控制器对象的使用不被支持。 一般地,如果下列的标准成立,驱动程序使用控制器对象来使操作同步到附加设备: 没有中断控制器不能完成长的操作,因此驱动程序不需要创建设备专用线程或者使用系统工作者线程。 连接到控制器的设备是类似的。就是说,它们不是拥有完全不同物理属性或者操作功能的设备,例如能连接到键盘和辅助设备控制器的键盘和鼠标设备。 驱动程序被设计成是整体式的:单层的涉及设备控制器和附加物理设备,而非被设计成使用在端口驱动程序上的一个或多个类驱动程序(对于附加的设备)的端口驱动程序(对于控制器)。 带有I/O通道和一组逻辑设备对象的驱动程序也可以使用控制器对象以在这样的设备的通道中间同步它们的I/O操作。 控制器对象没有名称,于是不是I/O请求的目标。对于来自一组设备对象的连续I/O,它是一种简单的同步机制。因为控制器对象没有名称,它对用户模式的保护子系统是不可见的,没有获得代表目标设备对象的文件对象的句柄,它不能做设备I/O请求。控制器对象对较高层驱动程序是也不可见的,这不能把它们自己设备对象附加到控制器对象。换句话说,既不是I/O管理器也不是一个较高层驱动程序能创建在被控制器对象代表的设备上请求I/O 的IRP。I/O请求总被发布到设备对象。仅仅驱动程序能使用控制器对象。 同步和重复I/O 拥有像“AT”盘控制器(参见第3部分)一样特性的物理设备的整体式驱动程序没有被要求使用控制器对象以使它们的设备I/O操作同步。例如,驱动程序作者可以试一试类似下列同步技术的方法来代替使用控制器对象: 创建命名的设备对象来代表是I/O请求目标的设备。在每个设备扩展或者单一设备扩展中维护有关哪个设备对象是当前I/O操作的目标的状态(或许是一组DeviceBusy标记)。为当前忙的设备对象完成I/O,并且为其他设备对象再排队进入的IRP,直到当前IRP被完成。 前面的同步化技术为所有驱动程序的目标设备对象序列化IRP处理。注意,它也强迫这样的驱动程序在其StartIo例程能开始处理下一个IRP之前完成当前IRP,进而降低驱动程序性能。 如果某种设备操作能被重复,使用控制器对象能增加驱动程序的I/O吞吐能力,因为这种同步化技术允许驱动程序在它创建物理设备之前确定它是否能重复操作。例如,盘控制器可以允许驱动程序重复在一个盘上搜索而在另一个盘上做读和写操作。 此外,使用控制器对象是在诸如“AT”盘控制器的单独的物理设备上同步化多个目标设备I/O操作的简单方法。使用控制器对象允许整体式驱动程序同步经过一组命名的设备对象的I/O操作,而没有必要在一个或多个设备扩展中维护每个设备和设备控制器的状态,并且没有必要重新排队IRP。 然而,被设计成重复I/O操作的一些设备,例如双工序列控制器或者总线控制器适配器,一般确实拥有为IRP创建内部队列的驱动程序。细节参见“设备队列对象和互锁队列”和第7章,“ StartIO和队列管理例程”。 创建带有控制器扩展的控制器对象 如果驱动程序使用控制器对象,在它已创建设备对象和其设备为I/O作好准备之后,它必须调用IoCreateController,典型地是在收到PnP IRP_MN_START_DEVICE请求之后。图3.13说明这个调用。 图3.13 控制器对象 像驱动程序创建的设备对象一样,所有的控制器对象拥有相关的控制器扩展。如图3.13所示,IoCreateController的调用者确定控制器扩展的大小。其结构和内容是由驱动程序定义的。 除了驱动程序维护的物理设备(或带有通道的设备)的设备特定的状态信息,图3.13还显示了控制器扩展的驱动程序定义的数据的一个代表集。 PtrToControllerObject返回IoCreateController必须被传递到驱动程序对IoAllocateController和IoFreeController调用中,这在“为I/O操作分配控制器对象”中描述。驱动程序必须将返回的控制器对象指针存储在其驱动程序创建设备对象的设备扩展或者在另一个驱动程序可访问的常驻存储区域(非页式存储池,由驱动程序分配)中。如果驱动程序被卸载,它也必须把控制器对象指针传递到IoDeleteController。 大多数创建控制器对象的驱动程序寻求方便的方法保存指向当前目标设备对象或控制器扩展中的设备扩展的指针。通常,这样的驱动程序将控制器对象指针存储在其每个设备扩展中,这样它能使用ControllerObject->ControllerExtension指针访问驱动程序保持的、所有目标设备对象的I/O操作的控制器特定状态。 如果被控制器对象代表的物理控制器产生中断,驱动程序也能使用控制器扩展作为对IoConnectInterrupt返回的PtrToInterruptObject存储位置。更多信息参见“中断对象”。 IoCreateController为控制器对象和扩展分配常驻存储区域,它用零初始化这个存储区域。如果它不能分配内存,IoCreateController返回NULL指针。如果这发生,驱动程序必须使设备启动失败,并将返回STATUS_INSUFFICIENT_RESOURCE。 I/O操作分配控制器对象 在使用控制器对象的驱动程序已开始其设备之后,它准备好处理被发送到其目标设备对象的IRP。只要当前IRP为I/O操作请求驱动程序编程控制器对象代表的物理设备,控制器对象驱动程序就调用IoAllocateController。图3.14说明这样一个调用。 如图3.14所示,驱动程序在其IoAllocateController时,必须提供多个由IoCreateController返回的ControllerObject指针。与这个指针一同,它必须把指针传递到代表当前I/O请求的目标设备对象,传递到驱动程序提供ControllerControl例程,和任何Context,它的ControllerControl例程将需要这个Context来为请求的I/O操作创建设备上下文。 如果控制器对象代表的设备已经忙于为目标设备对象做I/O ,IoAllocateController排队驱动程序提供ControllerControl例程。否则,ControllerControl例程立即被调用,这个调用使用图3.14中所示的输入参数。当驱动程序的ControllerControl例程运行时,指向IoAllocateController的输入Context指针被传递到这个例程。 图3.14 为I/O分配控制器对象 考虑下列的设计方针以确定这样的上下文区域的位置: 驱动程序提供的上下文区域不应该在控制器扩展中,除非驱动程序在开始在另一个物理控制器之上的操作之前处理每个IRP到完成。否则,控制器扩展中的上下文区域可以被其他的驱动程序例程或在收到新的IRP时被重写。 即使驱动程序为另一个设备对象重复设备I/O操作,目标设备对象的设备扩展中的上下文区域不能被重写。 如果为一个特殊的设备对象另一个I/O请求被做,并且驱动程序拥有StartIo例程,其设备扩展中的上下文区域也不能被重写,因为在驱动程序调用IoStartPacket时,引入的IRP将被排队,并且同样的IRP将保留在设备队列中,直到驱动程序调用IoStartNextPacket,这个调用是在它为那个设备对象完成当前IRP之前做的。 如果驱动程序有StartIo例程,I/O管理器把指向DeviceObject->CurrentIrp的指针传递到ControllerControl例程。如果驱动程序管理它自己的IRP队列,而不是拥有StartIo例程,I/O管理器不能将指向当前IRP的指针给ControllerControl例程。当驱动程序调用IoAllocateController,它应该传递当前IRP以作为Context可访问数据的一部分。 调用IoAllocateController的驱动程序例程在这个调用发生时必须在IRQL DISPATCH_LEVEL上被执行。从它的StartIo例程做这个调用的驱动程序已经在DISPATCH_LEVEL上运行。更多关于驱动程序的标准例程在其上执行的IRQL的信息,参见第5章到第15章,或者第16章中关于管理IRQ的节。更多关于支持例程特定的IRQL请求的信息,参见《Windows 2000驱动程序开发参考》卷2。 ControllerControl例程为IRP的请求操作创建物理控制器。 如图3.14所示,ControllerControl例程返回类型为IO_ALLOCATION_ACTION的值,它是下列系统定义的值之一: 如果ControllerControl例程能在物理控制器上开始另一个操作,它应该返回DeallocateObject,使得驱动程序能重复下一个请求I/O操作。 例如,如果ControllerControl例程能为一个在盘上的寻找操作编程磁盘控制器,完成那个IRP,并返回DeallocateObject,如果任何传输请求当前被排队到其他磁盘,ControllerControl例程能被再一次调用,从而为在其他盘上的传输操编程磁盘控制器。 如果当前IRP通过其他驱动程序例程请求进一步处理,ContollerControl例程必须返回KeepObject。 例如,如果驱动程序为传输操作编程磁盘控制器,但是直到传输完成还不能完成IRP,ControllerControl例程必须返回KeepObject。 当ControllerControl例程返回KeepObject,通常在设备中断时驱动程序的ISR运行,并且其DpcForIsr例程为目标设备对象完成I/O操作和当前IRP。 无论什么时候ControllerControl例程返回KeepObject,完成IRP的例程必须调用IoFreeController。这样的驱动程序例程尽快地调用IoFreeController,这样使得下一个设备I/O操作能快速地被创建。 中断对象 每个产生中断的物理设备的驱动程序在驱动程序开始设备时必须注册它的ISR。内核定义中断对象类型以跟踪以下信息:每个中断向量、其系统指定的DIRQL、当中断发生时被调用的ISR,如此等等。 所有的驱动程序应该是对所有Windows NT/Windows 2000平台可移植的,包括配有若干不同类型 (ISA,EISA,PCI如此等等)的I/O总线的SMP机器。因而,当最低层驱动程序和系统本身被装载时,下面列出的是可配置的: 处理器组,中断能在其上发生 对于任何Windows NT和Windows 2000平台,一组处理器确定多少中断对象必须被创建以注册设备驱动程序的ISR。在SMP机器中,每个设备驱动程序可以请求每处理器一个中断对象。 然而,系统能执行加载平衡,方法是通过限制任何特殊设备中断到可供使用的处理器子集。PnP管理器在列举期间和在设备启动时为这个信息请求总线驱动程序,传递信息到未经加工和转换的资源中。 系统提供的中断向量,为机器中每个I/O总线上的每个设备提供的 对于Windows NT/Windows 2000多总线机器,每个I/O总线上的设备的总线相关的中断向量必须被映射到一组系统指定的中断矢量。PnP管理器在列举期间为这个信息请求总线驱动程序。它然后映射向量,并在设备启动时分别传递它的总线相关的和映射的向量(在未经加工的和转换的资源结构中)给每个驱动程序。 被分配到每个系统中断向量的DIRQL 对于最低层驱动程序,被分配到其映射的系统向量的DIRQL值确定驱动程序的ISR在其上执行的硬件优先级。 平台特定的HAL为每个Windows NT和Windows 2000机器中的I/O总线、外围设备、和处理器提供硬件层支持给内核和I/O管理器,如图3.1所示。PnP管理器也与HAL通讯,以获得对使用中断对象的驱动程序提供的平台特定的支持,如下节所述。 获得系统指定的中断向量、DIRQL和处理器掩码 当PnP管理器把IRP_MN_START_DEVICE请求发送到驱动程序,它分别在Irp->Parameters.StartDevice.AllocatedResources和Irpe>Parameters.StartDevice.AllocatedResourcesTranslated传递未加工和转换的硬件资源。每个资源表包括一个全部资源描述符的列表,它反过来包含特定接口类型和总线数的局部资源描述符的列表。每个局部的资源描述符包含一个资源类型、提供给类型的共享规则、和针对那种类型的、标记和数据特定的资源。中断资源包含中断向量、DIRQL,和被要求创建中断对象和注册ISR的处理器掩码,还有它的共享规则和中断方式。 为了连接其中断,驱动程序使用在AllocatedResourcesTranslated.List.PartialResourceList.PartialDescriptors[]的资源。驱动程序必须扫描局部描述符的数组,以找到需求的资源,这个资源带有成员Type,它的值等于CmResourceTypeInterrupt。对于这个类型的资源,ShareDisposition成员规定资源是否或者如何能被分享,并且Flag成员指示中断方式。此外,每个这样的资源在u.Interrupt.Level包含系统DIRQL,在u.Interrupt.Vector包含系统中断向量,在u.Interrupt.Affinity包含处理器掩码,它指明设备能在其上中断的平台特定的一组处理器。 注册ISR 当驱动程序处理PnP IRP_MN_START_DEVICE请求,它必须调用IoConnectInterrupt注册其ISR。作为响应,I/O管理器调用内核以创建驱动程序的中断对象。 除非ISR为不同的设备处理多个中断向量,驱动程序使用下列的参数注册ISR: 指向驱动程序的存储区域的指针,这个区域是由IoConnectInterrupt返回的中断对象指针分配的 NULL自旋锁指针,除非驱动程序本身已分配存储,并且已经使用KeInitializeSpinLock初始化InterruptSpinLock 指向ServiceContext区域的指针和ISR入口点,当它被调用时将使用ServiceContext区域 (映射的)DeviceSystemVector,它连同IRP_MN_START_DEVICE请求被传送到转换的中断资源表 (u.Interrupt.Vector) 中 被分配到这个机器的那个向量的SystemDirql,它也可以从转换的中断资源列表(u.Interrupt.Level)中被获得 与给SynchronizeIrql的参数一样的IRQL值(u.Interrupt.Level) ProcessorMask,指明一组在这台机器中设备能在其上中断的处理器,也可以从转换的中断资源表(u.Interrupt.Affinity)中获得 中断模式是否是LevelSensitive(CM_RESOURCE_INTERRUPT_LEVEL_SENSITIVE)或者是Latched(CM_RESOURCE_INTERRUPT_LATCHE),这可以从转换的中断资源表(Flags)中获得 设备是否能共享矢量,这从转换的中断资源表(ShareDisposition) 中获得 当设备中断时,是否保存浮点寄存器 在对IoConnectInterrupt调用的响应中,I/O管理器做下列事情: I/O管理器为与输入ProcessorMask指明的同样多的中断对象分配足够的常驻内存,这个数目可以与SMP机器中的处理器一样多,或比这个小,这依赖于ProcessorMask。I/O管理器也为中断自旋锁提供存储,并且如果驱动程序传递NULL指针到IoConnectInterrupt,初始化它(见步骤1)。 I/O管理器调用内部的内核例程,以初始化每个中断对象,并且把它连接到机器中设备能在其上中断的一个特殊的处理器。连接中断对象的它的调用实际在特定处理器的内核中断发送表(IDT)中设置给定的DeviceSystemVector。 当所有必要的中断对象已经被初始化,并连接到处理器,IoConnectInterrupt返回指向中断对象组的指针。驱动程序例程必须传递在它们的调用中返回的PointerToInterruptObject到KeSynchronizeExecution。 何时设备中断在一台给定的处理器上发生,在那个处理器上驱动程序的ISR在DIRQL上被运行,并被传递一个指向ServiceContext的指针,当驱动程序调用IoConnectInterrupt时,这个ServiceContext被创建。 如果驱动程序必须处理来自多个设备的中断,每个带有不同的中断矢量,它能为所有它的设备拥有一个单独的多向量ISR,否则它能拥有多个ISR。这样的驱动程序必须注册ISR,如下: 驱动程序必须使用指向InterruptSpinLock的驱动程序提供的存储区的指针调用KeInitializeSpinLock,这个InterruptSpinLock必须在常驻内存内(在驱动程序分配的设备扩展、控制器扩展、或者非页式存储池)。 ISR处理的每个向量,当除了下列的参数,驱动程序如前所述调用IoConnectInterrupt: 驱动程序必须传递一个指向初始化过的InterruptSpinLock的它的存储的指针,而非一个NULL自旋锁指针。 驱动程序必须指定一个SynchronizeIrql值,这个值是分配设备的最高的SystemDirql,这里的设备是指它的ISR为其处理中断的任何设备。 这样的驱动程序的ISR必须在最高的DIRQL上运行,这个DIRQL被分配到驱动程序处理的DeviceSystemVectors,因为驱动程序提供的访问设备寄存器(或数据)的SynchCritSection例程共享一个ISR。 因为ISR在一个相对较高的IRQL上运行,它必须尽可能迅速地返回控制。因而,驱动程序的ISR应该做它能做的小的I/O处理,通常是以下事情: 确定它的设备是否实际上产生中断,并且如果没有的话,尽早地返回FALSE。 如果它的设备产生中断,停止设备这样做,保存对中断引起的操作是必要的上下文,并且排队一个DPC以在较低的IRQL上完成中断驱动的I/O操作。更多信息参见“DPC对象”。 更多信息,参见第16章中“使用自旋锁”;第8章,“中断服务例程”;和第10章,“SynchCritSection例程”。 DPC对象 像前面几节中描述的中断对象类型一样,DPC对象类型被内核定义。DPC对象代表一个延迟过程调用:一个调用者提供的例程,它稍晚在较低的硬件优先级(IRQL)上被运行。 注册ISR的任何驱动程序必须使用一个或多个DPC对象来注册其DpcForIsr和/或CustomDpc例程。驱动程序提供的DpcForIsr或CustomDpc例程负责完成中断驱动的I/O操作,所以ISR能尽可能迅速地返回控制。 一些系统驱动程序只有一个单独的DpcForIsr例程;这些驱动程序使用一个单独的DPC对象。然而,驱动程序能拥有一个或多个CustomDpc例程,每个使用一个DPC对象,除了DpcForIsr例程,或者作为DpcForIsr例程的替代。 I/O管理器为注册DpcForIsr例程和从ISR排队这个例程提供支持,如下节所述。内核为注册CustomDpc和从ISR排队这个例程提供支持。细节参见“注册和排队CustomDpc例程”。 注册和排队DpcForIsr例程 在驱动程序已经创建设备对象之后,它通过调用IoInitializeDpcRequest为设备对象注册DpcForIsr例程。驱动程序能由其AddDevice例程或者由处理PnP IRP_MN_START_DEVICE请求的代码作这个调用。在设备被开始并且处理中断驱动的I/O请求之后,ISR仅仅在它返回控制前调用IoRequestDpc,从而将DpcForIsr例程排队供执行。图3.15说明了对这些例程的调用。 图3.15 为DpcForIsr例程使用DPC对象 如图3.15所示,调用IoInitializeDpcRequest将一个内核DPC对象与驱动程序提供的DpcForIsr例程和驱动程序创建的设备对象相结合。I/O管理器为DPC对象分配内存,并且根据驱动程序的需要调用KeInitializeDpc。 当ISR被调用以处理设备在DIRQL上的中断,为了获得更好的系统整体和驱动程序性能,它应该尽早地把控制返回到系统。通常,ISR仅仅阻止设备产生更多的中断,收集DpcForIsr例程需要的任何上下文信息(被用来完成引起中断的操作),调用IoRequestDpc,并且返回。 如图3.15所示,ISR传递下述三个指针到IoRequestDpc:一个指向设备对象的指针,这个设备对象代表操作被完成的目标设备;一个指向DeviceObject->CurrentIrp的指针;以及一个指向操作的驱动程序确定的上下文的指针。I/O管理器根据驱动程序的需要调用KeInsertQueueDpc,并且相应的DPC对象被排队,直到在处理器上IRQL落在DISPATCH_LEVEL之下。然后,内核将DPC对象取出队列,并且在那个处理器上驱动程序的DpcForIsr例程被在IRQL DISPATCH_LEVEL上运行。 在入口处,DpcForIsr例程被给一个指向DPC对象的指针,连同DeviceObject、当前的Irp、和Context指针,这个指针在ISR对IoRequestDpc的调用中被传递。上下文易可访问的区域必须在常驻内存中。除非驱动程序为目标设备重复I/O,上下文区域通常在DeviceObject->DeviceExtension中,但是如果驱动程序使用控制器对象,它能在控制器扩展中,或者在被驱动程序分配的非页式存储池中。DpcForIsr例程负责做任何必要的事情以完成在当前IRP中请求的I/O。 因为ISR和DpcForIsr例程在对称多处理器机器中能被同时运行,这些例程应该遵循这些方针: 在ISR返回控制之前,它必须调用IoRequestDpc。否则,在ISR已为DpcForIsr例程完成设置上下文可访问的区域之前,DpcForIsr例程可以在另一个处理器上被运行。 在DpcForIsr例程被运行期间,或者在此之前,如果设备中断,ISR可以被再一次调用。当驱动程序使用设备扩展来保持关于其设备I/O操作的上下文,DpcForIsr例程应该从不为输入DeviceObject(也不要为输入DeviceObject将IRP从队列中取出,如果驱动程序管理它自己的IRP排队)调用IoStartNextPackct,直到在它使用当前Irp调用IoCompleteRequest之前。否则,驱动程序的StartIo或者队列管理例程可以开始一个设备I/O操作,这个操作在DpcForIsr例程完成当前操作之前重写共享的上下文区域。 与ISR共享上下文区域的DpcForIsr例程和任何其他驱动程序例程必须使用驱动程序提供SynchCritSection例程调用KeSynchronizeExecution,从而以多处理器安全方式访问共享的上下文区域。 甚至在单处理器机器中,在DpcForIsr例程被运行时,或者在此之前,如果设备发生中断,ISR可以再一次被调用。如果这发生,DpcForIsr例程仅仅被运行一次。换句话说,如果驱动程序为它的目标设备对象重复I/O操作,在ISR对IoRequestDpcde调用和DpcForIsr例程的实例之间没有一对一的对应关系。 更多信息参见第9章,“ DpcForIsr例程和CustomDpc例程”。 注册和排队CustomDpc例程 在创建设备对象之后,设备驱动程序能通过调用KeInitializeDpc来注册CustomDpc例程。 驱动程序应该从其AddDevice例程作这个调用。 仅仅在驱动程序的ISR返回控制之前,它能调用KeInsertQueueDpc以便为执行而排队CustomDpc例程。图3.16说明了调用这些例程。 图3.16 为CustomDpc例程使用DPC对象 如图3.16所示,有CustomDpc例程的驱动程序必须为DPC对象提供存储。因为驱动程序必须把指针从其ISR传递到DPC对象,存储必须在常驻的系统空间内存中。大多数带有CustomDpc例程的驱动程序在驱动程序创建的设备对象的设备扩展中为它们的DPC对象提供存储,但是存储空间能在控制器扩展中(如果驱动程序使用控制器对象),或者存储在被驱动程序分配的非分页存储池中。 当驱动程序调用KeInitializeDpc,它必须传递它的CustomDpc例程的入口点,还要传递指向DPC对象的驱动程序分配的存储空间和驱动程序定义的DeferredContext区域的指针,它被传递到CustomDpc例程。因为在DeferredContext的区域必须在提高的IRQL上是易访问的,这个区域也必须在常驻内存中。 不像DpcForIsr例程那样,CustomDpc例程没有被关联到一个设备对象。然而,CustomDpc例程的DeferredContext区域几乎总应该包括指向目标设备对象的指针和当前的IRP。像DpcForIsr例程一样,CustomDpc例程使用这个信息以在比ISR低的IRQL上完成一个中断驱动I/O操作。 如图3.16所示,ISR把指向DPC对象和两个附加参数(它们是由调用者确定的)的指针传递到KeInsertQueueDpc。如果机器中的所有处理器拥有正在大于或等于DISPATCH_LEVEL的IRQL上运行的代码,DPC对象被排队,直到在处理器上IRQL落到DISPATCH_LEVEL之下。然后,内核将DPC对象从队列中取出,并且在处理器上驱动程序的CustomDpc例程在IRQL_DISPATCH_LEVEL上被运行。 在任何时刻,任何一个DPC对象的实例只有一个能被排队。这就是说,如果在驱动程序的CustomDpc例程被运行之前ISR使用相同的DPC指针调用KeInsertQueueDpc多次,在IRQL跌落到处理器上的DISPATCH_LEVEL以下之后,CustomDpc例程运行。 CustomDpc例程负责做任何必要的事情,以完成中断引起的I/O操作。细节参见第9章,“DpcForIsr例程和CustomDpc例程”。 ISR和CustomDpc例程能同时在一台对称多处理器机器中运行。因此,当实现CustomDpc例程时,遵循的方针如上一节“注册和排队DpcForlsr程序” 中所述的。 带有相关的DPC的定时器对象 任何驱动程序能有一个或多个IoTimer或者CustomTimeDpc例程,它们被周期性地调用以满足驱动程序作者决定的某种目的。每个这种类型的标准驱动程序实际上是一个DPC,这个DPC在一些数量的时钟中断被系统处理后被排队执行。 内核定义定时器对象类型,而I/O管理器创建能关联到每个驱动程序创建的设备对象的定时器对象,从而为驱动程序的IoTimer例程提供支持。驱动程序能为每个设备对象拥有一个单独的IoTimer例程,所有其设备对象有一个单独的IoTimer例程,每个它的设备对象有一个单独的IoTimer例程,或者一定数量的IoTimer例程关联到驱动程序的设备对象的非交叉子集。 在驱动程序启用定时器后,IoTimer例程每秒被调用一次,直到驱动程序禁止定时器。 I/O管理器为注册带有设备对象的IoTimer例程和为启用和禁止定时器提供支持例程。 作为变通,驱动程序能拥有一个或多个CustomTimerDpc例程,这些例程能以精确的驱动程序指定的间隔被调用。内核为注册CustomTimerDpc例程和排队CustomTimerDpc提供支持例程,在一个指定的定时器间隔耗尽后,这个例程被调用。 更多的关于定时器例程的信息,参见下列几节: 3.7.l 注册和启用IoTimer例程 3.7.2 注册和排队CustomTimerDpc例程 注册和启用IoTimer例程 在任何驱动程序创建一个或多个设备对象之后,它能通过调用IoInitializeTimer注册IoTimer例程。在设备被开始之后,驱动程序能通过调用IoStartTimer启用定时器。图3.17说明这些调用。 如图3.17所示,驱动程序使用它的IoTimer例程的入口点和指向驱动程序创建的设备对象和TimerContext区域的指针调用IoInitializeTimer,驱动程序在TimerContext区域中维护它的IoTimer例程使用的任何上下文。I/O管理器把设备对象与内核定时器对象相联系,并设置定时器对象每秒一次超时。 在驱动程序调用IoStartTimer之后,它的IoTimer例程每秒被调用一次,直到驱动程序调用IoStopTimer。驱动程序可以使用IoStartTimer再次启用对其IoTimer例程的调用。 在入口处,IoTimer例程被传递一个指向DeviceObject的指针和一个指向TimerContext区域的指针,这个TimerContext区域在驱动程序调用IoInitiaIizeTimer时被创建。 因为IoTimer例程在IRQL DISPATCH_LEVEL被运行,其TimerContext区域必须在常驻的系统空间内存中。拥有IoTimer例程的大多数驱动程序使用相关的设备对象的DeviceObject->DeviceExtension作为上下文可访问区域,但是上下文可以在控制器扩展中(如果驱动程序使用控制器对象),或者在被驱动程序分配的非页式存储池中。 图3.17 为IoTimer例程使用定时器对象 遵循这些有关TimerContext区域的方针: 如果IoTimer例程把其TimerContext区域与驱动程序的ISR共享,它必须使用SynchCrilSection例程调用KeSynchronizeExecution,从而以多处理器安全方式访问TimerContext区域程序。 更多关于KeSynchronizeExecution的信息参见第16章中的“使用自旋锁”。 如果IoTimer例程不把其TimerContext区域与ISR相共享,但是确实把它与另一个驱动程序例程相共享,驱动程序必须用初始化的执行自旋锁来保护共用的上下文区域,从而以多处理器安全方式访问TimerContext。 更多信息,参见章14,“IoTimer和CustOmTimempc例程”。 注册和排队CustomTimerDpc例程 驱动程序能通过调用下列例程来注册CustomTimerDpc例程,通常从其AddDevice例程中调用: 创建其CustomTimerDpc例程的KeInitializeDpc 创建定时器对象的KeInitializeTimer或者KeInitializeTimerEx 初始化设备之后,驱动程序能调用KeSetTimer或者KeSetTimerEx以创建一个带有间隔和DPC对象的定时器对象,并且使得它的CustomTimerDpc在给定的间隔耗尽时被调用。图3.18说明了这些调用。 图3.18 为CustomTimerDpc例程使用定时器和DPC对象 如图3.18所示,如果驱动程序有CustomTimerDpc例程,它必须为DPC对象和定时器对象提供存储。有CustomTimerDpc例程的大多数驱动程序为这些对象提供存储在驱动程序创建的设备对象的设备扩展中,或者在驱动程序分配的其他常驻内存中。 驱动程序必须首先通过调用KeInitializeDpc来初始化DPC对象,然后通过调用KeInitializeTimer或者KeInitializeTimerEx来初始化定时器对象。更多关于调用KeInitializeDpc的信息,参见“注册和排队CustomDpc例程”。 如图3.18所示,当驱动程序调用KeInitializeTimer或者KeInitializeTimerEx,驱动程序必须传递指向其定时器对象的存储的指针。 在初始化设备之后,驱动程序能调用KeSetTimer以排队CustomTimerDpc,CustomTimerDpc在驱动程序确定间隔后被调用。在对KeSetTimer的调用中,驱动程序传递指向Dpc和Timer对象的指针,附带一个以100纳秒为单位的DueTime,如图3.18所示。DueTime的正值指定了一个CustomTimerDpc例程应该再其上被调用的绝对时刻(自从1601年1月1日以来)。 DueTime的负值表明相对的过期时间。绝对的过期时间跟踪系统时间的任何变化;相对的过期时间不受系统时间改变的影响。 为了重复地调用CustomTimerDpc例程,使用KeSetTimerEx来设置定时器,并在Period参数中规定一个再次发生的间隔。除了这个附加的参数,KeSetTimerEx就象KeSetTimer一样。 如图3.18所示,对KeSetTimer或者KeSetTimerEx的调用排队有一个指定间隔的定时器对象,如下: 当DueTime期满,定时器对象被出列,并被设置Signaled状态。 如果机器中的所有的处理器当前在大于或者等于DISPATCH_LEVEL的IRQL上运行代码,与定时器对象有关的DPC对象被放入DPC队列中。否则,CustomTimerDpc例程被调用。 如果当DueTime间隔期满的时候DPC对象已经队列中,在机器中任何处理器上的IRQL落到DISPATCH_LEVEL之下后,CustomTimerDpc例程被调用。 CustomTimerDpc例程,像所有DPC一样,在IRQL DISPATCH_LEVEL上被运行。 内核定时器对象间隔大约是十个毫秒,所以驱动程序能使用CustomTimerDpc例程以获得比使用IoTimer(每秒运行一次)更小的时间间隔。 如同DPC对象,在任何给定的瞬间,一个给的定时器对象只有一个实例能被排队。再一次用同样的定时器对象指针调用KeSetTimer或者KeSetTimerEx可以取消排队的定时器对象,并且重新设置它。 更多信息参见“IoTimer和CustomTimerDpc例程”。 相同的方针适用于CustomTimerDpc例程的DeferredContext区域和IoTimer例程的TimerContext区域。细节参见“注册和启用IoTimer例程”。 设备队列对象和互锁队列 除了文件系统驱动程序,I/O管理器将设备队列对象与驱动程序创建的每个设备对象相联系,如图3.2中所示。 大多数设备驱动程序调用I/O管理器的支持例程,以使用相关的设备队列,在对目标设备对象的设备I/O请求到达的速度比驱动程序能完成处理它们的速度更快的时候,这个设备队列保存IRP。使用这种技术,IRP被排队到驱动程序提供的StartIo例程。 为了获得更好的性能,与IRP到达的速度一样快,大多数中间的驱动程序简单地把IRP传递传递到较低层驱动程序,所以中间层驱动程序几乎从不使用与它们各自设备对象有关的设备队列。 然而,你能通过明确地设置个一个或多个设备队列对象或者互锁队列来设计驱动程序管理IRP的内部队列。如果驱动程序控制作重复I/O操作的设备,这种方法是特别有用的。对于这样的设备,仅仅使用一个单独的队列,为相同的目标设备对象,来管理两个或者更多IRP的并行处理是困难的。 你也可以选择为一组不同种类的物理设备在设备控制器的驱动程序中创建辅助的IRP队列。 例如,SCSI端口驱动程序为内部队列使用设备队列对象。这个驱动程序拥有StartIo例程,并且创建设备队列对象作为辅助队列,另外还拥有设备队列,这个设备队列被关联到它创建的用来代表HBA的设备对象。SCSI端口驱动程序使用其辅助的设备队列来持有发往在HBA控制的SCSl总线上的特殊逻辑单元的IRP。 系统软盘控制器驱动程序是一个没有StartIo例程并且使用一个互锁队列的驱动程序的例子。这个驱动程序创建一个双重连接的互锁队列,驱动程序和其设备专用线程向这个队列插入IRP,以及从这个队列移去IRP。 内核定义设备队列对象类型。运行支持(Executive Support)组件为在互锁队列中插入和移去IRP提供列程。下面两节解释如何使用设备和互锁队列: 3.8.1 设置设备队列对象和排队的IRP 3.8.2 设置一个互锁队列和排队的IRP 更多关于I/O管理器的内部的对在与设备对象相关的设备队列中排队的IRP的支持,参见第14章中“最低层设备驱动程序的阶段IRP处理”。 设置设备队列对象和排队IRP 驱动程序通过在驱动程序或者设备初始化时调用KeInitializeDeviceQueue来创建设备队列对象。在开始其设备之后,驱动程序通过调用KeInsertDeviceQueue或者KeInsertByKeyDeviceQueue将IRP插入这个队列。图3.20说明这些调用。 图3.20 使用设备队列对象 如图3.20所示,驱动程序必须为设备队列对象提供存储,它必须是常驻的。创建设备队列对象的驱动程序通常在驱动程序创建的设备对象的设备扩展中提供必要的存储,但是如果驱动程序使用控制器对象,存储能在控制器扩展中,存储还可以在驱动程序分配的非分页存储池中。 如果驱动程序为设备扩展中的设备队列对象提供存储,在创建设备对象之后和开始设备之前,它调用KeInitializeDeviceQueue。换句话说,驱动程序能从其AddDevice例程初始化队列,或者在它处理PnP IRP_MN_START_DEVICE请求的时候初始化队列。在对KeInitializeDeviceQueue的调用中,驱动程序传递指向它为设备队列对象提供的存储的指针。 在开始其设备之后,驱动程序能通过调用KeInsertDeviceQueue将IRP插入其设备队列,它将IRP放置在队尾,或者调用KeInsertByKeyDeviceQueue,它根据驱动程序确定SortKey值把IRP放置到队列中,如图3.20所示。 每个这样的支持例程返回一个表明IRP是否被插入队列的布尔值。如果队列当前是空的(Not-Busy),每个这样的调用也置设备队列对象的状态为忙。然而,如果队列是空(Not-Busy)的,KeInsert..DeviceQueue例程从不将IRP插入队列。它所做的是,设置设备队列对象的状态为忙并且返回FALSE。因为IRP尚未被排队,驱动程序必须为进一步处理将它传递到另一个驱动程序例程。 在设置辅助的设备队列时,遵从下面这个实现方针: 当对KeInsert的调用返回FALSE时,调用者必须传递它试图为进一步处理而试图排队的IRP到另一个驱动程序例程。 然而,对KeInsertDeviceQueue的调用改变设备队列对象的状态为忙,因此下一个到达的IRP被插入在队列中,除非驱动程序调用它。 当设备队列对象的状态被置为忙碌,驱动程序能为进一步处理将IRP出列,或者通过调用下列支持例程之一来重设状态为Not-Busy: KeRemoveDeviceQueue,在队列头移去IRP KeRemoveByKeyDeviceQueue,根据驱动程序确定的SortKey值移去IRP KeRemoveEntryDeviceQueue,在队列中移走一个特定的IRP,或者确定特定的IRP是否在队列中 KeRemoveEntryDevice队列返回的布尔值表明IRP是否在设备队列中。 调用这些例程任何一个以从空的但是忙的设备队列中除去一个项,可以改变队列状态为不忙。 每个设备队列对象被一个内部的执行自旋锁(图3.20中没有显示)保护。结果是,驱动程序能从任何运行在小于等于IRQL DISPATCH_LEVEL的驱动程序例程中以多处理器安全方式插入IRP到队列,或移走它们。由于这个IRQL的限制,驱动程序不能从它的ISR或者SynchCritSection例程调用任何Ke..DeviceQueue例程,它的ISR或者SynchCritSection例程在DIRQL上运行。 更多信息参见第16章中“管理硬件优先级和使用自旋锁”。 对于特定支持例程的IRQL请求,在《Windows 2000驱动程序开发参考》卷2中参见那个例程的参考页。 设置一个互锁队列和排队IRP 带有设备专用线程的驱动程序或者使用执行工作者线程的驱动程序,例如大多数系统FSD,最有可能是这样一种驱动程序,这种驱动程序在一个互锁队列中管理他们自己运行时内部IRP排队。所有的PnP驱动程序,包括WDM驱动程序,也必须在作PnP和电源状态转变时内在地排队确定的IRP。 通常,这些驱动程序创建一个双重连接的互锁队列;所有的IRP包含一个类型为LIST_ENTRY的成员,驱动程序能使用它双重连接到当前正在保持的IRP。如果驱动程序创建一个单独连接的互锁队列,它不能为重试而重排队IRP。 驱动程序必须在设备初始化时创建其互锁队列。图3.21说明一个双重连接互锁队列、驱动程序必须调用以创建这样的队列的支持例程、和一组ExInterlockease例程,驱动程序能调用这组例程把IRP插入到队列或从队列中除去IRP。 图3.21 使用一个互锁队列 如图3.21所示,为了创建双重连接的互锁队列,驱动程序必须为队列本身和为下列事物提供存储: ExecutiveSpinLock,驱动程序必须调用KeInitializeSpinLock以初始化它。通常,当驱动程序为它的设备对象在它的AddDevice中创建设备扩展时,它初始化这个自旋锁。 队列的ListHead,驱动程序必须通过调用InitializeListHead来初始化它。 使用双重连接互锁队列的大多数驱动程序在驱动程序创建的设备扩展中提供必要的存储。 如果驱动程序使用控制器对象,队列和执行的自旋锁能在控制器扩展中,还可以在被驱动程序分配的非页式存储池。 在驱动程序接受I/O请求期间,如果ListHead是类型LIST_ENTRY的,驱动程序能通过调用下列支持例程的任何一个将IRP插入其队列中: ExInterlockedInsertTailList,它在队列的尾部放置IRP ExInterlockedInsertHeadList,它在队列的前面放置IRP。仅仅在驱动程序必须再试特殊的请求时,它们通常调用这个例程。 驱动程序必须传递指向IRP(ListEntry)的指针,如它以前初始化过的ListHead和ExecutiveSpinLock(锁)指针一样,驱动程序将它们传递到这些ExInterlockedInsert..List例程的一个。当驱动程序通过调用ExInterlockedRemoveHeadList以从队列中移出IRP的时候,仅仅指向ListHead和Lock的指针被请求。为了防止死锁,驱动程序不保有它传递给任何ExInterlockedXxx例程的ExecutiveSpinLock。 因为互锁队列被执行的自旋锁保护,驱动程序从运行在小于等于IRQL DISPATCH_LEVEL的驱动程序以多处理器安全方式将IRP插入到其双重连接队列中,以及移走它们。 带有类型为LIST_ENTRY的ListHead的队列,如图3.21所示,是一个双重连接的表。带有类型为SLIST_HEADER的ListHead的队列是一个顺序的、单独连接的表。驱动程序通过调用ExInitializeSListHead为序列的单独连接的互锁队列初始化ListHead。 从不重试I/O操作的驱动程序能使用ExInterlockedPushEntrySList和ExInterlockedPopEntrySList以管理它的在序列的单独连接的互锁队列内的IRP排队。使用这种类型的互锁队列的任何驱动程序也必须为类型为SLIST_HEADER的ListHead和为ExecutiveSpinLock提供常驻的存储空间,如图3.21所示。在驱动程序调用ExInitializeSListHead以插入初始的项到它的队列中之前,它必须初始化自旋锁并创建它的队列。 更多信息,参见第16章中“管理硬件优先级和使用自旋锁”。关于特定的支持例程的IRQL请求的信息,参见《Windows 2000驱动程序开发参考》卷2中那个例程的参考页。 内核调度者对象 如在“带有相关DPC的定时器对象”中所提到的那样,内核为定时器对象定义对象类型,这个定时器对象能被关联到驱动程序提供的CustomTimerDpc例程。定时器对象是一组被称为调度者对象的内核定义的对象类型中的一个。 内核也为其他调度者对象定义类型,诸如事件、信号量、和互斥体。内核定义的线程对象,它代表在系统内执行的线程,也是调度者对象。 当在IRQL PASSIVE_LEVEL运行时,调度者对象能在一个非专用线程环境内被用作同步化机制。 调度者对象状态和线程属性 每个内核定义的线程调度者对象类型都有一个状态,要么被设置为有信号(Signaled),要么被设置为无信号(Not-Signaled)。 一组线程能同步它们的操作,方法是使用调度者对象让一个或多个线程调用KeWaitForSingleObject、KeWaitForMutexObject,或者KeWaitForMultipleObjects,并且等待直到另一个例程或者线程把调度者对象设置为有信号状态。 当线程使用调度者对象调用KeWaitForSingleObject,或者使用互斥体调用KeWaitForMutexObject,线程被放到等待状态中,直到调度者对象被设置为有信号状态。 当线程使用一个或多个调度者对象调用KeWaitForMultipleObjects,线程被放到等待状态中直到一个或者所有调度者对象被设置为有信号状态。 无论何时调度者对象被设置为有信号状态,内核改变任何等待那个对象的线程的状态为就绪(ready):根据它的当前运行时优先级和当前的处理器(拥有那个优先级的任何线程在其上执行)可用性,线程将被调度运行。同步化事件和同步化定时器是这项规则的例外情况;当同步化事件或者定时器被置为有信号,仅仅一个等待线程被置为就绪状态。 一些驱动程序创建它们自己的驱动程序或者设备专用的系统线程,并且把它们的线程的基础优先级设置为最低的实时优先级值。其他最高层驱动程序,特别是文件系统驱动程序,使用带有基本优先级的系统工作者线程,这些工作者线程通常被设置为最高的可变优先级值。内核调度带有最低实时优先级的线程,从而可以在每个带有可用优先级的线程,包括系统中几乎所有的用户模式线程,之前运行。 在所有当前处于就绪状态的线程之前,大多数标准的驱动程序例程在一个专用线程环境中被运行。 无论它们各自的运行时优先级是什么,线程都在IRQL PASSIVE_LEVEL上被运行。许多标准的驱动程序例程在提高的IRQL上被运行:就是说,在大于PASSIVE_LEVEL的硬件优先级上,例如IRQL DISPATCH_LEVEL或者DIRQL。 等待驱动程序中的调度者对象 对于大多数设备和中间层驱动程序,内核的调度者对象只有在下列环境被用于同步: DriverEntry、AddDevice、Reinitialize和Unload,还有一定的Dispatch例程,在系统线程的上下文中被运行,所以一个驱动程序能在这些例程之内等待调度者对象。 最高层的驱动程序的Dispatch例程也通常在一个非专用线程环境中被运行,通常是请求I/O操作的用户模式线程环境。 开始固有的同步I/O操作的低层驱动程序的Dispatch例程,例如创建、刷新、停止、关闭,也能为请求的操作的完成等待调度者对象。 然而,低层驱动程序的Dispatch例程不能为固有异步I/O操作的完成等待一个内核定义的调度者对象。 对SCSI微端口驱动程序考虑下面的设计方针: 为了移植性要求,SCSI不应该为端口驱动程序使用内核调度者对象。SCSI微端口驱动程序只应该调用ScsiPortXxx例程。 对任何驱动程序考虑下列的设计方针: 驱动程序不能,并且不可试图为向或从页式设备传输操作的完成等待调度者对象。 换句话说,读和写请求的驱动程序Dispatch例程通常不能等待调度者对象。带有I/O控制代码(它的传输类型值是除了MEHTOD_BUFFERED之外的值)的任何设备I/O控制请求的Dispatch例程也不能等待用调度者对象。更多关于驱动程序必须支持的设备类型特定的请求和I/O控制代码,参见《Windows 2000驱动程序开发参考》卷2。 所有的其他标准驱动程序例程在一个专用线程环境中被运行:当无论是什么驱动程序例程被调用以处理IRP或者处理设备中断,这个例程的上下文恰恰就是当前的。此外,大多数标准的驱动程序例程在提高的IRQL上被运行,要么是在DISPATCH_LEVEL,要么是为设备驱动程序在DIRQL。 记住下列的事实: 在提高的IRQL上为非零间隔等待内核调度者对象是一个致命的错误。 开始对设备异步I/O操作的驱动程序Dispatch例程和在提高的IRQL上运行的所有驱动程序例程不能使用调度者对象来同步它们的IRP处理。标准驱动程序例程在其上运行的IRQL的总结参见章l6中“管理硬件优先级”。 极少有设备和中间的驱动程序创建驱动程序创建的系统线程。极少有最高层的设备驱动程序使用系统工作者线程,FSD通常这样作。因此,大多数设备和中间层驱动程序仅仅在驱动程序初始化、卸载、或在同步I/O请求的Dispatch例程中能使用调度者对象。 如果必要的话,驱动程序能创建设备专用的线程,这个线程能等待一个调度者对象,那个驱动程序的其他例程(除ISR或者SynchCritSection程序之外)能设置这个调度者对象为有信号状态,或者重设为无信号状态。作为一般方针,如果期待的新设备驱动程序在I/O操作期间等待设备状态变化的时候经常需要等待大于50微秒的迟延,考虑用设备专用线程实现驱动程序。如果设备驱动程序也是最高层驱动程序,考虑使用系统工作者线程并实现一个或多个工作者线程回调例程。参见《Windows 2000驱动程序开发参考》卷2第8章中的“PsCreateSystemThread”,和第7章中的“使用驱动程序创建的线程管理互锁队列”。 下面几节描述定时器、事件、信号量,和有拥有驱动程序的互斥的调度者对象,这些驱动程序带有设备专用线程和/或者工作者线程的回调例程,或者必须在DriverEntry、Reinitialize、AddDevice、Unload、和同步I/O操作的Dispatch例程中等待一个或多个调度者对象。 任何在其DriverEntry、Reinitialize、Unload例程中等待调度者对象的驱动程序在系统线程上下文中运行。实际上,在其同步I/O操作的Dispatch例程中等待调度者对象的任何驱动程序从一个专用线程的总量中偷取一个周期。 为简单期间,下面几节说明对KeWaitForSingleObject的调用。可以视为对KeWaitForMultipleObjects调用。 定时器对象 任何驱动程序能在一个非专用线程环境之内使用定时器对象,以使在驱动程序的其他例程之内的操作超时,或者执行一个周期性的操作。驱动程序调用KeInitializeTimer或者KeInitializeTimerEx以创建定时器对象。 定时器能是通知定时器或者同步化定时器。当通知定时器被发信号,所有等待的线程满足它们的等待条件,而定时器的状态仍然是有信号,直到它被明确地复位。当同步化定时器期满,其状态被设置为有信号,直到一条单独的等待线程被释放,然后定时器被复位到无信号状态。KeInitializeTimer总是创建通知定时器(notification timer)。KeInitializeTimerEx接受一个Type参数,它可以是NotificationTimer或者SynchronizationTimer。通知和同步化定时器能可选地拥有一个相关的CustomTimerDpc例程。 定时器仅仅能期满一次,或者它能在一个给定间隔中被重复地设置为期满。KeSetTimer总设置一个将仅仅期满一次的定时器。KeSetTimerEx接受一个可选的Period参数,调用者能使用这个参数为定时器规定一个再次发生的间隔。 图3.22说明使用通知定时器为一个操作创建一个超时间隔,然后在其他驱动程序例程处理I/O请求时等待。 图3.22 等待定时器对象 如图3.22所示,驱动程序必须为定时器对象提供存储空间,这个定时器对象必须通过使用指向这个存储的指针来调用KcInitializeTimer,从而被初始化。驱动程序典型地从其AddDevice例程中作这个调用。 在一个特殊的线程的上下文之内,例如驱动程序创建的线程或请求同步I/O操作的线程,这个驱动程序可以等待它的定时器对象,如图3.22所示: 线程用指向定时器对象的指针和给定的DumTime调用KeSetTimer,DueTime的单位是100 纳秒。DueTime的正值规定定时器对象应该从内核的定时器队列中被除去,并设置为有信号状态的绝对时间。DueTime的负值规定与当前系统时间有关的间隔。 注意,如果等待定时器对象而不是排队CustomTimerDpc例程,在线程调用KeSetTimer的时候,线程(或者驱动程序例程试车系统线程)传递DPC对象的NULL指针如以前在图3.19中所示。 线程用指向定时器对象的指针调用KeWaltForSingIeObject,当定时器对象在内核的定时器队列中时,它把线程放到等待状态中。 给定的DueTime期满。 内核将定时器对象出列,设置它为有信号状态,并且将线程的状态从等待改变为就绪。 一旦处理器是可供使用的,内核为执行发送线程:就是说,没有其他带有较高优先级的线程当前在就绪状态中,没有在提高的IRQL(大于PASSlVE_LEVEL)上被运行的内核模式例程。 通过使用带有相关的DPC对象的定时器对象,运行在提高的IRQL上的驱动程序例程能超时请求,以排队驱动程序提供的CustomTimerDpc例程。只有在一个非专用线程环境之内运行的驱动程序例程能等待定时器对象非零的间隔,如图3.22。 像所有其他线程一样,驱动程序创建的线程由内核线程对象描述,这个内核线程对象也是调度者对象。因而,驱动程序不必使其驱动程序创建的线程使用定时器对象,以为给定的间隔将自己自愿地放到等待状态中。相反,线程能使用调用者提供的间隔调用KeDelayExecutionThread。更多关于这种技术的信息,参见第16章中“轮询设备”。 DriverEntry、Reinitialize和Unload例程也在系统线程环境中运行,因此当它们初始化或卸载时,驱动程序能使用驱动程序初始化的定时器对象调用KeWaitForSingleObject或调用KeDelayExecutionThread。如果设备驱动程序在其初始化期间必须等候设备,它能为一个十分短的间隔(最好是小于50微秒)调用KeStallExecutionProcessor。 然而,较高层驱动程序通常在它们的DriverEntry和/或Reinitialize例程中使用另一个同步化机制,而不是使用定时器对象。较高层驱动程序应该总是被设计成位于特殊类型设备的任何低层驱动程序之上。因此,如果更高层驱动程序等待定时器对象或调用KeDelayExecutionThread,它倾向于变成缓慢加载的,因为这样的驱动程序必须等待足够的间隔,以便可能最慢的设备支持它。也要注意,对于这样一个等待,“安全”但是最小的间隔是很难确定的。 同样地,PnP驱动程序不应该等候其他动作发生,但是作为代替应该使用PnP管理器的通知机制。 如果在定时器期满之前系统时间变化,相对定时器不受影响,除非系统调整每一个绝对的定时器。在指定数量的时间单元流逝之后,不管绝对系统时间如何,相对定时器总是期满。 一个绝对的定时器在特定系统时刻期满,在系统时间方面这样的变化改变一个绝对的定时器的等待时期。 事件对象 如在“内核调度者对象”中所述的,当相邻较低层驱动程序处理等待的驱动程序创建的IRP时,驱动程序使用事件对象来等待。拥有驱动程序创建的线程的驱动程序或等待同步I/O请求完成的驱动程序Dispatch例程也能使用事件对象来同步它们的线程和/或其他驱动程序例程之间的操作。 使用事件对象的任何驱动程序必须在它等待、设置、清除、或者复位事件之前调用KeInitializeEvent或者IoCreateXxxEvent。图3.23说明带有线程的驱动程序如何能为同步化使用事件对象。 图3.23 等待事件对象 如图3.23所示,这样一种驱动程序必须为事件对象提供存储,它必须是常驻的。驱动程序能使用驱动程序创建的设备对象的设备扩展、控制器扩展(如果驱动程序使用控制器对象),或者被驱动程序分配的非页式存储池。 当驱动程序调用KeInitializeEvent,它必须传递指向驱动程序的常驻的事件对象存储空间的指针。此外,调用者必须指定事件对象的初始状态(有信号,或无信号),如图3.23所示。调用者也必须为其事件对象指定Type,它能可以是下列值之一: SynchronizationEvent 当这样的事件被放置为有信号状态,正在等待这个事件的单独线程变得适合执行,并且事件的状态被自动复位为无信号。 这种类型的事件有时被自动清除事件(autoclearing event)调用,因为每次等待被满足后,其有信号状态自动地被复位。 NotificationEvent 当这样的事件被设置为有信号状态,等待这个事件的所有线程变得适合执行,并且事件保持有信号状态,直到明确将其复位为无信号:就是说,使用给定的Event指针调用KeClearEvent或者KeResetEvent。 很少设备或者中间层驱动程序有一个单独的驱动程序专用线程,更不用说可以通过等待保护共享资源的事件使它们的操作同步的一组线程了。 大多数使用事件对象来等候完成I/O操作的驱动程序在它们调用KeInitializeEvent时,设置输入Type为NotificationEvent。为IRP(驱动程序使用IoBuildSynchronousFsdRequest或者IoBuildDeviceIoControlRequest创建这个IRP)创建的事件对象几乎总是作为NotificationEvent被初始化,因为调用者将等待通知它的请求已经被一个或多个较低层驱动程序满足的事件。 在驱动程序已经初始化之后,其线程(如果有)和其他例程能在事件上使它们的操作同步。 例如,带有管理IRP排队的线程的驱动程序,诸如系统软盘控制器驱动程序,可以在事件上同步IRP处理,如图3.23所示: 已经为在设备上处理而将一个IRP取出队列的线程,使用指向驱动程序提供的初始化的事件对象的存储空间的指针调用KeWaitForSingleObject。 其他驱动程序例程完成必要的设备I/O操作以满足IRP,并且,当这些操作被完成时,驱动程序的DpcForIsr例程调用KeSetEvent,这个调用使用指向事件对象的指针、驱动程序确定的为线程而被提高的优先级(Increment,如图3.23中所示)、被设置为FALSE的布尔值Wait。调用KeSetEvent设置事件对象为有信号状态,进而等待的线程的状态为就绪。 一旦处理器是可供使用的,内核为执行调度线程:就是说,没有带有较高优先级的其他线程当前在就绪状态,被运行在提高的IRQL上(大于PASSIVE_LEVEL)。 如果DpcForIsr已经没有使用IRP调用IoCompleteRequest,线程现在能完成IRP,并且能将另一个IRP出列,使其在设备上被处理。 使用被设置成TRUE的Wait参数调用KeSetEvent表明调用者的意图,在从KeSetEvent返回时立即调用KeWait..Object(s)支持例程。 给KeSetEvent设置Wait参数要考虑以下方针: 运行在IRQL PASSIVE_LEVEL的可分页的线程或可分页的驱动程序例程从不应该使用被设置为TRUE的Wait参数调用KeSetEvent。如果在调用KeSetEvent和KeWait..Object(s)之间调用者碰巧被页调出,这样的调用会引起致命的错误。。 在大于PASSIVE_LEVEL的IRQL上运行的任何标准驱动程序例程不能在任何调度者对象上等待非零间隔而不降低系统性能(参见3.9)。然而,当在小于或者等于DISPATCH_LEVEL的IRQL上运行时,这样的例程能调用KeSetEvent。 想获得标准的驱动程序例程在其上运行的IRQL的总结,参见第16章中“管理硬件优先级”。 对于支持例程特定的IRQL请求,参见《Windows 2000驱动程序开发参考》卷2。 KeResetEvent返回给定的事件以前的状态:当对KeResetEvent的调用发生时,它是否被设置为有信号。 KeClearEvent简单地把给定的事件的状态设置为无信号。 当调用前面的支持例程时,考虑下列的方针: 要获得较好的性能,所有的驱动程序应该调用KeClearEvent,除非调用者需要由KeResetEvent返回的信息以确定下一步该怎么办。 信号量对象 任何驱动程序能使用信号量对象来使它的驱动程序创建的线程和其他驱动程序例程之间的操作同步。例如,当没有对驱动程序的显著I/O请求时,驱动程序专用线程可以把自己放到等待状态中,并且驱动程序的Dispatch例程刚好在它们排队IRP之后,可以设置信号量为有信号状态。 最高层的驱动程序的Dispatch例程,它在请求I/O操作的线程环境中被运行,可以使用信号量来保护在Dispatch例程中间共享的资源。同步I/O操作的低层驱动程序Dispatch例程也可以使用信号量来保护在Dispatch例程的子集中间或者与驱动程序创建的线程共享的资源。 在使用信号量对象的任何驱动程序等待或者释放信号量之前,它必须调用KeInitializeSemaphore。图3.24说明带有线程的驱动程序如何使用信号量对象。 图3.24 等待信号量对象 如图3.24所示,这样的驱动程序必须为信号量对象提供存储,这个存储应该是常驻的。如果驱动程序使用控制器对象,它能使用驱动程序创建的设备对象的设备扩展、控制器扩展(如果它使用控制器对象),或者被驱动程序分配的非页式存储池。 当驱动程序的AddDevice例程调用KeInitializeSemaphore,它必须传递指向为信号量对象提供的驱动程序的常驻存储空间的指针。此外,调用者必须为信号量对象指定一个Count,如图3.24所示,它确定它的初始状态 (非零表示有信号)。 调用者也必须为信号量规定Limit,它可以是下列值之一: Limit=1 当这个信号量被设置为有信号状态,等待信号量的一个单独的线程变得适合于执行,并且能访问任何被信号量保护的资源。 因为线程要么拥有,要么没有对信号量保护的资源的排它访问,这种类型的信号量也被称为二进制信号量(binary semaphore)。 Limit>1 当这种信号量被设置为有信号状态,一些等待信号量对象的线程变得适合于执行,并且能访问任何有信号量保护的资源。 这种类型的信号量被称为计数信号量(counting semaphore),因为设置这个信号量为有信号状态的例程也指定有多少线程可以使它们的状态由等待改变为就绪。这样等待的线程数目可以是Limit,Limit在信号量被初始化或某个数目小于这个预设Limit的时候被设置。 极少数设备或者中间层驱动程序有一个单独的驱动程序创建的线程:甚至更少有一组可以等待信号量的线程。极少有系统提供的驱动程序使用信号量对象,并且,即使它们这样做,甚至更少使用二进制信号量。虽然二进制信号量似乎可以在功能上类似一个互斥体对象,但是二进制信号量不提供内部保护以避免死锁,互斥体对象为在SMP机器中运行的线程提供这种保护。 在带有一个初始化信号量的驱动程序被装载之后,它能在保护一种共享资源的信号量上使操作同步。例如,带有管理IRP排队的设备专用线程的驱动程序,诸如系统软盘控制器驱动程序,可以在信号量上同步IRP排队,如图3.24所示: 线程用指向驱动程序提供的存储空间(为初始化的信号量对象提供这个存储空间)指针调用KeWaitForSingleObject,以把自己放到等待状态中。 IRP开始进入那个请求设备I/O操作的驱动程序。驱动程序的Dispatch例程在自旋锁控制下把每个这样的IRP插入一个互锁队列,并且使用下列参数调用KeReleaseSemaphore,包括一个指向信号量对象的指针、为线程提升的驱动程序确定的优先级 (Increment,如图3.24所示)、在每个IRP被排队时被添加到信号量的计数器的Adjustment、和一个被设置为FALSE的布尔值Wait。非零信号量计数器设置信号量对象为有信号状态,进而更改等待的线程状态为就绪。 一旦处理器是可供使用的,内核为执行调度线程:就是说,没有带有较高的的优先级的其他线程当前在准备状态中,没有内核模式例程被在提高的IRQL(大于PASSIVE_LEVEL)上被运行。 线程在自旋锁控制下从互锁队列中除去IRP,传递它到其他驱动程序例程以供进一步处理,并且再一次调用KeWaitForSingleOhject。如果信号量依旧被设置为有信号状态(即它的Count保持非零,表明更多的IRP在驱动程序的互锁队列中),内核再一次将线程状态从等待改变为就绪。 通过这种方式使用计数的信号量,在线程无论什么时候被运行,这样的驱动程序线程都“知道”存在一个从互锁队列中被除去的IRP。 使用被设置为TRUE的Wait参数调用KeReleaseSemaphore表明调用者的意图,即在从KeReleaseSemaphore返回时立即调用KeWait..Object(s)支持例程。 为KeReleaseSemaphore设置Wait参数,考虑下列方针; 在IRQL PASSIVE_LEVEL运行的可分页的线程或可分页的驱动程序例程从不应该使用被设置为TRUE的Wait参数调用KeReleaseSemaphore。如果调用者碰巧在对KeReleaseSemaphore和KeWait..Object(s)的调用之间被页调出,这样的调用将引起致命的错误。 如果不降低系统性能,在大于PASSIVE_LEVEL的IRQL运行的任何标准驱动程序例程不能在任何调度者对象上等待非零间隔;细节参见“内核调度者对象”。然而,当这样的例程正运行在低于等于DISPATCH_LEVEL的IRQL上,它可以调用KeReleaseSemaphore。 对于标准驱动程序例程在其上运行的IRQL的总结,参见第16章中的“管理硬件优先级”。关于支持例程特定的IRQL请求,参见《Windows 2000驱动程序开发参考》卷2。 互斥体对象 如其名所暗示的那样,互斥体对象是一个被设计成相互地保证对在一组内核模式线程之间共享的单个资源排它访问的机制。只有最高层的驱动程序,诸如使用执行的工作者线程的FSD,很有可能使用一个互斥体对象。 有可能,带有驱动程序创建的线程和/或工作者线程的回调例程的最高层驱动程序可以使用互斥体对象。然而,带有可分页线程或者工作者线程的回调必须非常仔细地管理它的互斥体对象的获得、等待和释放。 如已经在前面的节中被论及的那样,互斥体对象拥有内部的特性,它提供系统(只有内核模式)线程相互排斥地、没有死锁地对SMP中共享资源的访问。所有的互斥体对象有两个基本的内部特性,它为在系统线程中间共享的资源提供这种保护: 所有权 内核一次把给定的互斥体的所有权分配到一个单独的线程,这的结果可能如下: 如果拥有线程的优先级还不在实时范围内,提高那个线程的实时优先级到最低的实时优先级 防止拥有线程的处理离开平衡设置:即,处理将不被页调出到二级存储 防止正常的内核模式异步过程调用(APC)被交付。线程将不被APC先占,除非内核发布一个APC_LEVEL软件中断以运行特殊的内核APC,诸如把结果返回到I/O操作的原来请求者的I/O管理器的IRP完成例程。 线程能获取它已经拥有(递归的所有权)的一个互斥体对象的所有权,但是一个递归获取的互斥体对象不能被设置为有信号状态,直到线程完全释放其所有权。在另一个线程能获得这个互斥体之前,这样的线程必须明确释放互斥体的次数要和它获得所有权的次数相等。 如果首先释放互斥体并且设置它为有信号状态,内核从不允许拥有互斥体的线程引发一个向用户模式的转变。如果拥有互斥体的任何FSD创建的或者驱动程序创建的线程试图在释放互斥体的拥有权之前向I/O管理器返回控制,内核停止系统。 层 驱动程序线程能获取多个互斥体。在这种情况下,驱动程序应该被设计成每个互斥体对象的层确定这样一种线程是否能在任何特定时刻获取互斥体的所有权。特别地,驱动程序应该加强下列策略: 线程应该按指定层的上升次序来获取互斥体。当它初始化互斥体的时候,驱动程序能分配一个非零的层。 然而,互斥体的所有者不可试图获取带有较低Level的另一个互斥体,因为这个次序互斥体可以已经被另一个线程拥有,而这个线程正在试图获取第一个线程的较高的Level的互斥体。如果驱动程序不加强这项策略,结果将造成死锁,因为没有线程可以获取其他线程当前拥有的互斥体的所有权。 使用互斥体对象的任何驱动程序在它等待或释放其互斥体之前,必须调用KeInitializeMutex一次。图3.25说明两个系统线程可以怎样使用一个互斥体对象。 图3.25 等待一个互斥体对象 如图3.25所示,这样的驱动程序必须为互斥体对象提供存储。因为驱动程序仅仅能在线程上下文之内等待并且释放互斥体,创建它自己系统线程的驱动程序能在页式存储池中为它的互斥体对象提供存储,这里的页式存储池是由驱动程序分配的。 如果这样的最高层的驱动程序在其Dispatch例程中使用系统工作者线程或者等待互斥体,它必须在下列地方提供常驻存储:在驱动程序创建的设备扩展中、在控制器扩展中(如果它使用控制器对象)、或者在被驱动程序分配的非页式存储池中。 当AddDevice例程调用KeInitializeMutex,它必须传递指向为互斥体对象提供的驱动程序的存储空间的指针,内核初始化这个互斥体对象为有信号状态。此外,如果调用者使用多个互斥体,调用者能为互斥体对象(如图3.25中所示)指定一个非零的Level,以控制获得次序。 在这样的最高层的驱动程序已经初始化之后,它能管理相互排斥地对如图3.25中所示的共享资源的访问。例如,驱动程序的固有同步操作的Dispatch例程和线程可以使用互斥体为IRP保护驱动程序创建的队列。 因为KeInitializeMutex总把一个互斥体对象的初始状态设置为有信号,如图3.25所示: Dispatch例程使用互斥体指针对KeWaitForSingleObject的调用立即把当前线程放到就绪准备状态中,给线程互斥体的所有权,并且把互斥体的状态复位到无信号。一旦Dispatch例程恢复运行,它能把IRP安全地插入到互斥体保护的队列中。 当第二个线程(另一个Dispatch例程,驱动程序提供工作者线程回调例程,或者驱动程序创建的线程)使用互斥体指针调用KeWaitForSingleObject,第二个线程被放到就绪状态中。 当Dispatch例程结束如在步骤1中所说的排队IRP,它使用互斥体指针和一个Wait布尔值调用KeReleaseMutex,这个布尔值表明一旦KeReleaseMutex返回控制,它是否计划使用互斥体调用KeWaitForSingleObject(或者KeWaitForMutexObject)。 假定Dispatch例程在步骤3(等待放置把为FALSE)中释放其互斥体所有权,互斥体被KeReleaseMutex设置为有信号状态。互斥体当前没有所有者,因此内核确定是否另一个线程正在等待那个互斥体。如果如此,内核使第二个线程(见步骤2)成为互斥体的所有者,把线程的优先级有可能提升到最低的实时优先级值,并且把其状态改变成就绪。 一旦处理器可以使用,内核为执行发送第二个线程:即,没有其他带有较高优先级的线程当前是在就绪状态,并且没有内核模式的例程被运行在提高的IRQL(大于PASSIVE_LEVEL)上。第二个线程(排队IRP的Dispatch例程,或者驱动程序的工作者线程回调例程或者驱动程序创建的排队IRP的线程)能安全地访问互斥体保护的IRP队列,直到它调用KeReleaseMutex。 如果线程递归地获取一个互斥体对象的所有权,线程必须明确调用KeReleaseMutex与它等待互斥体一样多的次数,以设置互斥体对象为有信号状态。例如,如果线程调用KeWaitForSingleObject,然后使用同样的互斥体指针调用KeWaitForMutexObject,为了设置互斥体对象为有信号状态,当它获得互斥体时,它必须调用KeReleaseMutex两次。 使用被设置为TRUE的Wait参数调用KeReleaseMutex表明调用者的意向,即在从KeReleaseMutex返回时立即调用KeWait..Object(s)支持例程。 为KeReleaseMutex设置Wait参数,考虑下列的方针: 在IRQL PASSIVE_LEVEL运行的可分页线程或者可分页驱动程序例程应该从不使用被设置为TRUE的Wait参数调用KeReleaseMutex。如果调用者碰巧在调用KeReleaseMutex和KeWait..Object(s)中间被页调出,这样的调用将引起致命的错误。 如果没有停止系统,在大于PASSIVE_LEVEL的IRQL运行的任何标准驱动程序例程不能在任何调度者对象上等待非零间隔。(附加的方针参见“内核调度者对象”。)然而,这样的例程在它运行在小于等于DISPATCH_LEVEL的IRQL上的时候,如果它拥有互斥体,它可以调用KeReleaseMutex。 对于标准驱动程序例程在其上运行的IRQL的总结,参见第16章中的“管理硬件优先级”。关于支持例程特定的IRQL请求,参见《Windows 2000驱动程序开发参考》卷2。 线程在调度者对象上等待的报警和APC的处理 一个等待的线程对报警和APC的运送作出反应的方法依赖于一些因素。在某些情况下,一个报警将引起等待被放弃,而在其他情况下又不会这么样。同样地,仅仅在一定的环境下APC被传送等待线程。表3.1总结了线程在调度者对象上等待的报警和APC的处理。 四种内核支持例程在调度者对象上开始等待:KeWaitForSingleObject、KeWaitForMultipleObjects、KeWaitForMutexObject、和KeDelayExecutionThread。所有这些例程中的四个使用一对布尔值参数:Alertable和WaitMode。可能被分配到这两个参数的值的结果是复杂的。例如,把TRUE值分配到Alertable的参数,不保证等待的线程能被报警。本节余下的部分提供关于Alertable和WaitMode参数的设置如何影响等待线程的行为的总结。 表3.1 等待线程的报警和APC的处理 KeWaitForXxx例程的参数设置 特定的内核模式的APC 正常的内核模式的APC 用户模式的APC 警报   放弃等待? 交付和执行APC? 放弃等待? 交付和执行APC? 放弃等待? 交付和执行APC? 放弃等待?  Alertable=TRUE WaitMode=User 否 如果(A)则是 否 如果(B)则是 是 时,在线程返回到用户模式以后 是  Alertable=TRUE WaitMode=Kernel 否 如果(A)则是 否 如果(B)则是 否(因为WaitMode=Kernel) 否 是  Alertable=FALSE WaitMode=User 否 如果(A)则是 否 如果(B)则是 否(因为Alterable=FALSE) 否(随着例外。EX.^C以终止) 否  Alertable=FALSE WaitMode=Kernel 否 如果(A)则是 否 如果(B)则是 否(因为Alterable=FALSE并且WaitMode=Kernel) 否 否  IRQL<APC_LEVEL IRQL<APC_LEVEL,APC中线程没有就绪,线程不在临界区中   Alertable参数规定线程是否能被来自未说明的内核例程或由用户模式APC生成的警告报警,以及它的等待状态是否被放弃。如果警报的值是FALSE,那么无论WaitMode参数值是什么或报警的起源是什么,线程却不能被报警。这项规则的唯一例外情况是终止线程的情况。线程自动地被设成可报警的,例如,当被用户用Ctrl+C终止。 如果警报的值是TRUE,而下列的条件之一出现,线程将被报警: 警报的起源是内部的、没有说明的内核模式例程使用报警线程。 警报的起源是用户模式APC,并且WaitMode参数的值是UserMode。 在这两种情况的前一种中,线程的等待被使用完成状态STATUS_ALERTED满足;在第二种情况下,它被使用完成状态STATUS_USER_APC满足。 线程必须对用户模式APC被传送是可报警的。然而,即使线程不是被报警,内核模式APC能被传送和执行。一旦APC执行完成,线程的等待恢复。线程从不因内核模式APC的运送被报警,它的等待也不因此被放弃。 内核模式APC到一个等待线程的运送不依赖于线程是否能被报警。如果APC是一个特殊的内核模式APC,假若IRQL<APC_LEVEL,那么APC被传送。如果APC是一个正常的内核模式APC,假如下列的三个条件成立,那么APC被传送:(1)IRQL<APC_LEVEL;(2)没有内核模式APC是在处理中的;(3)线程不是在一个临界区中。 注意,不同于KeWaitXxx例程,程序KeDelayExecutionThread不获得Object参数。 KeDelayExecutionThread在与调用线程有关的调度者对象上开始等待。在调度者对象上等待的线程响应警报和APC传送的方式与在其他调度者对象上等待的线程相同。 回调对象 内核的回调机制为驱动程序提供一种一般方法使得当一定的条件被满足时,驱动程序请求和提供通知。 驱动程序能创建回调对象,而其他驱动程序能为与这个驱动程序定义的回调有关的条件请求通知。此外,系统定义两个回调对象供驱动程序使用。 所有的回调对象拥有名称和一组属性,它们在对象被创建时候被定义。系统定义的回调对象被命名为\Callback\SetSystemTime和\Callback\TowerState;驱动程序定义的回调不可复制这些名称。细节参见“系统定义的回调对象”。 为了从系统或者驱动程序定义的回调请求通知,驱动程序打开回调对象并且注册回调例程。当为回调定义的条件变成TRUE,其创建者触发通知。反过来,系统调用所有为回调注册的回调例程。 定义回调对象 驱动程序能创建回调对象,通过回调对象,其他驱动程序能请求由创建驱动程序定义的条件的通知。图3.26显示了与定义回调对象有关的步骤。 图3.26 定义回调对象 在创建对象之前,驱动程序调用InitializeObjectAttributes以设置其属性。回调对象必须有名称,它不能与系统定义的回调名称相同;它可以拥有其创建者认为适当的任何属性,典型地有OBJ_CASE_INSENSITIVE。其次,驱动程序调用ExCreateCallback,传递指向初始化的属性和接收句柄位置的指针到回调对象。它也传递两个布尔值,表明如果这样的一个命名的对象还不存在,系统是否应该创建回调对象,以及对象是否应该允许多个注册的回调例程。 驱动程序为它将要调用的那个注册的回调例程定义条件。条件的形式是两个变元,每个指向创建回调的驱动程序所定义的一个参数。你应该为驱动程序的客户说明这些条件,连同回调对象的名称和它在其上请求通知的IRQL。 当回调条件发生,驱动程序调用ExNotifyCallback,把其句柄传递到回调对象和两个变元。 系统按照回调例程被注册的次序调用所有为回调对象注册的回调例程,传递两个变元和指向在例程被注册时提供的上下文的指针。驱动程序必须在IRQL<=DISPATCH_LEVEL时调用ExNotifyCallback;系统在与它在其上作这个调用的相同的IRQL上调用回调例程。 所有带有回调对象的操作被完成后,创建回调的驱动程序应该调用ObDereferenceObject以减少它的引用计数,并且确保这个对象被删除。 使用驱动程序定义的回调对象 为了使用另一个驱动程序定义的回调对象,驱动程序打开这个对象,然后注册一个例程,这个例程在回调被触发时被调用,如图3.27所示。驱动程序请求通知必须知道回调对象的名称,并且必须理解传递到回调例程的变元的语义。 图3.27 为回调通知注册 在它能打开对象之前,驱动程序必须调用InitializeObjectAttributes以创建属性块,它指定了对象的名称。在它拥有指向属性块的指针之后,它调用ExCreateCallback,传递属性指针、在其中为回调接收句柄的位置、和值为FALSE的Create参数,这个参数表明它请求一个存在的回调对象。 驱动程序然后能使用返回的句柄调用ExRegisterCallback以注册其回调例程。 回调例程拥有以下原型: typedef VOID (*PCALLBACK_FUNCTION) ( IN PVOID CallbackContext, IN PVOID Argument1, IN PVOID Argument2 ); 每次回调例程被调用时,CallbackContext参数是被传递到回调例程的上下文。除非上下文仅仅是32位的数据,如果这个例程能被在DISPATCH_LEVEL调用,调用者应该从非页式存储池中分配它。两个变元被创建回调的组件定义。典型地,变元提供有关触发回调的条件的信息。 当回调的创造者触发通知,系统调用注册的例程,传递指向上下文的指针和两个变元。变元的值由创建回调的组件提供。回调例程在与创建驱动程序在其上触发的相同IRQL上被调用,通常是IRQL<=DISPATCH_LEVEL。 在其回调例程中,驱动程序能执行它为当前条件请求的任何任务。 当驱动程序不再请求通知,它应该调用ExUnregisterCallback以从注册的回调列表中移出它的例程,并且移走它的到回调对象的引用。 使用系统定义的回调对象 系统为驱动程序使用定义两个回调对象: \Callback\SetSystemTime \Callback\PowerState 使用系统时间(例如,文件系统驱动程序)的驱动程序可以为SetSystemTime回调注册。当系统时间变化,这个回调规定通知。 当下例情况之一发生,PowerState回调规定通知: 系统从AC到DC电源的切换,或者反之亦然。 由于用户或者应用请求而造成的系统电源策略变化。 即将到来的到系统睡眠或者关闭状态的转换。驱动程序能请求通知,这样它能把代码锁到内存中期待着中断。回调例程将在电源管理器发送系统设置电源IRP之前被通知。 为了使用系统定义的回调,驱动程序使用回调的名称初始化属性块(InitializeObjectAttributes),然后打开回调对象(ExCreateCaIlback),这个对象正好就是给驱动程序定义的回调的。驱动程序不应该请求回调对象被创建。 使用被ExCreateCallback返回的句柄,驱动程序调用ExRegisterCallback以注册通知例程,传递一个指向专用环境的指针和指向它的例程的指针。驱动程序能在任何时间注册其回调例程。当指定的条件发生,系统在IRQL<=DISPATCH_LEVEL调用注册的回调例程。 当驱动程序不再请求通知,它应该调用ExUnregisterCallback以从注册的回调列表中删除它的回调例程,并且移走对回调对象的引用。 驱动程序基本结构 本章概述了Microsoft Windows 2000 和WDM驱动程序的标准例程,以使你可以明确以下内容: 任何处理IRP的驱动程序所必需的标准例程 一个驱动程序可能需要的其他标准例程,这取决于下层设备的属性或该驱动程序的必要功能 通常是如何开始设计和开发一个新的驱动程序 一些驱动程序与系统提供的,定义了大量驱动程序必要功能的端口驱动程序或类驱动程序互相作用。例如,一个SCSI微端口驱动程序主要与SCSI端口驱动程序相互作用。对于这类驱动程序,详见必要和可选的驱动程序支持的类特定的文档。 本章包含以下信息: 4.1 标准驱动程序例程 4.2 最低层设备驱动程序的分段IRP处理 4.3 中间层设备驱动程序的分段IRP处理 4.4 设计和开发一个驱动程序 4.4.1 设备对象命名和设备接口注册 4.4.2驱动程序例程命名 4.4.3 开始设计 4.4.4 开始开发 关于每个标准驱动程序例程必要功能的具体内容,见第5章到第15章。关于任何本章中提及的系统提供支持的例程,见《Windows 2000 驱动程序开发指南》第2卷。 标准驱动程序例程 一个Microsoft Windows 2000 或WDM 驱动程序的基本结构包括一组必要的,系统定义的标准驱动程序例程,加上一些可选的标准例程与内部例程,这取决于驱动程序的类型和下层设备。公共标准例程组允许所有的内核模式驱动程序通过调用一个系统提供支持的例程来处理IRP。 图4.1显示一个驱动程序对象,表示一个驱动程序装入内存的情况,及内核模式驱动程序的标准例程组。 图4.1 驱动程序对象与标准驱动程序例程 如图4.1所示,所有的驱动程序,不管它们在附属驱动程序链中所处的层,都必须有一组基本标准例程以处理IRP。(那些当它们被调用时传送一个IRP的例程被用一个星号标出。)如图所示,这些例程的入口点在驱动程序对象中设定。一个驱动程序是否必须执行附加标准例程取决于该驱动程序是控制一个物理设备的驱动程序,还是在一个物理设备驱动程序之上的驱动程序,也取决于下层物理设备的属性。控制物理设备的最低层驱动程序比较高层驱动程序拥有更多要求的例程,较高层驱动程序一般传送IRP给较低层驱动程序处理。 表4.1列出了标准驱动程序例程,概述了它们的目的,以及每个例程在那些驱动程序中被需要。 例程 哪些驱动程序需要? 任务  DriveEntry 所有驱动程序 初始化驱动程序并设置其他标准例程的入口点  AddDevice 所有驱动程序 创建设备对象  Dispatch 所有驱动程序必须至少有一个 用一个或多个主要功能编码处理IRP  StartIo or Queue Management 对于在Dispatch例程中不能完成所有I/O请求的最低层设备驱动程序是必需的。对于更高层驱动程序是可选的 为驱动程序管理IRP队列  Reinitialize 对所有驱动程序可选 在其他驱动程序装入之后完成附加的初始化任务  InterruptServiceRoutine(ISR) 对于任何产生中断的最低层设备驱动程序是必需的 响应来自一个物理设备的中断  DpcForIsr or CustomDpc 对于任何包含ISR的驱动程序是必需的 在ISR返回后处理中断  SynchCritSection 对于任何与它的ISR或其他例程共享数据的驱动程序是必需的 同步访问共享数据  AdapterControl 对于完成DMA的驱动程序是可选的 完成DMA转换  ControllerControl 对于使用物理控制器的Windows 2000 驱动程序是可选的 管理物理控制器  Cancel 对于任何在一段不确定的时间段中对IRP排队的驱动程序是必需的。通常每个栈中的最高层驱动程序至少有一个撤销例程。 在一个IRP被撤消后清除它  IoCompletion 对任何分配与发送IRP的驱动程序必需。 释放驱动程序分配的IRP,并且执行任何其他任务  IoTimer 对任何驱动程序可选 以一个定长的、一秒间隔周期执行某个任务。  CustomTimerDpc 对任何驱动程序可选 以一个短于一秒或可变的时间间隔周期执行某个任务。  Unload 任何在系统继续运行时能够被卸载的驱动程序 清除以使驱动程序能够被卸载。   当前IRP及目标驱动程序对象是许多标准例程的输入参数。所有驱动程序通过其标准例程组分段处理每个IRP。就象在“最低层设备驱动程序分段IRP处理”和“中间层设备驱动程序分段IRP处理”中所描述的。 最低层设备驱动程序的分段IRP处理 如图4.1所示,最低层物理设备驱动程序拥有较高层驱动程序不需要的某些标准例程。这组最低层驱动程序标准例程也根据下列标准不同而不同: 每个驱动程序控制的设备的属性 驱动程序是否为其设备对象设置直接或缓冲I/O,如同第三章所描述的。 单个驱动程序的设计 为说明标准驱动程序例程的作用。图4.2显示了一个样例IRP的路径,它由一个最低层海量存储设备驱动程序处理。图中的驱动程序具有如下特点: 设备在每次I/O操作结束时产生中断。因此这个驱动程序有ISR和DpcForIsr例程。 该驱动程序拥有一个StartIo例程,而不是建立内部IRP队列并管理自己的队列。 这个驱动程序使用系统DMA,因此它为直接I/O设置其设备对象标志,如同第三章所描述的,并且它拥有一个AdapterControl例程。 图4.2 通过最低层驱动程序例程的IRP路径 如图4.2所示,对于给定的主功能码(IRP_MJ_XXX),一个IRP首先被发送给驱动程序的Dispatch例程,在这个例子中是DDDispatchReadWrite。因为在驱动程序初始化时,每个驱动程序在驱动程序对象中基于IRP_MJ_XXX设置其Dispatch例程,所以操作系统从来不会向一个驱动程序发送有未知功能码的IRP。 调用IoGetCurrentIrpStackLocation 处理多于一个IRP_MJ_XXX,用次级功能(IRP_MN_XXX)处理IRP_MJ_XXX,或者处理设备I/O控制请求(IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL)的Dispatch例程,及所有其他处理每个IRP的驱动程序例程必须调用IoGetCurrentIrpStackLocation以决定做什么及使用什么参数。 图4.2中的IRP请求一个数据传输操作(IRP_MJ_READ或者IRP_MJ_WRITE),并且这个驱动程序的I/O栈位置在IRP中最低,如图有不确定个较高层驱动程序的I/O栈位置在它上面。为简单起见,从DispatchReadWrite、StartIo、AdapterControl和DpcForIsr例程中调用IoGetCurrentIrpStackLocation没有在图4.2中显示。 调用IoMarkIrpPending和IoStackPacket 样例驱动程序不是在其Dispatch例程中完成IRP,而是在其StartIo例程中处理IRP。在它这样做之前,Dispatch例程调用IoMarkIrpPending以表明该IRP并没有完成。然后它调IoStartPacket将该IRP排队或传送该IRP给驱动程序的StartIo例程以便进一步处理。Dispatch例程也返回NTSTATUS值STATUS_PENDING。 图4.3 调用IoStartPacket 如果驱动程序正忙于在设备上处理其他IRP,IoStartPacket把该IRP插入到与设备对象相连的设备队列中。驱动程序可以选择提供一个Key值作为参数给IoStartPacket,以将驱动程序确定的命令作用于设备队列中的IRP。 如果驱动程序不忙,而且设备队列空,I/O管理器立即调用其StartIo例程,传送输入IRP。 当它因为以下两个原因调用IoStartPacket时,一个海量存储设备的最低层驱动程序不需要提供一个Cancel例程: 一个在这类驱动程序上分层的文件系统处理文件I/O请求的撤消。 海量存储设备驱动程序迅速处理IRP。 通常,在一系列分层的驱动程序中的最高层驱动程序处理IRP的撤消。 调用AllocateAdapterChannel和MapTransfer 假定图4.2中所示的StartIo例程发现传输请求可以由一个单独DMA操作来做,StartIo例程用驱动程序的AdapterControl例程入口点和IRP调用AllocateAdapterChannel。 当系统DMA控制器可用时,I/O管理器调用驱动程序的AdapterControl例程设置传输操作,如图4.2所示。AdapterControl例程调用MapTransfer设置系统DMA控制器。这时驱动程序为DMA操作对其设备编程并返回。(关于使用DMA和适配器对象的更详细的信息,见第3章。) 从驱动程序的ISR调用IoRequestDpc 当设备中断表明其传输操作完成时,驱动程序的ISR从产生中断处终止设备并调用IoRequestDpc,如图4.2所示。 这个调用对驱动程序的DpcForIsr例程排队,以在一个较低的硬件优先级(IRQL)上完成尽可能多的传输操作。 调用IoStartNextPacket和IoCompleteRequest 当DpcForIsr例程已经进行传输处理时,它迅速调用IoStartNextPacket以使驱动程序的StartIo例程被设备队列中下一个IRP调用,如果排队的话。DpcForIsr例程也设置刚完成的IRP的I/O状态块并用IRP调用IoCompleteRequest。 图4.4说明驱动程序对IoStartNextPacket和IoCompleteRequest的调用。 驱动程序应当调用IoStartNextPacket或IoStartNextPacketByKey,以尽可能快的开始下一个请求的I/O操作,这个调用最好在它们调用IoCompleteRequest之前,并且必须在它们返回控制之前做。 图4.4 调用IoStartNextPacket和IoCompleteRequest 如果IRP 为设备排队,IoStartNextPacket调用KeRemoveDeviceQueue将下一个IRP从队列中删除。这时I/O管理器调用驱动程序的StartIo例程,传送从队列中取出的IRP。如果当前设备队列中没有IRP,IoStartNextPacket只是返回到调用它的例程。 在一个IRP中设置I/O状态块 调用IoCompleteRequest之前,所有最低层驱动程序必须在IRP中设置I/O状态块(在图中,第二个共享区域表示状态块)。I/O状态块为任何较高层驱动程序提供信息,最终提供给该I/O操作的最初请求者。任何在图4.4中的驱动程序之上分层的较高层驱动程序都创建一个IoComplete例程以读取由该设备驱动程序设置的I/O状态块。当然,较高层驱动程序通常并不改变一个已由设备驱动程序完成的IRP的I/O状态块,除非这个较高层驱动程序正重试这个IRP,并且因此重新初始化I/O状态块。 所有完成一个IRP,而不是把它发送到相邻较低层驱动程序的较高层驱动程序也必须在调用IoCompleteRequest之前设置该IRP的I/O状态块。为了良好的I/O流量,一个较高层驱动程序应检查其每个IRP的I/O栈位置中的参数,并且,如果这个参数是无效的,应自己设置I/O状态块并完成该请求。无论何时,一个驱动程序应尽可能避免传送一个无效请求给驱动程序链中的较低层驱动程序。 I/O状态块被定义如下: typedef struct _IO_STATUS_BLOCK { NTSTATUS Status; ULONG Information; } IO_STATUS_BLOCK, *PIO_STATUS_BLOCK; 假定图4.4中的传输操作成功,图4.2中所示的DpcForIsr例程设置该IRP的I/O状态块中的Status为STATUS_SUCCESS,Information为传输的字节数。 很多标准驱动程序例程也返回NTSTATUS类型的值。关于象STATUS_SUCCESS这样的NTSTATUS常量的更多信息,见第16章的“错误记录和NTSTATUS值”。 在调用IoCompleteRequest时提高优先级 如果一个最低层驱动程序可以在其Dispatch例程中完成一个IRP,它用一个值为IO_NO_INCREMENT的PriorityBoost调用CompleteRequest。不需要在运行时提高优先级,因为驱动程序可以假定初始的请求者并不等待它的I/O操作。 否则,最低层驱动程序提供一个系统定义的,设备类型特定的值提高请求者的运行时优先级,以减少请求者等待其设备I/O请求所花的时间。关于这个提高值,见wdm.h或ntddk.h。 当它们用一个IRP调用IoCompleteRequest时,较高层驱动程序使用相同的PrioritBoost作为其各自的下层设备驱动程序。 调用IoCompleteRequest的结果 当驱动程序用一个IRP调用IoCompleteReqest时,I/O管理器在调用相邻较高层驱动程序之前将该驱动程序的I/O栈位置清零,这个较高层驱动程序创建一个IoCompletion例程为该IRP调用。 一个较高层驱动程序的IoCompletion例程仅能检查该IRP的I/O状态块以决定如何处理所有较低层驱动程序的请求。 IoCompleteRequest的调用者切不可试图访问刚刚完成的IRP。这种企图是一种会导致系统崩溃的程序性错误。 中间层驱动程序的分段IRP处理 如图4.1所示,与最低层设备驱动程序相比,较高层驱动程序有一组不同的标准例程,并有一个对两种类型的驱动程序都是公共的重叠的标准例程子集。 这组中间层和最高层驱动程序的例程也依下列标准的不同而变化: 下层物理设备的属性 下层物理设备是否为直接或缓冲I/O设置设备对象 单个较高层驱动程序的设计 图4.5举例说明一个IRP通过在一个前一部分所描述的最低层设备驱动程序上分层的中间层镜像驱动程序的路径。 图4.5 通过中间层驱动程序例程的IRP路径 图4.5所示的驱动程序有以下特点: 驱动程序在至少一个物理设备和可能至少一个设备驱动程序上分层。 驱动程序有时为较低层驱动程序分配附加IRP,这取决于输入IRP的请求操作。 驱动程序有至少一个文件系统驱动程序在其上分层,并且该文件系统驱动程序可以在比这一驱动程序更高的其他中间层驱动程序上分层。 如图4.5所示,I/O管理器首先发送一个IRP_MJ_WRITE请求给中间层驱动程序的Dispatch例程。这个IRP请求一个数据传输操作(IRP_MJ_WRITE)。该中间层驱动程序的I/O栈位置在中间显示,有不确定个较高层或较低层驱动程序的I/O栈位置在它上面或下面。 分配IRP 这个驱动程序向另一个物理设备映射写请求,而向拥有镜像分区的设备的驱动程序交替发送读请求。对于写请求,它为原始设备与数据被镜像的一个二级设备创建IRP,假定在输入IRP中的参数是有效的。 图4.5恰好显示一个对IoAllocateIrp的调用,但是较高层驱动程序可以调用其他支持例程为它们发给较低层驱动程序的请求分配IRP。关于分配IRP的更多信息可参见第6章。 当Dispatch例程调用IoAllocateIrp时,它指定每个新IRP中需要的I/O栈位置数量。驱动程序必须为链中每个较低层驱动程序指定一个栈位置,从每个位于镜像驱动程序之下的设备对象获取适当的值。根据驱动程序开发者的意愿,当它调用IoAllocateIrp以获取一个自己的栈位置(它在每个IRP中分配的)时,它可以为这个值加一,如同图4.5中的驱动程序所做的那样。 这个中间层驱动程序的Dispatch例程用一个初始IRP调用IoGetCurrentIrpStackLocation(不显示)以决定它应当做什么,如果可能的话,还检查参数。 因为它给每个新创建的IRP分配它自己的栈位置,所以调用IoSetNextIrpStackLocation和IoGetCurrentIrpStackLocation为它自己创建它随后要在IoCompetion例程中使用的环境。 接着,它用一个新创建的IRP调用IoGetNextIrpStackLocation以便它可以在它分配的IRP中设置相邻较低层驱动程序的I/O栈位置。镜像驱动程序的Dispatch例程拷贝IRP功能码与参数(指向传输缓存的指针,表示将为IRP_MJ_WRITE传输的比特长度)到相邻较低层驱动程序的I/O栈位置。这些驱动程序,依次为它们下层的驱动程序设置I/O栈位置。 调用IoSetCompletionRoutine和IoCallDriver 图4.5中的Dispatch例程用它分配的任一IRP调用IoSetCompletionRoutine。 一个较高层驱动程序可以在较低层驱动程序用STATUS_SUCCESS、STATUS_CANCELLED或错误状态中的一个、一些或任意一个完成一个IRP时请求其被调用的IoCompletion例程。由于图4.5中的驱动程序必须释放它所分配的IRP,该驱动程序在较低层驱动程序完成它的IRP时设置IoCompletion例程以供调用,而不管它们的完成状态值是什么。 由于图4.5中的驱动程序并行映射,它传送通过两次调用IoCallDriver分配给相邻较低层驱动程序的两个IRP。每次每个目标设备对象表示一个镜像的分区。 在驱动程序的IoCompletion例程中处理IRP 当两组较低层驱动程序完成请求的操作时,I/O管理器调用中间层镜像驱动程序的IoCompletion例程。当较低层驱动程序已完成所有DupIrpN请求时,镜像驱动程序在其自身初始IRP的I/O栈位置中维护一个计数值以跟踪它。 假定I/O栈位置表明一组较低层驱动程序已成功完成图4.5中所示的DupIRP1,镜像驱动程序的IoCompletion例程减小其计数,但是不能完成初始IRP,除非它将值减到零。如果减后的值不是零,IoCompletion例程用第一次返回的驱动程序分配的IRP(图4.5中的DupIRP1)调用IoFreeIrp,并返回STATUS_MORE_PROCESSING_REQUIRED。 当镜像驱动程序的IoCompletion例程被图4.5中的DupIRP2再次调用时,IoCompletion例程减小初始IRP中的计数值,并确定两组较低层驱动程序已执行的请求操作。 假定DupIRP2中的I/O状态块也被设成STATUS_SUCCESS,IoCompletion例程从DupIRP2拷贝I/O状态块到初始IRP并释放DupIRP2。它用初始IRP调用IoCompleteRequest并返回STATUS_MORE_PROCESSING_REQUIRED。返回这个状态防止I/O管理器尝试任何DupIRP2上进一步的完成处理;由于IRP没有与线程相连,它的完成处理应由创建它的驱动程序终止。 如果任一组较低层驱动程序没有成功完成镜像驱动程序的IRP,镜像驱动程序的IoCompletion例程应记录错误并尝试适当的镜像数据恢复。更多信息,见第16章的“错误记录和NTSTATUS值”。 设计和开发一个驱动程序 以下部分提供了关于开始设计和开发过程的一般建议。 设备对象命名和设备接口注册 用户模式的可以直接I/O请求的物理、逻辑或虚拟设备驱动程序必须为其用户模式客户提供某种名字。当然,PnP驱动程序并不采用Windows NT 4.0及其更早版本中的驱动程序那样的方式为设备对象指定名字。相反,对于每个这类I/O请求可能发送的设备对象,PnP驱动程序注册并激活一个与GUID连接的设备接口。 下面是设备驱动程序注册一个接口必须遵循的步骤: 当PnP总线驱动程序或其他最低层驱动程序调用IoCreateDevice创建PDO时,它应为设备名传送FILE_AUTOGENERATED_DEVICE_NAME和FILE_DEVICE_SECURE_OPEN标志,连同一个NULL串。作为响应,Windows为PDO选择一个唯一的设备名。 当中间层或较高层驱动程序(PnP功能与选择驱动程序)调用IoCreateDevice时,它们也传送FILE_DEVICE_SECURE_OPEN标志。这些驱动程序禁止给它们的设备对象命名。 创建设备对象并把它放入设备栈后,驱动程序调用IoRegisterDeviceInterface,传送一个标识被注册的接口的设备接口类GUID。显然,功能驱动程序从其AddDevice例程产生该调用。系统在设备特定的头文件中为公共设备接口定义GUIDs。 I/O管理器为设备接口创建一个注册键并返回一个符号链接到接口。 如果驱动程序能成功启动设备,它通过调用IoSetDeviceInterfaceState激活接口,传送符号链接。这个调用可以象驱动程序处理PnP IRP_MN_START_DEVICE请求那样做。 当一个应用程序或终端用户调用一个Win32函数连接一台打印机的端口,如LPT1,Win32子系统将应用程序I/O请求解释为一个对I/O管理器系统服务的调用,传送表示该打印机端口的象征性连接。 为驱动程序例程选择名字 除了DriverEntry例程,本文中的标准驱动程序例程名被用来描述每种类型标准例程的功能,并且有时候提供带有特定标准例程的驱动程序必须使用的何种类型对象的提示。在驱动程序中,你可以为大多数标准驱动程序例程起你喜欢的任何名字。 当然,使用一套区别于其他驱动程序例程命名机制的驱动程序会更容易开发、调试和测试。如果采用区别于驱动程序的标准例程及其内部例程的名字,一个驱动程序也更容易调试与测试。 因此,大多数系统提供的驱动程序采用遵循以下方针的例程名: 为一个所有例程预先计划一个驱动程序特定前缀,最好所有标准例程用一个前缀,而另一个是所有内部例程的前缀。 为每个例程创建一个表明它的功能以及调试时驱动程序中一个给定的IRP被处理的地方的名字。 例如,系统键盘类驱动程序预先定义KeyboardClass为它的标准驱动程序例程,Kbd为它的内部例程。另一个系统驱动程序使用一个单独定义的前缀给所有例程,但是预先定义小写字母“p”(私有的)作为它们的内部例程的前缀。 系统键盘类驱动程序中的每个例程也有一个表明该例程的功能的名字。该驱动程序的Dispatch例程根据每个例程控制的IRP_MJ_XXX命名:KeyboardClassOpenClose、KeyboardClassRead、KeyboardClassDeviceControl等等。该驱动程序中的其他标准例程命名为类似KeyboardClassStartIo和KeyboardClassCancel,以支持驱动程序开发者跟踪通过该驱动程序的IRP的路径。 键盘类驱动程序的命名约定也支持系统测试工程师评价该驱动程序的性能,以便发现任一特定驱动程序例程是否是一个性能瓶颈,并且如果错误发生时发现“bug”的准确位置。 开始设计 下列建议是一组一般的、可以用来开始设计一个驱动程序的设计标准: 哪个I/O请求? 编写任何代码之前,看一下DDK中关于该设备类型的文件以决定那个IRP主功能码是你的驱动程序必须处理的: 如果要设计一个最低层物理设备驱动程序,那么驱动程序必须处理与同类型外设的所有其他驱动程序相同的一组IRP_MJ_XXX和与设备I/O控制码。 如果要设计一个中间层驱动程序,识别下层物理设备或驱动程序将在其上被分层的设备。一个较高层驱动程序必须拥有其下的驱动程序必须处理的大多数IRP_MJ_XXX的Dispatch入口点。一个较高层驱动程序必须设置相邻较低层驱动程序在IRP中的I/O栈位置,并且当每个这样的IRP自己的I/O栈位置中的参数有效时,用IoCallDriver传送它们给较低层驱动程序。因此,任何中间层驱动程序必须在它的驱动程序对象中为那些将要传送的IRP_MJ_XXX设置Dispatch入口点。 所有驱动程序必须为IRP_MJ_PNP与IRP_MJ_POWER提供Dispatch入口点。这些调度例程必须准备用这两个主IRP码处理IRP或者把它们传送给较低层驱动程序。 多少个Dispatch例程? 当已经识别了驱动程序必须处理的IRP_MJ_XXX时,可以决定驱动程序可能拥有的Dispatch例程数量的上限。也可以开始考虑是否将特定IRP_MJ_XXX加进由特定Dispatch例程处理的邻近子集中。 例如,大多数最低层与中间层驱动程序需要为一个IRP_MJ_CREATE(对于这些类型的驱动程序来说,相当于一个“打开目标设备”请求)或IRP_MJ_CLOSE请求做很少,或什么也不做,或IoCompletion处理。除了带可分页映像分区的下层磁盘设备和驱动程序,包含IRP_MJ_CREATE的IRP通常仅仅通过I/O管理器的系统服务为较高层驱动程序和用户模式保护子系统建立目标设备对象。对于较低层驱动程序,包含IRP_MJ_CLOSE的IRP通常表明一个用户模式子系统(代表一个应用程序)已关闭了为它发送I/O请求的设备处理的文件对象。 对于创建/关闭请求,许多最低层设备及中间层设备简单设置IRP的I/O状态块中的Status成员为STATUS_SUCCESS和Information成员为零,接着用来自于它们的Dispatch例程的IRP调用IoCompleteRequest。因此,对于创建/关闭请求,许多最低层及中间层驱动程序有一个合成的Dispatch例程。 当然,一个最低层串行设备驱动程序通常为一个创建请求重新设置它的设备;它可以在处理创建请求时锁定一个可分页的映像段,并在处理关闭请求时解锁它的可分页的映像段。最低层磁盘设备驱动程序仅在一个较高层驱动程序调用IoAttachDeviceToDeviceStack、IoAttachDevice或IoGetDeviceObjectPointer时被创建请求调用;由于任何磁盘驱动程序都可以控制处理系统页面文件的设备,它们不用关闭请求调用。另一方面,文件系统驱动程序为创建/关闭请求进行相当多的处理。 如果要设计一个较高层驱动程序,考虑你的驱动程序必须处理的IRP主功能码组,并决定需要用来执行一个IoCompletion例程的请求组。 一般,一个较高层驱动程序不必对每种请求都有一个IoCompletion例程。它必须有一个IoCompletion例程来释放驱动程序分配的IRP,如果有的话。否则,仅仅那些要求在较高层驱动程序中做进一步处理的IRP_MJ_XXX需要IoCompletion例程,这取决于较低层驱动程序如何处理IRP_MJ_XXX请求。 多少个设备对象和存储在设备扩展中的是什么? 紧接着,考虑驱动程序必须创建的设备对象: 最低层驱动程序必须为每个可能成为I/O请求目标的物理或逻辑设备创建设备对象。例如,一个磁盘驱动程序必须为每个物理磁盘创建设备对象并且为该磁盘上的分区创建附加设备对象。 一个较高层驱动程序必须创建表示其自身的虚拟设备的设备对象,以使更高层驱动程序可以连接它们的设备对象到该驱动程序的设备对象。另外,一个较高层驱动程序通常创建一组与相邻较低层驱动程序创建的设备对象对应的虚拟或逻辑设备对象。 如同在“开始开发”中概述的,可以分阶段开发驱动程序,以使一个未开发的驱动程序不需要创建完全开发的驱动程序拥有的所有设备对象。 开发者还需要确定驱动程序的设备对象的设备扩展的内容与结构。例如,如果设备扩展包含与较低层设备驱动程序的ISR及它的其他例程共享的环境,考虑设置设备扩展以隔离所有该设备扩展部分所共享的环境。然后,驱动程序的非ISR例程可以访问该设备扩展中的其他区域,而不必用SynchCritSection例程调用KeSynchronizeExcution。 又例如,如果一个非ISR例程与另一个例程共享一块区域,驱动程序设备扩展将包含一个执行自旋锁。该自旋锁保护共享区域以防两个例程同时访问。更多信息,见第16章的“自旋锁的使用” 驱动程序线程环境中的StartIo例程或队列管理? 如果为一个物理设备编写驱动程序,确定由于设备状态改变,设备I/O设置是否要求等待超过50微秒: 假如这样,驱动程序应当拥有一个设备专用线程,或者在一个文件系统驱动程序中,可能有一个或多个工作者线程回调进程。这样的驱动程序必须管理其自己的内部IRP队列。 如果不是,驱动程序可以拥有一个StartIo例程,并可以在其Dispatch例程中调用IoStartPacket,在其DpcForIsr(或CustomDpc)例程中调用IoStartNextPacket或IoStartNextPacketByKey,如“最低层设备驱动程序的分段IRP处理”中所描述的。如果这样,该驱动程序依靠I/O管理器管理IRP队列。 开始开发 开发分阶段编码的驱动程序更容易些,所以为了建立一个基本PnP功能驱动程序的框架并使它很快运行,这是一本“秘诀”。注意,过滤器驱动程序与总线驱动程序有略微不同的要求。另外,本DDK中包含多种驱动程序样例代码,在开发过程中你可以发现它们很有用处。 装载一个基本功能驱动程序 编写一个为AddDevice、DispatchPnP、DispatchPower和DispatchCreate例程设置入口点的DriverEntry例程。 编写一个完成下面内容的AddDevice例程: 调用IoCreateDevice创建一个独立设备对象(如何设置一个设备对象见第3章)。 调用IoAttachDeviceToDeviceStack把它自己加入设备栈。 调用IoRegisterDeviceInterface为它的设备暴露一个接口。暴露的接口为访问该设备的用户模式应用程序提供了途径。 为其类型的设备执行任何其他必要动作。 为IRP_MJ_PNP请求编写一个基本DispatchPnP例程。该DispatchPnP例程必须准备处理具体的PnP IRP,例如IRP_MN_START_DEVICE请求。当驱动程序处理启动的IRP,它必须调用IoSetDeviceInterfaceState以激活它先前注册的接口。关于该发送例程至少要做的工作,见《即插即用、电源管理和安装设计指南》。 为IRP_MJ_POWER编写一个基本DispatchPower例程。关于该发送例程至少要做的工作的描述,见《即插即用、电源管理和安装设计指南》。 接下来,为IRP_MJ_CREATE请求编写一个基本DispatchCreate例程。关于DispatchCreate例程至少要做的工作的描述,见“开始设计”。对于任何请求,一个较高层驱动程序的AddDevice例程必须建立一个到相邻较低层驱动程序的链接,并且每个Dispatch例程必须在IRP中设置相邻较低层驱动程序的I/O栈位置。 编译并链接驱动程序。 编写一个基本INF文件,和其他必需的文件,安装与装载驱动程序。 使用控制面板上的“添加/删除硬件”来安装驱动程序。当进行到选择一个驱动程序时,点击“从磁盘安装”并提供编译及链接后的驱动程序路径。 用一个简单的测试程序检测该基本驱动程序。 测试程序应调用Win32函数CreateFile以确保驱动程序被装入,并且I/O管理器可以发送IRP给驱动程序的DispatchCreate例程。为确定在CreateFile调用中说明的正确名字,测试程序应调用SetupDi例程。 支持对更多目标设备对象的附加请求 修改DriverEntry例程为IRP_MJ_CLOSE请求设置一个Dispatch入口点。 注意,为IRP_MJ_CREATE请求工作的基本Dispatch例程通常可以处理对一个最低层或中间层驱动程序的IRP_MJ_CLOSE请求,因此是否需要编写一个单独的DispatchClose例程完全取决于开发者。 编译与链接修改后的驱动程序。 如前一部分所述,测试驱动程序以确定所有驱动程序的新设备对象可以用Win32 CreateFile与CloseHandle函数打开和关闭。 支持设备I/O请求 为要求比创建/关闭请求进行更多处理的IRP_MJ_XXX添加另一个Dispatch例程。 例如,编写一个从设备到内存读数据或截取一组较低层驱动程序读请求的DispatchRead例程,也就是说,处理IRP_MJ_READ请求。 修改DriverEntry例程以便在驱动程序对象中设置DispatchRead入口点。修改DispatchPnP例程中的IRP_MN_START_DEVICE处理以建立任何将要处理读请求的对象。编写必需处理读请求的任何附加驱动程序。 一个最低层驱动程序至少需要StartIo、ISR、和DpcForIsr例程,还可能需要SynchCritSection例程,并且如果设备使用DMA,还需要基本AdapterControl例程。 DriverEntry例程必须在驱动程序对象中设置StartIo入口点,并设置驱动程序的其他新例程。当DispatchPnP例程处理IRP_MN_START_DEVICE请求时,应注册ISR(中断对象),为DMA设置一个适配器对象,并初始化物理设备。注意,ISR可以在从IoConnectInterrupt成功返回后立即被调用,所以连接中断应当是启动设备编码的最后步骤。否则,驱动程序需要在注册ISR之前使设备的中断失效(之后再启用设备的中断)。 一个较高层驱动程序拥有一个基本IoCompletion例程,这个例程至少检测I/O状态块是否为STATUS_SUCCESS,并用IRP调用IoCompleteRequest。 DriverEntry例程必须在驱动程序对象中设置DispatchRead入口点。DispatchRead例程必须调用IoSetCompletionRoutine以在IRP中设置驱动程序的新IoCompletion例程。 必要的话,修改设备扩展的内部结构: 对于一个最低层驱动程序,设备扩展应当存储中断对象指针,让ISR和StartIo或DpcForIsr例程共享环境区域,如果设备使用DMA,还要存储适配器对象信息。 对于一个较高层驱动程序,设备扩展必须存储相邻较低层驱动程序的设备对象指针。否则,它的内容和结构由驱动程序定义。 编译并链接修改的驱动程序。 通过调用Win32 CreateFile、ReadFile和CloseHandle函数,并通过使用内核调试器跟踪经过新驱动程序标准例程的IRP的路径,来测试这组例程。 继续通过编写新的Dispatch例程为驱动程序添加功能,并且在较高层驱动程序中,可能要编写新的IoCompletion例程来处理这组要求的IRP_MJ_XXX。按照要求修改DriverEntry例程及设备扩展(或其他驱动程序分配的存储)。如同添加新Dispatch例程一样,增加驱动程序的已存在例程,例如添加错误记录。在每个开发阶段测试驱动程序,并使用调试器跟踪通过驱动程序的IRP的路径以发现所有“bug”。 关于记录I/O错误的更多信息,见第16章的“错误记录和NTSTATUS值”。关于调试驱动程序的更多信息,见在线DDK的“Debugging Drivers”。 DriverEntry 和 Reinitialize 例程 本章概述了内核模式驱动程序的标准DriverEntry例程的必要功能。也概述了一个可选的Reinitialize例程的必要功能。 本章包含以下信息: 5.1 DriverEntry例程需求: 5.1.1 DriverEntry例程语法与参数 5.1.2 DriverEntry返回值 5.2 DriveEntry例程功能: 5.2.1 DriverEntry例程必备功能 5.2.2 DriverEntry例程可选功能 5.3 Reinitialize例程需求与功能 DriverEntry例程需求 每个驱动程序必须有一个DriverEntry例程以用来初始化驱动程序范围的数据结构和资源。当I/O管理器装入驱动程序时,它调用DriverEntry例程。对于一个支持“即插即用”(PnP)的驱动程序,就象所有的驱动程序将要支持一样,DriverEntry例程负责驱动程序的初始化,而AddDevice例程负责设备初始化(也可能是处理PnP IRP_MN_START_DEVICE请求的Dispatch例程)。驱动程序初始化包括输出该驱动程序其他入口点,初始化该驱动程序使用的特定对象,并设置每个驱动程序系统资源。(早期的驱动程序有差别较大的要求,就象Windows NT 4.0及其更早版本的DDK中所描述的那样。) DriverEntry例程语法与参数 I/O管理器定义DriveEntry例程原型如下: NTSTATUS (*PDRIVER_INITIALIZE) ( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath ); 假如驱动程序为链接器命名,该例程可以取一个不同于DriverEntry的名字。链接器必须有一个初始化例程的名字以便链接驱动程序,把地址传输给操作系统装入。通过对一个驱动程序初始化例程的命名,DriverEntry自动地建立这种链接。 就象在声明中所显示的那样,每个DriverEntry例程用两个输入指针被调用。DriverObject输入指针允许DriverEntry例程为它在驱动程序对象中的Dispatch、AddDevice、StartIo和Unload例程设置适当入口点。 DriverObject输入指针也使驱动程序可以访问DriverObject->HardwareDatabase,它指向一个指定注册表\Registry\Machine\Hardware内容树路径的Unicode字符串。 这个输入到DriverEntry例程的RegistryPath指向一个指定一条到驱动程序注册码\Registr\Machine\System\CurrentControlSet\Services\DriverName的路径的Unicode字符串。驱动程序可以用这个注册码装入驱动程序指定的信息和参数;具体细节可参见 《即插即用、电源管理和安装设计指南》第4部分第2章“注册表中的驱动程序信息”。假如一个驱动程序在它的DrivereEntry例程完成后需要使用这条路径,I/O管理器就会在DriverEntry完成后释放RegistryPath缓冲,从而使该指针失效。 DriverEnty返回值 DriverEntry例程返回一个NTSTATUS值,它不是STATUS_SUCCESS就是一个具体的错误信息。 DriverEntry例程在它返回STATUS_SUCCESS之前将延迟任何对IoRegisterDriverReinitialization的调用。如果它不返回STATUS_SUCCESS,就不必进行这种调用。 如果DriverEntry例程返回的不是STATUS_SUCCESS,那么驱动程序就不再被继续加载。 DriverEntry例程在它返回控制之前必须释放所有它已经安装的系统对象、系统资源和注册表资源,否则初始化将失败。如果驱动程序支持这些请求,它应该将驱动程序对象的IRP_MJ_FLUSH_BUFFERS重置为驱动程序Dispatch入口点,并且/或者将IRP_MJ_SHUTDOWN设为NULL。 如果驱动程序初始化失败,DriverEntry例程还将在返回控制之前记录一个错误信息。参见第16章的“错误记录与NTSTATUS值”。 DriverEntry例程功能 DriverEntry例程在以下阶段中初始化: 为需要与其设备通信的硬件配置信息(如果有)分配内存。 在驱动程序对象中设置驱动程序的Dispatch、AddDevice、StartIo(如果有)和Unload(如果有)入口点。 建立所有驱动程序对象或其他系统资源,例如自旋锁,使驱动程序可以用它们来处理I/O请求。 释放任何不再需要的已分配内存,可能调用IoRegisterDriverReinitialization,并返回一个适当的NTSTATUS值。 每一步的细节可参见“DriverEntry例程必备功能”与“DriverEntry例程可选功能”。 每个DriverEntry例程都运行在位于IRQL PASSIVE_LEVEL的系统线程环境中。因此,只要驱动程序不控制持有系统页文件的设备,任何由ExAllocatePool在初始化过程中分配的专用内存都能从页式存储池中取得。在DriverEntry返回控制前,分配的内存必须由ExFreePool释放。当然,设置Reinitialize例程的驱动程序可以在它调用IoRegisterDriverReinitialization的时候传送一个指针到内存,从而使驱动程序的Reinitialize例程负责释放已分配的内存。 DriverEntry例程必备功能 一个DriverEntry例程的任务如下: 为驱动程序的Dispatch、AddDevice、StartIo(如果有)和Unload(如果有)例程填入驱动程序对象入口点。 注册驱动程序的其他入口点。 创建或初始化各种驱动程序范围的对象,类型,或驱动程序使用的资源。注意大多数标准驱动程序例程基于设备使用对象,所以驱动程序应当在它们的AddDevice例程中或在接收到一个IRP_MNSTART_DEVICE请求后建立这种对象。 如果驱动程序有一个设备专用线程或等待任何内核定义的调度者对象,DriverEntry例程将初始化这个调度者对象的线程或者通过调用适当的KeInitializeXxx支持的例程使驱动程序等待,这些例程有一个指向事件、信号量、互斥体和定时器对象(由驱动程序为它们提供存储空间)的指针,这取决于驱动程序如何使用对象。它将在它的AddDevice例程中或者在接收一个IRP_MN_START_DEVICE请求后执行这个任务。 因为它在系统线程环境中执行,DriverEntry例程本身可以在一个调度者对象上等待一个非零中断,这个调度者对象必须在等待开始前被初始化。例如,请求到一个通讯端口驱动程序的类驱动程序在初始化过程中通常等待一个事件对象,它们通过IRP连接起来。它们由IoBuildSynchronousFsdRequest分配并用IoCallDriver传送到物理设备的端口驱动程序。 关于等待调度者对象的更多信息,参见第3章。 返回的NTSTATUS表明驱动程序是否成功装入并能接收和处理来自PnP管理器的配置,增加及启动其设备的请求。 如果驱动程序分配了不再需要的内存,它将在返回前释放这些内存。 DrierEntry可选功能 这取决于一系列分层的驱动程序中一个特定驱动程序的位置,取决于下层驱动程序的属性,以及取决于驱动程序的设计。DriverEntry例程也要负责以下任务: 如果一个驱动程序需要在一个驱动程序级基础上存储数据,其DriverEntry例程可以调用IoAllocateDriverObjectExtension来创建和初始化一个驱动程序对象扩展。这个驱动程序对象扩展是一个驱动程序特定的数据结构,与设备特定的设备对象扩展类似。例如,一个驱动程序可以用它的驱动程序对象扩展来存储一条注册路径或其他的各种信息。 如果一个最高层的驱动程序使用执行工作者线程,就象很多文件系统驱动程序所做的那样,它可以调用PsCreateSystemThread来创建该线程。另外,它必须有一个WORKER_THREAD_ROUTINE类型的回调例程,这个例程获得一个单独的PVOID输入变量。 如果一个驱动程序有一个可选的Reinitialize例程,当且仅当其DriverEntry例程返回STATUS_SUCCESS,该DriverEntry例程调用IoRegisterDriverReinitialization。一旦Reinitialize例程运行的话,该驱动程序可以调用IoRegisterDriverReinitialization。通常,一个运行Reinitialize例程的驱动程序是一个较高层驱动程序。参见“Reinitialize例程需求和功能”。 与一个端口驱动程序或类驱动程序协同工作的特定设备的微端口驱动程序或微类驱动程序有某些类特定的初始化需求,而与这儿讨论的这些不同。详件DDK中的设备类型特定的文挡。 为系统资源提供存储空间 每个驱动程序必须为一切内核定义的对象及其使用的执行自旋锁提供存储空间。它还必须为指向从HAL或I/O管理器获得的具体对象的指针提供存储空间,就象第3章中所解释的那样。 在Windows 2000和WDM驱动程序中,每个设备对象应在AddDevice例程或者在处理PnP IRP_MN_START_DEVICE请求的Dispatch例程中分配,而不是在DriverEntry例程中,就象在Windows NT 4.0及其更早的版本中那样。 当然,一个驱动程序可能需要为其他驱动程序范围的使用分配额外的系统存储空间。如果这样,DriverEntry例程可以调用一个(或多个)下列例程: IoAllocateDriverObjectExtension,创建一个与驱动程序对象连接的上下文区间。 ExAllocatePool,用于页式或非页式系统内存空间。 MmAllocateNonCachedMemory或MmAllocateContiguousMemory,用于分配给高速缓存的非页式系统内存空间(用于I/O缓冲区) 关于内存使用的更多信息,参见第16章。关于DMA公用缓冲区使用的更多信息,参见第3章。 声明硬件资源 尽管早期的驱动程序从注册表声明资源,但PnP驱动程序既不在注册表中声明设备资源,也不直接对注册表写请求。相反,这些驱动程序报告请求以响应某些PnP IRP,这是PnP管理器的列举处理的一部分。一个PnP驱动程序在一个PnP IRP_MN_START_DEVICE请求中接收它已分配资源。 不直接与PnP管理器相互作用的驱动程序(如微端口驱动程序)和直接与PnP管理器相互作用的类或端口驱动程序可能有不同的报告需求。这些请求对设备类是特定的。 一个DriverEntry例程可以使用注册表获得一些它需要的信息以初始化驱动程序。这个例程也在注册表中为其他驱动程序或者受保护的子系统设置信息以便使用。这些信息的属性取决于设备的类型。 关于设备和类的特定细节,参见DDK中相关设备的文档。 关于注册表使用及驱动程序安装的更多信息,参见《即插即用、电源管理和安装设计指南》和《Windows 2000驱动程序开发指南》第1卷。 Reinitialize例程需求与功能 Reinitialize例程由I/O管理器定义如下: VOID ( *PDRIVER_REINITIALIZE) ( IN PDRIVER_OBJECT DriverObject, IN PVOID Context, IN ULONG Count ); 就象DriverEntry例程那样,一个Reinitialize例程在IRQL_PASSIVE_LEVEL级系统线程环境中运行。 如果一个驱动程序不能在其DriverEntry例程中完全初始化自己,它可以在DriverEntry例程返回控制一段时间之后提供一个Reinitialize例程被调用。显然,这个Reinitialize例程执行另一个驱动程序启动后必须要做的任务。 例如,系统键盘类驱动程序,kbdclass,既支持PnP也支持早期的键盘端口。如果一个系统包含一个或多个PnP管理器不能检测到的早期端口,键盘类驱动程序必须为每个端口创建一个设备对象并且对该端口在较低层驱动程序之上划分层次。因此,类驱动程序在其DriverEntry例程和AddDevice例程被调用,及其他驱动程序被装入后,将调用一个Reinitialize例程。这个Reinitialize例程检测端口,为它创建一个设备对象,并对这个设备在其他较低层驱动程序之上划分驱动程序层次。 如果一个驱动程序有一个Reinitialize例程,它的DriverEntry例程通过调用IoRegisterDriverReinitialization一次使Reinitialize例程延迟运行。DriverEntry例程负责在它调用IoRegisterDriverReinitialization之前设置一切要传送给Reinitialize例程的环境数据。 如果其Reinitialize例程使用注册表,环境中的数据应包括注册表路径指针,它被传送到DriverEntry例程,因为这个指针不是Reinitialize例程的一个输入指针,就象它的声明中所显示的。 DriverEntry例程可以调用IoRegisterDriverReinitialization。如果Reinitialize例程在其他驱动程序调用的Reinitialize例程返回控制后再次运行,Reinitialize例程调用IoRegisterDriverReinitialization的次数可以等于驱动程序的Reinitialize例程应当运行的次数。传送到Reinitialize例程的计数器指示了这个例程被调用的次数,包括现行的调用。 注意,一个驱动程序的Reinitialize例程不应被注册,除非其DriverEntry例程返回STATUS_SUCCESS,表明驱动程序已被成功装入。 通常,一个带有Reinitialize例程的驱动程序是一个控制PnP与早期设备的较高层驱动程序。除了为这些PnP管理器检测到的设备创建设备对象(为此PnP管理器调用驱动程序的AddDevice例程),驱动程序也必须为PnP管理器检测不到的早期设备创建设备对象。Reinitialize例程创建那些设备对象并且在下层设备的相邻较低层驱动程序之上对驱动程序分层。 如果一个驱动程序有Reinitialize例程,它的初始化与“DriverEntry例程功能”中描述的基本步骤相同,它也与DriverEntry例程有相同的基本需求。 Dispatch例程 本章概述了驱动程序标准Dispatch例程的必需功能和一些实现Dispatch例程的最一般方针。它包含以下信息: 6.1 Dispatch例程需求 6.1.1 Dispatch例程一般实现 6.1.1 Dispatch例程条件实现 6.2 Dispatch例程基本功能 6.2.1 何时检查I/O栈位置 6.2.2 如何在一个Dispatch例程中完成一个IRP 6.2.3 何时在Dispatch例程中完成一个IRP 6.2.4 如何从一个Dispatch例程向IRP传送有效参数 6.2.5 为较低层驱动程序分配IRP 6.2.6 处理异步传输请求 6.3 设计Dispatch例程 6.3.1 DispatchCreate与DispatchClose功能 6.3.1.1 基本DispatchCreateClose例程 6.3.1.2实现DispatchCreateClose的注意事项 6.3.2 DispatchCleanup功能 6.3.3 DispatchRead或DispatchWrite功能 6.3.3.1 使用缓冲I/O的DispatchReadWrite 6.3.3.2 使用直接I/O的DispatchReadWrite 6.3.3.3 较高层驱动程序中的DispatchReadWrite 6.3.3.4 实现DispatchReadWrite的注意事项 6.3.4 Dispatch(Internal)DeviceControl功能 6.3.4.1 最低层驱动程序中的DispatchDeviceControl 6.3.4.2 较高层驱动程序中的DispatchDeviceControl 6.3.4.3 类/端口驱动程序中的Dispatch(Internal)DeviceControl 6.3.4.4 实现Dispatch(Internal)DeviceControl的注意事项 Dispatch例程需求 Dispatch例程由I/O管理器定义如下: NTSTATUS (*PDRIVER_DISPATCH) ( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp ); 在由DriverEntry例程设置的Dispatch例程中处理任何IRP首先处理已给定的主功能码(IRP_MJ_XXX),这个编码在每个到达的IRP的驱动程序的I/O栈位置中设置。 大多数驱动程序的Dispatch例程在一个位于IRQL PASSIVE_LEVEL的专用线程环境中被调用,除了下列情况: 任何最高层驱动程序的Dispatch例程在产生I/O请求的线程环境中被调用,它一般是一个用户模式应用线程。 换句话说,文件系统驱动程序及其他最高层驱动程序的Dispatch例程在一个位于IRQL PASSIVE_LEVEL的非专用线程环境中被调用。 系统页式路径中最低层设备驱动程序及其之上的中间层驱动程序的DispatchRead、DispatchWrite和DispatchDeviceControl例程,可以在IRQL APC_LEVEL及一个专用线程环境中被调用。 DispatchRead、DispatchWrite和也在这样一个最低层设备或者中间层设备驱动程序中处理读/写请求的其他例程必须任何时候都常驻内存。这些驱动程序例程既不能分页,也不能成为驱动程序的页式映像分区的一部分;它们不能访问任何页式内存。此外,它们不应当依赖于任何模块化的调用(例如带有一个非零超时的KeWaitForSingleObject)。更多信息见在线DDK。 在“冬眠”和/或页路径中的驱动程序的DispatchPower例程可在IRQL DISPATCH_LEVEL中被调用。这类驱动程序的DispatchPnP例程必须准备处理PnP IRP_MN_DEVICE_USAGE_NOTIFICATION请求。 请求在启动时加电的DispatchPower例程可以在lRQL DISPATCH_LEVEL中被调用。 驱动程序能有与IRP_MJ_XXX函数一样多的Dispatch例程为驱动程序控制编码。 Dispatch例程一般实现 通常,大多数驱动程序必须处理一些或者所有以下请求: LRP_MJ_PNP表明一个包括PnP设备识别的请求,硬件配置,或者资源分配的请求。这类请求显然从PnP管理器或者从一个与较高层驱动程序密切相关的设备发送到一个设备驱动程序。 关于这个例程的更多信息,参见《即插即用、电源管理和安装设计指南》 第l部分第2章的“DispatchPnP例程”。 IRP_MJ_POWER表明一个属于设备或者系统的电源状态请求。这类请求通过电源管理器或者一个密切连接的较高层驱动程序发送到设备驱动程序。关于这个例程的更多信息,参见《即插即用、电源管理和安装设计指南》第l部分第2章的“DispatchPower例程”。 IRP_MJ_CREATE表明用户模式保护子系统,可能代表应用或者特定子系统驱动程序,已请求一个与目标设备对象有关的文件对象的控制,或者一个较高层的驱动程序把其设备对象连接或者附加到目标设备对象。 IRP_MJ_CLEANUP表明,为代表目标设备对象的文件对象的处理被关闭,因此任何在清除IRP的驱动程序I/O栈位置中找到的,在给定文件对象目标设备的当前队列中的IRP, 应该被取消。 IRP_MJ_CLOSE表明代表目标设备对象的文件对象句柄或指向目标设备对象的指针已经被关闭,还表明没有未完成的、对文件对象指针的引用。 IRP_MJ_READ表明I/O请求从下层物理设备向系统传输数据。 IRP_MJ_WRITE表明I/O请求从系统向下层物理设备传输数据。 IRP_MJ_DEVlCE_CONTROL表明一个带有系统定义的,指定一个被发送给设备驱动程序的具体设备操作的特定设备类型I/O控制码的请求。 关于最低层设备驱动程序必须支持的,系统定义的(有时也叫公共的)I/O控制码的更多信息,见《Windows 2000 驱动程序开发指南》第2卷。注意,较高层驱动程序也必须支持IRP_MJ_DEVICE_CONTROL以传送这些IRP到它们之下的设备驱动程序。 IRP_MJ_INTERNAL_DEVICE_CONTROL表明一个传给设备驱动程序的请求,大多数情况下是来自一个紧密连接的较高层驱动程序,通常带有一个私有的、驱动程序特定的及特定设备类型的或特定设备的I/O控制码,该控制码请求一项特定设备类型的或特定设备的操作。 仅仅某些种类的驱动程序要去处理系统定义的内部设备I/O控制请求,包括SCSI驱动程序,键盘或鼠标设备驱动程序,及与系统支持的驱动程序进行互操作的并行驱动程序。 任何驱动程序都必须在驱动程序对象中设置的Dispatch例程入口点根据下层物理设备的类型和功能的不同而不同。关于驱动程序必须处理的IRP主功能码的特定设备类型信息,见DDK中特定设备类型的文档。关于PnP与电源IRP的更多信息,见《即插即用、电源管理和安装设计指南》。关于SCSI与视频微端口驱动程序的信息,见在线DDK。关于其他类型的SCSI驱动程序的更多信息,见在线DDK。 Dispatch例程条件实现 设备或中间层驱动程序可能处理的其他请求如下: 用于某些最高层驱动程序IRP_MJ_QUERY_INFORMATION和IRP_MJ_SET_INFORMATION: 这类请求表明一个用户模式应用程序、组件或驱动程序请求关于文件对象(代表驱动程序的设备对象)长度的信息,为此,用户模式的请求方要进行一项处理,或者,用户模式的请求方试图设置该文件对象的文件结束标志。 并行类与串行设备驱动程序通过将FILE_STANDARD_INFORMATION或FILE_POSITION_INFORMATION长度或位置置为零来处理这些请求。其他最高层设备驱动程序应支持这些请求,特别是在一个用户模式应用程序或用户模式驱动程序可能调用运行时C函数以处理文件对象的情况下。文件系统驱动程序必须比这些最高层设备驱动程序更完全地支持这些请求。 IRP_MJ_FLUSH_BUFFERS用于在设备中高速缓存数据或在驱动程序分配的内存中内在地缓冲数据的驱动程序: 接受这一请求表明驱动程序应写它的缓冲数据或刷新设备高速缓存的数据,或者应丢弃从设备读到的缓冲区或高速缓存数据。 例如,系统键盘和鼠标类驱动程序,它们带有从设备输入数据的内置环形缓冲器,支持刷新请求。海量存储设备的驱动程序及在它们之上分层的驱动程序也支持该请求。 IRP_MJ_SHUTDOWN用于任何在系统本身关闭之前被调用的驱动程序: 如果下层设备不是海量存储设备,当它初始化时驱动程序可以在驱动程序对象中设置DispatchShutdown入口点。驱动程序的DriverEntry例程必须用指向设备对象的指针调用IoRegisterShutdownNotification,它的DispatchShutdown例程必需在电源管理器发送一个系统设置电源IRP以关闭系统之前进行任何驱动程序确定的清除(cleanup)。 当系统将要关闭时,海量存储设备的驱动程序及在它们之上分层的中间层驱动程序可以依靠最高层文件系统驱动程序发送其关机IRP 。简而言之,FSD负责在系统关机之前保证任何缓存数据写入外围设备,并调用下层驱动程序刷新来自于它们的设备高速缓存与缓冲区(如果有)中的数据。 缓存数据的海量存储设备的驱动程序必须在它初始化时在驱动程序对象中为它的DispatchShutdown与DispatchFlush例程设置入口点。如果这样一个驱动程序在内存中分配数据缓冲区,但是它的设备没有内部高速缓存,它也必须在初始化时在驱动程序对象中设置DispatchShutdown与DispatchFlush入口点。 任何在处理IRP_MJ_FLUSH与IRP_MJ_SHUTDOWN请求的驱动程序之上分层的中间层驱动程序也必须在它初始化时在驱动程序对象中设置DispatchShutdown与DispatchFlush入口点。 Dispatch例程基本功能 一个特定Dispatch例程的功能要求根据它处理的IRP_MJ_XXX,驱动程序列中该独立驱动程序的位置,及下层物理设备类型的不同而不同。 大多数Dispatch例程如下处理输入IRP: 在IRP中检测驱动程序的I/O栈位置以决定做什么,并且如果有参数的话,检测参数的有效性。 尽可能满足请求并完成IRP;否则,由较低层驱动程序或其他设备驱动程序例程传送它以做进一步处理。 驱动程序是否必须检查其I/O栈位置以确定做什么并检查参数取决于所给的IRP_MJ_XXX,就还取决于驱动程序是否为每个驱动程序处理的IRP_MJ_XXX建立了单独的Dispatch例程。 驱动程序是否必须传送进一步处理的IRP取决于参数(如果有)的有效性,还取决于IRP_MJ_XXX和驱动程序在一连串分层驱动程序中的所在层(如果有)。 何时检查I/O栈位置 如果满足下列任何条件,驱动程序的Dispatch例程必须检查其IRP本身的I/O栈位置以确定做什么: Dispatch例程控制多个IRP_MJ_XXX。 Dispatch例程必须为确定的主功能码处理一组次功能码。带有次功能码的IRP包括所有IRP_MJ_PNP和IRP_MJ_POWER请求,和某些SCSI端口驱动程序及文件系统驱动程序必须处理的IRP。 设备驱动程序或紧密连接的较高层驱动程序的Dispatch例程处理IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL请求,它有一组关联的特定设备类型,或可能是私有定义且特定设备的I/O控制码。 为确定要求的是什么操作及使用的是什么参数(如果特定IRP_MJ_XXX有),Dispatch例程调用IoGetCurrentIrpStackLocation以获得一个指向它自己在输入IRP中的I/O栈位置的指针。 较高层驱动程序的Dispatch例程总是调用IoGetCurrentIrpStackLocation,它还调用IoGetNextIrpStackLocation以获得一个指向它们为相邻较低层驱动程序建立的IRP中的相邻较低层驱动程序I/O栈位置的指针,就象第4章提及的。 设备驱动程序的Dispatch(Internal)DeviceControl例程,或者其可能紧密连接的类驱动程序,必须确定在驱动程序的Parameters.DeviceIoControl.IoControlCode I/O栈位置为每个请求设置的是哪个I/O控制码。这类驱动程序的Dispatch(Internal)DeviceControl例程的特征经常是有Switch语句,如同在“Dispatch(Internal)DeviceControl功能”中所述的。 在大多数情况下,一个较高级驱动程序的Dispatch(Internal)DeviceControl例程仅在IRP中建立了栈位置之后在其上传送一个IRP_MJ_DEVICE_CONTROL或者IRP_MJ_INTERNAL_DEVlCE_CONTROL请求到相邻较低层驱动程序。当然,SCSI类驱动程序必须检查确定的IOCTL_SCSI_XXX控制码,以便能在传送这些请求之前正确地设建立SCSI端口驱动程序的I/O栈位置。关于这些对SCSI类驱动程序的请求的更多信息,见在线DDK。 如何在一个Dispatch例程中完成一个IRP 如果一个输入IRP能被迅速完成,Dispatch例程要做以下工作: 通常将IRP的I/O状态块中的Status与Information成员设置为恰当的值: Dispatch例程将Status设置为STATUS_SUCCESS或者一个适当错误STATUS_XXX,可以是调用一个支持例程所返回的值,或者对于某些的同步请求来说,是一个较低层驱动程序返回的值。 如果较低层驱动程序返回STATUS_PENDING,较高层驱动程序不应当用IRP调用IoCompleteRequest,除非它首先用该IRP调用了IoMarkIrpPending。当然,较高层驱动程序的Dispatch例程未必为返回STATUS_PENDING的较低层驱动程序完成任何IRP。 如果满足了一个传输数据的请求,例如一个读或写请求,则将Information设置为成功传输的比特数。 对其他以STATUS_SUCCESS完成的IRP的不同特定请求而将Information设置为不同的值。 对以警告性STATUS_XXX完成的IRP的不同特定请求而将Information设置为不同的值。例如,对于象STATUS_BUFFER_OVERFLOW这样的警告,将Information设置为传输的字节数。 通常,对以一个错误STATUS_XXX完成的请求,将Information置为零。 用IRP和PriorityBoost IO_NO_INCREMENT调用IoCompleteRequest。 返回已在I/O状态块中设置的适当的STATUS_XXX。注意,对IoCompleteRequest的调用使给定的IRP不能被调用者访问,因此来自一个Dispatch例程的返回值不能从一个已完成的IRP的I/O状态块中设置。 以下是用IRP调用IoCompleteRequest的实现方针: 调用IoCompleteRequest之前,总是释放驱动程序持有的自旋锁。 完成一个IRP的时间是不确定的,尤其是在一系列分层的驱动程序中。另外,如果一个较高层驱动程序的IoCompletion例程向一个正持有自旋锁的较低层驱动程序发送一个IRP ,就会产生死锁。 何时在Dispatch例程中完成一个IRP 通常,驱动程序不在它们的Dispatch例程中完成IRP,除非给定请求的参数是无效的,或者在设备驱动程序中,特定IRP_MJ_XXX不要求设备I/O操作。 一个较高层驱动程序应根据以下方针在它的Dispatch例程中完成IRP或者将它传送给较低层驱动程序来处理: 如果Dispatch例程确定它自己的I/O栈位置中有参数是无效的,它用一个恰当错误STATUS_XXX,例如STATUS_INVALID_PARAMETER,迅速完成该IRP。 如果IRP包含功能码IRP_MJ_CLEANUP,DispatchCleanup例程必须为驱动程序清除IRP的I/O栈位置文件对象,完成当前在目标设备对象排队的每个IRP,并完成清除IRP. 一个清除请求表明应用程序被终止或者已关闭一个代表驱动程序设备对象的文件对象的文件处理。当DispatchCleanup例程返回时,通常驱动程序的DispatchClose例程被再次调用。 否则,一个较高层驱动程序可以仅仅通过传送它到相邻的较低层驱动程序满足请求。 一系列分层的驱动程序中的所有驱动程序都可以检查它自己的,输入到驱动程序的Dispatch例程每个IRP的I/O栈位置中的参数的有效性。在这一系列中,每个驱动程序的Dispatch例程仅完成那些在它的I/O栈位置中可以确定参数有效性的IRP。在最高层驱动程序的Dispatch例程里完成带有无效参数的IRP,将改善任何系列的驱动程序与系统整体的I/O流量。 一个最低层驱动程序按照以下方针在它的Dispatch例程里完成IRP: 如果Dispatch例程确定它自己的I/O栈位置里有参数是无效的,它应当用一个适当的错误STATUS_XXX立即完成该IRP。 通常,任何较高层驱动程序已经为一个请求的操作检查参数,但最低层设备驱动程序也可以执行它们自己的参数“健康检查”。当然,最低层设备驱动程序不必检查IRP_MJ_INTERNAL_DEVICE_CONTROL请求,因为一个紧密连接的较高层驱动程序负责设置这些请求的参数。 如果IRP包含功能码IRP_MJ_CLEANUP,DispatchCleanup例程必须为给定的驱动程序清除IRP的I/O栈位置的文件对象,完成当前在目标设备对象排队的所有IRP,并完成清除IRP。 一个清除请求表明一个应用程序被终止或关闭了一个代表驱动程序设备对象的文件对象的文件处理。当Dispatch例程返回时,通常驱动程序的DispatchClose例程被再次调用。关于处理可取消的IRP的更多信息,也参见第12章。 如果请求不要求设备I/O操作,Dispatch例程应满足请求并完成IRP。 例如,一个驱动程序可以在设备扩展中存储该设备的当前模式,尤其如果它在初始化后很少改变设备模式。它的DispatchDeviceControl例程可以满足一个通过返回存储信息来查询当前设备模式的请求。 否则,Dispatch 例程必须用IRP调用IoMarkIrpPending,将IRP排队到其他驱动程序例程以做进一步处理,并返回STATUS_PENDING。 如何从一个Dispatch例程传送带有效参数的IRP 当Dispatch例程检查了一个输入IRP中它自己的I/O栈位置并确定任何参数都是有效的,它必须为进一步通过较低层驱动程序或其他设备驱动程序例程处理传送IRP,如果它无法满足并完成Dispatch例程本身给定的请求。 一个较高层驱动程序应如下传送一个请求到相邻较低层驱动程序: 用输入IRP调用IoGetCurrentIrpStackLocation,如果Dispatch例程还没有这样做,这样就为IRP中它自己的I/O栈位置获得一个指针。 在大多数情况下,一个较高层驱动程序的Dispatch例程已经进行了该调用。然而,一个Dispatch例程可能只是处理特定的IRP_MJ_XXX,它或者带有驱动程序无法验证有效性的参数或者不带参数,这种Dispatch例程可能会调用IoGetCurrentIrpStackLocation。 如果驱动程序传送输入IRP到相邻较低层驱动程序,调用IoGetNextIrpStackLocation。如果它为一个或多个较低层驱动程序分配了附加IRP,Dispatch例程用每个它分配的IRP进行这两组调用: 如果驱动程序没有在新IRP中分配栈位置,调用IoGetNextIrpStackLocation获得一个指向相邻较低层驱动程序I/O栈位置的指针 调用IoSetNextIrpStackLocation(在它之后是IoGetCurrentIrpStackLocation)获得一个指向新IRP中它自己的栈位置的指针,在那里它可以设置任何IoCompletion例程需要的环境;然后调用IoGetNextIrpStackLocation获得一个指向相邻较低层驱动程序I/O栈位置的指针 设置相邻较低层驱动程序的I/O栈位置,这通常是通过拷贝它自己在初始IRP中的I/O栈位置到相邻较低层驱动程序的I/O栈位置来实现的。不过,Dispatch例程可以为某些请求修改相邻较低层驱动程序的I/O栈位置中的参数。 例如,当下层的设备已经知道传输能力限制,并重新使用IRP发送部分传输请求到下层的设备驱动程序时,一个较高层驱动程序可以为一个大的传输请求修改参数 用每个Dispatch例程分配的IRP调用IoSetCompletionRoutine,以便驱动程序的IoCompletion例程在较低层驱动程序完成它时释放每个这样的IRP。 Dispatch例程也可以用已设置相邻较低层驱动程序I/O栈位置的输入IRP调用IoSetCompletionRoutine。这时,它的IoCompletion例程可以检查较低层驱动程序如何完成请求;为部分IRP传输重新使用IRP;如果跟踪IRP,更新任何驱动程序维护的状态;可能的话重试一个返回错误的请求,等等。 关于IoCompletion例程的更多信息,见第13章. 用每个将由较低层驱动程序处理的IRP调用IoCallDriver。 返回适当的NTSTATUS值,例如: 如果输入IRP是一个象IRP_MJ_READ或IRP_MJ_WRITE那样的异步请求,通常是STATUS_PENDING 如果输入IRP是一个象IRP_MJ_CREATE或IRP_MJ_DEVICE_CONTROL那样的同步请求,则经常是对IoCallDriver的调用结果 一个最低层设备驱动程序将任何在它的Dispatch例程中无法完成的IRP传送到下列其他驱动程序例程: 用输入IRP调用IoMarkIrpPending。 调用IoStartPacket以传送或排队IRP到驱动程序的StartIo例程,除非驱动程序管理它自己的内部IRP队列。关于驱动程序管理队列的更多信息见第7章。 如果驱动程序不拥有StartIo例程但却处理可撤消的IRP,在将IRP排队以做进一步处理之前,它必须用Cancel例程与IRP的入口点调用IoSetCancelRoutine。关于Cancel例程的更多信息见第7章。 返回STATUS_PENDING。 为较低层驱动程序分配IRP 根据它在一系列分层的驱动程序中的位置,驱动程序可以通过调用某些系统支持例程为较低层驱动程序分配IRP。 最高层驱动程序可以调用IoMarkAssociatedIrp分配IRP并为一系列较低层驱动程序建立它们。只要驱动程序不用初始IRP或任何它分配的相连的IRP调用IoSetCompletionRoutine ,I/O管理器当所有它相连的IRP都被完成时就自动完成初始IRP。然而,最高层驱动程序决不能为任何请求缓冲I/O操作的IRP分配相连的IRP。 一个中间层驱动程序不能通过调用IoMakeAssociatedIrp为较低层驱动程序分配IRP。任何中间层驱动程序接收的IRP也是相连的IRP,并且驱动程序不能把这类IRP与另一个IRP相连。 如果中间层驱动程序为较低层驱动程序创建IRP,它应调用IoAllocateIrp、IoBuildDeviceIoControlRequest、IoBuildSynchronousFsdRequest或IoBuildAsynchronousFsdRequest。然而,IoBuildSynchronousFsdRequest仅可以在下列环境中被调用: 由一个驱动程序创建的线程为读或写请求建立IRP,因为这样一个线程可以在非专有线程环境中等待一个调度对象,例如驱动程序初始化的Event传送到IoBuildSynchronousFsdRequest 在系统线程环境中初始化或卸载时 为自同步操作建立IRP,例如创建、刷新、关机、关闭和设备控制请求 然而,相比IoBuildSynchronousFsdRequest,驱动程序更喜欢调用IoBuildDeviceIoControlRequest来分配设备控制IRP。 处理异步传输请求 除了最高层驱动程序,所有驱动程序都异步处理IRP_MJ_READ与IRP_MJ_WRITE请求。即使最高层驱动程序也不能在它的Dispatch例程中为较低层驱动程序等待,以完成处理一个异步的读或写请求。它必须传送这样的请求给较低层驱动程序并从它的DispatchRead或DispatchWrite例程返回STATUS_PENDING。 同样,最低层设备驱动程序必须传送这类传输请求给其他处理设备I/O请求的驱动程序例程,并从它的DispatchRead及/或DispatchWrite例程,或者DispatchReadWrite例程返回STATUS_PENDING。 如6.2.4节所提到的那样,较高层驱动程序有时必须设置部分传输IRP,并把它们传送给较低层驱动程序。当部分传输请求已由较低层驱动程序完成时,较高层驱动程序仅仅完成初始读/写IRP。 例如,一个SCSI类驱动程序的DispatchReadWrite例程被用来把超出下层的HBA的传输能力的大传输请求分割成一组部分传输请求。类驱动程序必须在部分传输IRP中设置参数以便SCSI端口/微端口驱动程序可以在单个DMA操作中满足每个部分传输请求。 其他使用DMA或PIO的设备驱动程序也需要为它们自身分割大传输请求。 关于DMA与PIO使用的更多信息,见第3章的“设置用户缓冲区的访问”和“适配器对象和DMA”。关于SCSI类驱动程序要求的更多信息,也见在线DDK。 Dispatch例程设计 本节更全面地描述设备与中间层驱动程序的最一般实现,包括以下内容: DispatchRead和DispatchClose DispatchCleanup DispatchRead和DispatchWrite DispatchDeviceControl与DispatchInternalDeviceControl 关于DispatchPnP和DispatchPower例程的信息,见《即插即用、电源管理和安装设计指南》第1部分第2章 “DispatchPnP例程和DispatchPower例程”。 DispatchCreate和DispatchClose功能 创建请求可以在一个用户模式子系统试图获得代表设备(可能代表一个应用程序或子系统层的驱动程序)的文件对象的句柄时产生,或者一个较高层驱动程序对IoGetDeviceObjectPointer或IoAttachDevice调用时产生。 与它相逆的关闭请求在一个用户模式子系统关闭与驱动程序设备对象相连的文件对象句柄时产生。 这些请求自然是同步的。 IRP_MJ_CREATE与IRP_MJ_CLOSE请求的驱动程序Dispatch例程可能仅仅以STATUS_SUCCESS完成输入IRP,就象第4章提到的那样。 IRP_MJ_CREATE与IRP_MJ_CLOSE请求的另一驱动程序Dispatch例程做更多的工作,这取决于下层设备驱动程序或下层设备。 考虑以下情况: 一收到创建请求,类驱动程序就初始化内部队列并发送IRP_MJ_INTERNAL_DEVICE_CONTROL请求给相关请求设备配置信息或到一个控制器端口的专有连接的端口设备驱动程序。 一收到关闭请求,最高层驱动程序就需要延迟直到为所有当前排队到目标设备对象的未完成IRP完成设备I/O处理。 一收到创建请求,一个不常用设备的驱动程序不能调用MmLockPageableCodeSection以常驻一些处理其他IRP_MJ_XXX请求的驱动程序例程。一收到相逆的关闭请求,当所有为这类驱动程序的设备对象处理的文件对象被关闭时,驱动程序调用MmUnlockPageableImageSection以通过将可分页映像分区所在页调出内存来保留内存。 虽然许多驱动程序对创建与关闭请求只有一个Dispatch例程,但可以分别实现单独的DispatchCreate与DispatchClose例程。一些驱动程序仅为了对称才处理IRP_MJ_CLOSE请求,因为在设备对象被一个保护的子系统或较高层驱动程序打开后,较低层驱动程序的设备对象直到系统关机时才关闭。 如果有一个较低层驱动程序控制的设备必须为系统继续运行而存在,驱动程序的DispatchClose例程通常不会被调用。 例如,键盘与鼠标驱动程序建立代表系统运行时必须被功能化的物理设备的设备对象,因此驱动程序可以拥有最低限度DispatchClose例程或者结合的DispatchCreateClose例程。 再例如,一些系统磁盘驱动程序没有DispatchClose例程,但是在系统关机之前,这些驱动程序通常有DispatchFlush与DispatchShutdown例程来完成任何未完成的文件I/O操作。 基本DispatchCreateClose例程 许多驱动程序收到创建请求时仅仅需要建立自身,并且仅仅需要知道收到关闭请求,特别是分层的驱动程序链中的较低层驱动程序。 例如,一个带有一个或多个连接紧密的调用IoGetDeviceObjectPointer的类驱动程序的设备控制器的端口驱动程序有最低限度的DispatchCreateClose例程。这类端口驱动程序的DispatchCreateClose例程只如下完成IRP: : : { Irp->IoStatus.Status = STATUS_SUCCESS; Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } 最低限度DispatchCreateClose例程将I/O状态块的Information成员置为零,表明文件系统为创建请求打开;对于关闭请求的Information是没有意义的。它用STATUS_SUCCESS设置Status成员,并返回状态值。当类驱动程序调用IoGetDeviceObjectPointer时,这个状态表明它已准备好从使创建IRP 发送到端口驱动程序的DispatchCreateClose例程的较高层类驱动程序目标设备对象接收I/O请求。 最低限度DispatchCreateClose例程完成创建IRP,而没有提高IRP(IO_NO_INCREMENT)始发者的优先级,因为假定请求始发者在请求完成过程中等待一个不确定但非常小的间隔。 DispatchCreateClose例程做多少工作一方面取决于驱动程序设备或下层设备的属性,一方面取决于驱动程序的设计。一个驱动程序可以在单独的DispatchCreate和DispatchClose例程中处理这些请求,尤其是如果该驱动程序为每个请求执行完全不同的操作。 对于一个打开代表一个逻辑或物理设备的文件对象的创建请求,最高层驱动程序做以下工作: 调用IoGetCurrentIrpStackLocation以获取一个指向IRP中I/O栈位置的指针。 如果FileName中的Unicode串长度为零,检查I/O栈位置中的FileObject.FileName并用STATUS_SUCCESS完成IRP;否则,用STATUS_INVALID_PARAMETER完成IRP。 按照上述步骤可以确保不试图在设备上打开一个可能在以后产生问题的伪文件。例如,它能防止试图打开一个不存在的\\device\parallel0\temp.dat。 实现DispatchCreateClose的注意事项 当实现DispatchCreateClose例程时要考虑以下几点: 至少,Dispatch例程必须做以下工作: 用适当的NTSTATUS,通常是STATUS_SUCCESS设置输入IRP的I/O状态块中的Status域。 将输入IRP的I/O状态块中的Information域置为零。 用IRP和值为IO_NO_INCREMENT的PriorityBoost调用IoCompleteRequest。 返回在IRP的I/O状态块的Status域中设置的NTSTATUS。 在最高层或中间层驱动程序中,该例程可能需要为处理一个创建及/或关闭请求做额外工作,这取决于设备或下层设备的属性与驱动程序的设计。 对于打开一个代表逻辑或物理设备的文件对象的创建请求,最高层驱动程序应检查I/O栈位置中的FileObject.FileName,并用STATUS_SUCCESS完成IRP,如果FileName中Unicode串长度为零。否则,它应用STATUS_INVALID_PARAMETER完成IRP。 最低层驱动程序的DispatchCreateClose例程仅仅在相邻的较高层驱动程序调用IoAttachDeviceToDeviceStack,IoGetDeviceObjectPointer,或IoAttachDevice时被调用。一系列分层的驱动程序中的最低层驱动程序常常仅进行一个创建或关闭请求的最低要求的处理。 DispatchCleanup功能 任何驱动程序中保持一个不确定的间隔的IRP应有一个DispatchCleanup例程。如果在为代表下层物理或逻辑设备的文件对象的一个用户模式处理被关闭时,IRP可以被排队,为那些设备控制IRP界限的最高层驱动程序必须有一个DispatchCleanup例程。 一个驱动程序的DispatchCleanup例程被调用以撤消当前被排在文件对象的目标设备对象的所有IRP,这个文件对象在清除IRP(cleanup IRP)的驱动程序的I/O栈位置中被指定。 通常,一个DispatchCleanup例程必须做以下工作: 在当前设备队列(或者驱动程序的内部IRP队列)中的每个IRP中使用IoSetCancelRoutine将驱动程序的Cancel入口点(如果有)重设为NULL,这个设备队列是目标设备对象和给定文件对象的队列。 撤消所有当前为目标设备对象和清除IRP的驱动程序I/O栈位置中特定的文件对象排队的IRP。 完成输入清除IRP并返回STATUS_SUCCESS。 DispatchCleanup例程必须在它完成每个要被撤消的IRP之前,在每个为给定文件对象排队到目标设备对象的IRP中设置I/O状态块如下: Status 设置为STATUS_CANCELLED Information 设置为零 DispatchCleanup例程在它用每个撤消的IRP调用IoCompleteRequest时指定IO_NO_INCREMENT。当它撤消了为给定文件对象排队的全部IRP之后,DispatchCleanup例程用STATUS_SUCCESS和一个IO_NO_INCREMENT的PriorityBoost完成请求本身。 如果一个驱动程序管理它自己的IRP内部队列,而不是使用它的设备对象连接的设备队列,驱动程序应当建立一个用执行自旋锁保护的互锁队列。否则,另一个驱动程序例程(运行在一个SMP机器的另一个处理器上)可能将一个应由DispatchCleanup例程撤消的IRP从队列中取出。为从队列中删除一个IRP并撤消它,DispatchCleanup例程调用一个带有驱动程序提供的执行自旋锁的ExInterlocked..List支持例程。 由DispatchCleanup例程和另一个驱动程序例程共享的目标设备对象的设备扩展中的任何区域也必须由自旋锁保护。 每个驱动程序必须使它对可撤消的IRP的访问与I/O管理器和它自己的Cancel例程(如果有)同步。 为在必须用一个或多个Cancel例程使之同步的DispatchCleanup例程内管理自旋锁的使用,应考虑以下方针: 如果DispatchCleanup例程必须获得系统撤消自旋锁和一个或多个驱动程序自己的自旋锁,它应在取得它自己的自旋锁之前调用IoAcquireCancelSpinLock。 DispatchCleanup例程必须以相反顺序释放获得的嵌套自旋锁:换句话说,它必须在调用IoReleaseCancelSpinLock之前释放它自己的自旋锁。 如果驱动程序在设备对象中使用I/O管理器提供的设备队列,这时它的DispatchCleanup例程在它调用IoSetCancelRoutine将IRP中的Cancel入口点重置为零之前必须调用IoAcquireCancelSpinLock。 如果驱动程序管理它自己的IRP队列,这时DispatchCleanup例程通常在调用IoSetCancelRoutine之前获得撤消自旋锁。更多的信息见第12章“Cancel例程”。 在它用IRP调用IoCompleteRequest之前,DispatchCleanup例程必须释放所有正持有的自旋锁,包括它撤消的每个IRP。 关于Cancel例程的更多信息,见第12章。 当实现一个DispatchCleanup例程时记住以下要点: 在它完成输入清除IRP之前,该例程必须遍历目标设备对象的IRP队列,并撤消所有对于给定文件对象未完成的请求。清除IRP在代表用户模式设备的文件对象的操作被关闭时发送。 队列应由自旋锁保护以防止将被撤消的IRP被任何别的驱动程序例程从队列中取出。与设备对象连接的设备队列带有自旋锁。如果驱动程序建立它自己的IRP队列,应建立一个互锁队列。 如果驱动程序有一个Cancel例程,该例程必须在它调用IoSetCancelRoutine将每个排队的IRP中的Cancel入口点重置为NULL之前用IoAcquireCancelSpinLock获得系统撤消自旋锁。除非驱动程序管理它自己的IRP队列并用系统提供的自旋锁使对它们的访问同步;驱动程序不需要在调用IoSetCancelRoutine之前获得撤消自旋锁。 该例程必须在它完成每个这类IRP之前设置每个排队的IRP的I/O状态块,将Status设置为STATUS_CANCELLED,Information设置为零。 在用一个IRP调用IoCompleteRequest之前,该例程必须释放所有它保持的自旋锁。 当它完成每个撤消的IRP与清除IRP时,该例程应设置PriorityBoost为IO_NO_INCREMENT。 DispatchRead或DispatchWrite功能 一个设备(数据可以从它传输到系统)的所有驱动程序必须有一个DispatchRead例程,并且必须在初始化时设置它在驱动程序对象中的入口点。一个设备(数据可以从系统传输到它)的所有驱动程序必须有一个DispatchWrite例程,并且必须在初始化时设置它在驱动程序对象中的入口点。任何双向传输数据的最低层设备驱动程序可以有一个结合的DispatchReadWrite例程,如同在它之上的分层的较高层驱动程序那样。 任何在这样一个最低层设备驱动程序之上的分层的较高层驱动程序必须设置它的驱动程序对象中的Dispatch例程入口点,以响应由下层设备驱动程序处理的IRP_MJ_READ和/或IRP_MJ_WRITE请求。 如6.2.6节所提到的,较低层驱动程序异步地处理IRP_MJ_READ与IRP_MJ_WRITE请求。因此,DispatchRead和/或DispatchWrite例程必须为进一步处理传送这些请求,假如IRP的驱动程序的I/O栈位置中的请求有效参数。 驱动程序是否为缓冲或直接I/O建立设备对象影响它如何处理传输请求,如同已在第3章中提到的。特别地,使用直接I/O进行DMA操作的驱动程序需要将大的传输请求分割成一系列较小的传输操作,以便满足IRP_MJ_READ或IRP_MJ_WRITE请求。 以下小节讨论了一些使用缓冲I/O与直接I/O的最低层设备驱动程序,以及它们之上的较高层驱动程序中的DispatchReadWrite例程的设计与实现要点。6.3.3.4小节概述了实现DispatchRead、DispatchWrite或结合的DispatchReadWrite例程的注意事项。 使用缓冲I/O的DispatchReadWrite 任何为缓冲I/O建立设备对象的最低层设备驱动程序通过返回从设备传输到锁定的位于Irp->AssociatedIrp.SystemBuffer的系统空间缓冲区的数据来满足一个读请求。它通过从相同的缓冲区到设备传输数据来满足一个写请求。 因此,这类设备驱动程序的DispatchReadWrite例程通常一收到一个传输请求就做以下工作: 调用IoGetCurrentIrpStackLocation,并且,如果必要的话,确定传输请求的方向。 为该请求检查参数有效性。 对于一个读请求,该例程通常检查驱动程序的IoStackLocation->Parameters.Read.Length值以确定缓冲区是否足够包含从设备传输来的离散数量的数据。 例如,系统键盘类驱动程序处理来自Win32用户输入线程的读请求。该驱动程序定义了一个结构,KEYBOARD_INPUT_DATA,存放来自设备的键击,并在任何给定时刻,保存一些内部环形缓冲区中的结构编号,以满足到来的读请求。 对于一个写请求,这类驱动程序通常检查位于Parameters.Write.Length处的值,并且如果必要,检查Irp->AssociatedIrp.SystemBuffer处的数据的有效性;也就是说,是否设备只接收包含已定义的值域的成员的结构化数据包。 如果任一参数是无效的,DispatchReadWrite例程立即完成IRP,就如6.2.2节中描述的。否则,DispatchReadWrite由其他驱动程序例程为进一步处理传送IRP,如同6.2.4节描述的。 使用缓冲I/O的最低层设备驱动程序通常必须满足一个带有一些离散数量的数据的传输请求,请求的始发方可以使用或者设置这些数据。这类驱动程序往往为来自或要被发送到设备的数据定义结构,并往往如系统键盘类驱动程序那样内在地缓冲结构化的设备。 内在地缓冲数据的驱动程序支持IRP_MJ_FLUSH_BUFFERS请求,并且也支持IRP_MJ_SHUTDOWN请求(见6.1.2节)。 通常,一系列驱动程序中最高层驱动程序负责在传送一个读/写请求到较低层驱动程序之前检查输入IRP的参数。因此,许多较低层驱动程序可以假定一个读/写IRP中的I/O栈位置有有效参数。如果它的设备对那个设备驱动程序的数据传输有特殊限制,一系列驱动程序中最低层驱动程序需要检查I/O栈位置中参数的有效性。 使用直接I/O的DispatchReadWrite 任何为直接I/O建立设备对象的较低层设备驱动程序通过返回从其设备到系统物理内存所传输的数据,这个物理内存由位于Irp->MdlAddress的MDL所描述,满足一个读请求。它通过从系统物理内存到它的设备传输数据满足写请求。 如6.2.6节提及的,较低层驱动程序必须异步地处理读/写请求。所有较低层驱动程序的DispatchReadWrite例程必须传送带有有效参数的IRP_MJ_READ与IRP_MJ_WRITE IRP到其他驱动程序例程,就如6.2.4节所描述的。 对于所有发送给较低层驱动程序的读/写IRP,由位于Irp->MdlAddress处的MDL描述的页式物理内存已被检查过有正确的访问权以执行请求的传输,并且已被一系列驱动程序中的最高层驱动程序或I/O管理器锁定。任何为直接I/O建立设备对象的中间层或最低层驱动程序不应调用MmProbeAndLockPages,因为它已经被调用。如果设备使用PIO传输数据,一个Windows 2000最低层驱动程序调用MmGetSystemAddressForMdlSafe,而一个WDM最低层驱动程序调用MmGetSystemAddressForMdl以获得系统映射虚拟地址。 如果它无法确信最高层驱动程序仅传送带有效参数的IRP,任何中间层或最低层设备驱动程序的DispatchReadWrite例程应检查它的读/写IRP的I/O栈位置的参数。如果这类驱动程序的DispatchReadWrite例程在一个到来的传输IRP中找到一个参数错误,它将用一个适当错误STATUS_XXX值完成IRP,如6.2.2节所描述的。否则,这类中间层驱动程序的DispatchReadWrite例程必须传送进一步处理请求,根据6.3.3.3节中的方针。 任何设备驱动程序的DispatchReadWrite例程必须调用带传输请求的IoMarkIrpPending,传送IRP以供其他驱动程序例程做进一步处理,并返回STATUS_PENDING,就象6.2.4节描述的。注意,设备驱动程序的DispatchReadWrite例程可以通过用一个驱动程序确定的Key值调用IoStartPacket为控制在它的设备上排队的IRP的次序以得到更快的I/O吞吐。驱动程序内的另一个例程稍后从队列中取出IRP,决定请求的长度是否必须分割进行部分传输操作,并且为传输数据的设备编程。 通常,必须将大的传输请求分割以满足设备限度的设备驱动程序应延迟这些操作直到为一个给定的传输请求建立设备之前。这类设备驱动程序的DispatchReadWrite例程不应该为特定设备传输约束而检查输入的IRP的I/O栈位置,也不应该试图计算部分传输量,如果驱动程序能延迟这些检查直到其StartIo(或其他驱动程序例程)为传输操作对设备编程之前的话。 较高层驱动程序的DispatchReadWrite 较低层设备驱动程序的DispatchReadWrite例程队列为在设备上进一步处理传输带有效参数的IRP到其他驱动程序例程,并返回STATUS_PENDING。 除了文件系统,在这类设备驱动程序之上分层的较高层驱动程序通常不拥有任何IRP内部驱动程序队列。这类驱动程序的DispatchReadWrite例程可能在建立了它的IoCompletion例程之后,可以传送带有效参数的IRP到较低层驱动程序,如6.2.4节描述的。 然而,如果必要,在它发送一个带主功能玛IRP_MJ_READ或IRP_MJ_WRITE的IRP到SCSI端口/微端口驱动程序对之前,一个SCSI类驱动程序的DispatchReadWrite例程负责分割传输数据。关于对SCSI类驱动程序的要求与如何分割一个大的传输请求的更多信息,见在线DDK和第3章的适配器对象部分。 如果一个较高层驱动程序分配一个或多个IRP来请求部分传输,这些IRP是它在DispatchReadWrite例程中为相邻较低层驱动程序建立的,那么DispatchReadWrite例程必须用每个驱动程序分配的IRP调用IoSetCompletionRoutine。驱动程序必须注册它的IoCompletion例程以跟踪在每个部分传输中传输的数据量,以便IoCompletion例程可以释放所有驱动程序分配的IRP,并且,最终完成初始请求。 如果下层驱动程序控制一个可删除的媒体设备,任何由较高层驱动程序分配的IRP必须拥有一个线程环境。为建立线程环境,分配驱动程序必须用输入传输IRP中的相同值设置每个新分配的IRP中的Irp->Tail.Overlay.Thread。 如果下层设备驱动程序为部分传输返回一个带有错误的IRP,在释放所有较高层驱动程序分配的IRP和内存之后,IoCompletion例程可以重置部分传输请求或者用返回的错误设置I/O状态块完成初始IRP。 如果较高层驱动程序的DispatchReadWrite例程为部分传输操作分配内存,并且它的分配将被该驱动程序(或者下层设备驱动程序)的IoCompletion例程访问,DispatchReadWrite例程必须从非页式缓冲池分配内存 实现DispatchReadWrite的注意事项 当实现DispatchRead、DispatchWrite或DispatchReadWrite例程时要记住以下要点: 一系列分层的驱动程序中的最高层驱动程序负责检查输入的读/写IRP的参数有效性,在设置一个IRP中相邻较低层驱动程序的I/O栈位置之前。 中间层和最低层驱动程序通常可以依靠该系列中最高层驱动程序来传送带有效参数的传输请求。然而,任何驱动程序可以进行IRP中它的I/O栈位置的参数检查,并且每个设备驱动程序应当在任何可能违反设备赋予的约束的情况下检查参数。 如果DispatchReadWrite例程完成一个带有错误的IRP,它应设置将I/O栈位置中的Status成员置为适当的NTSTATUS类型的值,将Information成员置为零,并用这个IRP与一个值为IO_NO_INCREMENT的PriorityBoost调用IoCompleteRequest。 如果驱动程序使用缓冲I/O,它需要定义一个存放要传输的数据的结构,并需要在这些结构内部缓存一些数据。 如果驱动程序使用直接I/O,它需要检查位于Irp->MdlAddress处的MDL是否描述一个包含对下层设备太过多数据(或太多的页中断)的缓冲区,以致无法在一个独立传输请求中的处理的。如果这样,驱动程序必须把初始传输请求分割为一系列较小的传输操作。 一个连接紧密的类驱动程序可能在它的DispatchReadWrite例程中为下层端口驱动程序分割这样的请求。SCSI类驱动程序(特别是对于海量存储设备)更需要这么做。关于SCSI驱动程序要求的更多信息,见在线DDK。 较低层设备驱动程序的DispatchReadWrite例程应延迟分割一个大的传输请求为部分传输直到另一个驱动程序例程把为传输而设置系统的IRP从队列中取走。 如果较低层设备驱动程序把一个读/写IRP排队以供自己的例程做进一步处理,它必须在把IRP加入队列之前调用IoMarkIrpPending。在这些情况下,DispatchReadWrite例程也必须用STATUS_PENDING返回控制。 如果DispatchReadWrite例程传送一个IRP到较低层驱动程序,它必须为IRP中的相邻较低层驱动程序设置I/O栈位置。较高层驱动程序是否也要在用IoCallDriver传送之前在IRP中设置IoCompletion例程取决于驱动程序的设计与它之下的那些驱动程序。 当然,较高层驱动程序必须在它调用IoCallDriver之前调用IoSetCompletionRoutine,如果它分配资源,例如IRP或内存。当较低层驱动程序已经完成请求,但IoCompletion例程调用带有初始IRP的IoCompleteRequest之前,IoCompletion例程必须释放所有驱动程序分配的资源。 如果较高层驱动程序为可能包括一个下层可删除媒体设备驱动程序的较低层驱动程序分配IRP,分配驱动程序必须在它分配的每个IRP内建立线程环境。 Dispatch(Internal)DeviceControl功能 对于所有一般类型的外围设备,系统为IRP_MJ_DEVICE_CONTROL请求定义一组I/O控制码。每种类型的设备的新驱动程序必须支持这些请求。在大多数情况下,这些每种类型的设备的公共I/O控制码不输出到用户模式应用程序。 一些系统定义的I/O控制码被较高层的驱动程序使用,这些驱动程序使用IoBuildDeviceIoControlRequest为下层设备驱动程序IRP。通过调用Win32函数DeviceIoControl,其他的被Win32驱动程序或部分Win32组件用来与一个下层设备驱动程序通信。该Win32函数调用一项系统服务,然后I/O管理器用主功能码IRP_MJ_DEVICE_CONTROL和Parameters.DeviceIoControl.IoControlCode处的I/O栈位置中的给定的I/O控制码设置一个IRP。接着,I/O管理器用IRP调用较高层驱动程序的DispatchDeviceControl例程。 对于某些系统提供的、设计成为与新驱动程序互操作地、并支持新驱动程序的驱动程序,操作系统也为IRP_MJ_INTERNAL_DEVICE_CONTROL请求定义了一组I/O控制码。在大多数情况下,这些公共I/O控制码允许附加的较高层驱动程序与一个下层设备驱动程序互操作。 例如,IOCTL_SCSI_GET_INQUIRY_DATA与IOCTL_SCSI_GET_CAPABILITIES码支持SCSI类驱动程序的初始化和配置。SCSI类驱动程序发送这些设备控制请求给SCSI端口驱动程序以查找每种类驱动程序类型的SCSI外围设备(对于那些带有连接的外围设备的类驱动程序类型来说),并且对于那些带一个连接的外围类驱动程序类型的,收集有关下层SCSI微端口HBA的相关信息,例如最大传输能力和每个DMA传输的最大页中断数等。 再例如,系统并行端口驱动程序支持一组附加并行类驱动程序在IRP_MJ_INTERNAL_DEVICE_CONTROL请求中设置的公共I/O控制码,以与它们的设备通过一个并行口通信。系统并行端口驱动程序可被替换,一旦这类新并行端口驱动程序为由现存的和新的并行端口类驱动程序继续支持同一组内部设备控制请求。 几乎所有系统定义的I/O控制码都使用缓冲I/O,因为该类型的请求很少要求传输大量的数据。换句话说,除了与Win32多媒体驱动程序联系紧密的几种最高层设备驱动程序之外,即使为直接I/O建立它们设备对象的驱动程序也为设备控制请求被发送IRP,连带被传输到位于Irp->AssociatedIrp.SystemBuffer的缓冲区的数据。 另外,驱动程序可以定义一组其他驱动程序用来与它进行通信的私有I/O 控制码。新的公共I/O控制码只有与Microsoft公司合作才能加入到系统中,因为公共I/O控制码是嵌入到操作系统本身的。 关于这组不同操作系统必须支持的公共I/O控制码和已定义的私有I/O控制码的具体信息,见《Windows 2000 驱动程序设计指南》第2卷。 最低层驱动程序中的DispatchDeviceControl 最低层驱动程序的IRP_MJ_DEVICE_CONTROL请求需要驱动程序改变其设备的状态或者提供其设备状态的信息。由于大多数驱动程序都要处理大量的I/O控制码,它们的DispatchDeviceControl例程通常包含一个如下的Switch语句: : : switch(IRPp->Parameters.DeviceIoControl.IoControlCode) { case IOCTL_DeviceType_XXX; case IOCTL_DeviceType_XXX; if (IRPp->Parameters.DeviceIoControl.InputBufferLength < (sizeof(IOCTL_XXXYYY_STRUCTURE))) { status = STATUS_BUFFER_TOO_SMALL; break; } else { IoMarkIrpPending(Irp); : : // 为进一步处理传送IRP case … : : 就如这个代码段所示,DispatchDeviceControl例程也检查参数,有时在每个驱动程序必须支持的I/O控制码上,有时在这些IOCTL_XXX组上。 对于设备驱动程序的DispatchDeviceControl例程考虑下列实现方针: 对于任何IRP和驱动程序的Dispatch例程,DispatchDeviceControl必须检查参数有效性,并立即完成带参数错误的IRP,就如6.2.3节所描述的。 首先加入DispatchDeviceControl例程能为其满足和完成IRP的I/O控制码,以提高性能,因为驱动程序可以更快的返回控制。 为了更好的性能,所有最低层设备驱动程序应在其DispatchDeviceControl例程中满足任何它可以满足的设备控制请求,而不用让IRP到其他设备排队。 稍后加入不经常指定要求的操作的I/O控制码,这也可以在处理IRP_MJ_DEVICE_CONTROL_REQUESTS时改进设备性能。 当测试参数有效性时,对(特定)case语句中的I/O控制码分组从驱动程序的性能与规模及代码维护角度来看是经济的。从前面的代码段可以看出,使用公共结构的I/O控制码非常适合这类case组。 对于任何DispatchDeviceControl例程可以完成的IRP,它应该用值为IO_NO_INCREMENT的PriorityBoost调用IoCompleteRequest。DispatchDeviceControl例程必须用每个为进一步处理而在设备排队的请求调用IoMarkIrpPending,并且它必须返回STATUS_PENDING。 较高层驱动程序中的DispatchDeviceControl 通常,一个较高层驱动程序的DispatchDeviceControl例程只是简单地为相邻较低层驱动程序设置IRP中的I/O栈位置,并用IoCallDriver传送。DispatchDeviceControl例程很少检查输入IRP中的参数有效性,因为下层设备驱动程序被假定拥有关于在其自己的设备上如何处理每个设备类型特定的I/O控制请求的更好信息。 对于这个一般规则的例外是在一个类/端口驱动程序对的类驱动程序中的DispatchDeviceControl。关于处理成对的类/端口驱动程序的设备控制请求的更多信息,见6.3.4.3节。 任何与特定设备驱动程序连接并不紧密的新的较高层驱动程序应为相邻较低层驱动程序设置I/O栈位置,并为进一步处理传送IRP_MJ_DEVICE_CONTROL请求。 设备控制请求通常被同步地处理。也就是说,较高层驱动程序的DispatchDeviceControl例程经常如下返回控制到系统: : : return IoCallDriver(DeviceObject->NextDeviceObject, Irp); 当然,如果较低层驱动程序为这样一个请求返回STATUS_PENDING,较高层驱动程序不能使用前面的技术。在这些情况下,较高层驱动程序应设置IRP中的IoCompletion例程。当其IoCompletion例程被IRP调用时,它可以检查I/O状态块以决定IRP是否仍然挂起。如果这样,IoCompletion例程可能重试该请求,或者可能用IRP调用IoMarkIrpPending,在它调用IoCompleteRequest并返回STATUS_PENDING之前。较高层驱动程序决不能用STATUS_PENDING完成一个IRP,除非它首先用IRP调用IoMarkIrpPending。 如果下层设备驱动程序在完成请求之前必须处理大量由设备传送的数据,这时较高层驱动程序可以异步地处理这样一个设备控制请求。也就是说,较高层驱动程序可以设置IRP中它的IoCompletion例程,传送IRP到较低层驱动程序,并且从其自身的DispatchDeviceControl例程返回控制。 几乎所有系统定义的I/O控制码都要求下层设备驱动程序传送合适量的数据,通常远小于一个PAGE_SIZE量。作为一条一般规律,较高层驱动程序应异步地处理这些请求,就如前面代码段所示,因为较低层驱动程序迅速返回控制。也就是说,调用较高层驱动程序的IoCompletion例程的开销无法补偿驱动程序在如此短的间隔内所进行的附加IRP处理。 用IoBuildDeviceIoControlRequest为下层设备驱动程序分配IRP的较高层驱动程序可以同步地处理这些设备控制请求。较高层驱动程序等待一个被传送到IoBuildDeviceIoControlRequest的可选事件,并连接设备分配的IRP。在用那个事件指针调用IoBuildDeviceIoControlRequest之前,驱动程序必须为事件对象提供存储,并且必须用KeInitializeEvent初始化其事件。 类/端口驱动程序中的Dispatc(Internal)DeviceControl 类/端口对的较高层驱动程序有时在其DispatchDeviceControl例程完成IRP。例如,如果一个类驱动程序在初始化时收集并存储关于下层设备特性的信息,这可以在一个传统IRP_MJ_DEVICE_CONTROL请求中查找到,类驱动程序可以满足这样一个请求而不用传送它到下层设备驱动程序以存储处理时间。按照设计者的意愿,类驱动程序也可以为其成对端口驱动程序检查IRP参数以便它可以发送带有效参数的唯一请求。 连接紧密的类/端口驱动程序也定义一组驱动程序的和/或特定设备的I/O控制码,类驱动程序用来为IRP_MJ_INTERNAL_DEVICE_CONTROL请求设置IRP到端口驱动程序。 例如,系统键盘和鼠标类驱动程序发送系统定义的内部设备控制请求以激活/废止键盘/鼠标的,从每个类驱动程序的DispatchCreateClose例程到下层端口驱动程序的中断。这些系统类驱动程序处理IRP_MJ_DEVICE_CONTROL请求,通过为一个下层端口驱动程序设置IRP_MJ_INTERNAL_DEVICE_CONTROL请求。任何与这些系统类驱动程序互操作的新的键盘或鼠标端口驱动程序也必须支持这些公共内部设备控制请求。 关于这些公共内部设备控制请求和如何为新的类/端口驱动程序定义私有I/O控制码的更多信息,见《Windows 2000 驱动程序开发指南》第2卷。 对于一对连接紧密的端口/类驱动程序,类驱动程序可以处理某些设备控制请求过程,而不用传送它们到端口驱动程序。在一个新的类/端口驱动程序对内,类驱动程序的DispatchDeviceControl例程可以做以下工作: 在IRP自身的I/O栈位置中检查参数有效性,如果它找到任何参数错误则设置I/O状态块,并且用IRP和值为IO_NO_INCREMENT的PriorityBoost调用IoCompleteRequest;否则,调用IoGetNextIrpStackLocation,拷贝其自身I/O栈位置到端口驱动程序的I/O栈位置,并用IoCallDriver传送IRP。 或者,仅仅在IRP中设置端口驱动程序的I/O栈位置,而不检查参数并传送到端口驱动程序。 SCSI类驱动程序对于处理设备控制请求有特定要求。关于这些要求的更多信息,见在线DDK。 实现Dispatch(Internal)DeviceControl的注意事项 在实现一个新的DispatchDeviceControl或DispatchInternalDeviceControl例程时要记住以下要点: 至少,较高层驱动程序必须拷贝一个IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL请求的参数从IRP中其自身I/O栈位置到相邻较低层驱动程序的I/O栈位置。这时,它必须用一个指向相邻较低层驱动程序的设备对象的指针和IRP调用IoCallDriver。 当较高层驱动程序为一个较低层驱动程序同步控制的请求返回控制时,它应当传播由IoCallDriver返回或者在返回的I/O状态块中设置的状态值。 下层设备驱动程序必须处理设备控制请求,除非它有一个代表它完成这些请求的子集的、连接紧密的类驱动程序。设备驱动程序的DispatchDeviceControl例程通常通过替换每个IRP的I/O栈位置中的Parameter.DeviceIoControl.IoControlCode来开始处理这些请求。 较低层设备驱动程序应检查请求传送的参数,并用一个适当的错误使IRP失效,如果任何参数无效的话。对这些请求的参数有效性的最一般检查有如下规范: if (Irp->Parameters.DeviceIoControl.InputBufferLength < (sizeof(IOCTL_SPECIFIC_STRUCTURE))) { status = STATUS_XXX; 或者 if (Irp->Parameters.DeviceIoControl.OutputBufferLength < (sizeof(IOCTL_SPECIFIC_STRUCTURE))) { status = STATUS_XXX?; 状态值设置为STATUS_BUFFER_TOO_SMALL或STATUS_INVALID_PARAMETER。 所有设备驱动程序的DispatchDeviceControl或DispatchInternalDeviceControl例程必须通过用适当NTSTATUS值设置I/O状态块,将其Information域置为零,并用一个IO_NO_INCREMENT的PriorityBoost完成IRP来处理对一个未被承认的I/O控制码的接收。 一个设备驱动程序控制的部分I/O控制码必须为相同类型的设备包括任何特定设备类型的、系统定义的I/O控制码。关于系统对于每种类型的设备和每个相关(SDK)的、用ntdd前缀开始的头文件的信息,以及关于系统为这些I/O控制码定义的结构声明的要求的更多信息,参见在线DDK。 一个连接紧密的类/端口驱动程序对的类驱动程序可以处理并完成一部分设备控制请求,而不用传送它们给下层端口驱动程序。当然,这样一个类驱动程序必须传送所有有效的、要求改变设备状态的和那些要求返回设备限制信息(例如当前波特率、流量或视频模式)的设备控制请求。 StartIo和队列管理例程 本章概述了驱动程序的标准StartIo例程的必要功能。也概述了Windows NT/Windows 2000为在驱动程序中建立内部队列提供的支持,并讨论了一些管理驱动程序创建的内部队列的问题。 本章包含以下信息: 7.1 排队IRP 7.2 StartIo例程需求 7.2.1 设备驱动程序中的StartIo例程 7.2.2 StartIo例程功能 7.2.2.1 使用缓冲I/O的设备驱动程序StartIo例程 7.2.2.2 使用直接I/O的设备驱动程序StartIo例程 7.2.2.3 在StartIo中处理设备控制请求 7.2.3 对设备编程以实现I/O操作 7.2.4 较高层驱动程序中的StartIo例程 7.2.5 实现StartIo的注意事项 7.3 使用互锁队列或设备队列的需求 7.3.1 驱动程序管理的队列的存储需求 7.3.2 建立驱动程序管理的队列 7.3.3 驱动程序管理的队列中排队的IRP 7.3.4 用驱动程序创建的线程管理互锁队列 7.3.5 管理补充设备队列 排队IRP 因为Windows NT/Windows 2000在多任务及多线程环境中支持异步I/O,I/O请求可以比驱动程序处理完成更快地到达,特别是在一个多处理器的机器中。因此,任何具体设备在忙与处理另一个IRP时,必须让超出限度的IRP在驱动程序上排队。 因此,最低层驱动程序必需以下之一: StartIo例程,I/O管理器用一个IRP调用它来启动一个该驱动程序的设备上的I/O操作 它自己的IRP排队机制,它用来控制比驱动程序的处理速度更快地到达的IRP 仅有一个最低层设备驱动程序能在其Dispatch例程中满足和完成所有可能的IRP,而不需要StartIo例程和/或驱动程序管理的IRP队列. 较高层驱动程序几乎从来没有StartIo例程。大多数中间层驱动程序也没有内部队列;一个中间层驱动程序通常从其Dispatch例程传送带有效参数的IRP,并且为任何其IoCompletion例程中的IRP做必要的处理后操作。 通常,以下内容描述了一些确定实现一个StartIo例程是否使用内部的、系统管理的IRP队列的设计考虑。 驱动程序中的StartIo例程 大多数设备驱动程序有StartIo例程,因为大多数PC外设同时仅能处理一个设备I/O操作。对于有一个StartIo例程的驱动程序,I/O管理器提供IoStartPacket和IoStartNextPacket来管理驱动程序中的IRP队列。 较高层驱动程序通常没有StartIo例程。关于在较高层驱动程序中折衷实现StartIo例程的更多信息,见“较高层驱动程序中的StartIo例程”。 驱动程序中的内部IRP队列 如果一个下层设备可以支持多个并发的I/O操作,最低层设备驱动程序必须建立内部请求队列,并管理其自身的IRP队列。例如,系统串行驱动程序为其设备上的读、写、清除和等待操作维护不同的队列,因为它支持全双工串行设备。 发送请求给一些下层设备驱动程序的较高层驱动程序也维护内部IRP队列。例如,文件系统驱动程序几乎总拥有内部IRP队列。 更多信息,见“使用互锁队列或设备队列的需求”。 驱动程序中带有StartIo例程的内部队列 管理其自身内部队列的驱动程序可以拥有StartIo例程,但是并不需要。大多数最低层设备驱动程序拥有StartIo例程或者管理它们的IRP队列。 SCSI类驱动程序是一个例外,它有一个StartIo例程并管理内部IRP队列。I/O管理器在与驱动程序创建的、代表一个SCSI HBA的设备对象连接的设备队列中排队IRP到端口驱动程序的StartIo例程。SCSI端口驱动程序也为每个机器中任意HBA驱动的SCSI总线上的目标设备(与一个SCSI逻辑单元相关的)建立各自的设备队列对象,并管理IRP队列。 无论何时SCSI总线上某个设备特别忙,SCSI端口驱动程序使用它的补充设备队列保有从在LU特定队列中SCSI类驱动程序发送来的IRP。实际上,该驱动程序的补充的、LU特定的设备队列允许SCSI端口驱动程序为通过HBA的不同SCSI设备序列化操作,而同时保持在那个HBA的SCSI总线上的设备尽可能的忙。 内部队列和驱动程序线程 带有设备专有线程的驱动程序和使用执行工作者线程的最高层驱动程序(包括大多数文件系统驱动程序)通常在它们设备对象的设备扩展中设置一个互锁的IRP队列。该队列由驱动程序线程或驱动程序提供的回调工作者线程和其他处理IRP的驱动程序例程共享。 驱动程序的Dispatch例程把IRP插入到互锁队列,并且一个驱动程序创建的线程或驱动程序的工作者线程通过调用ExInterlocked..List支持例程把它们移去。 例如,系统软盘控制器驱动程序使用这样一个互锁队列。其设备专有线程进行与其他设备驱动程序的StartIo例程所做的相同的IRP处理,以及一些与其他设备驱动程序的DpcForIsr例程所做的相同的IRP处理。 StarIo例程需求 StartIo例程由I/O管理器定义如下: VOID ( *PDRIVER_STARTIO) ( IN PDRIVER_OBJECT DeviceObject, IN PIRP Irp ); StartIo例程运行在位于IRQLDISPATCH_LEVEL上的任意线程环境中。 运行在IRQLDISPATCH_LEVEL上限制了这组StartIo例程可以调用的支持例程。关于管理IRQL的更多信息见第16章中“硬件优先级管理”。关于任何特定支持例程可能在其上被调用的IRQL的具体信息,见《Windows 2000 驱动程序开发指南》第2卷。 设备驱动程序中的StartIo例程 正如它的名字所暗示的,最低层设备驱动程序中的StartIo例程负责启动物理设备上的I/O操作。 当一个最低层设备驱动程序的StartIo例程被调用时,它假定有输入设备对象代表的目标设备并不忙。刚刚调用IoStartPacket的驱动程序Dispatch例程和IRP并没有被插入到与目标设备相连的设备队列,并且驱动程序的DpcForIsr例程也正完成另一个请求,并刚刚调用过IoStartNextPacket。 在最高层设备驱动程序中的StartIo例程被调用之前,驱动程序的Dispatch例程必须查找并锁定用户缓冲区。如果必要,在对其StartIo例程排队的IRP中设置有效映射缓冲区地址。如果最高层设备驱动程序为直接I/O(或非直接也非缓冲I/O)建立其设备对象,驱动程序不能延迟锁定用户缓冲区到其StartIo例程;所有StartIo例程在一个位于IRQLDISPATCH_LEVEL的任意线程环境中被调用。 记住以下情况: 任何由一个驱动程序的StartIo例程访问的缓冲内存必须被锁定或者从常驻系统空间内存中分配,并且必须是在一个任意线程环境中可访问的。 为缓冲I/O建立其设备对象的驱动程序可以依赖I/O管理器传送所有它发送给驱动程序的IRP中的有效缓冲区。为直接I/O建立设备对象的较低层驱动程序可依靠同系列中最高层驱动程序传送所有经由任意一个中间层驱动程序到下面的较低层设备驱动程序的IRP。 通常,任何较低层设备驱动程序的StartIo例程负责用输入IRP调用IoGetCurrentIrpStackLocation,并进行任何必需在其设备上启动I/O操作的特定请求处理。特定请求处理包括以下: 建立或更新任何有关当前驱动程序保留的请求的状态信息。该状态信息可能存放在目标设备对象的设备扩展或者其他由驱动程序分配的非页式缓冲池中。 例如,如果一个设备驱动程序维护一个关于当前传输操作的InterruptExpected布尔量,其StartIo例程可能设置这个变量为TRUE。如果该驱动程序为当前操作维护一个超时计数器,其StartIo例程可以设置这个值,或者StartIo例程可以对驱动程序的CustomerTimerDpc 例程排队。 如果状态信息或任何其他资源是与其他驱动程序例程共享的,该状态区域或资源必须由一个自旋锁保护。如果StartIo例程与其他“非ISR”例程共享状态或资源,它必须在访问之前获得保护状态或资源的自旋锁。如果StartIo例程与驱动程序的ISR共享状态,StartIo必须用一个驱动程序提供的、设置状态的SynchCritSection例程调用KeSynchronizeExecution。 更多细节,见第16章的“自旋锁的使用”。 分配一个序列号给IRP以防驱动程序在处理IRP时必须注册一个设备I/O错误 更多信息,见第16章的“错误记录和NTSTATUS值”。 如果必要,将驱动程序I/O栈位置中的参数转换为系统特定值 例如,一个磁盘驱动程序可能需要为一次传输操作计算开始扇区和到磁盘物理地址的偏移量,并且计算请求的传输长度是否将超出一个特定扇区范围或超出其物理设备的传输能力。 如果驱动程序控制一个可删除媒体设备,在该设备为I/O编程之前检查媒体变化,如果媒体有变化的话,通知其上的文件系统 更多信息,见第16章“可删除媒体控制”。 如果设备使用DMA,检查请求的长度(将被传输的字节数,可在IRP的驱动程序的I/O栈位置找到)是否应被分割成部分传输操作,就象第3章所说明的,假定连接紧密的较高层驱动程序并不为该设备驱动程序事先分割大的传输请求 这类设备驱动程序的StartIo例程也负责调用KeFlushIoBuffers,并且,如果该驱动程序使用基于包的DMA,则它负责用驱动程序的AdapterControl例程调用AllocateAdapterChannel。 更多细节,见第11章“AdapterControl和ControllerControl例程”,以及第16章的“为DMA和PIO维护高速缓存一致性”。 如果设备使用PIO,对于一个系统空间地址,用Windows 2000上的MmGetSystemAddressForMdlSafe或者WDM驱动程序中的MmGetSystemAddressForMdl映射缓冲区虚拟基地址(在IRP 的Irp->MdlAddress处)。 对于读请求,设备驱动程序的StartIo例程负责在PIO操作开始之前调用KeFlushIoBuffers。更多信息,见第16章的“为DMA和PIO维护高速缓存一致性”。 如果Windows 2000驱动程序使用一个控制器对象,用其ControllerControl例程调用IoAllocateController。 如果驱动程序控制可撤消的IRP,检查输入IRP是否已经被撤消 如果一个输入IRP可以在它完成前被撤消,StartIo例程必须用IRP和驱动程序的Cancel例程入口点调用IoSetCancelRoutine。StartIo例程必须为它对IoSetCancelRoutine的调用获得撤消自旋锁。 StartIo例程功能 到达一个设备驱动程序Dispatch例程的IRP不一定非要驱动程序在其设备上进行I/O处理。通常,每个驱动程序的Dispatch例程完成一部分要求的请求,并为完成它们传送一些给其他驱动程序例程。Dispatch例程也完成任何驱动程序I/O栈位置中有参数错误的IRP。 当然,所有带一个StartIo例程的设备驱动程序往往为IRP_MJ_READ与IRP_MJ_WRITE请求从其Dispatch例程调用IoStackPacket,并且通常对于部分I/O控制码,它支持IRP_MJ_DEVICE_CONTROL请求。 I/O管理器对驱动程序的Dispatch例程的调用是满足一个设备I/O请求的第一阶段。StartIo例程是第二阶段。当一个Dispatch例程用IRP调用IoStartPacket,I/O管理器为进一步处理直接传送IRP到StartIo例程,或者在创建包时插入包到与目标设备对象连接的设备队列。 作为一般规则,使用缓冲I/O的驱动程序有一个比使用直接I/O的驱动程序更简单的StartIo例程。使用缓冲I/O的驱动程序为每个传输请求传输少量的数据,而那些使用直接I/O的驱动程序(不论DMA或PIO)传输大量数据到/从锁定的缓冲区,这些缓冲区可以在系统内存中扩展物理页面边界。 在物理设备驱动程序之上分层的较高层驱动程序通常建立它们的设备对象与它们各自的设备驱动程序相匹配。当然,一个最高层驱动程序,特别是一个文件系统驱动程序,可以建立非直接也非缓冲I/O的设备对象。关于建立直接I/O和缓冲I/O的设备对象的更多信息,见第3章的“设备对象和设备扩展”。 使用缓冲I/O的设备驱动程序StartIo例程 假定I/O管理器的DispatchRead、DispatchWrite或DispatchDeviceControl例程已经确定一个请求的有效性,并调用了IoStackPacket,那么I/O管理器调用一个最低层设备驱动程序的StartIo例程立即处理包,如果设备不忙的话。如果这个包由IoStartPacket排队,最终从驱动程序的DpcForIsrh或CustomDpc例程对IoStackNextPacket的调用会引起StartIo例程被使用出列的IRP调用。 StartIo例程调用IoGetCurrentIrpStackLocation,并确定为满足请求必须执行哪些操作。它在对执行I/O请求的物理设备编程之前,用任何必要的方法预处理IRP。 如果对物理设备(或设备扩展)的访问必须由驱动程序的ISR同步,StartIo例程必须调用驱动程序的SynchCritSection例程以执行必要的设备编程。更多信息,见“I/O操作设备编程”。 一个使用缓冲I/O的物理设备驱动程序传输数据到(读)或从(写)一个由I/O管理器分配的、驱动程序在每个IRP的Irp->AssociatedIrp.SystemBuffer找到的系统空间缓冲区。 使用直接I/O的设备驱动程序StartIo例程 假定其DispatchRead、DispatchWrite或DispatchDeviceControl例程已经确定一个请求的有效性,并调用了IoStartPacket,一个设备驱动程序的StartIo例程被调用以处理下一个包。如果这个包由IoStartPacket排队,最终从驱动程序的DpcForIsrh或CustomDpc例程对IoStackNextPacket的调用会引起StartIo例程被使用出列的IRP调用。 StartIo例程调用IoGetCurrentIrpStackLocation,并确定哪个请求必须执行以满足请求。它用任何必要的方法预处理IRP,例如分割一个大的DMA传输请求为部分传输容量,并存储一个将要到达的,必须被分割的传输请求的长度的相关状态。这时它为执行I/O请求对物理设备编程。 如果对物理设备(或设备扩展)的访问必须由驱动程序的ISR同步,StartIo例程必须使用一个驱动程序提供的SynchCritSection例程来执行必要的编程。更多信息,见“I/O操作设备编程”。 任何使用直接I/O的驱动程序对一个锁定的、由驱动程序在IRP中Irp->MdlAddress处找到的内存描述符(MDL)描述的缓冲区读或写数据。这样一个驱动程序一般为设备控制请求使用缓冲I/O。更多信息,见“处理StartIo中的设备控制请求”。 作为由内存管理器定义,专用的,并且可变的类型,所有MDL对驱动程序都是不透明的。考虑以下实现方针: 驱动程序不能直接使用包含在任何MDL中的地址映射,包括位于Irp->MdlAddress处的MDL。 相反,使用PIO的驱动程序重新映射用户缓冲区,通过用Irp->MdlAddress调用MmGetSystemAddressForMdlSafe(仅在Windows 2000驱动程序中)或MmGetSystemAddressForMdl(WDM驱动程序)。在它们的带缓冲区地址的传输操作重新映射到它们的驱动程序的逻辑范围时,使用DMA的驱动程序也传送Irp->MdlAddress到支持例程。 如果一个连接紧密的较高层驱动程序不为下层的设备驱动程序预先分割大的DMA传输请求,一个最低层设备驱动程序的StartIo例程必须分割每个大于其设备在一次单独传输操作中所能完成的容量的传输请求。使用系统DMA的驱动程序被要求分割对于系统DMA控制器或者其设备在一次单独的传输操作中的处理能力来说过大的传输请求。 如果该设备是一个从属 DMA设备,其驱动程序必须同步一个带有驱动程序分配的适配器对象,代表DMA通道的系统DMA控制器和一个驱动程序提供的AdapterControl例程之间的传输。一个总线控制DMA设备的驱动程序也必须使用一个驱动程序分配的适配器对象使它的传输同步,并且必须提供一个AdapterControl例程(如果它使用系统基于包的DMA支持)或者一个AdapterListControl例程(如果它使用系统的散布/收集支持)。 依据驱动程序的设计,可以使用一个控制器对象使一个物理设备上的传输与设备控制操作同步,并提供一个ControllerControl例程。 更多信息,见第3章的“适配器对象和DMA”和第11章的“AdapterControl和ControllerControl例程”。 在StartIo中处理设备控制请求 通常,仅有部分设备I/O控制请求被从DispatchDevice(Internal)Control例程传送,以供最低层设备驱动程序的StartIo例程做进一步处理。也就是说,任何最低层驱动程序的StartIo例程必须处理要求设备状态改变或返回当前设备状态易失信息的有效设备控制请求。 每个新的驱动程序必须与所有同类设备的其他驱动程序一样支持相同的公共I/O控制码组。系统为作为缓冲请求的IRP_MJ_DEVICE_CONTROL请求定义公共的、特定设备类型的I/O控制码。 因此,物理设备驱动程序做从/到一个系统空间缓冲区的数据传输,每个驱动程序在设备控制请求的IRP中的Irp->AssociatedIrp.SystemBuffer找到这个缓冲区。每个为直接I/O建立设备对象的驱动程序使用缓冲I/O满足带有公共I/O控制码的设备控制请求。 每个I/O控制码的定义决定请求的数据传输是否被缓冲。任何为驱动程序对之间的特定驱动程序的IRP_MJ_INTERNAL_DEVICE_CONTROL请求私有定义的I/O控制码却能定义一个带缓冲方法、直接方法或两者都不是的代码。作为一般规律,如果一个连接紧密的较高层驱动程序必须为该请求分配缓冲区,任何私有定义的I/O控制码应不能用这三种中的最后一种定义。 关于任何一般类型的PC设备必须支持的一个新驱动程序的I/O控制码和私有I/O控制码的定义的更多信息,见在线DDK。 I/O操作设备编程 通常,设备驱动程序中的StartIo例程必须通过用系统提供的SynchCritSection例程调用KeSynchronizeExecution同步访问任何内存或它与驱动程序的ISR共享的设备注册器。 换句话说,一个最低层设备驱动程序的StartIo例程通常导致SynchCritSection例程为位于DIRQL的I/O对物理设备编程。 在用对设备编程的SynchCritSection例程调用KeSynchronizeExecution之前,StartIo例程必须进行该请求必要的处理。预先处理包括计算一个初始化部分传输容量,并存储任何关于对其他设备例程初始请求的状态信息。 如果设备驱动程序使用DMA,其StartIo例程通常用一个系统提供的AdapterControl例程调用AllocateAdapterChannel。在这些情况下,StartIo例程把对物理设备编程的响应推迟到AdapterControl例程。紧接着,它调用KeSynchronizeExecution使一个驱动程序提供的SynchCritSection例程为一个DMA传输对设备编程。 关于驱动程序例程如何与一个ISR共享资源,或者多个非ISR例程之间如何以多处理器安全方式共享资源,见第16章的“使用自旋锁”。关于DMA使用的更多信息,见第3章的“适配器对象和DMA”和第11章的“AdapterControl和ControllerControl例程”。 较高层驱动程序中的StartIo例程 任何较高层驱动程序都有一个StartIo例程。当然,这样一个驱动程序往往不能与现存的较低层驱动程序共同操作,并且往往表现出很差的性能特性。 较高层驱动程序中的StartIo例程有下列作用: 可以通过从驱动程序的Dispatch例程调用IoStartPacket和从其IoCompletion例程调用IoStartNextPacket对到达的IRP排队,从而使得通过StartIo例程一次处理一个IRP。 在繁重的I/O命令期间,驱动程序的I/O吞吐量显著变小,因为其StartIo例程变成了一个瓶颈。 驱动程序的StartIo例程用位于IRQLDISPATCH_LEVEL的每个IRP调用IoCallDriver,由此使得所有较低层驱动程序的Dispatch例程也在IRQLDISPATCH_LEVEL上运行。这限制了较低层驱动程序可以在Dispatch例程中调用的支持例程。因为大多数驱动程序开发者假定它们的驱动程序的Dispatch例程在IRQLDISPATCH_LEVEL上运行,较高层驱动程序往往不能与许多现存的较低层驱动程序共同操作。 StartIo例程减少系统整体吞吐量,因为它和所有较低层驱动程序的Dispatch例程在提高的IRQL上处运行。 关于标准驱动程序例程在其上运行的IRQL的更多信息,见第16章的“硬件优先级管理”。关于任何具体的标准驱动程序的特定信息,见第5章到第15章的相关章节。 没有一个系统提供的较高层驱动程序有StartIo例程,因为对于所有其上和其下的驱动程序以及系统整体,它延缓驱动程序本身的IRP处理。 大多数较高层驱动程序只是从它们的Dispatch例程发送IRP到较低层驱动程序,并在它们的IoCompletion例程中进行任何必要的清除处理。 当然,较高层驱动程序可以为请求特定操作的IRP建立内部队列,或者建立内部队列以控制一组不同种类的下层设备,象SCSI端口驱动程序,的IRP界限,如“IRP排队”中所示的。 实现StartIo的注意事项 当实现StartIo例程时记住以下要点: StartIo例程必须使它对物理设备和对驱动程序在设备扩展中维护的共享状态信息或资源的访问与访问相同设备、内存位置或资源的这个驱动程序的其他例程同步。 如果StartIo例程与ISR共享设备或状态,它必须用系统提供的SynchCritSection例程调用KeySynchronizeExecution以对设备编程或访问共享状态。 如果StartIo例程与ISR之外的其他例程共享状态或资源,它必须用一个驱动程序初始化的、驱动程序提供存储的执行自旋锁保护共享的状态与资源。 如果一个整体式的Windows 2000设备驱动程序建立一个控制器对象,其StartIo例程可以通过一个共享的物理设备使用该控制器对象与附属(类似)设备同步操作。 更多信息,见第3章的“控制器对象”和第11章“AdapterControl和ControllerControl例程”。 如果一个密切相关的较高层驱动程序不为其下层设备驱动程序预先分割大的DMA传输请求,下层设备驱动程序的StartIo例程必须为部分传输请求分割大的传输请求,并且驱动程序必须执行一系列部分传输设备操作。每个部分传输必须使其规模适合硬件能力:要么是驱动程序的设备的能力,要么,对于一个从属DMA设备来说,是任意一个有严格限制的系统DMA控制器的能力。 关于系统或总线控制DMA的使用的更多信息,见第3章的“适配器对象和DMA”。 使用DMA的驱动程序的StartIo例程必须同步使用适配器对象的传输。 StartIo例程在IRQL DISPATCH_LEVEL上运行 ,限制了一组调用的支持例程。 例如,一个StartIo例程既不能访问,也不能分配可分页内存,并且它也无法在一个适配器对象上等待。另一方面,一个StartIo例程可以用KeAcquireSpinLockAtDpcLevel和KeRealseSpinLockFromDpcLevel获得和释放一个驱动程序分配的执行选自旋锁,KeAcquireSpinLockAtDpcLevel和KeRealseSpinLockFromDpcLevel可以比KeAcquireSpinLock和KeRealseSpinLock运行的更快些。 更多信息,见第16章的“硬件优先级管理”和“自旋锁的使用”。 如果驱动程序以可撤消状态持有IRP,其StartIo例程必须在它为其设备上的请求启动任何处理之前检查输入IRP是否已经被撤消。关于可撤消IRP的管理的更多信息,见第12章“Cancel例程”。 使用互锁队列或设备队列的需求 最低层驱动程序要么必须拥有一个StartIo例程,要么必须建立及管理其自身的内部IRP队列。较高层驱动程序可以拥有其自身的内部IRP队列,但不需要这么做。 根据要求的功能与下层设备的属性,,你可以选择实现一个StartIo例程还是建立一个或多个驱动程序管理的IRP补充队列。 系统为以下驱动程序可以使用的内部IRP队列提供支持: 一个双重连接的互锁队列,驱动程序通过调用ExInterlockedInsertTailList或ExInterlockedINsertHeadList插入IRP到其中,并且一个设备专有线程、回调工作者线程或另一个驱动程序例程通过调用ExInterlockedRemoveHeadList从其中移去IRP 一个与设备对象连接的补充设备队列,驱动程序通过调用KeInsertDeviceQueue或KeInsertByKeyDeviceQueue插入IRP到其中,并且它可以通过调用KeRemoveDeviceQueue、KeRemoveByKeyDeviceQueue或KeRemoveEntryDeviceQueue从其中移去IRP 如果驱动程序拥有一个StartIo例程,它可以通过调用IoStartNextPacket或IoStackNextPacketByKey从与其设备对象连接的设备队列中加入和删除IRP。 一个互锁队列更容易管理,尤其是对于带设备专有线程或回调工作者线程的驱动程序。另一方面,补充设备队列给驱动程序更多的关于给定IRP是否被放置在队列中以及排序队列中项的控制。 驱动程序不应使用一个单连接的IRP互锁队列。 驱动程序管理的队列的存储需求 任何使用一个互锁或补充设备队列的驱动程序必须为必需的系统定义的对象和资源提供存储空间,如下: 对于一个双重连接互锁队列,既要为ExInterlocked..List例程的参数要求的一个执行自旋锁提供存储空间,也要为LIST_ENTRY类型的队列头提供存储空间。 对于一个设备队列,为一个内核定义设备队列对象提供存储空间。 每个设备队列对象有一个与执行自旋锁和一个队列头的连接;当驱动程序为其设备队列对象提供存储空间时这些资源的存储是自动分配的。 驱动程序通常在其设备对象的设备扩展中提供该存储区间,但是它们也可以调用PoolType参数设置为NonPagedPool的ExAllocatePool分配存储区间。另一种方法是,使用一个控制器对象的Windows 2000驱动程序可以在一个控制器扩展中提供存储区间。 关于缓冲池内存分配的更多信息,见第16章的“内存使用管理”。 建立驱动程序管理的队列 当驱动程序处理PnP管理器的IRP_MN_START_DEVICE请求时,它在设备启动期间建立其队列。队列必须如下来建立: 对于使用一个双重连接互锁队列的驱动程序,它必须通过调用一个运行时库例程InitializeListHead初始化队列头。还必须通过调用KeInitializeSpinLock初始化执行自旋锁。 对于使用一个补充设备队列的驱动程序,它必须调用KeInitializeDeviceQueue以初始化设备队列对象。 如果一个新驱动程序为一个未确定(但在任何给定情况下都是有限的)数量的固定大小的,驱动程序结构化的入口建立内部队列,后备队列可以被用来分配和释放这类入口的内存。也可参见《Windows 2000驱动程序开发指南》第2卷中的Ex..LookasideList。 关于建立与使用设备队列的更多信息,见第3章的“设备队列对象和互锁队列”。 驱动程序管理的队列中排队的IRP 驱动程序可以通过用相关特定IRP参数调用ExInterlocked..List或Ke..DeviceQueue来支持例程对两种队列插入或删除IRP: 对于双重互锁队列是&Irp->Tail.Overlay.ListEntry 对于补充设备队列是&Irp->Tail.Overlay.DeviceQueueEntry 驱动程序也可以为驱动程序定义的数据结构建立内部队列或缓冲区,如第6章的“DispatchRead或DispatchWrite功能”中提到的。 用驱动程序创建的线程管理互锁队列 如同系统软盘控制器驱动程序,它带有设备专有线程,而不是StartIo例程的驱动程序,通常在一个双重连接互锁队列内管理其自身的IRP队列。当设备上有任务运行时,驱动程序从其互锁队列中取出IRP。 通常,驱动程序必须管理与任何由线程和其他驱动程序例程共享的资源的线程的同步。驱动程序也必须有方法通知其驱动程序创建的线程IRP被排队。通常,线程等待一个调度者对象,在设备扩展中存储,直到驱动程序的Dispatch例程在插入一个IRP到互锁队列之后设置调度者对象为Signaled状态。 当驱动程序的Dispatch例程被调用时,每个例程检查输入IRP的I/O栈位置中的参数,如果它们有效的话,为进一步处理对请求排队。对于每个为驱动程序专有线程排队的IRP,Dispatch例程应建立其线程需要处理的任何环境,在它调用ExInterlockedInsert..List之前。每个IRP中的驱动程序I/O栈位置使驱动程序线程可以访问目标设备对象的设备扩展,在那里驱动程序可以与其线程共享环境信息,就如同线程从队列中移走每个IRP一样。 当驱动程序调用PsCreateSystemThread时,任何驱动程序创建的线程运行在IRQL PASSIVE_LEVEL上和一个前面已设置好的基本运行时优先级处。当从驱动程序内部队列中移出IRP时,线程对ExInterlockedRemoveHeadList的调用暂时提升了当前处理器上DISPATCH_LEVEL的IRQL。在从该调用返回时,初始IRQL被恢复成PASSIVE_LEVEL。 任何驱动程序线程(或驱动程序提供的工作者线程回调)必须管理它在其上运行的IRQL。例如,考虑以下内容: 因为系统线程通常运行在IRQL PASSIVE_LEVEL,一个驱动程序线程可能等待内核定义的调度者对象。 例如,一个设备专有线程可以为其他驱动程序例程等待一个事件以满足并完成一定量的、该线程用IoBuildSynchronousFsdRequest设置的部分传输IRP。 当然,这样一个设备专有线程必须在调用某些支持例程之前提高当前处理器上的IRQL。 例如,如果一个驱动程序使用DMA,其设备专有线程必须在调用KeRaiseIrql和KeLowerIrql之间嵌套调用AllocateAdapterChannel和FreeAdapterChannel,因为这些例程和其他的DMA操作具体支持例程必须在IRQL DISPATCH_LEVEL处调用。 记住StartIo例程在DISPATCH_LEVEL处运行,所以使用DMA的驱动程序不需要从它们的StartIo例程调用Ke..Irql。 驱动程序创建的线程可以访问页式内存,因为它在位于IRQL PASSIVE_LEVEL的非任意线程环境(它自身的)运行,但是许多其他的标准驱动程序例程在提升的IRQL处运行。 因此,如果一个驱动程序创建的线程分配可由其他驱动程序例程访问的内存,它必须必须从非页式缓冲池分配内存。例如,如果一个设备专有线程分配任何可由驱动程序的ISR或SynchCritSection、 AdapterControl、 AdapterListControl、 ControllerControl、 DpcForIsr、 CustomDpc、 IoTimer,、CustomTimerDpc或在较高层驱动程序中的IoCompletion例程访问的缓冲区,该线程分配的内存不能是可分页的。 如果驱动程序在设备扩展中维护共享的状态信息或资源,驱动程序例程(例如StartIo例程)必须将它对物理设备和共享数据的访问与访问相同设备、内存位置或资源的驱动程序的其他例程同步。 如果线程与ISR共享设备或状态,它必须用驱动程序提供的SynchCritSection例程调用KeSynchronizeExecution从而对设备编程或访问共享数据。 如果线程与例程,而不是与ISR,共享状态或资源,驱动程序必须用一个驱动程序初始化的、由驱动程序提供存储空间的执行自旋锁保护共享的状态或资源。 关于使用一个慢速设备的驱动程序线程的折衷设计的更多信息,见第16章的“轮询设备”。也可参考第16章的“硬件优先级管理”。关于调用任何特定支持例程所必需的IRQL的详细信息,见在线DDK。 管理补充设备队列 I/O管理器(除FSD外)通常在驱动程序调用IoCreateDevice时创建一个相连的设备队列对象。它也提供IoStartPacket和IoStartNextPacket,驱动程序可以调用它们以使I/O管理器插入IRP到相连的设备队列或者调用它们的StartIo例程。 因此,驱动程序为IRP建立自己的设备队列对象是没有必要的,或者说是没有什么特别的用处。这主要是对驱动程序,例如SCSI端口驱动程序,它必须为通过一个独立控制器或总线适配器服务的不同种类的设备,以协调来自一些连接紧密的类驱动程序的将要到达的IRP。 换句话说,一个磁盘阵列控制器的驱动程序更可能使用一个驱动程序创建的控制器对象,而不是建立补充设备队列对象;然而一个附加总线适配器的驱动程序和一组类驱动程序中的驱动程序使用补充设备队列的可能性就要小一些。 对补充设备队列的支持 KeInsert..DeviceQueue例程返回一个Boolean值表明给定IRP是否被插入到设备队列。KeInsertDeviceQueue要么插入在设备队列尾部插入一个项并设置设备队列对象状态为忙,要么简单地将状态设置为忙并返回FALSE以表明IRP没有被排队。一个建立了补充设备队列的驱动程序可以通过调用KeInsertByKeyDeviceQueue将一个驱动程序确定的指令强加到IRP处理过程中,这个调用还要返回一个Boolean值表明给定的IRP是否被插入到设备队列中。 对于直接调用KeInsertDeviceQueue或KeInsertByKeyDeviceQueue的驱动程序,考虑以下实现方针: 如果返回值为FALSE,调用者必须立即通过其他驱动程序例程为进一步处理传送IRP。 对KeInsertDeviceQueue或KeInsertByKeyDeviceQueue的调用设置设备队列对象为Busy,甚至队列为空以及输入IRP并不插入到队列中也是如此。逆调用KeRemove..DeviceQueue重置一个空设备队列对象的状态为Not-Busy。 驱动程序可以通过调用KeRemoveDeviceQueue从其补充设备队列中删除IRP(或者重置设备队列状态为Non-Busy)。驱动程序也可以通过调用KeRemoveEntryDeviceQueue删除一个特定入口或确定一个特定入口是否在当前队列。 使用带StartIo例程的补充设备队列 通过调用IoStartPacket和IoStartNextPacket,驱动程序的Dispatch和DpcForIsr(或CustomDpc)例程同步对其使用设备队列的StartIo例程的调用,这个设备队列是在驱动程序创建设备对象时由I/O管理器调用的。对于带有StartIo例程的端口驱动程序,IoStartPacket和IoStartNextPacket插入及删除端口驱动程序的共享设备控制器/适配器的设备队列中的IRP。如果端口驱动程序也建立补充设备队列以控制来自相关的较高层类驱动程序的请求,它必须对输入的IRP分类排进其补充设备队列,通常在其StartIo例程中。 在试图插入IRP到适当的队列中之前,端口驱动程序必须确定每个IRP属于哪个补充设备队列。一个指向目标设备对象的指针被一个IRP传送到驱动程序的Dispatch例程。驱动程序将存储该指针以在对到达IRP分类时使用。注意,传送给StartIo例程的设备对象指针是驱动程序自身的设备对象,代表设备控制器/适配器,所以它不能为该目的使用。 在IRP排队之后,驱动程序为其共享的控制器/适配器编程以执行该请求。因此,端口驱动程序可以基于“先来先服务”为所有设备处理到达请求,直到一个对KeInsertDeviceQueue的调用将一个IRP放入一个特定类驱动程序的设备队列中。 通过对所有将由StartIo处理的IRP均使用自己的设备队列,下层端口驱动程序通过共享设备(或总线)控制器/适配器串行操作所有附加设备。通过有时在一个单独设备队列中为每个支持的设备持有IRP,端口驱动程序在为所有通过其共享资源进行I/O的其他设备增加I/O流量时,禁止对于一个已忙着的设备处理IRP。 作为对调用来自端口驱动程序的Dispatch例程的IoStartPacket的响应,I/O管理器要么立即调用该驱动程序的StartIo例程,要么把IRP放入与端口驱动程序共享的控制器/适配器的设备对象相连的设备队列中。 端口驱动程序必须维护其自身的、关于每个不同类型的、它通过共享的设备控制器/适配器为之服务的设备的状态信息。 当用补充设备队列设计类/端口驱动程序时,记住以下几点: 驱动程序不容易获得指向由其上的驱动程序创建的设备对象的指针,除了其设备栈中位于栈顶的设备对象。 通过设计,I/O管理器不再为获得这样一个指针提供支持例程。另外,位于驱动程序被装入处的指令不可能为较低层驱动程序获得较高层驱动程序的设备对象的指针,这些设备对象在任何较低层驱动程序加入到其设备时还没有创建。 虽然IoGetAttachedDeviceReference返回一个指向一个驱动程序栈中的最高层设备对象的指针,驱动程序应当仅仅使用该指针设计一个其栈的I/O请求的目的地址。驱动程序不应试图读或写该设备对象。 驱动程序不能使用一个指向由任何其上的驱动程序创建的设备对象的指针,除了给它自己的设备栈的栈顶发请求。 在多处理器安全模式下,无法同步访问两个驱动程序之间的一个单独的设备对象(和其设备扩展)。驱动程序也不能对其他驱动程序当前正进行的I/O处理进行任何假定。 即使对于联系紧密的类/端口驱动程序,每个类驱动程序应仅仅使用指向端口驱动程序的设备对象的指针传送使用IoCallDriver的IRP。下层端口驱动程序必须维护其自身的、可能在该端口驱动程序的设备扩展中的、关于它为任何联系紧密的类驱动程序的设备处理的请求的状态。 通过驱动程序例程管理补充设备队列 任何为一组联系紧密的类驱动程序而在补充设备队列中排队的IRP的端口驱动程序也必须有效处理以下情况: 其Dispatch例程为一个特定设备在驱动程序创建的设备队列中为该设备插入IRP。 其他设备的IRP继续到达,排队等待驱动程序的带IoStartPacket的StartIo例程,并通过共享的设备控制器被处理。 设备控制器不会变空闲,但是每个驱动程序创建的设备队列中的IRP一有可能的话也必须排队等待驱动程序的Dispatch例程。 因此,无论何时端口驱动程序完成一个IRP,端口驱动程序的DpcForIsr必须尝试从一个特定设备的驱动程序内部设备队列传输一个IRP到共享适配器/控制器设备队列。如下: DpcForIsr例程调用IoStartNextPacket以使StartIo例程开始处理共享设备控制器队列中下一个IRP。 DpcForIsr例程调用KeRemoveDeviceQueue以从其内部设备队列中移去下一个IRP,该设备作为将要完成一个IRP的代表。 如果KeRemoveDeviceQueue返回一个非NULL指针,DpcForIsr例程用刚从队列中取出的IRP调用IoStartPacket以使它排队等待共享的设备控制器/适配器。否则,对KeRemoveDeviceQueue的调用简单地重置设备队列对象状态为Not-Busy,并且DpcForIsr省略对IoStartPacket的调用。 这时,DpcForIsr用输入IRP调用IoCompleteRequest,端口驱动程序要么通过设置I/O状态块为一个错误,要么通过满足I/O请求刚好为这些IRP完成I/O处理。 注意,预先排序意味着DpcForIsr例程还必须确定哪个设备完成当前(输入)IRP以便有效管理内部IRP队列。 如果在其补充设备队列中取出IRP之前,端口驱动程序试图等待,直到其共享的控制器/适配器空闲,在它立即服务其他每个设备(这些设备当前的I/O任务实际上比较轻)期间,驱动程序可能 “饿死”有繁重I/O任务的设备。 中断服务例程 本章概述了一个驱动程序的标准中断服务例程的必要功能和建立一个ISR的需求。 本章包含以下信息: 8.1 ISR需求 8.1.1 ISR性能 8.1.2 附加的需求的驱动程序例程 8.1.3 建立一个ISR 8.2 ISR基本功能 8.3 ISR重叠I/O操作功能 ISR需求 一个产生中断的物理设备的所有驱动程序必须有一个ISR。中断服务例程由内核定义如下: BOOLEAN ( *PKSERVICE_ROUTINE ) ( IN PKINTERRUPT Interrupt, IN PVOID ServiceContext ); ISR运行在DIRQL上,特别是在驱动程序用IoConnectInterrupt注册其ISR时说明的SynchronizeIrql层上。当驱动程序的ISR运行时,所有带一个中等或较低IRQL值的中断被当前处理器所屏蔽。 当然,另一个带有一个较高的系统分配的DIRQL的设备可以中断,或者一个高IRQL系统中断可以在任何时间发生在Windows NT/Windows 2000机器上。 记住以下情况: 一个驱动程序的ISR是可中断的。 因为ISR运行在一个相对高的、系统分配的DIRQL上,因此在当前处理器上用一个中等或较低的IRQL屏蔽中断,ISR应尽可能快的返回控制。 在DIRQL运行一个ISR限制了该ISR可以调用的支持例程。关于IRQL管理的更多信息,见第16章。关于任何特定支持例程都能被调用的IRQL的说明信息,见在线DDK。 ISR性能 Windows NT/Windows 2000在驱动程序ISR方面完全不同于其他一些操作系统。在Windows NT/Windows 2000系统上,如果其ISR能尽可能快的返回控制,而不是试图保持对CPU的控制并在其ISR中做尽可能多的I/O处理,尤其是在SMP机器上,那么驱动程序会有更好的表现。 反之,ISR应从中断处停止设备,并保存一切必要的关于导致中断的操作的状态信息或该操作的环境。ISR应在常驻内存中保存这些信息或环境,这类内存通常位于设备扩展中。这时,它应对驱动程序的DpcForIsr例程或一个CustomDpc排队以完成这个位于一个较低的IRQL(通常是IRQL DISPATCH_LEVEL)上的操作。 ISR返回一个Boolean,表明驱动程序的设备是否产生中断。对于共享一个中断向量或DIRQL的设备的驱动程序,每个ISR一旦确定其设备不是中断源,就应返回FALSE。 附加的需求的驱动程序例程 所有拥有一个ISR的驱动程序也必须拥有DpcForIsr或CustomDpc例程。驱动程序也可以有附加的CustomDpc例程,以用来完成特定的中断驱动的I/O操作。 如果任何驱动程序例程与驱动程序的ISR分享数据、设备寄存器或环境信息,该驱动程序还必须有一个或多个SynchCritSection例程。 ISR运行在比DpcForIsr或CustomDpc例程更高的IRQL上。因此,在一台Windows NT/Windows 2000单处理器的机器上,ISR必须在DpcForIsr或CustomDpc例程执行之前返回。当然,在一台SMP机器上,ISR和DpcForIsr(或CustomDpc)可以并行运行。 建立一个ISR 驱动程序通过在设备启动时调用IoConnectInterrupt注册其ISR。驱动程序在处理一个PnP IRP_MN_START_DEVICE请求时作为最终步骤应连接中断。 每个拥有一个ISR的驱动程序必须为至少一个中断对象指针提供常驻内存。通常,该指针被存放在代表产生中断的物理设备的设备对象的设备扩展中。如果驱动程序创建一个控制器对象,中断对象指针可以存放在控制器扩展中,或者它可以存放在由驱动程序分配的,非页式缓冲池中。 如果下列两者之一为真,驱动程序必须为中断自旋锁提供存储空间以连接其所有设备的所有中断对象: 驱动程序有一个单独ISR为两个或多个设备处理不同向量的中断。 驱动程序的ISR处理一个在多个向量上中断的设备 驱动程序在注册其ISR之前必须通过调用KeInitializeSpinLock初始化中断自旋锁。驱动程序也必须为它处理的、与中断对象指针一样多的IRQ提供存储区间。 ISR基本功能 在入口处,ISR被赋予一个指向驱动程序的中断对象的指针和一个指向驱动程序在调用IoConnectIntertupt时建立的任意区域的ServiceContext指针。大多数驱动程序设置ServiceContext指针以代表产生中断的物理设备的设备对象或者该设备对象的设备扩展。在设备扩展中,驱动程序可以为驱动程序的DpcForIsr例程设置状态信息,DpcForIsr例程通常进行几乎所有的I/O处理以满足每个导致设备中断的请求。 在没有重叠设备I/O操作的驱动程序中,ISR应做以下工作: 确定中断是否为假。如果是的话,立即返回FALSE以使中断设备的ISR迅速被调用。否则,继续中断处理。 从中断处停止设备。 收集所有DpcForIsr(或CustomDpc)例程需要用来完成为当前操作的I/O处理的环境信息。 存放该环境信息于DpcForIsr或CustomDpc例程可访问的区域,通常在处理当前导致中断的I/O请求的目标设备对象的设备扩展中。 如果驱动程序有一个DpcForIsr例程,用指向当前IRP、目标设备对象和存储的环境信息的指针调用IoRequestDpc。IoRequestDpc对DpcForIsr例程排队以便IRQL一低于处理器上的DISPATCH_LEVEL就运行。 如果驱动程序有一个CustomDpc例程,用一个指向DPC对象(与CustomDpc例程连接)的指针和指向任何保存的环境(CustomDpc例程将需要它来完成操作)的指针调用KeInsertQueueDpc。通常,ISR也传送指向当前IRP的指针与目标设备对象。一旦IRQL低于处理器上的DISPATCH_LEVEL ,CustomDpc例程便运行。 返回TRUE以表明其设备产生中断。 通常,一个ISR不做实际的I/O处理以满足一个IRP。相反,它从中断处停止设备,建立必要的状态信息,然后将驱动程序的DpcForIsr 或?CustomDpc排队以便进行任何满足当前导致设备中断请求的必要I/O处理。 考虑以下实现方针: 为了获得可能最短的间隔,一个ISR必须运行于DIRQL。 根据上述策略可以为机器中的所有设备增加I/O流量,因为运行于DIRQL屏蔽了所有系统已经分配了一个较低或中等IRQL值的中断。 驱动程序的中断对象的SynchronizeIrql(在驱动程序调用IoConnectInterrupt时被指定)确定驱动程序的ISR与SynchCritSection例程在其上运行的DIRQL。 当一个驱动程序的StartIo例程用驱动程序的SynchCritSection例程调用KeSynchronizeExecution时,该调用者也传送指向与ISR相连的中断对象的指针。因此,来自设备的中断被屏蔽在处理器运行的SynchCritSection例程上。与此同时,KeSynchronizeExecution持有与中断对象相连的中断自旋锁,以使ISR不能从另一个处理器访问设备寄存器或者设备扩展中共享的状态,直到驱动程序的SynchCritSection例程返回控制。 关于使用一个中断自旋锁的KeSynchronizeExecution的调用者的更多信息,见第16章的“使用自旋锁”。 ISR重叠I/O操作功能 当Windows NT/Windows 2000 SMP机器内的ISR和DpcForIsr(或CustomDpc)可以并行运行时,只有代表DpcForIsr或CustomDpc例程的DPC对象的一个实例可以为在任何给定的时刻执行而被排队。 如果相同DPC对象在DpcForIsr(或CustomDpc)例程运行之前通过一个ISR不止一次地排队,相关DpcForIsr或CustomDpc例程仅被调用一次。如果DPC对象在DpcForIsr(或CustomDpc)运行时排队,该例程的两个实例可以在Windows NT/Windows 2000 SMP机器内并行运行。 因此,任何在其设备上重叠I/O操作的驱动程序必须拥有DpcForIsr和/或CustomDpc例程,在这些例程被调用时它们可以完成多个IRP。对于驱动程序的ISR的基本要求与没有重叠I/O操作的设备驱动程序一样。见“ISR基本功能”。 当然,如果一个驱动程序重叠I/O操作,其ISR必须为DpcForIsr或CustomDpc例程设置附加状态。附加状态包括一个关于DPC例程需要完成而没有完成的请求的数量以及相关的环境信息。此外,如果ISR在DPC运行之前被调用以处理另一个中断,ISR必须小心不要覆盖为没有完成的请求存储的环境信息。 因为驱动程序的DPC例程与 ISR共享该状态,其DPC例程必须用一个系统提供的SynchCritSection例程调用KeSynchronizeExecution以访问代表每个DPC例程的共享状态。 关于这些例程的更多信息,见第9章“DpcForIsr和CustomDpc例程”。关于ISR和为ISR排队的DPC互动的更多信息,见第16章的“使用自旋锁”。 DpcForIsr例程和CustomDpc例程 本章概述了一个驱动程序的标准DpcForIsr例程与CustomDpc例程的必要功能。它包含以下信息: 9.1 DpcForIsr和CustomDpc例程需求 9.1.1 DpcForIsr例程与CustomDpc例程 9.1.2 DpcForIsr例程基本需求 9.1.3 CustomDpc例程基本需求 9.1.4 其他需要的驱动程序例程 9.1.5 建立DpcForIsr和CustomDpc例程 9.2 DpcForIsr和CustomDpc功能 9.2.1 使用缓冲I/O和满足设备控制IRP 9.2.2 使用直接I/O 9.2.3 实现一个DpcForIsr或CustomDpc例程的注意事项 DpcForIsr和CustomDpc例程需求 任何拥有一个ISR的驱动程序也必须有一个DpcForIsr或CustomDpc例程以完成其对中断驱动的I/O操作的处理。 一个DpcForIsr或CustomDpc例程运行在位于IRQL DISPATCH_LEVEL的任意线程环境中。 运行于IRQL DISPATCH_LEVEL限制了一个DpcForIsr或CustomDpc例程可以调用的支持例程。更多信息见第16章的“硬件优先级管理”。 关于任何特定支持例程可以被调用的IRQL的说明信息,见在线DDK。 DpcForIsr例程与CustomDpc例程 根据驱动程序的设计,它可以有一个单独DpcForIsr以完成所有中断驱动的I/O操作,有一个DpcForIsr和一组特定操作CustomDpc例程以从其ISR排队,或者有一组一个或多个CustomDpc例程。 大多数最低层设备驱动程序有一个单独DpcForIsr例程以便为每个IRP(它要求多个操作)在其各自设备上完成I/O处理。使用一个单独DpcForIsr以在一台一次做一个操作的设备上完成预先请求的,中断驱动的I/O操作相对容易一些。这样的驱动程序的ISR仅需要为每个中断驱动的I/O操作调用IoRequestDpc。 使用一个单独DpcForIsr以在一台可以进行并行操作的设备上完成重叠的、中断驱动的I/O操作,这可以通过精心设计实现,但相对要困难一些。另外,或替代排列一个DpcForIsr,一个ISR可以通过调用KeInsertQueueDpc排队一组特定操作地、驱动程序提供的CustomDpc例程。 例如,考虑一些包含在编写一系列驱动程序中的设计挑战。作为一个完全双向的设备的驱动程序,一系列驱动程序不能依赖一个在IRP排队等待一个StartIo例程的指令和一个多任务多处理器系统内的设备中断序列之间一对一的通信。此外,串行驱动程序必须处理超时请求和异步用户产生的请求以撤消先前的请求操作,清除缓冲区数据,等等。 因此,一个串行驱动程序可以保留为用户模式应用程序可以请求的读、写、清除和等待操作内部队列。它也可以保留相关数据或使用一些其他跟踪机制,例如一组为IRP在其内部队列中使用的标志。其ISR将用任何驱动程序分配和初始化的DPC对象调用KeInsertQueueDpc,它们每个与驱动程序提供的CustomDpc例程相关联。 一个驱动程序是否有一个单独DpcForIsr例程和/或一组取决于其下层设备属性的CustomDpc例程,以及它必须支持的一组I/O请求。 DpcForIsr例程基本需求 DpcForIsr例程由I/O管理器定义如下: VOID ( * PIO_DPC_ROUTINE ) ( IN PKDPC Dpc, IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID Context ); 在入口处,DpcForIsr被赋予指向当前IRP、目标设备对象和环境信息的指针,该指针在ISR对IoRequestDpc的调用中被传送。 通常,DpcForIsr例程负责以下工作: 为目标设备对象完成由IRP请求的I/O处理,使用由ISR设置的环境 确保下一个IRP尽可能快地被处理,通常通过调用IoStartNextPacket或IoStartNextPacketByKey以使驱动程序的StartIo例程可以在目标设备上启动下一个请求的I/O操作 如果一个驱动程序管理其自身的队列,其DpcForIsr例程必须有办法通知驱动程序将下一个IRP从队列中移出并为下一个请求开始设备I/O处理。 设置输入IRP中的I/O状态块,并用这个刚完成的请求调用IoCompleteRequest 关于DpcForIsr例程功能的更详细的信息,见“DpcForIsr和CustomDpc例程”。 CustomDpc例程基本需求 CustomDpc例程由Kernel定义如下: VOID ( *PKDEFERRED_ROUTINE ) ( IN PKDPC Dpc, IN PVOID DeferredContext, IN PVOID SystemArgument1, IN PVOID SystemArgument2 ); 在入口处,任何传送给CustomDpc例程的环境信息都是驱动程序确定的。当ISR调用KeInsertQueueDpc时,它可以传送指向任何它在SystemArgument1或SystemArgument2设置的相关环境的指针。驱动程序必须在它初始化其DPC对象时为其CustomDpc例程定义一个DeferredContext指针,以使其ISR也能为位于DeferredContext的CustomDpc例程设置环境。 一个ISR经常为每个中断驱动的I/O操作传送指向目标设备对象的设备扩展的指针和一个指向当前IRP的指针给其CustomDpc例程。每个CustomDpc例程负责完成一个与中断驱动的I/O操作相连的任务。 如果一个驱动程序没有DpcForIsr例程,其CustomDpc例程负责完成与DpcForIsr例程处理的相同的基本操作。在这种情况下,驱动程序的CustomDpc例程通常负责下列任务。 为设备上产生中断的目标设备对象完成由当前IRP请求的I/O处理,使用由ISR设置的环境 确保下一个IRP尽可能快地被处理,可能通过调用IoStartNextPacket或IoStartNextPacketByKey以使驱动程序的StartIo例程可以启动目标设备上的下一个请求的I/O操作。 如果一个驱动程序管理其自身的队列,CustomDpc例程必须有办法通知驱动程序将下一个IRP从队列中移出并为下一个请求开始设备I/O处理。 设置当前IRP中的I/O状态块,并用这个刚完成的请求调用IoCompleteRequest。 更多信息见“DpcForIsr和CustomDpc例程”。 其他需要的驱动程序例程 一个最低层设备驱动程序的ISR为其DpcForIsr或CustomDpc例程存储环境信息。有时,DpcForIsr或CustomDpc例程负责重试操作,建立另一个DMA传输操作以满足一个给定的请求,或者否则的话,象ISR所做的那样访问相同的设备寄存器或环境域。 当驱动程序例程与ISR共享环境信息或设备寄存器时,该例程必须用一个驱动程序提供的SynchCritSection例程调用KeSynchronizeExecution以便安全地访问共享的环境。 一个驱动程序必须有至少一个SynchCritSection例程,除非它在为下一个IRP启动设备I/O操作之前完成每个请求,从而串行化其IRP处理。这时,对于ISR和其他例程,例如其DpcForIsr或CustomDpc例程,几乎不可能并行地访问共享状态,如果驱动程序串行化IRP处理,用这种方式处理IRP以防止驱动程序为其设备获得可能是最好的I/O流量。 注意,串行化其I/O处理的驱动程序可以运行在Windows NT/Windows 2000 SMP机器上,而不比运行在单处理器的机器上快。 建立DpcForIsr和CustomDpc例程 在其AddDevice例程中,驱动程序必须注册其DpcForIsr并初始化每个CustomDpc例程,如下: 用DpcForIsr的入口点和一个指向代表产生中断的物理设备的驱动程序创建的设备对象的指针调用IoInitializeDpcRequest。 用一个指向驱动程序DPC对象的存储空间的指针、CustomDpc例程的入口点和一个指向环境信息的DeferredContext指针调用KeInitializeDpc,它在运行时被传送到CustomDpc例程。 如果驱动程序有一个CustomDpc例程,它必须为一个内核定义的DPC对象提供常驻内存。通常,驱动程序在代表产生中断的物理设备的设备对象的设备扩展中存储DPC对象。驱动程序的ISR往往访问设备扩展以存储Custom例程环境,并且ISR必须在它给CustomDpc例程排队时传送DPC对象指针给KeInsertQueueDpc。 当然,驱动程序可以在驱动程序分配的非页式缓冲池,或者在控制器扩展中(如果驱动程序设置控制器对象的话)为其DPC对象和中断驱动I/O操作的环境提供存储区间。 注意,CustomDpc例程与CostomTimerDpc例程在如何排队上是不同的,前者用KeInsertQueueDpc而后者用KeSetTimer或KeSetTimerEx。也就是说,对于一个CustomTimerDpc例程,驱动程序还必须为一个内核定义的定时器对象分配内存并初始化它,并且它不能用KeSetTimer或KeSetTimerEx传送SystemArgument1和SystemArgument2指针到其CustomTimerDpc例程。 更多信息,见第3章的“DPC对象”和第14章“IoTimer和CustomTimerDpc例程”。 DpcForIsr和CustomDpc功能 除了在“DpcForIsr例程基本需求”和“CustomDpc例程基本需求”概述的任务,所有DpcForIsr或CustomDpc例程要负责使用ISR提供的环境以完成一个或多个中断驱动的I/O操作。 换句话说,除了确保下一个设备I/O操作迅速被启动和完成当前IRP,由任何DpcForIsr或CustomDpc例程完成的工作取决于驱动程序的设计与其设备的属性。 例如,DpcForIsr或CustomDpc例程也可以做以下工作以便完成一个中断驱动的I/O操作: 重试一个超时或失败的操作。 调用IoAllocateErrorLogEntry,建立错误记录包以报告设备I/O错误,并调用IoWriteErrorLogEntry。 关于处理I/O错误的更多信息,见第16章的“错误记录和NTSTATUS值”。 如果驱动程序使用缓冲I/O或当前IRP请求一个设备控制操作,在完成当前IRP之前传输从设备中读的数据到位于Irp->AssociatedIrp.SystemBuffer的系统缓冲区。 如果驱动程序使用直接I/O,存储关于一个刚刚完成的部分传输操作的状态,计算下一个部分传输容量,并且使用一个系统提供的SynchCritSection例程以为下一个传输操作对设备编程。 如果其设备有传输能力限制,即使使用缓冲I/O的驱动程序也不得不分割一个传输请求。 如果驱动程序使用基于包的DMA,在每个设备传输操作之后调用FlushAdapterBuffers,并在一系列部分传输被完成及完全传输请求被满足时调用FreeAdapterChannel或FreeMapRegisters。 如果请求的传输通过一个单独DMA操作仅仅被部分满足,DpcForIsr通常负责建立一个或多个DMA操作直到当前IRP的请求长度(字节数)被完全传输。 关于DMA使用的更多信息,见第3章的“适配器对象和DMA”;第11章“AdapterControl和ControllerControl例程”;以及第16章的“维护高速缓存一致性”。 如果驱动程序使用PIO且当前IRP请求读,则在每个设备传输操作之后调用KeFlushIoBuffers。 如果一个请求的传输仅通过一个单独PIO操作被部分满足,DpcForIsr(或CustomDpc)通常负责建立一个或多个传输操作直到当前IRP的请求长度(字节数)已经被完全传输。 关于PIO使用的更多信息,见直接I/O的使用,也见第16章的“为DMA和PIO维护高速缓存一致性”。 如果驱动程序有一个ControllerControl例程,在一个请求的操作完成时调用IoFreeController。 更多信息,见第3章的“控制器对象”。 注意,一个DpcForIsr(或CustomDpc)例程通常进行大部分驱动程序的设备I/O处理以满足IRP。该例程也负责在带驱动程序Dispatch例程的设备上对IRP排队。 记住下面的一般设计准则: 任何DpcForIsr或CustomDpc例程一旦能安全地调用IoStartNextPacket,就应调用它。也就是说,驱动程序的StartIo例程或任何其他由StartIo例程引发的例程不可能导致资源冲突或竞态条件。 如果驱动程序管理其自身的IRP队列,其DpcForIsr或CustomDpc例程应注意一旦下一个IRP安全从队列中取出就通知驱动程序,并设置下一个请求的设备。 一个DpcForIsr或CustomDpc例程必须在DpcForIsr或CustomDpc返回控制之前调用IoStartNextPacket,或者否则的话,在下一个请求的设备I/O处理可以启动时通知适当的驱动程序例程。根据驱动程序及其设备,它可以在DpcForIsr或CustomDpc例程用IoCompleteRequest完成当前IRP之前进行,或者它可以在该例程完成当前IRP并返回控制前立即进行。 使用缓冲I/O和满足设备控制IRP 如第7章已提到的,使用缓冲I/O的驱动程序的StartIo例程通常比那些使用直接I/O的驱动程序的StartIo例程要简单,因为前者每次中断时传输相对较少的数据进出一个由I/O管理器分配的系统空间缓冲区。大多数使用缓冲I/O的设备驱动程序的DpcForIsr例程也比那些使用直接I/O的驱动程序的DpcForIsr例程要简单。 即使在I/O控制码用缓冲方法定义时,为直接I/O建立其设备对象的驱动程序为IRP_MJ_DEVICE_CONTROL请求使用缓冲I/O。 使用缓冲I/O的驱动程序传输数据进出由I/O管理器分配的,位于Irp->AssociatedIrp.SystemBuffer的系统空间缓冲区。 任何驱动程序的DpcForIsr或CustomDpc例程通常仅为一组驱动程序必须支持的公共I/O控制码完成I/O处理。特别地,DPC例程为具有以下特点的设备控制请求完成操作: 改变物理设备的状态的请求 需要返回物理设备的本身易失信息的请求 关于系统定义的,驱动程序必须支持的特定设备类型I/O控制码的更多信息,见在线DDK。 直接I/O使用 使用直接I/O传输数据的驱动程序将不得不把一个大的传输请求分割成几个与其设备,或者对于一个从属DMA设备,是系统DMA控制器的能力相符的传输操作。因此,这样一个驱动程序的DpcForIsr例程可能为多个传输操作对设备重新编程以满足给定的读/写请求并完成该IRP。 因为DpcForIsr或CustomDpc例程运行在IRQL DISPATCH_LEVEL处。它必须用一个SynchCritSection例程调用KeSynchronizeExecution以更新任何与驱动程序的ISR共享的状态,例如设备寄存器。 使用直接I/O的驱动程序可以被请求以传输大量数据。IRP中由MDL描述的,传送给这类驱动程序的DispatchRead或DispatchWrite例程的锁定缓冲区可以大于系统定义的页大小。另外,位于Irp->MdlAddress的MDL可以描述一个由系统内存中几个不连续的物理页面支持的缓冲区。 此外,这类驱动程序传送请求数据所通过的设备有它自身的传输限制。例如,由于磁盘控制器的限制,系统“AT”磁盘驱动程序必须分割所有大于256扇区的传输请求。 当这类设备驱动程序的StartIo、AdapterControl或ControllerControl例程被一个传输超出物理设备所能处理数量的数据的请求调用时,它可以建立其设备只传输该设备允许量的数据。在驱动程序的ISR处理一个最大设备传输中断之后,DpcForIsr(或CustomDpc)必须再次设置设备,以传输满足该IRP所需的剩余数据。 实现一个DpcForIsr或CustomDpc例程的注意事项 在实现一个DpcForIsr或CustomDpc例程时记住以下要点: 一个DpcForIsr或CustomDpc例程必须同步它对物理设备和任何驱动程序在设备扩展中维护的,与驱动程序的其他访问相同设备或内存位置的例程共享的状态信息或资源的访问。 如果DpcForIsr或CustomDpc与ISR共享设备或状态,它必须用一个驱动程序提供的SynchCritSection例程调用SynchronizeExection以对设备编程或访问共享的状态。 如果DpcForIsr或CustomDpc与ISR之外的其他例程共享状态或资源,例如一个互锁队列或一个定时器对象,它必须用一个驱动程序初始化的、由驱动程序提供常驻内存的执行自旋锁来保护共享的状态或资源。 DpcForIsr或CustomDpc例程运行于IRQL DISPATCH_LEVEL,这限制它们所能调用的支持例程。 例如, DpcForIsr或CustomDpc例程既不能访问,也不能分配可分页内存,并且它不能等待一个调度者对象。另一方面,DpcForIsr或CustomDpc例程可以用KeAcquireSpinLockAtDpcLevel和KeReleaseSpinLockFromDpcLevel获得并释放驱动程序执行自旋锁,KeAcquireSpinLockAtDpcLevel和KeReleaseSpinLockFromDpcLevel可以比KeAcquireSpinLock与KeReleaseSpinLock更快运行。 DpcForIsr或CustomDpc例程负责启动设备上的下一个I/O操作。 对于使用直接I/O的最低层物理设备驱动程序,其任务包括在驱动程序调用IoStartNextPacket之前使用一个SynchCritSection例程对传输大量数据以满足当前IRP的设备编程。 如果一个最低层物理设备驱动程序在其DriverEntry例程中建立了一个控制器对象以使通过控制器对附加设备的I/O操作同步,其DpcForIsr或CustomDpc例程负责在它完成当前IRP之前用IoFreeControl释放控制器对象,并返回控制。 如果驱动程序使用DMA并且其AdapterControl例程返回KeepObject或DeallocateObjectKeepRegisters(因此为附加传输操作而保留系统DMA控制器通道或总线控制器适配器),驱动程序的DpcForIsr或CustomDpc例程负责在它完成当前IRP之前用FreeAdapterChannel或FreeMapRegisters释放适配器对象或映射寄存器,并返回控制。 DpcForIsr或CustomDpc例程通常负责记录一切在给定请求处理期间发生的设备错误,如果必要(也可能)的话重试当前请求,并设置I/O状态块以及用当前IRP调用IoCompleteRequest。 在其设备上重叠操作的驱动程序的DpcForIsr或CustomDpc不能依靠输入到StartIo例程的请求和对IoRequestDpc或KeInsertQueueDpc的ISR调用之间的一对一通信。换句话说,为完成仅仅那个IRP,这类驱动程序的DpcForIsr或CustomDpc不必使用指向IRP和ISR提供的环境的指针,也不必使用目标设备对象中的CurrentIrp指针。 在任何给定的情况下,相同DPC对象不能两次排队。如果这类驱动程序的ISR在相关DpcForIsr或CustomDpc执行之前不止一次调用IoRequestDpc或KeInsertQueueDpc,一旦一个处理器上的IRQL降到低于DISPATCH_LEVEL,仅有一个该DPC例程的实例运行。另一方面,如果ISR在相关DpcForIsr或CustomDpc运行于另一个处理器上时调用IoRequestDpc或KeInsertQueueDpc,该DPC例程的两个实例可以并行地运行。 因此,任何在其设备上重叠中断驱动的I/O操作的驱动程序必须有以下: 可以完成一些驱动程序维护其个数的,调用时未完成的请求的DpcForIsr或CustomDpc 从不覆盖为一个中断驱动的I/O操作存储的环境,直到DpcForIsr或CustomDpc已使用该环境信息并为存储的环境完成IRP为止的ISR 连接代表DpcForIsr或CustomDpc例程的环境区的SynchCritSection例程。 更多信息,见第10章“SynchCritSection例程”和第11章“AdapterControl和ControllerControl例程”。关于本节中提到的任何特定支持例程的细节,见在线DDK。 SynchCritSection例程 本章概述了设备驱动程序的SynchCritSection例程的必需功能。本章包含以下信息: 10.1 SynchCritSection例程需求 10.1.1 使用SynchCritSection例程 10.1.2 运行SynchCritSection例程 10.1.3 SynchCritSection例程性能需求 10.2 SynchCritSection例程功能 10.2.1 为I/O操作对设备编程 10.2.2 维护有关中断驱动I/O操作的状态 SynchCritSection例程需求 任何带有与其ISR共享状态、资源或设备端口/寄存器的例程的设备驱动程序必须拥有SynchCritSection例程。大多数设备驱动程序拥有至少两个SynchCritSection例程,并且许多驱动程序拥有更多的该例程。 SynchCritSection例程由内核定义如下: BOOLEAN ( *PKSYNCHRONIZE_ROUTINE ) ( IN PVOID SynchronizeContext ); SynchronizeContext参数提供一个指向由驱动程序建立的环境域的指针。 象ISR一样,SynchCritSection例程运行在驱动程序用IoConnectInterrupt注册其ISR时指定的SynchronizeIrql层上。 运行于DIRQL限制了SynchCritSection例程可以调用的支持例程。 关于管理驱动程序中的IRQL的更多信息,见第16章的“硬件优先级管理”。关于任何特定支持例程可以在其上被调用的IRQL的说明信息,见在线DDK。 使用SynchCritSection例程 就象其名字所暗示的,SynchCritSection例程是用来访问一个共享区域的代码中的临界区,它对该区域的访问必须与来自一个ISR的可能访问同步。 对于设备驱动程序,StartIo与DpcForIsr或CustomDpc例程经常必须访问一些相同内存或设备端口/寄存器,就象驱动程序的ISR。Dispatch、AdapterControl、ControllerControl或驱动程序提供的定时器例程也可以访问驱动程序维护的状态和/或与其ISR共享的设备端口/寄存器,这取决于驱动程序的设备或其设计。 如果任何非ISR例程试图直接访问这样一个共享区域,设备可以在其例程对设备编程或更新状态信息时中断。换句话说,无论一个设备中断发生在何时,ISR可以在Dispatch、StartIo、DpcForIsr、CustomDpc、AdapterControl、ControllerControl、IoTimer或CustomTimerDpc例程中改变状态或设备寄存器。 运行SynchCritSection例程 一个设备驱动程序中的所有非ISR例程必须同步访问与其ISR共享的区域,通过用一个指向与ISR相连的中断对象的指针和一个驱动程序提供的SynchCritSection例程调用KeSynchronizeExecution。 这一调用通过与驱动程序的中断对象相连的自旋锁保护共享的数据或设备端口/寄存器。控制自旋锁屏蔽了SynchCritSection例程处理器上较低或中等IRQL的设备中断,并且防止共享区域被来自Windows NT/Windows 2000 SMP机器中的ISR同时访问。 SynchCritSection例程性能需求 象ISR一样,SynchCritSection例程必须尽可能快地执行,仅进行必要的设置寄存器或更新环境数据,在返回控制到调用KeSynChronizeExecution的例程之前。 记住以下设计与实现方针: 因为它运行于DIRQL,任何SynchCritSection例程必须尽可能快地返回控制。 因为KeSynchronizeExection在其SynCritSection例程运行时持有一个设备驱动程序的中断自旋锁,所以驱动程序的ISR不能进行任何工作,直到SynchCritSection例程返回控制。 每个设备驱动程序应使位于DIRQL的每个设备上处理的IRP的运行时间最小。 在从KeSynchronizeExecution返回后,调用者的IRQL被恢复。 SynchCritSection例程功能 驱动程序为以下两个基本目的使用SynchCritSection例程: 为I/O操作对一个与ISR共享的物理设备编程 访问与来自其他驱动程序例程的ISR共享的状态信息 为I/O操作对设备编程 对于任何输入IRP,在它们为I/O操作对其设备编程之前,设备驱动程序在其Dispatch例程(或可能是驱动程序线程)中的IRQL PASSIVE_LEVEL上或者IRQL DISPATCH_LEVEL上进行尽可能多的I/O处理,如同其StartIo例程那样。 另外,设备驱动程序的StartIo例程必须延迟其用SynchCritSection例程对KeSynchronizeExecution的调用,对以下标准驱动程序例程的设备编程: 如果一个Windows 2000驱动程序建立一个控制器对象来代表一个带附加设备的设备控制器,其StartIo例程不能对设备控制器编程,该设备控制器可能已被用于一个在另一台附加设备上的I/O操作,直到物理控制器不忙。 驱动程序的StartIo例程必须用其ControllerControl例程调用IoAllocateController以同步物理控制器的操作。 只有这时它的ControllerControl例程可以使用SynchCritSection例程调用KeSynchronizeExecution,这里SynchCritSection例程为请求的从/到目标设备的操作重新对物理设备编程。 如果一个通过驱动程序的控制器传输操作不能满足一个特定IRP,驱动程序的DpcForIsr例程也必须用相同的SynchCritSection例程调用KeSynchronizeExecution以便为每个满足当前IRP的必要附加传输对物理控制器编程。 如果一个驱动程序为系统或基于包的总线控制DMA建立了适配器对象,其StartIo例程(或者可能是驱动程序创建的线程或ControllerControl例程)不能对一个从属设备编程,直到可能忙于为另一从属设备传输数据的系统DMA控制器空闲,或者直到系统已经为驱动程序的总线控制器适配器分配DMA映射寄存器。 驱动程序的StartIo例程必须用其AdapterControl例程调用AllocateAdapterChannel以便同步通过系统DMA控制器或驱动程序的总线控制DMA适配器的操作。 这时仅其AdapterControl例程可以用一个为DMA传输对驱动程序的设备编程的SynchCritSection例程调用KeSynchronizeExecution。 如果一个驱动程序的设备/适配器上的DMA传输操作不能满足一个特定IRP,驱动程序DpcForIsr例程也必须用相同SynchCritSection例程调用KeSynchronizeExecution以便为每个满足当前IRP必要的附加DMA传输对设备/适配器编程。 关于分级DMA操作的更多信息,见第3章“适配器对象和DMA”。 为设计、实现和调用为I/O操作对设备编程的SynchCritSection例程,考虑以下一般方针: 在它调用KeSynchronizeExecution以拥有一个驱动程序提供的、为I/O操作对设备编程的SynchCritSection例程之前,设备驱动程序为一个IRP进行所有预处理,为其他也处理设备I/O请求的驱动程序例程设置任何必要状态,并获得所有必要资源,例如对设备控制器、系统DMA通道或者DMA映射寄存器组的访问。 一个为I/O操作对设备编程的SynchCritSection例程必须尽可能快的返回控制。为此,SynchCritSection例程应仅进行必要的设备I/O设置。 如果必要,设备驱动程序可以有一组SynchCritSection例程对设备编程。例如,建立读请求明显不同于某些设备控制请求的设备的驱动程序,它可能有不同的SynchCritSection例程为每种类型的请求对其设备编程。 驱动程序不应当拥有一个单独的、大的、通用目的的SynchCritSection例程,这样的SynchCritSection例程会用一个Switch语句或许多嵌套的if..then..else语句决定它将执行什么来为I/O操作对设备编程,和/或更新什么状态信息。另一方面,驱动程序也应避免拥有一组大量SynchCritSection例程,每个仅负责对一个单独的设备寄存器编程。 所有SynchCritSection例程必须尽可能快地返回控制,因为运行任何SynchCritSection例程可防止驱动程序的ISR获得任何并行运行的工作。 维护有关中断驱动I/O操作的状态 其DpcForIsr、CustomDpc、ControllerControl、AdatperControl、IoTimer或CustomTimerDpc例程与ISR共享设备扩展中状态的驱动程序也使用各种技术使其SynchCritSection例程尽可能快地运行。 例如,设备驱动程序可以使用一些以下方案中描述的技术以使其SynchCritSection例程的执行间隔最小。考虑一个在其设备扩展中维护一个定时器计数器的设备驱动程序以检查设备I/O操作是否超时以及在其设备上是否可以进行重叠I/O操作。几种驱动程序例程可能访问定时器计数器,使用SynchCritSection例程,如下: 其StartIo例程为当前I/O请求用一个设备决定的超时值初始化定时器计数器。驱动程序给该设备超时值增加一秒以防其IoTimer例程恰好返回控制。 当为请求的操作被调用以处理一个来自设备的中断时,其ISR必须使该定时器计数器减一。 其IoTimer例程每秒被调用一次以读取定时器计数器的值,并决定ISR是否已经将它重置为-1。如果不是,IoTimer例程通过用SynchCritSection例程调用KeSynchronizeExecution减小计数器。 如果计数器变成零,表明设备上的请求超时,SynchCritSection_1例程调用SynchCritSection_2例程编程重置设备。如果计数器减一,IoTimer例程仅仅返回。 如果驱动程序的DpcForIsr必须对设备重新编程以开始另一个部分传输操作,它必须重新初始化定时器计数器,就象StartIo所做的那样。 DpcForIsr也必须用SynchCritSection_2例程调用KeSynchronizeExecution,或者可能是SynchCritSection_3例程,为另一个传输操作对设备编程。 在这种方案中,驱动程序有多于一个SynchCritSection例程,都有各自特定的任务,一个维护其定时器计数器,另一个对设备编程。因此,每个SynchCritSection例程可以尽可能快地返回控制,因为每个只完成一个单独特定的任务,而不做其他。 也要注意,这类驱动程序有一个单独的SynchCritSection_1例程与驱动程序的ISR一起维护定时器计数器的状态。因此,几个SynchCritSection例程与ISR之间不会争用访问定时器计数器。 为设计和实现维护状态的SynchCritSection例程,考虑以下一般方针: 如果一个设备驱动程序维护关于设备(或控制器)扩展中的中断驱动I/O操作的状态,放置所有状态信息于设备扩展的一个子域中。这使得其他互相共享资源的非ISR例程可以访问“它们的”设备扩展区域,而不可能与ISR和SynchCritSection例程争用。 例如,一个驱动程序管理的互锁队列不能被一个ISR或SynchCritSection例程访问,因为它们两者都不能调用一个来自DIRQL的ExInterlocked..List例程,在其他驱动程序例程同步访问来自IRQL DISPATCH_LEVEL的队列。这类队列头与其保护自旋锁对其他驱动程序例程是可访问的,如果其ISR和SynchCritSection例程总是发现这些状态的话,它们在设备扩展(不包括任何被其他驱动程序例程访问的资源)中一个定义的子域内编辑这些状态。 另一方面,如果ISR或SynchCritSection例程访问分散于设备扩展的状态变量,则争用驱动程序互锁队列或其他共享资源的访问冲突更为常见。 给每个保存状态信息的SynchCritSection例程各自不同的状态信息。也就是说,避免编写维护重复状态信息的SynchCritSection例程。 这可以防止试图同时访问相同状态的SynchCritSection例程(与ISR)之间的冲突,以及可能的竞态条件。 这也确保每个SynchCritSection例程尽可能快地返回控制,因为一个SynchCritSection例程从不用等待另一个更新相同状态信息的SynchCritSection例程以便返回控制。 避免编写一个单独的、庞大的、一般目的的、为确定做什么而做了比实际工作更多的条件测试的SynchCritSection例程。当然,另一方面,也要避免有很多从不执行一个条件语句的SynchCritSection例程,因为每个只更改一字节的状态信息。 所有SynchCritSection例程必须尽可能快地返回控制,因为运行任何SynchCritSection例程都可防止驱动程序的ISR获得正并行运行的工作。 AdapterControl和ControllerControl例程 本章包含以下信息: 11.1 驱动程序控制例程 11.1.1 驱动程序控制例程语法和参数 11.1.2设计有DriverControl例程驱动程序 11.1.3 从DriverControl例程返回 11.1.4 存储需求 11.1.5 建立AdapterControl和ControllerControl例程 11.2 AdapterControl例程需求 11.3 ControllerControl例程需求 本章概述了驱动程序的标准AdapterControl和ControllerControl例程的必要功能。AdapterControl例程由WDM和Microsoft Windows 2000驱动程序使用。ControllerControl例程仅由Windows 2000驱动程序使用而不被WDM所支持。 关于由执行发散/收集DMA的驱动程序使用的AdapterListControl例程的更多信息,见第3章的“执行发散/收集DMA”和《Windows 2000驱动程序开发指南》第2卷第1部分第3章的GetScatterGatherList。 关于支持系统与总线控制DMA设备驱动程序以及适配器对象的更多信息,见第3章的“适配器对象和DMA”。关于控制器对象的更多信息,也见第3章的“控制器对象”。 驱动程序控制例程 AdapterControl和ControllerControl例程运行在一个位于IRQL DISPATCH_LEVEL上的专有线程环境中。 运行于IRQL DISPATCH_LEVEL限制了AdapterControl或ControllerControl例程可以调用的支持例程。 更多信息,见第16章的“硬件优先级管理”。关于任何特定支持例程可以被调用的IRQL处的详细信息,见在线DDK中该例程相关文档。 驱动程序控制例程语法和参数 AdapterControl和ControllerControl例程由I/O管理器定义如下: IO_ALLOCATION_ACTION ( *PDRIVER_CONTROL ) ( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID MapRegisterBase, IN PVOID Context ); 每个驱动程序控制例程用四个参数调用: 一个指向当前I/O请求的目标的设备对象(DeviceObject)的指针 一个指向当前IRP的指针,如果驱动程序不管理其自身的IRP队列的话 一个指向MapRegisterBase的指针,仅在AdapterControl例程中使用(ControllerControl例程应将其认为是一个系统保留值。) 一个指向驱动程序定义的环境域的指针 从DriverControl例程返回 AdapterControl或ControllerControl例程必须返回一个IO_ALLOCATION_ACTION类型的值,定义如下: Typedef enum _IO_ALLOCATION_ACTION { KeepObject, DeallocateObject, DeallocateObjectKeepRegisters } IO_ALLOCATION_ACTION, *PIO_ALLOCATION_ACTION; DeallocateObjectKeepRegisters值仅对使用基于包的总线控制器DMA的驱动程序有意义。 如果ControllerControl例程完成一个IRP或者如果它可以建立一个操作,例如一次磁盘搜索,对于一个可能被对另一个设备对象的操作重叠的目标设备对象,它应返回DeallocateObject。否则,它应返回KeepObject。 设计有DriverControl例程驱动程序 大多数DMA设备的驱动程序必须有一个AdapterControl例程: 一个连接系统DMA控制器的DMA设备的所有驱动程序必须有一个AdapterControl例程。 一个总线控制DMA设备的任何驱动程序必须有一个AdapterControl例程,除非该驱动程序设计者决定独占DMA支持的系统公用缓冲区。 根据驱动程序编写者的意愿,一个Windows 2000驱动程序可以有一个ControllerControl例程以便通过一个单独物理控制器或适配器同步其对相似设备的操作。 I/O管理器为AdapterControl与ControllerControl例程定义了一个单独类型(DRIVER_CONTROL),因为当驱动程序需要同步访问一个可能已忙的物理设备时,每个例程被间接地调用,通常是由StartIo例程调用: 当驱动程序调用AllocateAdapterChannel时,如果系统DMA控制器或总线控制器适配器对于DMA操作可用,其AdapterControl例程立即运行。否则,AdapterControl例程排队直到DMA控制器或总线控制器适配器空闲。 当驱动程序调用IoAllocateController时,如果由控制器对象代表的硬件对I/O操作可用,其ControllerControl例程立即运行。否则,ControllerControl例程排队直到控制器空闲。 对于ControllerControl例程,在DRIVER_CONTROL声明中所示的输入PVOID MapRegisterBase是一个系统保留值。 存储需求 如果有AdapterControl例程,驱动程序必须为以下内容提供常驻内存: 将要在DMA操作中使用的环境信息 一个由IoGetDmaAdapter返回的AdapterControl指针 一个ULONG类型变量,保存系统确定的,任何给定的DMA传输请求可行的最大值NumberOfMapRegisters 驱动程序可以在设备扩展、控制器扩展或由驱动程序分配的非页式缓冲池中提供必要存储空间。 如果有ControllerControl例程,Windows 2000驱动程序必须为以下内容提供常驻内存: 一个由IoCreateController返回的ControllerObject指针 驱动程序可以在设备扩展或由驱动程序分配的非页式缓冲池中提供必要存储空间。通常,使用控制器对象的驱动程序在每个代表一个由控制器对象代表的硬件所控制的物理或逻辑设备的设备对象的设备扩展中存储ControllerObject指针。 一个不能指定名字的控制器对象不能成为一个I/O请求的目标。它代表的硬件通常控制一组同类设备,这些设备是I/O请求的实际目标。 建立AdapterControl和ControllerControl例程 设备启动期间,最低层驱动程序必须为它们的AdapterControl或ControllerControl例程建立相连的内核定义对象。 对于AdapterControl例程,一个PnP IRP_MN_START_DEVICE请求的驱动程序Dispatch例程必须做以下工作: 通过填入一个DEVICE_DESCRIPTION结构和调用IoGetDmaAdapter为设备的DMA能力建立适配器对象。 存储由IoGetDmaAdapter返回的AdapterObject指针和NumberOfMapRegisters。 由IoGetDmaAdapter返回的平台特定的最大NumberOfMapRegisters或者驱动程序的设 备的传输能力决定驱动程序是否必须分割一个所给的大的传输请求,并在其设备上执行多个DMA操作以满足IRP。 返回的AdapterObject指针、驱动程序的AdapterControl例程的入口点、代表当前IRP的目标设备的DeviceObject指针、一个指向一个已为AdapterControl例程设置的区域的环境指针和一个NumberOfMapRegisters值(它可以小于较小传输请求的最大可能量)必须在对AllocateAdapterChannel的调用中传送。通常,一个驱动程序的StartIo(或可能是ControllerControl)例程在它调用AllocateAdapterChannel之前建立位于Context的区域。 对于一个Windows 2000驱动程序的ControllerControl例程,一个PnP IRP_MN_START_DEVICE请求的Dispatch例程必须做以下工作: 调用IoCreateController建立控制器对象,为系统由非页式缓冲池分配并初始化为零的控制器扩展指定驱动程序确定的Size。 存储由IoCreateController返回的ControllerObject指针,通常在每个表示一个由控制器对象表示的硬件所控制的物理或逻辑设备的设备对象的设备扩展中。 设置并初始化驱动程序确定的ControllerObject->ControllerExtension的内容。 返回的ControllerObject指针,驱动程序的ControllerControl例程的入口点,表示当前IRP的目标设备的DeviceObject指针,以及一个指向已为ControllerControl例程建立的区域的Context指针必须在驱动程序对IoAllocateController的调用中传送。通常,一个驱动程序的StartIo例程在它调用IoAllocateController之前建立位于Context的区域。 AdapterControl例程需求 至少,一个AdapterControl例程必须做以下工作: 存储输入MapRegisterBase值连同一切驱动程序需要用来为当前IRP执行一个或多个DMA传输操作的其他环境信息。当每个DMA传输完成时,驱动程序必须传送MapRegisterBase值给FlushAdapterBuffers 返回适当的IO_ALLOCATION_ACTION值: 如果设备是一个从属设备,因此驱动程序使用系统DMA,则为KeepObject。 如果设备是一个总线控制器,因此驱动程序使用基于包的总线控制器DMA,则为DeallocateObjectKeepRegisters。 根据驱动程序的设计,其AdapterControl例程在返回控制之前也可以做以下工作: 为设备上的传输确定开始位置。 计算可能的传输容量,已知传输开始位置对其设备的一切限制。 通常,调用AllocateAdapterChannel的例程负责确定是否必须分割成部分传输,它的起因是每个DMA传输操作可行的NumberOfMapRegisters上的平台特定的限制,就象在前面章节提到的以及在“分割传输请求”中详述的。 在设备(或控制器)扩展中设置一切驱动程序维护关于每个传输请求的状态。 例如,一个AdapterControl例程可能用一个驱动程序超时DMA传输 的CustomDpc例程入口点调用KeSetTimer。关于CustomTimerDpc例程的更多信息,见第14章。 用位于Irp->MdlAddress处传送的MDL指针调用MmGetMdlVirtualAddress以获得一个传输开始的索引,适合于传送给MapTransfer。 调用MapTransfer以设置系统DMA控制器或或者获得一个总线控制器设备的物理对逻辑地址映射。 通过用SynchCritSection例程调用KeSynchronizeExecution为一个传输操作对驱动程序的设备编程。 如果一个传输请求需要驱动程序执行一系列部分传输操作以满足当前IRP,驱动程序的DpcForIsr例程通常负责为后继传输操作重新对设备编程。对于每个到达的传输IRP,一个AdapterControl例程仅被调用一次。 完成当前IRP的驱动程序例程(通常为DpcForIsr)也通过分别调用FreeAdapterChannel或FreeMapRegisters负责释放系统DMA控制器或总线控制器适配器。当其最后一次部分传输操作被完成时,该驱动程序例程一旦可能就应进行适当的调用,以便从属DMA设备的驱动程序可以分配系统DMA控制器或者一个总线控制器驱动程序可以开始迅速处理下一个传输IRP。 ControllerControl例程需求 如同它的名字所暗示的,一个ControllerControl例程与一个内核定义的控制器对象相连。当ControllerControl例程执行时,由控制器对象代表的硬件被释放,并且控制器扩展通常不能由另一个驱动程序例程访问,除非控制器扩展包含与驱动程序的ISR共享的内容。 通常,一个ControllerControl例程至少做以下工作: 更新或初始化驱动程序在目标设备对象的设备扩展与控制器扩展中维护的任何内容 如果驱动程序使用DMA,其ControllerControl例程通常负责确定一个给定的传输请求由于系统或设备赋予的每个DMA传输规模限制是否必须被分割成部分传输。在这些情况下,如果驱动程序有一个AdapterControl例程,ControllerControl例程也负责调用AllocateAdapterChannel。 如果驱动程序使用PIO且其硬件需要的话,其ControllerControl例程也负责分割大的传输请求为部分传输容量,并且用位于Irp->MdlAddress的MDL调用MmGetSystemAddressForMdlSafe。 为请求的I/O操作对其硬件编程 如果设备或控制器扩展可以从ISR访问,ControllerControl例程必须用一个SynchCritSection例程调用KeSynchronizeExecution以便对硬件编程或建立与ISR共享的环境。 如果驱动程序有Cancel例程,其ControllerControl例程也必须检查Irp->Cancel域以确定当前IRP是否应被撤消,并进行以下两者之一: 如果Irp->Cancel被设为TRUE,ControllerControl例程必须做以下工作: 设置IRP的I/O状态块中的Status为STATUS_CANCELLED,Information为零。 调用IoFreeController释放控制器对象,以便下一个设备操作可以迅速启动。 调用IoStartNextPacket或将下一个IRP从队列中取出,如果驱动程序管理其自身的队列。 用IoCompleteRequest完成撤消的IRP,并返回控制。 如果Irp->Cancel没有设成TRUE,ControllerControl例程则必须做以下工作: 调用IoSetCancelRoutine以重置IRP的Cancel例程入口点为NULL。如果驱动程序使用设备对象中I/O管理器提供的设备队列,为该调用获得撤消自旋锁。 用一个驱动程序提供的SynchCritSection例程调用KeSynchronizeExecution以便为请求的I/O操作对硬件编程。 关于处理可撤消IRP的更多信息,见第12章“Cancel例程”。 对于除物理控制器/适配器附属的不同设备上的重叠操作之外的大多数中断驱动I/O操作,ControllerControl例程应返回KeepObject,因为DpcForIsr或CustomDpc例程完成操作与IRP。 一旦满足当前请求的I/O操作被进行,将完成IRP的例程应调用IoFreeController和IoStartNextPacket,以使下一个请求尽可能快地被处理。 如果ControllerControl例程自己完成一个IRP或者它设置一个对目标设备对象(磁盘)的操作,例如一个磁盘搜索,这个操作可能被一个对另一个设备对象的操作所覆盖,ControllerControl例程应返回DeallocateObject。 Cancel例程 这一章总结了驱动程序标准Cancel例程所需的功能,提出了管理其他驱动程序例程中可撤消IRP的策略。 本章包括下列内容: 12.1 Cancel例程需求 12.2 处理可撤消IRP 12.2.1 对传递IRP的驱动程序例程中可撤消IRP的处理 12.2.2 对处理请求的驱动程序例程中可撤消IRP的处理 12.2.3 使用系统撤消自旋锁 12.2.4 使用驱动程序提供的自旋锁和Cancel例程 12.3 Cancel例程功能 12.3.1 带有StartIo例程的驱动程序中的Cancel例程 12.3.2 没有StartIo例程的驱动程序中的Cancel例程 12.4 处理可撤消IRP时的注意事项 Cancel例程需求 I/O管理器 对Cancel例程的定义如下: VOID (*PDRIVER_CANCEL)( IN PDEVICE_OBJECT DeviceObject IN PIRP Irp ); Cancel例程的目的是尽快完成IRP,以确保驱动程序的数据结构是一致的。 Cancel例程在将一个IRP的I/O状态块中的Status设为STATUS_CANCELLED、 将Information设为0后,为它的目标设备对象完成此IRP。 Cancel例程总是用代表它的系统撤消自旋锁调用。在Cancel例程调用IoReleaseCancelSpinLock之前,它都运行在值为DISPATCH_LEVEL的IRQL上,而且它可运行在专用线程环境中。 运行在IRQL为DISPATCH_LEVEL和专用线程环境中限定了Cancel例程可以调用的支持例程的集合。 如果想对管理NT驱动程序中的IRQL有更多的了解,请看本书第16章中管理硬件优先级部分。如果想要了解任何特定支持例程都可被在其上调用的IRQL的具体内容,请看在线DDK。 驱动程序中的Cancel例程 其中IRP可以在不确定的时间内被保持为未决状态的驱动程序,必须有一个或多个Cancel例程。键盘驱动程序就是这样的一个例子:驱动程序可能不确定地等待用户按下一个键。相反,如果驱动程序能确定5分钟内它不能排队比能完成的更多的IRP,它可能就不需要Cancel例程。 例如,假设一个用户模式的线程发出了一个I/O请求,这个请求被最高层设备驱动程序的Dispatch例程排队,并且在这个IRP排队期间,请求线程被终止了。终止线程被排队的IRP应该被撤消,因此驱动程序必须在每个排队的IRP中建立一个驱动程序提供的Cancel例程。 创建相关IRP的驱动程序必须在主IRP(master IRP)被撤消时,撤消其他相关的IRP。因为相关IRP与请求线程无关,所以主IRP的Cancel例程应该在主IRP被撤消时,撤消所有与其相关的IRP。 一个驱动程序所具有的Cancel例程的个数取决于这个驱动程序的设计。一般来说,IRP可以在不确定间隔内保持未决状态,在这种情况发生在驱动程序的I/O处理期间时,这个驱动程序应该为此期间的每个阶段设置一个Cancel例程,也可以称这些IRP被保持于可撤消状态。 在设计驱动程序时应该考虑以下几点: 在一系列分层的驱动程序中,如果最高层驱动程序要对IRP排队或使IRP保持于可撤消状态,那么它至少应该有一个Cancel例程。需要的话,它还可以有更多的Cancel例程。 对于低层驱动程序,如果其中的IRP要在较长时间内被保持于可撤消状态,那么它也应当有一个或多个Cancel例程。 如果一个驱动程序要管理它自己的IRP内部队列,那么它的每个队列都应当有一个单独的Cancel例程。 某些交互设备的最高层驱动程序(例如键盘、鼠标、声卡、并行类和串行驱动程序)必须有Cancel例程。某些低层驱动程序也应该有Cancel例程,例如一个长时间使IRP排队等待一些更高层类驱动程序的并行驱动程序。 处于海量存储设备驱动程序之上的中间层驱动程序和海量存储设备驱动程序一般没有Cancel例程。也就是说,由文件系统驱动程序来处理文件I/O请求的撤消,而输入到低层海量存储设备驱动程序的IRP由于完成处理太快,以致于不能被撤消。 没有Cancel例程的高层驱动程序中的可撤消IRP 高层驱动程序无法知道现有的低层驱动程序是否或怎样处理可撤消IRP。高层驱动程序一旦用IRP调用了IoCallDriver,它就不再拥有这个IRP,而且它既不能确定也不能控制低层驱动程序对此IRP的处理。 然而,任何高层驱动程序在调用IoCallDriver之前,都可以通过调用IoSetCompletionRoutine在IRP中设置一个IoCompletion例程。这个高层驱动程序在给低层驱动程序传递IRP之前,可以通过调用IoSetCompletionRoutine并把参数InvokeOnCancel设为TRUE,从而确定是否低层驱动程序中每个未决的IRP都被撤消了。这样做可以确保驱动程序的IoCompletion例程将被调用,而不管IRP是被撤消了还是被完成了。 高层驱动程序可以用分配给它的任何未决的IRP来调用IoCancelIrp。然而,这样调用不能保证分配给驱动程序的IRP在完成时它的I/O状态块被设为STATUS_CANCELLED,因为另一个线程可能已经完成了这个IRP。为了检查IRP是否被撤消了,高层驱动程序必须在传递此IRP到下一个低层驱动程序之前,用值为TRUE 的InvokeOnCancel参数调用IoSetCompletionRoutine。参见第13章IoCompletion例程以了解更多关于完成例程的内容。 高层驱动程序决不能用没有分配的IRP来调用IoCancelIrp。 处理可撤消IRP 从驱动程序角度看,一个IRP可在任何时刻被撤消。IRP撤消是异步发生的,因此,驱动程序必须能处理大量可能的竞态条件(race condition),其中包括IRP是否在下列时刻被撤消: 在驱动程序例程被调用之后,而在对IRP排队之前 在驱动程序例程被调用之后,而在试图处理IRP之前。例如,一个IRP可能是在驱动程序的StartIo例程被调用之后,并且在StartIo例程将此IRP移出设备队列之前撤消的。 在驱动程序例程将此IRP从队列中取出之后,而在开始请求的I/O之前。 注意:在驱动程序将一个IRP排队并释放了任何保护队列的自旋锁之后,另一个线程可以访问并且改变这个IRP。当原来的线程继续时(甚至只是到下一行代码),这个IRP可能已经被撤消了或者被修改了。 下列IRP成员具有关于撤消的关键信息: Irp->Cancel指明一个IRP是否正被撤消或是否应被撤消。 Irp->CancelRoutine指明一个IRP是否是可撤消的。如果这个成员是一个指向Cancel例程的指针,那么这个IRP就是可撤消的;如果这个成员是NULL,那么那么这个IRP就是不可撤消的。如果这个成员是NULL,但Irp->Cancel为“1”,这就表明Cancel例程正在运行,而且这个IRP正被撤消。 如果驱动程序要处理可撤消IRP,那么它就要为其中被保持于可撤消状态的每个IRP设立适当的Cancel例程。 如果想知道下列情况下驱动程序例程的详细策略,请看: 12.2.1对传递IRP的驱动程序例程中可撤消IRP的处理 12.2.2对处理请求的驱动程序例程中可撤消IRP的处理 12.2.3使用系统撤消自旋锁 12.2.4使用驱动程序提供的自旋锁和Cancel例程 对传递IRP的驱动程序例程中可撤消IRP的处理 如果设备驱动程序有StartIo例程,那么它的Dispatch例程必须在调用IoStartPacket时提供可选的CancelFunction参数。 如果设备驱动程序没有StartIo例程,那么它的Dispatch例程在将一个IRP排队以供其他驱动程序例程做进一步处理之前,必须做下列事情: 1.调用IoAcquireCancelSpinLock。 2.用输入IRP和驱动程序提供的Cancel例程的入口点调用IoSetCancelRoutine。 3.调用IoReleaseCancelSpinLock。 类似的,对于传递IRP以供该驱动程序中其他例程处理的驱动程序例程,如果这个IRP在被另一个驱动程序例程处理之前可能被保持于可撤消状态,那么也需要为它建立Cancel例程。 管理自己的IRP队列,而不用I/O管理器提供的设备队列的驱动程序,在调用IoSetCancelRoutine时,不需要获得撤消自旋锁。然而,这些驱动程序应该检查IoSetCancelRoutine返回的Cancel例程指针,以确定是否撤消例程已经开始。 对处理请求的驱动程序例程中可撤消IRP的处理 驱动程序的StartIo例程(如果有的话)和其他任何将被保持于可撤消状态的IRP移出队列或用这样的IRP调用的例程必须做下列事情: 调用IoAcquireCancelSpinLock。 检查Irp是否等于DeviceObject->CurrentIrp。如果不是的话,调用IoReleaseCancelSpinLock并返回控制。 如果这两个不等,说明CurrentIrp可能已经在IoStartPacket释放撤消自旋锁之后,并且在此例程获得它之前被撤消了 用一个值为NULL的CancelRoutine指针调用IoSetCancelRoutine,以将IRP从可撤消状态中解除。 检查Irp->Cancel域以确定是要撤消这个IRP,还是要开始处理请求。 如果Irp->Cancel为TRUE,做下列事情: 调用IoReleaseCancelSpinLock。 将Irp->IoStatus.Status设为STATUS_CANCELLED。 将Irp->IoStatus. Information设为0。 在StartIo例程中调用IoStartNextPacket,从而开始下一个包。 用值为IO_NO_INCREMENT的优先级提高值调用IoCompleteRequest以完成这个IRP。 如果Irp->Cancel为FALSE,调用IoReleaseCancelSpinLock,然后为此IRP做请求的处理,或将此IRP传递给低一层驱动程序。 其他从队列中移出IRP或用被保持于可撤消状态的IRP调用的例程也应该做类似的事情。这种例程必须检查IRP是否可被撤消,或者必须在它开始为IRP做I/O处理之前,将此IRP从可撤消状态中解除。 管理自己的IRP队列、而不用I/O管理器提供的设备队列的驱动程序,在调用IoSetCancelRoutine时,不需要获得撤消自旋锁。然而,这些驱动程序应该检查IoSetCancelRoutine返回的Cancel例程指针,以确定是否撤消例程已经开始。 在处理可撤消IRP的驱动程序里,在底层设备已为请求的I/O操作编程之前处理IRP的驱动程序例程应该检查所有输入IRP是否处于可撤消状态。特别对于既有StartIo例程又有ControllerControl例程的最高层设备驱动程序,在这两个例程中都应该处理输入的IRP。 使用系统撤消自旋锁 改变可撤消IRP状态的驱动程序例程(包括所有可能以STATUS_CANCELLED完成IRP的例程)必须按照这一节中的策略来获得和释放系统撤消自旋锁。 在用I/O管理器提供的设备队列的驱动程序中,除了Cancel例程以外,所有改变可撤消IRP状态的驱动程序例程必须先调用IoAcquireCancelSpinLock以获得系统撤消自旋锁。 获得系统撤消自旋锁保证了只有调用者可以改变这个IRP的可撤消状态。当调用者持有自旋锁时,I/O管理器不能用此IRP调用这个驱动程序的Cancel例程。类似的,另一个驱动程序例程(如DispatchCleanup例程)不能同时试图改变这个IRP的可撤消状态。 在管理自己的IRP队列并用驱动程序提供的自旋锁来同步队列访问的驱动程序中,驱动程序例程不需要在调用IoSetCancelRoutine之前获得撤消自旋锁。然而,这些驱动程序应该检查IoSetCancelRoutine返回的Cancel例程指针,以确定是否撤消例程已经开始。请参见下一节《用驱动程序提供的自旋锁和Cancel例程》。 使用驱动程序提供的自旋锁和Cancel例程 管理自己的IRP队列的驱动程序可以用驱动程序提供的自旋锁(而不是系统撤消自旋锁)来同步对队列的访问。为了改善性能,除非不得已,否则最好避免使用撤消自旋锁。 因为系统只有一个撤消自旋锁,所以驱动程序有时必须等待这个自旋锁变得可用。用驱动程序提供的自旋锁消除了这种可能的延迟,它使得撤消自旋锁对I/O管理器和其他驱动程序始终可用。尽管系统在调用驱动程序的Cancel例程时仍然获得撤消自旋锁,但是驱动程序可以用它自己的自旋锁去保护它的IRP队列。 即使驱动程序没有将未决的IRP排队,而是以其他某种方式保持着所有权,它仍然必须为此IRP建立一个Cancel例程,并且必须用自旋锁保护IRP指针。例如,假设驱动程序将一个IRP标记为“未决”,然后将此IRP指针作为环境传递给IoTimer例程。这个驱动程序必须建立一个能撤消定时器的Cancel例程,并且当访问此IRP时,必须在Cancel例程和定时器回调中使用同一个自旋锁。 对自己的IRP排队并用自己的自旋锁的驱动程序必须做下列事情: 创建保护队列的自旋锁。 只有当持有此自旋锁时,才能建立并清除Cancel例程。 如果驱动程序正在将一个IRP从队列中移出时,Cancel例程已开始运行,那么允许Cancel例程完成此IRP。 获得保护Cancel例程中队列的锁。 为了创建自旋锁,驱动程序要调用KeInitializeSpinLock。在下面的例子中,驱动程序将自旋锁和它创建的队列保存在DEVICE_CONTEXT结构中: typedef struct{ LIST_ENTRY irpQueue; KSPIN_LOCK irpQueueSpinLock; }DEVICE_CONTEXT; VOID InitDebiceContext(DEVICE_CONTEXT *deviceContext) { InitializeListHead(&deviceContext-> irpQueue); KeInitializeSpinLock(&deviceContext-> irpQueueSpinLock); } 为了将IRP排队,驱动程序要先获得自旋锁,调用InsertTailList,然后将这个IRP标记为“未决”,如下面例子所示: NTSTATUS QueueIrp(DEVICE_CONTEXT *deviceContext,PIRP Irp) { PDRIVER_CANCEL oldCancelRoutine; KIRQL oldIrql; NTSTATUS status=STATUS_PENDING; KeAcquireSpinLock(&deviceContext-> irpQueueSpinLock,&oldIrql); //Queue the IRP and call IoMarkIrpPending //to indicate that the IRP may complete on a different thread. //N.B. It’s okay to call these inside the spin lock because they’re macros,not //functions. IoMarkIrpPending(Irp); InsertTailList(&deviceContext-> irpQueue,&Irp->Tail.Overlay.ListEntry); //Must set a Cancel routine before checking the Cancel flag. oldCancelRoutine=IoSetCancelRoutine(Irp,IrpCancelRoutine); ASSERT(!oldCancelRoutine); if(Irp->Cancel){ //The IRP was canceled.Check whether our cancel routine was called. oldCancelRoutine=IoSetCancelRoutine(Irp,NULL); if(oldCancelRoutine){ //The cancel routine was NOT called. //So dequeue the IRP now and complete it after releasing the spinlock. RemoveEntryList(&Irp->Tail.Overlay.ListEntry); status= Irp->IoAtatus.Status=STATUS_CANCELLED; }//endif else{ // The cancel routine WAS called. //As soon as we drop our spin lock it dequeues and completes the IRP. //So leave the IRP in the queue and otherwise don’t touch it. //Return pending since we’re not completing the IRP here. }//endif }//endif KeReleaseSpinLock(&deviceContext-> irpQueue,oldIrql); //Normally you shouldn’t call IoMarkIrpPending //and return a status other than STATUS_PENDING. //But you can break this rule if you complete the IRP. if (status!= STATUS_PENDING){ IoCompleteRequest(Irp,IO_NO_INCREMENT); } return status; } 这个例子表明,当驱动程序建立并清空Cancel例程时,它持有着自旋锁。例子中的排队例程包括了两个对IoSetCancelRoutine的调用。 第一个调用为IRP建立Cancel例程。然而,由于当排队例程运行时,这个IRP可能已经被撤消了,所以驱动程序必须检查此IRP的Cancel成员。 如果Cancel已被置为1,就说明撤消已被请求,因此驱动程序必须再次调用IoSetCancelRoutine以确定先前设置的Cancel例程是否被调用了。 如果IRP已被撤消,但Cancel例程却还没有被调用,那么当前例程将此IRP从队列中移出,将其状态设为STATUS_CANCELLED,然后完成它。 如果IRP已被撤消而且Cancel例程已被调用,那么当前返回将这个IRP标记为“未决”,然后返回STATUS_PENDING。Cancel例程会完成此IRP。 下面的例子表明了怎样将一个IRP从先前创建的队列中移出: PIRP DequeueIrp(DEVICE_CONTEXT *deviceContext) { KIRQL oldIrql; PIRP nextIrp=NULL; KeAcquireSpinLock(&deviceContext->irpQueueSpinLock,&oldIrql); while(!nextIrp&&!IsListEmpty(&deviceContext->irpQueue)){ PDRIVER_CANCEL oldCancelRoutine; PLIST_ENTRY listEntry=RemoveHeadList(&deviceContext->irpQueue); //Get the next IRP off the queue. nextIrp=CONTAINING_RECORD(listEntry,IRP,Tail.Oberlay.ListEntry); //Clear the IRP’s cancel routine oldCancelRoutine=IoSetCancelRoutine(nextIrp,NULL); //IoCancelIrp() could have just been called on this IRP. //What we’re interested in is not whether IoCancelIrp() was called //(nextIrp->Cancel flag set), //but whether IoCancelIrp() called (or is about to call) cancel routine. //To check that,check the result of test-and-set macro IoSetCancelRoutine. if(oldCancelRoutine){ //Cancel routine not called for this IRP.Return this IRP. ASSERT(oldCancelRoutine= =IrpCancelRoutine); } else{ //This IRP was just canceled and our cancel routine was (or will be) //called. //The cancel routine will complete this IRP as soon as we drop the //spin lock,we don’t do anything with the IRP. //Also,the cancel routine will try to dequeue the IRP, //so make the IRP’s listEntry point to itself. ASSERT(nextIrp->Cancel); InitializeListHead(&nextIrp->Tail.Overlay.ListEntry); nextIrp=NULL; } } KeReleaseSpinLock(&deviceContext->irpQueueSpinLock,oldIrql); return nextIrp; } 在这个例子中,驱动程序在访问队列之前先获得相关的自旋锁。在持有此自旋锁时,驱动程序检查队列是否为空。如果是,就把下一个IRP从队列中取出。然后调用IoSetCancelRoutine,从而为此IRP重置Cancel例程。因为当驱动程序将IRP从队列中移出并重置Cancel例程时,这个IRP可能已经被撤消了,所以驱动程序必须检查IoSetCancelRoutine返回的值。如果IoSetCancelRoutine返回NULL,表明Cancel例程已经或很快就会被调用,那么上面例子中的例程就让Cancel例程完成这个IRP,然后它释放保护队列的锁并返回。 注意在上面例程中InitializeListHead的使用。驱动程序可能还会将这个IRP再次排队,因此可以用Cancel例程将它移出,但更简单的做法是调用InitializeListHead。InitializeListHead重新初始化这个IRP的ListEntry域,使它指向此IRP本身。用自引用指针非常重要,因为在Cancel例程获得自旋锁之前,这个表的结构可能会改变。如果表结构改变了,可能使ListEntry的初始值变为无效,那么Cancel例程就可能在它将此IRP从队列中移出时破坏这个表。但是如果ListEntry指向这个IRP本身,那么Cancel例程就会永远使用正确的IRP。 接下来,Cancel例程只是做下列事情: VOID IrpCancelRoutine(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp) { DEVICE_CONTEXT *deviceContext=DeviceObject->DeviceExtension; KIRQL oldIrql; //Release the global cancel spin lock. //Do this while not holding any other spin locks so that we exit at the right IRQL. IoReleaseCancelSpinLock(Irp->CancelIrql); // //Dequeue and complete the IRP. //The enqueue and dequeue functions synchronize properly so that if this //cancel routine is called,the dequeue is safe and only the cancel routine will //complete the IRP.Hold the spin lock for the IRP queue while we do this. // KeAcquireSpinLock(&deviceContext->irpQueueSpinLock,&oldIrql); RemoveEntryList(&Irp->Tail.Overlay.ListEntry); KeReleaseSpinLock(&deviceContext->irpQueueSpinLock,oldIrql); //Complete the IRP.This is a call outside the driver,so all spin locks //must be released by this point. Irp->IoStatus.Status=STATUS_CANCELLED; IoCompleteRequest(Irp,IO_NO_INCREMENT); return; } I/O管理器总是在调用Cancel例程之前获得全局撤消自旋锁,因此Cancel例程的第一个任务就是释放这个自旋锁。然后它获得保护此驱动程序IRP队列的自旋锁;将当前IRP从队列中移出;释放它的自旋锁;以STATUS_CANCELLED完成此IRP;如果没有优先级提高,就返回。 Cancel例程功能 I/O管理器用将被撤消的输入IRP和代表请求目标的DeviceObject指针调用驱动程序提供的Cancel例程。 这个IRP可以是在用户关闭当前Win32应用程序时由驱动程序的DispatchReadWrite例程已经排队的IRP,也可以是高层驱动程序显式撤消的IRP。这取决于底层设备的性质。 当Cancel例程被调用时,如果驱动程序有StartIo例程,那么输入IRP可能已经是目标设备对象中的CurrentIrp,或可能已经在与目标设备对象相关的设备队列中了。如果驱动程序没有StartIo例程,那么当Cancel例程被调用时,这个IRP可能在驱动程序管理的IRP内部队列中。无论哪种情况,I/O管理器都要将输入IRP的Cancel入口点重置为NULL,然后用这个IRP调用Cancel例程。 对于有相关IRP的主IRP,Cancel例程负责调用IoCancelIrp以撤消那些相关IRP。 所有Cancel例程都必须遵守下列策略: 因为Cancel例程总是在持有系统撤消自旋锁时被调用,所以它决不能在调用IoReleaseCancelSpinLock之前调用IoAcquireCancelSpinLock。 当Cancel例程返回控制时,不能正持有着系统撤消自旋锁。也就是说,每个Cancel例程在返回控制之前,至少调用一次IoReleaseCancelSpinLock。 如果调用了IoAcquireCancelSpinLock,Cancel例程必须尽快做相反调用IoReleaseCancelSpinLock。 当持有自旋锁时,决不要用IRP调用IoCompleteRequest。在持有自旋锁时试图完成一个IRP会导致死锁。 带有StartIo例程的驱动程序中的Cancel例程 只有IRP在相关设备队列对象中排队时,I/O管理器才维护此设备对象中的CurrentIrp域。也就是说,只有驱动程序有StartIo例程时,这个域才是有效的。 在有StartIo例程的驱动程序中,一个典型的Cancel例程必须做下列事情: 检查输入IRP的指针是否与目标设备对象的CurrentIrp地址匹配。 如果这些指针是相等的,Cancel例程调用IoReleaseCancelSpinLock,传递Irp->CancelIrpl,然后返回控制。 如果撤消的IRP不是当前IRP,通过用IRP的Tail.Overlay.DeviceQueueEntry指针调用KeRemoveEntryDeviceQueue,来检查是否输入撤消IRP在与目标设备对象相关的设备队列中。 如果IRP在设备队列中,调用KeRemoveEntryDeviceQueue将它从队列中移出。Cancel例程调用IoReleaseCancelSpinLock;将IRP的I/O状态块的Status设为STATUS_CANCELLED,Information设为0;用撤消的IRP调用IoCompleteRequest;然后返回控制。 如果IRP不在设备队列中,Cancel例程调用IoReleaseCancelSpinLock,然后返回控制。 驱动程序的Cancel例程应该调用KeRemoveEntryDeviceQueue,以检查IRP是否在设备队列中。这个支持例程要么将给定IRP从设备队列中移走,要么只是返回FALSE以表明给定入口没有排队。Cancel例程无法确定输入IRP在设备队列中的具体位置,因此它不能调用KeRemoveEntryDeviceQueue或KeRemoveByKeyDeviceQueue来比较返回IRP的指针和输入IRP的指针。 有Cancel例程的驱动程序也能够处理IRP_MJ_CLEANUP请求,与此相关的详细内容请参见第6章中的DispatchCleanup功能。 没有StartIo例程的驱动程序中的Cancel例程 只有IRP在相关设备队列对象中排队时,I/O管理器才维护此设备对象中的CurrentIrp域。 没有StartIo例程的驱动程序管理自己的IRP内部队列。在这样的驱动程序中,Cancel例程可以用一个既不是输入目标设备对象CurrentIrp,又不是驱动程序内部队列中的IRP的输入IRP来调用。驱动程序必须维护自己的、关于哪个IRP当前正被处理的状态。此外,它还应该为其每个队列分别建立一个Cancel例程。驱动程序的内部队列应该是互锁的队列,因为它的内部队列必须被执行自旋锁保护。 当驱动程序的Cancel例程被调用时,它通常做下列事情: 调用IoReleaseCancelSpinLock,传递Irp->CancelIrpl。 获得保护其互锁队列的自旋锁,然后遍历队列,以寻找Irp->Cancel为TRUE的IRP。 如果在互锁队列中找到了这样的IRP,那么就将它移出队列;释放保护此队列的自旋锁;将此IRP的I/O状态块的Status设为STATUS_CANCELLED,将Information设为0;对撤消的IRP调用IoCompleteRequest;然后返回控制。 如果没有找到这样的IRP,Cancel例程就释放其持有的所有自旋锁,然后返回控制。 有Cancel例程的驱动程序也能处理IRP_MJ_CLEANUP请求,与此相关的详细内容请参见第6章中的DispatchCleanup功能。 处理可撤消IRP时的注意事项 这一节讨论实现Cancel例程及处理可撤消IRP的一些策略。 适用于所有Cancel例程的一般策略 I/O管理器在调用驱动程序Cancel例程时一直持有自旋锁。因此,每个Cancel例程都必须: 在返回控制之前,调用IoReleaseCancelSpinLock。 不要调用IoAcquireCancelSpinLock,除非先调用了IoReleaseCancelSpinLock。 对每个IoAcquireCancelSpinLock,都做逆调用IoReleaseCancelSpinLock。 Cancel例程每次调用IoReleaseCancelSpinLock时,都必须传递由最近一次调用IoAcquireCancelSpinLock返回的IRQL。在释放I/O管理器获得的自旋锁时,Cancel例程必须传递Irp->CancelIrpl。 驱动程序决不能在持有自旋锁时调用外部例程,因为这样会导致死锁。 使用I/O管理器定义的队列 除非驱动程序管理自己的IRP内部队列,否则它的Cancel例程可以用下列输入IRP之一调用: 输入目标设备对象中的CurrentIrp 与目标设备对象相关的入口 除非驱动程序自己管理自己的IRP内部队列,否则驱动程序的Cancel例程应该用输入IRP调用KeRemoveEntryDeviceQueue,以检测出它是否是与目标设备对象相关的设备队列中的入口。驱动程序的Cancel例程不能调用KeRemoveDeviceQueue或KeRemoveByKeyDeviceQueue,因为它无法确定给定IRP在设备队列中的确切位置。 输入IRP的当前状态 如果Cancel例程是用驱动程序已经开始为它做I/O处理并且请求将很快完成的IRP调用的,那么这个Cancel例程应该释放系统撤消自旋锁,并返回控制。 如果输入IRP的当前状态是“未决”(Pending),Cancel例程必须做下列事情: 将这个输入IRP的I/O状态块中的Status设为STATUS_CANCELLED,Information设为0。 释放它持有的所有自旋锁,包括系统撤消自旋锁。 用给定的IRP调用IoCompleteRequest。 将IRP保持在可撤消状态 要把一个IRP保持在可撤消状态的驱动程序例程必须调用IoMarkIrpPending,而且还必须调用IoSetCancelRoutine以在IRP中设置Cancel例程的入口点。只有这样做了之后,此驱动程序例程才可以调用IoStartPacket、IoAllocateController等其他支持例程,或ExInterlockedInsert.List例程。 接着处理可撤消IRP的驱动程序例程必须检查:在IRP开始被处理以满足请求前,是否它已被撤消了。这个例程必须调用IoSetCancelRoutine,从而在IRP 中将其Cancel例程入口点重设为NULL。只有这样做了之后,这个例程才能开始为输入IRP进行I/O处理。 如果驱动程序要传递IRP以供其他驱动程序例程作进一步处理,那么它可能也需要将IRP中的Cancel例程入口点重设。这些IRP可能会被保持在可撤消状态。 将IRP保持在可撤消状态的高层驱动程序,必须在它把此IRP用IoCallDriver传递给低一层驱动程序之前,将其Cancel例程入口点重设为NULL。 撤消IRP 高层驱动程序可以用它分配的IRP调用IoCancelIrp,然后传递它以供低层驱动程序做进一步处理。然而,这样的驱动程序无法保证给定IRP能够被低层驱动程序以STATUS_CANCELLED状态完成。 同步 驱动程序可以(或必须,这取决于它的设计)维护其设备扩展(device extension)中的其他状态信息,以跟踪IRP的可撤消状态。如果这种状态是与运行在IRQL<=DISPATCH_LEVEL的驱动程序例程共享的,那么共享数据应该用驱动程序分配并初始化的自旋锁保护起来。 驱动程序应该管理获得的自旋锁,并谨慎地释放系统撤消自旋锁和它自己的自旋锁。它应该在尽可能短的时间间隔内持有系统撤消自旋锁。在访问可撤消IRP之前,这种驱动程序应该先检查IoSetCancelRoutine返回的值,以确定Cancel例程是否已经在运行(或正要开始运行)。如果是,它应当命令Cancel例程完成这个IRP。 如果设备驱动程序维护各种驱动程序例程与其ISR共享的可撤消IRP的状态信息,其他的驱动程序必须同步对共享状态的访问。只有驱动程序提供的SynchCritSection例程可以访问这种在多处理器安全方式下与ISR共享的状态信息。 请参见第16章“使用自旋锁”、第8章“中断服务例程”和第10章“SynchCritSection例程”。 IoCompletion例程 这一章总结了驱动程序的标准IoCompletion例程所需要的功能,并提出了建立被IoCompletion例程处理的IRP的一些策略。 本章包括下列内容: 13.1 使用IoCompletion例程 13.1.1 建立IoCompletion例程 13.1.2 调用IoCompletion例程 13.1.3 为低层驱动程序分配IRP 13.2 IoCompletion例程需要的功能 使用IoCompletion例程 I/O管理器对IoCompletion例程的定义如下: NTSTATUS {*PIO_COMPLETION_ROUTINE}( IN PDEVICE_OBJEXT DeviceObject, IN PIRP Irp, IN PVOID Context ); 高层驱动程序完成各种操作的IRP需要多少个IoCompletion例程,就可以建立多少个IoCompletion例程。 IoCompletion例程是运行在等于或低于DISPATCH_LEVEL的IRQL上和专用线程环境中的。驱动程序无法预先知道IoCompletion例程将在哪一级IRQL上被调用。 下面是IoCompletion例程设计时的策略: 假设每个IoCompletion例程总是运行在DISPATCH_LEVEL上。 运行在IRQL DISPATCH_LEVEL限定了IoCompletion例程可以调用的支持例程的集合。 如果想对如何管理驱动程序中的IRQL有更多的了解,请参见第16章中的“管理硬件优先级”。如果想具体知道每个支持例程能在哪个IRQL上被调用,请参见在线DDK。 建立IoCompletion例程 高层驱动程序的Dispatch例程一般要负责执行下列任务: 检查它们各自的输入IRP的I/O栈中参数的有效性,对带有无效参数的请求予以拒绝。 用有效参数发送IRP。对那些Dispatch例程无法确定是否有效的,用IoCallDriver传送给低层驱动程序。 如果想更深入地了解高层驱动程序的Dispatch例程,请参见第6章“Dispatch例程”。 Dispatch例程可以用驱动程序提供的IoCompletion例程入口点调用IoSetCompletionRoutine,也可以用调用IoCallDriver时要传递给低层驱动程序的任何IRP来调用。 当调用IoSetCompletionRoutine时,Dispatch例程可以指定下列任一个或多个NTSTATUS值是否应该使IoCompletion例程运行: STATUS_SUCCESS:表示所有低层驱动程序都成功地完成了请求的操作。 STATUS_CANCELLED:表示低层驱动程序中的IRP被撤消了。 STATUS_XXX:表示低层驱动程序发生了错误,IRP出现了故障。 IoCompletion例程的用途是监视低层驱动程序用IRP做了些什么,并且对高层驱动程序传递给低层驱动程序处理的IRP做必要的清理工作。 如果想更多地了解NTSTATUS和I/O错误,请参见第16章中的“错误记录和NTSTATUS值”。 如果想知道本章中提到的每个支持例程的具体信息,请参见在线DDK。 调用IoCompletion例程 高层驱动程序的IoCompletion例程最常见的用途如下: 清除(dispose)驱动程序用IoAllocateIrp或IoBuildAsynchronousFsdRequest分配的IRP。 使用这些支持例程之一分配到IRP的高层驱动程序必须在这个IRP中建立它的IoCompletion例程。这个IoCompletion例程必须调用IoFreeIrp清除这些分配给驱动程序的IRP。 重用输入IRP以请求低层驱动程序完成某些操作(如部分传输),直到初始请求能被满足,并且被IoCompletion例程完成为止。 重试一个低层驱动程序以错误状态完成的请求。 比起中间层驱动程序(除了与端口驱动程序紧密相关的类驱动程序以外),最高层驱动程序(如文件系统)更可能有试图重试请求的IoCompletion例程。然而,任何中间层驱动程序都可以从它的IoCompletion例程中重试请求。 最高层或中间层驱动程序的DispatchReadWrite例程通常确定了指定IRP是否需要驱动程序建立IoCompletion例程。驱动程序的DispatchDeviceControl或其他Dispatch例程也能够为任何传递给低层驱动程序的给定IRP建立IoCompletion例程。 为使驱动程序分配并重用IRP,Dispatch例程必须用下列布尔类型的参数调用IoSetCompletionRoutine: 将InvokeOnSuccess设为TRUE。 将InvokeOnError设为TRUE。 如果任何低层驱动程序都可能处理可撤消IRP,则将InvokeOnCancel设为TRUE。 通常,无论IRP的返回状态是否是STATUS_CANCELLED,InvokeOnCancel都被设为TRUE。这样可以保证IoCompletion例程能释放掉每个分配给驱动程序的IRP,或者能能检查IRP的每个重用的完成状态。 当在高层驱动程序的Dispatch例程中调用IoSetCompletionRoutine例程时,考虑下列实现策略: 任何用IoAllocateIrp或IoBuildAsynchronousFsdRequest为低层驱动程序分配IRP的Dispatch例程,都必须在每个分配给驱动程序的IRP中建立IoCompletion例程。 Dispatch例程必须建立初始IRP和分配给IoCompletion例程使用的IRP的状态信息。至少,IoCompletion例程需要知道访问初始IRP的路径,以及有多少个附加IRP被分配了。 Dispatch例程应该为它分配的IRP,用值为TRUE 的InvokeOnXxx参数调用IoSetCompletionRoutine。 如果Dispatch例程在IRP中分配了一个它自己的I/O栈位置,那么它必须在调用IoGetCurrentIrpStackLocation之前调用IoSetNextIrpStackLocation,从而在它自己的I/O栈位置中为IoCompletion例程建立环境。(参见第6章中“如何从Dispatch例程中用有效参数传递IRP”) Dispatch例程必须用初始IRP调用IoMarkIrpPending,而不是用分配给驱动程序的IRP,因为IoCompletion例程会释放它们。 如果Dispatch例程正在为部分传输分配IRP,而且下层设备驱动程序可能要控制一个可删除存储介质设备,那么Dispatch例程必须根据初始IRP中Tail.Overlay.Thread的值,在它新分配的IRP中建立线程环境。 底层可删除存储介质设备的驱动程序可能会用一个分配给驱动程序的IRP调用ToSetHardErrorOrVerifyDevice,这个例程引用了指向Irp->Tail.Overlay.Thread的指针。如果底层设备驱动程序调用了这个支持例程,文件系统驱动程序可以向适当的用户线程发送一个弹出菜单,提示用户撤消、重试或完成设备驱动程序无法成功执行的操作。 更多信息参见第16章中的“处理可删除介质”。 Dispatch例程在把所有分配给驱动程序的IRP传给低层驱动程序后,必须返回STATUS_PENDING。 驱动程序的IoCompletion例程必须在它用初始IRP调用IoCompleteRequest之前,用IoFreeIrp释放所有分配给驱动程序的IRP。当它完成此初始IRP时,IoCompletion例程必须在它返回控制之前释放所有分配给驱动程序的IRP。 如果要执行一系列操作或者如果IoCompletion例程可以重试请求,那么Dispatch例程必须用将被重用的IRP调用IoSetCompletionRoutine。 Dispatch例程必须建立初始IRP的状态信息,以供IoCompletion例程使用。 例如,DispatchReadWrite例程在为低一层驱动程序在输入IRP中建立部分传输之前,必须为IoCompletion例程保存此输入IRP的相关传输参数。如果DispatchReadWrite例程修改了IoCompletion例程用来确定初始请求何时被满足的参数时,保存这些参数就显得格外重要。 如果IoCompletion例程可以重试请求,Dispatch例程必须建立一个驱动程序决定的上限,用来限制在它以错误状态完成初始IRP之前,它的IoCompletion例程可以执行多少次重试。 对于将被重用的IRP,Dispatch例程应当以值为TRUE的InvokeOnXxx参数调用IoSetCompletionRoutine。 对于异步请求,中间层驱动程序的Dispatch例程必须用初始IRP调用IoMarkIrpPending。 对于异步请求,中间层驱动程序的Dispatch例程必须在它把IRP传送给下层驱动程序之后,返回STATUS_PENDING。 驱动程序的IoCompletion例程必须维护IRP的每个重用的状态,直到初始请求被满足(或被拒绝)为止。然后它用这个IRP调用IoCompleteRequest。IoCompletion例程可能还要负责重新初始化公共状态区,或通知另一个驱动程序例程将下一个待处理的IRP移出队列。 重试IRP的IoCompletion例程必须保存重试次数,这样如果请求的操作不能被成功完成时,它可以用这个参数决定何时拒绝一个IRP。 为传递给低层驱动程序的一类请求分配每IRP(per-IRP)资源的Dispatch例程,必须用这种类型的任一IRP调用IoSetCompletionRoutine。 例如,如果Dispatch例程用IoAllocateMdl分配了一个MDL,并为一个它分配的部分传输IRP调用了IoBuildPartialMdl,那么IoCompletion例程必须用IoFreeMdl释放这个MDL。如果它分配了用来维护初始IRP状态的资源,它必须释放这些资源。最好在它用初始IRP调用IoCompleteRequest之前,最迟在它返回控制之前。 通常,在释放或完成一个IRP之前,IoCompletion例程应当释放由Dispatch例程分配给它的所有每IRP资源。此外,在IoCompletion例程从完成初始请求返回控制之前,驱动程序必须保存将被释放的资源的状态。 Dispatch例程可以用它传递给低层驱动程序的其他任何一个IRP调用IoSetCompletionRoutine。 为低层驱动程序分配IRP 最高层或中间层驱动程序的DispatchRead和/或DispatchWrite例程常常会在IRP中设立IoCompletion例程,因为传输请求必须被低层驱动程序异步处理。为了给一个异步请求分配IRP(此IRP将被低层驱动程序在专用线程环境中处理),DispatchReadWrite例程可以调用下列支持例程之一: IoAllocateIrp:调用者可以用它在IRP中为自己分配一个I/O栈位置,并在此IRP中将所有I/O栈位置初始化为0。 Dispatch例程必须在新分配的IRP中设置低一层驱动程序的I/O栈位置,通常是从初始IRP中它自己的栈位置拷贝(可能会有些修改)过来的。如果高层驱动程序在一个新分配的IRP中分配了一个它自己的I/O栈位置,Dispatch例程可以在那里建立每请求(per-request)环境信息以供IoCompletion例程使用。 IoBuildAsynchronousFsdRequest:根据调用者指定的参数,为调用者建立低一层驱动程序的I/O栈位置。 高层驱动程序可以调用这个例程为IRP_MJ_READ、IRP_MJ_WRITE、IRP_MJ_FLUSH_BUFFERS和IRP_MJ_SHUTDOWN请求分配IRP。 当IoCompletion例程被这样的IRP调用时,它能够检查I/O状态块。如果需要(或可能)的话,它在此IRP中再次建立低一层驱动程序的I/O栈位置,然后重试请求或重用它。然而,IoCompletion例程没有在IRP中为它自己保存本地环境,因此驱动程序必须在存储器的其他地方保存初始请求的环境。 IoMakeAssociatedIrp:最高层驱动程序可以用它在IRP中为自己分配一个I/O栈位置,但调用者很少在一个相关IRP中设立IoCompletion例程。 中间层驱动程序不能调用IoMakeAssociatedIrp来为低层驱动程序创建IRP。 调用IoMakeAssociatedIrp来为低层驱动程序创建IRP的最高层驱动程序,可以在传送出它的相关IRP并用主(初始)IRP调用了IoMarkIrpPending之后,将控制返回给I/O管理器。当所有相关IRP被低层驱动程序完成以后,最高层驱动程序可以靠I/O管理器来完成主IRP。 如果最高层驱动程序为它创建的相关IRP调用了IoSetCompletionRoutine,而且驱动程序从其IoCompletion例程返回STATUS_MORE_PROCESSING_REQUIRED,那么I/O管理器不完成主IRP。在这种情况下,驱动程序的IoCompletion例程必须用IoCompleteRequest显式地完成这个主IRP。 高层驱动程序的DispatchDeviceControl例程可以用IoBuildDeviceIoControlRequest分配IRP。理论上,它可以为这样分配的IRP建立IoCompletion例程,但实际上很少有人这么做。如果驱动程序为一个事件对象分配了常驻内存并将此对象初始化了,那么当其DispatchDeviceControl例程传递分配给驱动程序的IRP以满足异步设备控制请求时,它会等待一个事件。请参见高层驱动程序的DispatchDeviceControl这部分内容。 通常,在这种情况下,高层驱动程序不用IoBuildSynchronousFsdRequest在一个已被分配的IRP中设立它的IoCompletion例程。如果想知道更多关于调用这一例程的限制,请参见第6章中“为低层驱动程序分配IRP”这部分内容。 IoCompletion例程需要的功能 在入口点上,IoCompletion例程用DeviceObject、Irp和Context指针调用。调用IoSetCompletionRoutine的Dispatch例程能把Context指针传递给一个驱动程序决定的环境,这个环境是它在处理给定IRP时为IoCompletion例程建立的。注意:环境区不能是可分页的(pageable),因为IoCompletion例程可以在DISPATCH_LEVEL上调用。 IoCompletion例程负责做驱动程序请求的任何附加的IRP处理,以及满足此请求所需的任何清理操作,这取决于Dispatch例程是如何建立请求的。 实现IoCompletion例程时,应考虑以下几点: 如果输入IRP是由Dispatch例程用IoAllocateIrp或IoBuildAsynchronousFsdRequest分配的,IoCompletion例程必须调用IoFreeIrp释放这个IRP,最好在它完成初始IRP之前调用。 IoCompletion例程必须释放Dispatch例程为分配给驱动程序的IRP分配的每IRP资源,最好在它释放相应的IRP之前。 例如,如果Dispatch例程为IoCompletion例程处理的部分传输IRP分配了一个MDL,IoCompletion例程必须在它从完成初始请求返回控制之前,调用IoFreeMdl释放这个MDL。 如果IoCompletion例程不能以STATUS_SUCCESS完成初始IRP,它必须将初始IRP中的I/O状态块的值设为分配给驱动程序的IRP中返回的值,这个IRP使得IoCompletion例程拒绝了初始请求。 如果IoCompletion例程将以STATUS_PENDING完成初始IRP,它必须在调用IoCompleteRequest之前用此初始IRP调用IoMarkIrpRending。 如果IoCompletion例程必须以STATUS_Xxx错误拒绝初始IRP,那么它能够记录错误。然而,由于记录任何设备I/O错误是下层设备驱动程序的任务,所以IoCompletion例程一般不记录错误。 更多信息参见第16章中的“错误记录和NTSTATUS值”。 当IoCompletion例程已经处理并释放了分配给驱动程序的IRP之后,它必须以STATUS_MORE_PROCESSING_REQUIRED返回控制。 从IoCompletion例程返回STATUS_MORE_PROCESSING_REQUIRED防止了I/O管理器对分配给驱动程序并被释放的IRP做完成处理。第二次调用IoCompleteRequest使得I/O管理器继续调用IRP的完成例程。 如果IoCompletion例程重用了一个输入IRP以给下层驱动程序发送一个或多个请求,它应该:在再次建立低一层驱动程序的I/O栈位置之前,更新IoCompletion例程维护的此IRP的每个重用(或重试)的所有环境;用它自己的入口点调用IoSetCompletionRoutine;用此IRP调用IoCallDriver。 IoCompletion例程不应该在IRP的每个重用(或重试)中调用IoMarkIrpRending。 Dispatch例程已经将初始IRP标记为“未决”。它将保持为“未决”,直到各层驱动程序都用IoCompleteRequest完成了初始IRP为止。 在重试一个请求之前,IoCompletion例程应该将I/O状态块中的Status重设为STATUS_SUCCESS ,将Information重设为0。可能的话,需要在保存了返回的错误信息之后这样做。 通常,每次重试时,IoCompletion例程将一个由Dispatch例程建立的重试计数器递减。一般,当限定次数的重试都失败了时,IoCompletion例程必须调用IoCompleteRequest撤消这个IRP。 在IoCompletion例程用正被重用或重试的IRP调用了IoSetCompletionRoutine和IoCallDriver后,它必须返回STATUS_MORE_PROCESSING_REQUIRED。 以STATUS_MORE_PROCESSING_REQUIRED从IoCompletion例程返回防止了I/O管理器对重用或重试IRP做完成处理。 如果IoCompletion例程不能以STATUS_SUCCESS完成初始IRP,它必须将I/O状态块恢复到导致IoCompletion例程拒绝此IRP的重用或重试操作时低层驱动程序返回的状态。 如果IoCompletion例程将以STATUS_PENDING完成初始IRP,它必须在调用IoCompleteRequest之前用此初始IRP调用IoMarkIrpRending。 如果IoCompletion例程必须以STATUS_Xxx错误拒绝初始IRP,那么它能够记录错误。然而,由于记录任何设备I/O错误是下层设备驱动程序的任务,所以IoCompletion例程一般不记录错误。 详细内容请参见第16章中的错误记录和NTSTATUS值部分。 所有在IRP中设立了IoCompletion例程然后将此IRP传递给低层驱动程序的驱动程序,都应该检查IoCompletion例程中的IRP->PendingReturned标志。如果标志为1,IoCompletion例程必须用这个IRP调用IoMarkIrpRending。然而,注意:将IRP下传然后等待事件发生的驱动程序不应该将此IRP标记为“未决”。相反,它的IoCompletion例程应该向这个事件发信号,并返回STATUS_MORE_PROCESSING_REQUIRED。 IoCompletion例程必须释放由Dispatch例程分配给它用来处理初始 IRP的所有资源,最好在IoCompletion例程用此初始IRP调用IoCompleteRequest之前,最晚在IoCompletion例程从完成此初始IRP返回控制之前。 如果还有更高层驱动程序在这个初始IRP中设立了它的IoCompletion例程,直到所有低层驱动程序已经用此初始IRP调用了IoCompleteRequest之后,此驱动程序的IoCompletion例程才会被调用。 IoTimer和CustomTimerDpc例程 这一章总结了驱动程序标准IoTimer和CustomTimerDpc例程的需求,提出了使用这些例程的策略。本章包括以下内容: 14.1 定时器例程需求 14.2 IoTimer例程功能 14.2.1 建立IoTimer例程 14.2.2 为IoTimer例程建立环境 14.2.3 使用IoTimer例程 14.3 CustomTimerDpc例程功能 14.3.1 建立CustomTimerDpc例程 14.3.2 为CustomTimerDpc例程建立环境 14.3.3 使用CustomTimerDpc例程 如果想知道关于定时器对象的更多内容,请参见第3章中《与DPC相关的定时器对象》部分。 定时器例程需求 驱动程序通常为了下列目的使用定时器例程: 监视I/O操作是否超时 重启动超时的设备操作 当某项操作超时或发生了设备错误时,记录错误 除此之外,实际上驱动程序可以用定时器例程实现能满足它或它的设备需要的任何目的。 系统定义了两种类型的定时器例程:IoTimer例程和CustomTimerDpc例程。驱动程序可以有与其设备对象一样多的IoTimer例程,可以有任意多个CustomTimerDpc例程。 一般,驱动程序用IoTimer例程为需要固定的1秒时间间隔的操作定时,用CustomTimerDpc例程为需要可变时间间隔或时间间隔小于1秒的操作定时。 IoTimer例程功能 I/O管理器对IoTimer例程的定义如下: VOID (*PIO_TIMER_ROUTINE)( IN PDEVICE_OBJECT DeviceObject, IN PVOID Context ); DeviceObject参数表示与IoTimer例程相关的设备对象。Context参数指向驱动程序定义的环境信息。请参见“为IoTimer例程建立环境”。 每个IoTimer例程都与一个驱动程序创建的设备对象相关。驱动程序的所有设备对象可以共用一个IoTimer例程,也可以每个设备对象有一个独立的IoTimer例程,还可以设备对象的每个不相交子集有一个相关的IoTimer例程。 IoTimer例程运行在DISPATCH_LEVEL及专用线程环境中。 运行在DISPATCH_LEVEL限定了IoTimer例程可以调用的支持例程的集合。请参见第16章中的“管理硬件优先级”。 建立IoTimer例程 驱动程序通常在它的AddDevice例程中建立每个IoTimer例程,如下所示: 调用IoCreateDevice建立IoTimer例程将被相关的设备对象。 调用IoInitializeTimer,将IoTimer例程入口点——一个Context指针传递给所有IoTimer例程将会用到的驱动程序决定的环境区,并传递指定调用定时器例程的目标设备的DeviceObject指针。 调用IoInitializeTimer为指定设备对象注册了驱动程序的IoTimer例程。当驱动程序为指定设备对象调用IoDeleteDevice时, I/O管理器将定时器例程注销并释放与它相关的环境。 在建立了IoTimer例程之后,驱动程序通过调用IoStartTimer使这个例程可用,给相关的设备对象传递一个指针。在IoStartTimer返回后,I/O管理器约每秒调用一次IoTimer例程。然后驱动程序可以通过用同一个DeviceObject指针调用IoStopTimer使此设备对象的定时器停止工作。驱动程序可以再次调用IoStartTimer来重新使这个定时器可用,因此IoTimer例程再次以1秒的时间间隔为指定设备对象被自动调用。 通常,驱动程序用当前IRP的I/O栈位置中的DeviceObject指针来调用IoStopTimer。 不要在驱动程序的IoTimer例程内部调用IoStopTimer。 请参见“与DPC相关的定时器对象”和IoInitializeTimer。 为IoTimer例程建立环境 传递给IoInitializeTimer的Context指针指向一个环境区,其他驱动程序例程及IoTimer例程自己可以用这个环境区来保存被定时操作的状态。I/O管理器每次调用IoTimer例程时,都会传递这个Context指针。 环境区必须在驱动程序分配的常驻内存中。虽然这个环境区可以位于驱动程序分配的非页式存储池中,但通常它都在相关设备对象的设备扩展中。如果驱动程序使用了一个控制器对象,那么环境区就可以在此控制器扩展中。环境区的内容是由驱动程序决定的。 如果IoTimer例程与驱动程序的ISR共享其环境区,为了能安全访问共享的环境,IoTimer例程必须用驱动程序提供的SynchCritSection例程调用KeSynchronizeExecution。 如果IoTimer例程只是与非ISR驱动程序例程共享其环境区,由Context指定的共享区必须用执行自旋锁保护起来。请参见第16章中的“使用自旋锁”部分。 使用IoTimer例程 当相关设备对象的定时器可用时,IoTimer例程约每秒被调用一次。然而,I/O管理器是用Kernel定义的定时器对象调用驱动程序的IoTimer例程的。所以每次IoTimer例程调用的间隔最终取决于系统时钟的精度,因此IoTimer例程不会总是精确地以1秒的时间间隔被调用。 IoTimer例程最常见的应用是在事件到时停止当前IRP的设备I/O操作。在设备驱动程序中将IoTimer例程作为一个运行定时器使用时,需要考虑下列情况: 当启动设备时,驱动程序将此设备扩展中的定时计数器初始化为-1,表明当前没有设备I/O操作。然后在返回STATUS_SUCCESS之前调用IoStartTimer。 每次IoTimer例程被调用时,它都要检查定时计数器是否是-1。如果是,就返回控制。 驱动程序的StartIo例程将设备扩展中的定时计数器初始化为一个上限。为了防止IoTimer例程刚刚被运行,所以要给此上限加上附加的秒数。然后用SynchCritSection_1例程调用KeSynchronizeExecution,来对物理设备编程以实现当前IRP所请求的操作。 在将驱动程序的DpcForIsr例程或CustomDpc例程排队前,驱动程序的ISR将定时计数器重设为-1。 每次IoTimer例程被调用时,它都要检查定时计数器是否已被ISR重设为-1。如果是,就返回控制;否则,IoTimer例程用SynchCritSection_2例程调用KeSynchronizeExecution,SynchCritSection_2例程能在驱动程序决定的秒数范围内调节定时计数器。 只要当前请求还没有超时,SynchCritSection_2例程就会向IoTimer例程返回TRUE。如果定时计数器变为0,SynchCritSection_2例程将定时计数器重设为一个驱动程序决定的重设超时值,在它的环境区中为它自己(和DpcForIsr)设一个重设期望标志(reset-expected flag),试着重设设备,然后返回TRUE。 如果SynchCritSection_2例程的对设备的重设操作仍然超时,它会在返回FALSE时被再次调用。如果重设成功,DpcForIsr例程根据重设期望标志确定设备已被重设并重试请求,重复第2步描述的StartIo例程的动作。 如果SynchCritSection_2例程返回FALSE,由于重设的努力已经失败,IoTimer例程认为物理设备处于一种不可知的状态。在这种情况下,IoTimer例程将CustomDpc例程排队,然后返回。这个CustomDpc例程记录一个设备I/O错误,调用IoStartNextPacket,拒绝当前IRP,然后返回。 如果设备驱动程序的ISR将共享的定时计数器重设为-1(像第3步中描述的那样),那么驱动程序的DpcForIsr例程将会完成当前IRP的中断驱动的I/O处理。重设的定时计数器表示这个设备的I/O操作还没有超时,因此IoTimer例程不需要改变定时计数器。 在大多数情况下,前面的SynchCritSection_2例程只是减小定时计数器。只有在当前I/O操作超时时(即定时计数器变为0时),SynchCritSection_2例程才会试着重设设备。而且只有当重设设备失败后,SynchCritSection_2例程才会向IoTimer例程返回FALSE。 因此,前面的IoTimer例程和它的支持例程——SynchCritSection_2例程在正常情况下执行的时间非常短。用这种方式使用IoTimer例程,设备驱动程序能够保证每个有效的设备I/O请求都可以被重试(如果需要的话),它还能够保证:只有当不可修复的硬件错误使IRP无法被满足时,DpcForIsr例程才会拒绝这个IRP。此外,驱动程序执行这项功能在时间上的开销是非常小的。 然而,前面所说的简单方案要求设备每次只做一项操作,还要求驱动程序在正常情况下不做重叠的(overlap)I/O操作。执行重叠设备I/O操作的驱动程序,或使用IoTimer例程使得驱动程序分配的被发送到多个低层驱动程序链的IRP超时的高层驱动程序,需要管理更复杂的超时情况。 CustomTimerDpc例程功能 Kernel对CustomTimerDpc例程的定义如下: VOID (*PKEDFERRED_ROUTINE)( IN PKDPC Dpc, IN PVOID DeferredContext, IN PVOID SystemArgument1, //reserved for system use IN PVOID SystemArgument2 //reserved for system use ); Dpc参数指明了与定时器相关的DPC对象,DeferredContext指向驱动程序提供的环境区,在CustomTimerDpc例程中SystemArgument1和SystemArgument2没有使用。详细内容请参见下一节。 CustomTimerDpc例程运行在DISPATCH_LEVEL及专用线程环境中。 运行在DISPATCH_LEVEL限定了CustomTimerDpc例程可以调用的支持例程的集合。请参见第16章中的管理硬件优先级部分。 内核定义的定时器对象的时间间隔的精度大约是10毫秒,它与系统时钟精度有关。因此,CustomTimerDpc例程可以以驱动程序决定的可变间隔调用。它的调用间隔比IoTimer例程小,后者每秒被调用一次。 设置CustomTimerDpc例程 每个CustomTimerDoc例程都被关联到一个内核定义的定时器对象和一个DPC对象。驱动程序调用KeInitializeDpc以创建这个DPC对象,以及调用KeInitializeTimer或KeInitializeTimerEx来创建这个定时器对象并将它关联到那个DPC对象,如第3章中的“注册和排队CustomTimerDpc例程”所叙述的那样。 作为最低限度,任何拥有一个或多个CustomTimerDpc例程的驱动程序必须为至少一个定时器对象(KTIMER类型的)提供常驻存储空间,并且为一定数量(这取决于这个驱动程序拥有多少DPC例程)的DPC对象(KDPC类型的)提供常驻存储空间。 拥有CustomTimerDpc例程的驱动程序应该为驱动程序拥有的每个驱动程序的定时器对象和DPC对象提供常驻存储空间,几乎没有例外情况。 为设置定时器对象,驱动程序必须在它的AddDevice例程中作下列事情: 使用CustomTimerDop例程和指向驱动程序定义的环境(Context)区域的DeferredContext指针调用KeInitializeDpc以设置DPC对象。 调用KeInitializeTimer或KeInitializeTimerEx来设置定时器对象被关联到这个CustomTimerDpc例程 设置CustomTimerDpc例程与设置CustomDpc例程非常相似,只是多了个额外的步骤来初始化定时器对象。(事实上,他们的原型是相同的。) 当然,与CustomDpc例程不同,CustomTimerDpc例程不能使用在其原型中声明的两个SystemArgument指针。在驱动程序的ISR能传递这两个指针(当驱动程序调用KeInsertQueueDpc以排队CustomDpc例程时,使用这两个指针)期间,当驱动程序调用KeSetTimer或KeSetTimerEx以激活定时器时,驱动程序不能传递这些额外的上下文指针到它的CustomTimerDpc例程。 更多信息参见第9章,“DpcForIsr例程和CustomDpc例程”,以及第3章中的“与Dpc相关的定时器对象”。关于本章内提到的任何支持例程的详细信息参见在线DDK。 为CustomTimerDpc例程设置环境 传递给KeInitializeDpc的DeferredContext指向其他驱动程序例程和CustomTimerDpc例程自己能在这里维护状态信息的环境区域。内核在每个对DPC例程的调用中传递DeferredContext指针。 与IoTimer例程不同,CustomTimerDpc与驱动程序创建的设备对象间没有特别的关联。然而,驱动程序可以通过在调用KeInitializeDpc时,将一个指针作为DeferredContext传递给设备对象,从而使CustomTimerDpc例程与一个驱动程序创建的设备对象相关联。 环境区必须在驱动程序分配的常驻内存中。通常,这个环境区在设备扩展中,但是它也可以在驱动程序分配的非页式存储池中。如果驱动程序使用了控制器对象,它也可以在控制器扩展中。环境区的内容是由驱动程序决定的。 如果CustomTimerDpc例程与驱动程序的ISR共享环境信息,为了安全地访问共享环境,CustomTimerDpc例程必须用SynchCritSection例程调用KeSynchronizeExecution。 如果CustomTimerDpc例程与其他非ISR驱动程序例程共享环境信息,DeferredContext处的区必须用一个执行自旋锁来保护。详见第16章的“使用自旋锁”。 使用CustomTimerDpc例程 在初始化了AddDevice例程中的定时器和DPC对象后,驱动程序必须显式地将CustomTimerDpc例程与一个定时器对象相关联。为了这样做,它调用KeSetTimer或KeSetTimerEx,传递指向定时器和DPC对象的指针和表示定时器到期的DueTime参数。KeSetTimer使得一旦定时器到期就调用CustomTimerDpc例程。KeSetTimerEx设置一个循环(recurring)定时器,它除了上述参数外,还要接受一个Period参数。这个参数定义了CustomTimerDpc例程应该被调用的时间间隔。 到期时间是以100纳秒为单位被指定的。正值表示从1601年1月1日起以100纳秒的间隔为单位的绝对时间。负值表示相对于当前系统时间的间隔。(调用KeQuerySystemTime以获得当前系统时间。) 当DueTime到了时,内核将CustomTimerDpc例程排队。因为这个例程可能要在队列中待不确定的时间长度,所以在CustomTimerDpc例程开始运行前的总时间可能会比驱动程序在调用KeSetTimer或KeSetTimerEx时指定的DueTime时间间隔长些。 如果在定时器到期前,系统时间有改变,那么相对定时器将不受影响,但系统会调节绝对定时器。相对定时器总是在指定个时间单位过去后到期,不管绝对系统时间如何改变。绝对定时器在明确的系统时间到期,因此对系统时间的改变会改变绝对定时器的等待。 为了使先前设立的定时器对象不可用,驱动程序调用KeCancelTimer。如果驱动程序例程用Dpc指针为它的CustomTimerDpc例程调用KeSetTimer或KeSetTimerEx,接下来用同一Timer指针调用KeCancelTimer会产生下列结果之一: 如果传递给KeSetTimer或KeSetTimerEx的最近指定的时间间隔还没有到,那么定时器对象和代表CustomTimerDpc例程的DPC对象将被从队列中移出并被设为不可用。也就是说,KeCancelTimer调用撤消了对驱动程序CustomTimerDpc例程的调用。 如果传递给KeSetTimer或KeSetTimerEx的最近指定的时间间隔在KeCancelTimer调用时正要到期,CustomTimerDpc例程可能会也可能不会被调用,这取决于在KeCancelTimer执行之前系统时钟中断是否使定时器对象从定时器队列中移出。 在前面指定的时间间隔到时前,用相同的Timer指针和Dpc指针再次调用KeSetTimer或KeSetTimerEx将会产生下列结果: 对KeSetTimer或KeSetTimerEx的新调用为已排队的定时器对象隐式地撤消了先前指定的DueTime。 在定时器对象中设置了新的DueTime。 对于KeSetTimer,CustomTimerDpc例程在最近指定的DueTime到时后只被调用一次。对于一个用KeSetTimerEx设置的周期定时器,这个定时器的一个实例被删除。(因此CustomTimerDpc例程的一个执行也被删除了) 将同一个定时器对象用于不同目的会引起竞态条件或严重的驱动程序错误。例如,同一个Timer指针既被驱动程序用来建立对CustomTimerDpc例程的调用,又被用来建立在驱动程序专用线程中的等待。无论何时驱动程序专用线程用此公共Timer指针调用KeSetTimer(或KeSetTimerEx或KeCancelTimer),如果定时器对象已经在排队等待CustomTimerDpc调用,那么这个线程可能会随机地撤消掉对CustomTimerDpc例程的调用。 对于有CustomTimerDpc例程且在非专用线程环境中等待定时器对象的驱动程序: 决不要在非专用线程环境中使用线程环境敏感的定时器对象,反之亦然。 每个CustomTimerDpc应该有自己的相关定时器对象。在非专用线程环境中调用的每个驱动程序线程和/或驱动程序例程的集合应该有自己的“可等待”定时器对象集合。 如果使用了CustomTimerDpc例程,要慎重选择在驱动程序调用KeSetTimer或KeSetTimerEx时传递的时间间隔。此外,要考虑使用来自其他驱动程序例程的同一定时器对象调用KeCancelTimer的所有可能结果,特别是在Windows NT/Windows 2000 SMP平台上。 记住下列关于CustomTimerDpc例程和CustomDpc例程的事实: 在任何时刻,只有代表一个特定的DPC例程的DPC对象的一个实例可以排队等待执行。 如果在第一个调用者指定的时间间隔到时之前,第二个驱动程序例程调用KeSetTimer或KeSetTimerEx运行同一个CustomTimerDpc例程,那么这个CustomTimerDpc例程只有在第二个调用者指定的时间间隔到时之后才会运行。在这种情况下,CustomTimerDpc例程不会做第一个例程调用KeSetTimer或KeSetTimerEx时要做的任何工作。 对于有CustomTimerDpc例程而且使用周期定时器的驱动程序: 驱动程序不能从DPC例程中重新分配一个周期定时器。驱动程序只能从DPC例程中重新分配非周期定时器。 下面是既有CustomDpc例程又有CustomTimerDpc例程的驱动程序的设计策略: 为了防止出现竞态条件,决不要把同一个Dpc指针传递给KeSetTimer或KeSetTimerEx和KeInsertQueueDpc。 也就是说,如果StartIo例程调用KeSetTimer或KeSetTimerEx以运行CustomTimerDpc例程,而且ISR用同一个Dpc指针从另一个处理器同时调用KeInsertQueueDpc,那么这个DPC例程将会在处理器上的IRQL降至DISPATCH_LEVEL以下或定时器间隔到时时被运行。具体是二者中的哪一个,得看哪个先到。无论哪个先到,只是由DPC例程完成为StartIo例程或ISR所做的必要工作。 此外,被两个标准驱动程序例程以很小功能差别使用的DPC的性能特性比用独立的CustomDpc和CustomTimerDpc例程时的差。DPC不得不决定究竟执行哪些操作,这取决于它被StartIo例程或ISR排队的原因。在DPC中检测这些条件需要额外的CPU循环。 Unload例程 这一章总结了驱动程序标准Unload例程的需求。本章包括以下内容: 15.1 Unload例程需求 15.2 Unload例程环境 15.3 Unload例程功能 15.3.1 PnP驱动程序的Unload功能 15.3.2 非PnP驱动程序的Unload功能 15.3.2.1 释放分配给驱动程序的资源 15.3.2.2 释放设备和控制器对象 如果想知道本章提到的支持例程的具体内容,请参见在线DDK。 Unload例程需求 任何在系统运行时可以被替换或卸载并重载的驱动程序都必须有Unload例程。 I/O管理器对Unload例程的定义如下: VOID (*PDRIVER_UNLOAD)( IN PDRIVER_OBJECT DriverObject ); DriverObject参数是传递给驱动程序的DriverEntry例程的指针。 视频、键盘或鼠标设备和能保存系统当前页文件的海量存储设备的驱动程序没有Unload例程,因为这些类型的设备必须在当前系统运行时始终可用。位于这些设备驱动程序之上的高层驱动程序也没有Unload例程。 Unload例程负责在驱动程序自身被卸载之前释放驱动程序使用的所有系统对象和资源。具体内容请参见Unload例程功能一节。 像DriverEntry例程一样,Unload例程运行在PASSIVE_LEVEL上并在系统线程的环境中被调用。 Unload例程环境 I/O管理器和PnP管理器一起工作来卸载驱动程序。要么是I/O管理器,要么是PnP管理器调用驱动程序的Unload例程。 当驱动程序被替换,或驱动程序服务的所有设备均被移除,或驱动程序初始化失败时,操作系统会卸载这个驱动程序。在卸载程序段开始时(有可能在调用驱动程序的Unload例程之前),I/O管理器或PnP管理器将此驱动程序对象和它的设备对象标记为“卸载未完”(Unload Pending)。 在驱动程序被标记为“卸载未完”后,没有其他驱动程序可以附属于这个驱动程序,也不能对此驱动程序的设备对象作任何其他引用。驱动程序可以完成未决的IRP,但I/O管理器不会向这个驱动程序发送任何新的IRP。当所有对此驱动程序及其设备对象的引用都被释放后,I/O管理器调用驱动程序的Unload例程。 因此,I/O管理器只有在下列条件均满足时,才会调用驱动程序的Unload例程: 驱动程序创建的设备对象的引用均没有保留。也就是说,与底层设备相关的文件不能被打开,而且驱动程序的任何设备对象的IRP都不能是未决的。 没有其他驱动程序附属于这个驱动程序。 驱动程序将所有它以前注册的PnP通知(notification)撤消注册。(参见《Windows 2000驱动程序开发指南》第1卷中的IoRegisterPlugPlayNotification和IoUnregisterPlugPlayNotification) 当驱动程序从它的DriverEntry例程返回一个错误状态时,I/O管理器也会调用Unload例程。 如果驱动程序在处理完一个IRP_MN_REMOVE_DEVICE请求后,已没有其他设备对象,那么PnP管理器会调用PnP驱动程序的Unload例程。PnP驱动程序释放设备指定的资源;从设备栈中移除;删除其函数和过滤器设备对象以响应这个请求。在设备移除后,PnP管理器调用驱动程序的Unload例程,并且不再向此驱动程序发送IRP。 在系统关闭时,PnP管理器和I/O管理器均不会调用Unload例程。需要执行特殊关闭处理的驱动程序应该注册一个DispatchShutdown例程。(参见《Windows 2000驱动程序开发指南》第1卷中的IoRegisterShutdownNotification) Unload例程功能 在Unload例程中,驱动程序必须释放所有对象和所有分配给驱动程序的资源。总之,它必须撤消相应的DriverEntry例程和Reinitialize例程在初始化驱动程序及其设备和资源时所做的所有工作。 就像PnP驱动程序的DriverEntry例程通常很简单一样,它们的Unload例程也很简单,如PnP驱动程序的Unload功能所描述。 文件系统和不处理PnP管理器的IRP_MN_REMOVE_DEVICE请求的早期驱动程序一般必须在Unload例程中释放设备指定的对象和资源。具体内容请参见非PnP驱动程序的Unload功能部分。 PnP驱动程序的Unload功能 PnP驱动程序释放设备指定的资源和设备对象以响应PnP设备移除IRP。PnP管理器用IoReportDetectedDevice发送代表设备的IRP,这些设备包括PnP管理器列举的所有PnP设备和驱动程序报告的根列举(root-enumerated)的早期设备。 因此,PnP驱动程序的Unload例程通常是很简单的,一般只包括一个Return语句。然而,如果驱动程序在它的DriverEntry例程中分配了驱动程序范围的资源,它必须先在其Unload例程中收回这些资源(除非它已经这么做过了)。一般,卸载PnP驱动程序的处理是同步操作。 I/O管理器用IoAllocateDriverObjectExtension释放驱动程序对象和所有驱动程序分配的驱动程序对象扩展。 非PnP驱动程序的Unload功能 高层文件系统和不处理PnP设备移除请求的早期驱动程序必须在它们的Unload例程中释放资源、删除设备对象并从设备栈中移除。 如果它还没有这么做,早期设备驱动程序在其Unload例程中应该做的第一件事是使设备中断无效。否则,在Unload例程正释放设备扩展(ISR需要用它来处理中断)中的资源期间,它的ISR可能被调用以处理设备中断。即使它的ISR在这种情况下能成功返回,此ISR排队的DpcForIsr或CustomDpc例程,可能还有运行在IRQL>=DISPATCH_LEVEL的其他驱动程序例程,会在Unload例程重新获得控制之前执行,这样就增加了Unload例程删除一个其他驱动程序例程还在引用的资源的可能性。请参见第16章中的“管理硬件优先级”。 在关闭了中断后,文件系统和早期驱动程序必须释放资源和对象。具体内容请参见以下两节: 15.3.2.1 释放分配给驱动程序的资源 15.3.2.2 释放设备和控制器对象 释放分配给驱动程序的资源 每个驱动程序怎样使用注册表、怎样在其设备扩展或控制器扩展或分配给驱动程序的非页式存储池中建立系统对象和资源都是不同的。然而,所有Unload例程都必须分阶段释放驱动程序使用的资源。 实现Unload例程时应考虑下列策略: 所有驱动程序的Unload例程都必须保证:在它释放资源之前,没有其他驱动程序例程正在使用或可能很快会使用这个资源。 一般,Unload例程用下列步骤释放所有分配给驱动程序的资源: 如果驱动程序还没有禁止物理设备的所有中断,那么关闭它们。一旦中断被关闭了,就调用IoDisconnectInterrupt。 确保没有其他驱动程序例程能引用Unload例程将要释放的资源。 例如,如果驱动程序的IoTimer例程正为某个设备对象使用,那么Unload例程必须调用IoStopTimer。它必须保证在释放其调用对象的存储区之前,没有线程正在等待驱动程序的任何调度对象,而且它的定时器对象没有排队等待其CustomTimerDpc例程的调用。如果它有ISR可能已经排队的CustomTimerDpc例程,它必须调用KeRemoveQueueDpc,其余同理。 如果驱动程序调用了IoQueueWorkItem,它必须保证工作项(work item)已经完成。IoQueueWorkItem会取得一个对相关设备对象的引用。如果这种引用保留的话,驱动程序不能被卸载。 如果驱动程序调用了PsCreateSystemThread,Unload例程还必须使这个驱动程序创建的线程被运行,这样这个线程自身就可以在驱动程序被卸载之前调用PsTerminateSystemThread。驱动程序不能通过用PsCreateSystemThread返回的ThreadHandle调用ZwClose来释放驱动程序创建的系统线程。 释放所有分配给驱动程序的设备指定的资源。这样做可能需要调用下列系统支持例程: IoDeleteSymbolicLink:如果DriverEntry例程或Reinitialize例程调用了IoCreateSymbolicLink或IoCreateUnprotectedSymbolicLink;IoDeassignArcName:如果驱动程序调用了IoAssignArcName ExFreePool:如果DriverEntry例程或任何其他驱动程序例程调用了ExAllocatePool,而且驱动程序还没有释放分配的内存 MmUnmapIoSpace:如果DriverEntry例程或Reinitialize例程调用了MmMapIoSpace MmFreeNonCachedMemory:如果DriverEntry例程或Reinitialize例程调用了MmAllocateNonCachedMemory MmFreeContiguousMemory:如果DriverEntry例程或Reinitialize例程调用了MmAllocateContiguousMemory FreeCommonBuffer:如果DriverEntry例程或Reinitialize例程调用了AllocateCommonBuffer IoAssignResourecs或IoReportResourceUsage:如果DriverEntry例程或Reinitialize例程调用了这些支持例程之一,或调用了HalAssignSlotResources以在配置注册表中为它自己和/或它的物理设备分别声明硬件资源 释放DriverEntry例程或Reinitialize例程在设备对象的设备扩展或控制器对象(如果创建了的话)的控制器扩展中建立的系统对象和资源。特别地,驱动程序必须在试图删除此设备对象(IoDeleteDevice)或控制器对象(IoDeleteController)前做下列事情: 调用IoDisconnectInterrupt以释放存储在相应设备或控制器扩展中的中断对象指针。 如果调用了IoGetDeviceObjectPointer,并将指向低一层驱动程序的文件对象的指针存储在了设备或控制器扩展中,用这个指针调用ObDereferenceObject。 如果调用了IoAttachDevice或IoAttachDeviceToDeviceStack,并将指向下层驱动程序的设备对象的指针存储在了设备或控制器扩展中,用这个指针调用IoDetachDevice。 如果有的话,释放DriverEntry例程或Reinitialize例程为驱动程序的物理设备声明的硬件资源,它们位于注册表的\Registry\Machine\Hardware\ResourceMap下。 删除DriverEntry例程或Reinitialize例程存储在注册表的\Registry…\DeviceMap下的所有设备名。 在驱动程序释放了设备、系统和硬件资源之后,它可以删除它的设备和控制器对象,这在下一节中详细说明。 释放设备和控制器对象 在驱动程序删除设备或控制器对象之前,它必须先释放对存储在相应设备或控制器扩展中的外部资源的引用,如指向其他驱动程序对象和/或中断对象的指针。然后它可以为每个驱动程序创建的设备对象调用IoDeleteDevice。同理,前面调用了IoCreateController的Windows 2000驱动程序必须调用IoDeleteController。 驱动程序在设备扩展中保存的Kernel定义的对象,在Unload例程用相应设备对象调用IoDeleteDevice时自动被释放。一般,对于DriverEntry例程或Reinitialize例程通过调用KeInitializeXxx建立的对象,如果驱动程序在其设备扩展中为它提供了存储区,那么它可以通过调用IoDeleteDevice自动释放。例如,如果驱动程序有CustomTimerDpc例程,而且在它的设备扩展中为必要的DPC和定时器对象提供了存储区,那么调用IoDeleteDevice可以释放掉这些系统资源。 类似地,驱动程序在控制器扩展中保存的Kernel定义的对象,在Unload例程用相应控制器对象调用IoDeleteController时自动被释放。 如果DriverEntry例程或Reinitialize例程调用了IoGetConfigurationInformation以增加对特定类型设备的记数值,Unload例程也必须调用IoGetConfigurationInformation,并在它删除相应设备对象时,在I/O管理器的全局配置信息结构中减小对此设备的记数值。 在返回控制前,Unload例程还要负责释放分配给其他驱动程序而且还没有被其他驱动程序释放的所有资源。 常见的驱动程序设计问题 这一章介绍了所有驱动程序开发者都会感兴趣的一些内容,主要包括以下几部分: 总结了标准驱动程序例程运行的缺省硬件优先级(IRQL)以及在适当的IRQL上调用支持例程的一些策略 关于使用自旋锁的一般策略,这些自旋锁用来同步对驱动程序例程共享的数据或资源的访问 关于用内核栈和后备列表分配系统空间内存的一般策略。 驱动程序应该怎样处理I/O错误,以及NTSTATUS值是怎样定义的 怎样使所有或部分驱动程序映像可分页 怎样注册设备接口以使其他内核模式和用户模式的代码可以访问设备 怎样避免会影响驱动程序可靠性的的常见问题 这一章还讨论了设备类型决定或设计决定的设计问题,包括下列内容: 对最低层设备驱动程序,是驱动程序轮询设备,还是建立一个等待Kernel定义的调度者对象的线程,即是用时间还是用信号量 对于DMA或PIO驱动程序,怎样在传输操作期间维护缓存的一致性和数据的完整性 对于可删除存储介质设备(removable-media)的驱动程序,怎样处理用户引起的错误(如提供了错误的存储介质或移除了在其上有文件打开的存储介质) 这一章的目录如下: 16.1 管理硬件优先级 16.2 使用自旋锁 16.2.1 为自旋锁和被保护数据提供存储空间 16.2.2 初始化自旋锁 16.2.3 调用使用了自旋锁的支持例程 16.2.4 快速释放自旋锁 16.2.5 使用自旋锁时防止错误或死锁的出现 16.3 轮询设备 16.4 管理内存的使用 16.4.1 使用系统内存 16.4.1.1 访问用户空间内存的驱动程序 16.4.1.2 为部分传输请求建立MDL 16.4.1.3 分配系统空间内存 16.4.1.4 将总线相关(Bus-Relative)的内存空间地址重新映射为虚地址 16.4.2 使用内核栈 16.4.3 使用后备列表(lookaside list) 16.5 对DMA和PIO维护缓存的一致性 16.5.1 在DMA操作期间刷新缓存数据 16.5.2 在PIO操作期间刷新缓存数据 16.6 错误记录和NTSTATUS值 16.6.1 调用IoAllocateErrorLogEntry 16.6.2 填充错误记录包 16.6.3 设置错误记录包中的NTSTATUS值 16.6.4 调用IoWriteErrorLogEntry 16.6.5 定义新的IO_ERR_XXX 16.6.6 定义私有NTSTATUS常量 16.7 处理可删除存储介质 16.7.1 响应来自文件系统的验证(Check-Verify)请求 16.7.2 通知文件系统可能的存储介质改变 16.7.3 检查设备对象中的标志 16.7.4 在中间层驱动程序中建立IRP 16.8 使设备对应用程序和驱动程序可用 16.8.1 注册设备接口 16.8.2 使设备接口可用和不可用 16.8.3 使用设备接口 16.9 可分页代码和数据 16.9.1 使驱动程序代码可分页 16.9.2 锁住可分页代码或数据 16.9.3对整个驱动程序分页 16.10 常见的驱动程序可靠性问题 16.10.1 缓冲I/O中的错误 16.10.2 引用用户空间地址时的错误 16.10.3 直接I/O中的错误 16.10.4 调用者输入和设备状态的错误 16.10.5 Dispatch例程中的错误 16.10.6 多处理器环境中的错误 16.10.7 处理IRP时的错误 管理硬件优先级 特定设备或中间层驱动程序例程运行的IRQL决定了它能调用哪些内核模式的支持例程。例如,有些支持例程要求调用者运行在为DISPATCH_LEVEL的IRQL上。其他例程在调用者运行在提高的(raised)IRQL(即高于PASSIVE_LEVEL的IRQL)时不能被安全地调用。 表16.1列出了最常见的标准驱动程序例程被调用的缺省IRQL以及Kernel定义的IRQL值(由低到高)。 表16.1 驱动程序例程的缺省IRQL IRQL(由低到高) 屏蔽掉的中断 运行在此IRQL的支持例程  PASSIVE_LEVEL 无 Dispatch、DriverEntry、AddDevice、Reinitialize、Unload例程、驱动程序创建的线程、工作者线程(work-thread)回调、文件系统驱动程序  DISPATCH_LEVEL DISPATCH_LEVEL和APC_LEVEL中断被屏蔽掉了。设备、时钟和电源错误中断仍可发生 StartIo、AdapterControl、AdapterListControl、ControllerControl、IoTimer、Cancel(持有撤消自旋锁时)、DpcForIsr、CustomTimerDpc、CustomDpc例程  DIRQL 驱动程序中断对象中所有IRQL<=DIRQL的中断。时钟和电源错误中断仍可发生 ISR、SyncCritSection例程  当运行在下列三种IRQL之一时,由最低层驱动程序处理IRP: PASSIVE_LEVEL:没有处理器中断被屏蔽掉,在驱动程序的Dispatch例程中。 DriverEntry、AddDevice、Reinitialize和Unload例程也运行在PASSIVE_LEVEL,此外还有驱动程序创建的系统线程 DISPATCH_LEVEL:处理器的DISPATCH_LEVEL和APC_LEVEL中断被屏蔽掉了,在StartIo例程中。 AdapterControl、AdapterListControl、ControllerControl、IoTimer、Cancel(持有撤消自旋锁时)、DpcForIsr、CustomTimerDpc和CustomDpc例程也都运行在DISPATCH_LEVEL。 Device IRQL(DIRQL):处理器上所有低于或等于驱动程序中断对象的SynchronizeIrql的中断都被屏蔽掉了,在ISR和SyncCritSection例程中。 当运行在下列两种IRQL时,由更高层驱动程序处理IRP: PASSIVE_LEVEL:没有处理器中断被屏蔽掉,在驱动程序的Dispatch例程中。 DriverEntry、AddDevice、Reinitialize和Unload例程也运行在PASSIVE_LEVEL,此外还有驱动程序创建的系统线程、工作者线程回调或文件系统驱动程序。 DISPATCH_LEVEL:处理器的DISPATCH_LEVEL和APC_LEVEL中断被屏蔽掉了,在驱动程序的IoCompletion例程中。 IoTimer、Cancel和CustomTimerDpc例程也都运行在DISPATCH_LEVEL。 有时,海量存储设备的中间层和最低层驱动程序在等于APC_LEVEL的IRQL上被调用。特别是,这种情况会在文件系统驱动程序向低层驱动程序发送IRP_MJ_READ请求导致页错误时发生。 大多数标准驱动程序例程运行在仅能使它们调用适当的支持例程的IRQL上。例如,当设备驱动程序运行在等于DISPATCH_LEVEL的IRQL上时,它必须调用AllocateAdapter或IoAllocateController。由于多数设备驱动程序从StartIo例程中调用这些例程,因此它们通常运行在DISPATCH_LEVEL。 应注意的是,对于没有StartIo例程的设备驱动程序,因为它建立并管理自己的IRP队列,所以当它应该调用AllocateAdapter(或IoAllocateController)时,不一定非要运行在等于DISPATCH_LEVEL的IRQL上。这样的驱动程序必须在调用KeRaiseIrql和调用KeLowerIrql之间调用AllocateAdapter,于是当它调用AllocateAdapter时,就能运行在要求的IRQL上,而且当调用例程重新获得控制时,能够恢复初始IRQL。 为了能在适当的IRQL调用支持例程并能在驱动程序中成功地管理硬件优先级,应当注意下列情况: 用低于当前IRQL的输入NewIrql值调用KeRaiseIrql会导致一个致命错误。调用KeLowerIrql以期望恢复初始IRQL(也就是,在调用KeRaiseIrql之后)也会导致一个致命错误。 当运行在提高的IRQL上时,用Kernel定义的调度者对象调用KeWaitForSingleObject或KeWaitForMultipleObjects以在非零时间段中等待会导致一个致命错误。只有运行在非任意线程和PASSIVE_LEVEL的驱动程序例程(如驱动程序创建的线程、DriverEntry例程和Reinitialize例程、或像大多数设备I/O控制请求那样的同步I/O操作的Dispatch例程)能在非零时间段中安全地等待时间、信号量、互斥体或定时器。 即使运行在PASSIVE_LEVEL上,可分页代码也决不能在输入Wait参数为TRUE的情况下,用它调用KeSetEvent、KeReleaseSemaphore或KeReleaseMutex。这样的调用会导致一个致命的页错误。 运行在高于APC_LEVEL的IRQL上的例程既不能从页式存储池中分配内存,也不能安全地访问页式存储池中的内存。如果这样的例程引起了一个页错误,这个错误将是致命的。 当驱动程序调用KeAcquireSpinLockAtDpcLevel和KeRelaeseSpinLockFromDpcLevel时,它必须运行在DISPATCH_LEVEL上。 当驱动程序调用KeAcquireSpinLock时,它可以运行在低于DISPATCH_LEVEL的IRQL上,但是它必须通过调用KeRelaeseSpinLock释放这个自旋锁。也就是说,通过调用KeRelaeseSpinLockFromDpcLevel释放由调用KeAcquireSpinLock获得的自旋锁是编程错误。 当驱动程序运行在高于DISPATCH_LEVEL的IRQL上时,它绝对不能调用KeAcquireSpinLockAtDpcLevel、KeRelaeseSpinLockFromDpcLevel、KeAcquireSpinLock或KeRelaeseSpinLock。 如果调用者还没有运行在这些提高后的IRQL上,调用使用了自旋锁的支持例程(如ExInterlockedXxx)会将当前处理器上的IRQL提高到DISPATCH_LEVEL或DIRQL。 运行在提高IRQL上的驱动程序代码应该尽快执行。为了获得好的整体性能,例程运行的IRQL越高,就越应该将例程执行速度调得尽可能快。例如,调用KeRaiseIrql的驱动程序应当尽快地做逆调用KeLowerIrql。 请使用在线DDK参见“使用自旋锁”部分和例程相应的参考部分。 使用自旋锁 自旋锁是由内核定义的内核模式仅有(kernel-mode-only)的一种同步机制,它以一种不透明类型KSPIN_LOCK向外界输出。当在Windows NT/Windows 2000 SMP机器上同时执行并运行在提高IRQL上的例程同时访问共享数据或资源时,自旋锁用来保护这些共享数据或资源。 包括驱动程序在内的许多组件(component)都使用了自旋锁。任何类型的驱动程序可能都要使用一个或多个执行自旋锁。例如,大多数文件系统在FSD的设备扩展中使用一个互锁的工作队列,来保存由文件系统的工作者线程回调例程和FSD处理的IRP。互锁工作队列用执行自旋锁来保护,这个锁可以解决FSD中一个试图将IRP插入队列,而同时有其他线程要将IRP移出队列时所引起的问题。又如,系统软盘控制器驱动程序用两个执行自旋锁。一个保护与驱动程序设备专用线程共享的互锁工作队列,另一个用来保护三个驱动程序例程共享的定时器对象。 每个有ISR的驱动程序都使用一个中断自旋锁来保护被其ISR和其SynchCritSection例程(通常在驱动程序的StartIo和DpcForIsr例程中调用它)共享的数据或硬件。中断自旋锁与驱动程序调用IoConnectInterrupt时创建的中断对象集相关,在《注册ISR》部分对此有详尽的阐明。 在驱动程序中使用自旋锁时,应遵守下列规则: 在常驻系统空间内存(非页式存储池,如图16.3所示)中,为自旋锁保护的所有数据或资源和相应的自旋锁提供存储空间。驱动程序必须为它使用的所有执行自旋锁提供存储空间。然而,设备驱动程序不需要为中断自旋锁提供存储空间,除非它有多重矢量(multivector)ISR或者有一个以上的ISR,在注册ISR部分对此有详尽的阐明。 在使用驱动程序提供存储空间的每个自旋锁,以同步对被保护的共享数据或资源的访问之前,先要调用KeInitializeSpinLock来初始化这些自旋锁。 在适当的IRQL上调用每个使用了自旋锁的支持例程。一般,对于执行自旋锁,IRQL<=DISPATCH_LEVEL;对于与驱动程序中断对象相关的中断自旋锁,IRQL<=DIRQL。 实现例程时,应使其在持有自旋锁时尽快地执行。所有例程持有自旋锁的时间都不应超过25毫秒。 实现例程时注意,当它持有自旋锁时一定要避免做下列事情: 引起硬件异常或软件异常 试图访问可分页内存 做可能引起死锁或自旋锁持有时间超过25毫秒的递归调用 试图获得另一个自旋锁(这样做可能会导致死锁) 调用一个违反了上述任一条规则的外部例程 参见下列部分以更深入地了解这些规则: 16.2.1 为自旋锁和被保护数据提供存储空间 16.2.2初始化自旋锁 16.2.3调用使用了自旋锁的支持例程 16.2.4快速释放自旋锁 16.2.5使用自旋锁时防止错误或死锁的出现 为自旋锁和被保护数据提供存储空间 作为设备启动工作的一部分,驱动程序必须在下列各处之一为所有自旋锁保护的数据或资源以及相应的自旋锁分配常驻存储空间: 驱动程序通过调用IoCreateDevice建立的设备对象的设备扩展 驱动程序通过调用IoCreateController建立的控制器对象的控制器扩展 驱动程序通过调用ExAllocatePool获得的非页式系统空间内存 当持有自旋锁时,如果试图访问可分页数据而这一页不在内存中,就会导致一个致命的页错误。引用无效自旋锁(原来被保存在可分页内存中,而现在它所在的页已被调出内存(paged-out))也会导致一个致命的页错误。 驱动程序必须为下列各种可能用到的执行自旋锁提供存储空间: 调用了KeAcquireSpinLock和KeRelaeseSpinLock 或者调用了KeAcquireSpinLockAtDpcLevel和KeRelaeseSpinLockFromDpcLevel的非ISR驱动程序用来同步对驱动程序定义数据的访问的所有自旋锁 通过调用资源确定的ExInterlockedXxx例程集来同步对驱动程序分配资源的访问的所有自旋锁 驱动程序可以从其ISR或SynchCritSection例程调用ExInterlocked..List例程,然而当它运行在高于DISPATCH_LEVEL的IRQL上时,它不能调用KeAcquireSpinLock和KeRelaeseSpinLock 或者KeAcquireSpinLockAtDpcLevel和KeRelaeseSpinLockFromDpcLevel。因此,所有在调用Ke..SoinLock和ExInterlockedXxx时重用了自旋锁的驱动程序,都必须在运行的IRQL低于DISPATCH_LEVEL时做每个调用。 驱动程序可以将相同的自旋锁传递给ExInterlockedInsertHeadList,就像传递给另一个ExInterlockedXxx例程一样,这样做的前提是两个例程在相同的IRQL上使用此自旋锁。如果想深入了解自旋锁的使用对性能有何影响,请参见快速释放自旋锁一节。 除了为其执行自旋锁提供存储空间以外,如果设备驱动程序有多重矢量ISR或一个以上的ISR,那么它还必须为与其中断对象相关的另一个自旋锁提供存储空间。 初始化自旋锁 在调用需要访问调用者提供的执行自旋锁的支持例程之前,驱动程序必须调用KeInitializeSpinLock来初始化相应的执行自旋锁。需要初始化执行自旋锁的支持例程如下: KeAcquireSpinLock和随后的KeRelaeseSpinLock KeAcquireSpinLockAtDpcLevel和随后的KeRelaeseSpinLockFromDpcLevel ExInterlockedXxx例程 在调用IoConnectInterrupt和KeSynchronizeExecution之前,最低层驱动程序必须调用KeInitializeSpinLock以初始化由它提供存储空间的中断自旋锁。 调用使用了自旋锁的支持例程 调用KeAcquireSpinLock可以将当前处理器上的IRQL设为DISPATCH_LEVEL,直到用对应的KeRelaeseSpinLock调用将此IRQL恢复到改变前的值为止。因此,当驱动程序调用KeAcquireSpinLock时,它必须在低于DISPATCH_LEVEL的IRQL上执行。 KeAcquireSpinLockAtDpcLevel和KeRelaeseSpinLockFromDpcLevel的调用者运行得更快一些,因为它们已经运行在DISPATCH_LEVEL上,所以这些支持例程不需要将当前处理器上的IRQL重新设置。因此,在大多数Windows NT/Windows 2000平台上,当运行在低于DISPATCH_LEVEL的IRQL上时,调用KeAcquireSpinLockAtDpcLevel是个致命的错误。通过调用KeRelaeseSpinLockFromDpcLevel来释放用KeAcquireSpinLock获得的自旋锁也是一个致命的错误,因为没有恢复调用者的初始IRQL。 持有执行自旋锁的例程(如ExInterlockedXxx)通常运行在DISPATCH_LEVEL上,直到它们释放了这个自旋锁并向调用者返回控制为止。然而,只要传给ExInterlockedXxx集的自旋锁是被驱动程序的ISR和SynchCritSection例程排他地使用,这个ISR这个SynchCritSection例程(运行在DIRQL)就可以调用其中的某个ExInterlockedXxx例程(如ExInterlocked..List例程)。 持有中断自旋锁的例程运行在相关中断对象集的DIRQL上。因此,驱动程序绝对不能从它的ISR或SynchCritSection例程中调用KeAcquireSpinLock和KeRelaeseSpinLock例程,也不能调用其他任何使用了执行自旋锁的例程。这种调用会导致系统死锁,需要用户重新启动他的计算机。还应注意,如果驱动程序的ISR或SynchCritSection例程调用了ExInterlocked..List例程,那么此驱动程序就不能在它调用Ke..SpinLock或Ke..SpinLock..DpcLevel时重用它传给ExInterlocked..List例程的自旋锁。 如果驱动程序有多重向量ISR或多个ISR,当运行IRQL高于相关中断对象指定的SynchronizeIrql值时,它可以调用KeSynchronizeExecution。 请参见“管理硬件优先级”。如果想对管理支持例程确定的IRQL需求有更多的了解,请参见在线DDK。 快速释放自旋锁 将驱动程序持有自旋锁的时间最小化可以很明显地改善驱动程序和系统整体的性能。例如,图16.1表示了中断自旋锁怎样保护SMP机器上必须被ISR和StartIo及DpcForIsr例程共享的设备确定的数据。 图16.1 使用中断自旋锁 驱动程序的ISR运行在一个处理器的DIRQL上,而它的StartIo例程运行在第二个处理器的DISPATCH_LEVEL上。内核中断处理者在驱动程序的设备扩展中持有此驱动程序ISR的InterruptSpinLock,它用来访问设备确定的被保护数据,如设备寄存器(SynchronizeContext)的状态或指针。已准备好访问SynchronizeContext的StartIo例程调用KeSynchronizeExecution,传递指向相关中断对象的指针、共享SynchronizeContext和驱动程序的SynchCritSection例程(图16.1中的AccessDevice)。 KeSynchronizeExecution在第二个处理器上一直循环以防止AccessDevice访问SynchronizeContext,直到ISR返回(从而释放了驱动程序的InterruptSpinLock)时为止。然而,KeSynchronizeExecution还提高了第二个处理器上的IRQL,使它等于中断对象的SynchronizeIrql的值,从而防止了在这个处理器上发生其他设备中断,因此ISR一返回,AccessDevice就可以运行在DIRQL上。不过,其他设备的更高级的DIRQL中断、时钟中断和电源错误中断仍可以在两个处理器中的任一个上发生。 当ISR将驱动程序的DpcForIsr排队并返回时,第二个处理器上的AccessDevice运行在等于相关中断对象SynchronizeIrql值的IRQL上,而且访问了SynchronizeContext。同时,另一个处理器上的DpcForIsr运行在DISPATCH_LEVEL。DpcForIsr也已准备好访问SynchronizeContext,因此它调用KeSynchronizeExecution,调用参数与步骤1中StartIo例程的参数相同。 当KeSynchronizeExecution获得自旋锁并代表StartIo例程运行AccessDevice时,驱动程序提供的同步例程AccessDevice可以排他地访问SynchronizeContext。因为AccessDevice运行在SynchronizeIrql值指定的IRQL上,所以驱动程序的ISR直到自旋锁被释放时,才能获得此自旋锁并访问相同的存储区,否则,即使AccessDevice正在运行时另一个处理器上发生了设备中断也不行。 AccessDevice返回时释放自旋锁。StartIo例程继续在第二个处理器的DISPATCH_LEVEL上运行。现在KeSynchronizeExecution在第三个处理器上运行AccessDevice,因此它可以代表DpcForIsr访问SynchronizeContext。然而,如果设备中断在第2步中DpcForIsr调用KeSynchronizeExecution之前就发生了,那么此ISR可能会在KeSynchronizeExecution获得自旋锁并在第三个处理器上运行AccessDevice之前在另一个处理器上运行。 如图16.1所示,当一个处理器上运行的例程持有自旋锁时,其他每个试图获得此自旋锁的例程都无法成功。每个试图获得已占用自旋锁的例程都在其当前处理器上循环,直到持锁者释放了这个自旋锁为止。一个自旋锁被释放后,有且只有一个例程能够获得它,没有获得此自旋锁的其他各例程将继续循环。 任何自旋锁的持锁者都运行在提高IRQL上,对于执行自旋锁,在DISPATCH_LEVEL;对于中断自旋锁,在DIRQL。KeAcquireSpinLock的调用者运行在DISPATCH_LEVEL上,直到它们调用KeRelaeseSpinLock为止。KeSynchronizeExecution的调用者自动将当前处理器上的IRQL提高为中断对象的SynchronizeIrql值,直到调用者提供的SynchCritSection例程退出且KeSynchronizeExecution返回控制为止。请参见调用使用自旋锁的支持例程。 记住下列使用自旋锁的规则: 在被自旋锁持有者占用或其他例程占用的处理器集合上,运行在低级IRQL上试图获得相同自旋锁的代码将无法实现其目的。 因此,最小化驱动程序持锁时间可以极大地改善驱动程序的性能和系统的整体性能。 如图16.1所示,在多处理器机上,Knernel中断处理者按“先到先服务”的原则执行那些在相同IRQL上运行的例程。Knernel还要做下列事情: 当驱动程序例程调用KeSynchronizeExecution时,Knernel使驱动程序的SynchCritSection例程运行在调用KeSynchronizeExecution的处理器上。(见步骤1和3) 当驱动程序的ISR将其DpcForIsr排队时,Knernel使DPC运行在IRQL低于DISPATCH_LEVEL的第一个可用的处理器上。它不一定是IoRequestDpc调用发生的那个处理器。(见步骤2) 在单处理器机上,驱动程序中断驱动的I/O操作可能需要串行化。但是在SMP机上,同样的操作完全可以真正异步实现。如图16.1所示,在驱动程序的DpcForIsr开始处理那些ISR已经为其处理设备在CPU1上中断的IRP之前,此驱动程序的ISR可以运行在SMP机中的CPU4上。 也就是说,在DpcForIsr例程或CustomDpc例程运行之前,中断自旋锁不能阻止:ISR在运行于一个处理器上时保存的操作指定数据,在另一个处理器上发生设备中断时被此ISR写覆盖。 尽管驱动程序可以试着将所有中断驱动的I/O操作串行化以保存ISR收集的数据,但是这个驱动程序在SMP机器上的运行不会比在单处理器机上快多少。在保持Windows NT/Windows 2000单处理器和多处理器平台之间可移植性的前提下,为了获得尽可能好的性能,驱动程序应该用其他技术保存那些由ISR获得的以供DpcForIsr随后处理的操作指定数据。 例如,ISR可以在它传给DpcForIsr的IRP中保存操作指定的数据。对这种方法的一种改进是:将DpcForIsr实现为可以查询ISR增加的计数值(ISR-augmented count),用ISR提供的数据来处理计数值代表的IRP个数,然后在返回前将计数值重置为0。当然,必须用驱动程序的中断自旋锁来保护这个计数值,因为驱动程序的ISR和SynchCritSection例程会动态改变它的值。 使用自旋锁时防止错误或死锁的出现 驱动程序持有自旋锁时,只要它引起了硬件或软件异常,系统性能就会下降。这也就是说,驱动程序的ISR和驱动程序在调用KeSynchronizeExecution时提供的任何SynchCritSection例程,都不能引起页错误或算法异常这样的错误或陷阱,也不能引起软件异常。调用KeAcquireSpinLock的例程在释放了它的执行自旋锁而且不再运行在DISPATCH_LEVEL上之前,也不能引起硬件或软件异常。 可分页数据和支持例程 持有自旋锁时,驱动程序决不能调用任何访问可分页数据的例程。记住:驱动程序可以访问某些访问可分页数据的支持例程,当且仅当此调用发生时驱动程序运行在低于DISPATCH_LEVEL的IRQL上。对IRQL的这个限定使得驱动程序在持有自旋锁时不可能调用这些支持例程。如果想对某个具体的支持例程的IRQL需求有更多了解,请在在线DDK上参见此例程的相应参考部分。 递归 试图递归地获得自旋锁必然会引起死锁:递归例程的持有实例在第二个实例循环,以试图获得相同自旋锁时,不会释放此自旋锁。 在递归例程中使用自旋锁应遵守下列策略: 递归例程决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。 当递归例程持有自旋锁时,如果递归可能导致死锁或可能使调用者的持锁时间超过25毫秒,那么另一个驱动程序例程决不能调用这个递归例程。 如果想对递归驱动程序例程有更多的了解,请参见“使用内核栈”。 获得嵌套自旋锁 当持有自旋锁时,试图获得第二个自旋锁也会导致死锁或很差的驱动程序性能。 在实现持有自旋锁的驱动程序时,应该遵守下列策略: 驱动程序决不能调用使用自旋锁的支持例程,除非能保证不会发生死锁。 即使不会死锁,驱动程序也不应该调用使用自旋锁的支持例程,除非替代它的程序技术无法提供同等的驱动程序性能和功能。 如果驱动程序做了嵌套调用以获得自旋锁,它必须以相反的顺序释放那些自旋锁。也就是说,如果驱动程序在获得自旋锁B之前获得了自旋锁A,那么它必须先释放B,后释放A。 一般情况下,应避免使用嵌套自旋锁来保护重叠的共享数据和资源的子集或离散集(discrete set)。应当考虑:如果驱动程序使用两个执行自旋锁来保护离散资源(比如,可能由不同驱动程序例程来单独或共同设置的一对定时器对象),那么可能会发生什么情况。在SMP机上,当两个各持有一个自旋锁的例程中的一个试图获得对方的自旋锁时,驱动程序会间歇地发生死锁。 即使能够设计出不会死锁的驱动程序来使用嵌套自旋锁,它也很难成功地实现。在Windows NT/Windows 2000 SMP机器上,很难充分地调试并检测嵌套自旋锁。此外,使用嵌套自旋锁会极大地降低驱动程序和系统的性能。 轮询设备 除非不得已,否则设备驱动程序应该尽量避免轮询(pulling)其设备,而且设备驱动程序不该用整时间片轮询。轮询设备是一项开销很大的操作,它使操作系统在做轮询的驱动程序内受计算限制(compute-bound)。要做很多轮询的设备驱动程序与其他设备上的I/O操作相冲突,从而使系统变得很慢,甚至对用户不做响应。 现在开发的设备和运行Windows NT/Windows 2000的处理器一样,它们技术先进,很少需要驱动程序轮询它的设备以确保设备已准备好启动I/O操作或操作完成。 不过,有些仍在使用的设备是以前设计的,它们和数据总线窄、时钟速率慢的老式处理器一起协同工作。老式处理器上的操作系统执行同步I/O,而且是单用户单任务的。这样的设备可能需要轮询或用其他方式等待设备更新它的寄存器,特别是对Windows NT/Windows 2000来说,因为它们是被设计为在具有宽数据总线和快速时钟速率的新型处理器上做异步I/O的。 虽然通过编写一个增加计数器的简单循环来解决慢速设备的问题似乎可行(这样可以在设备更新寄存器时,“浪费”少量的时间),但这样的驱动程序往往不能在Windows NT/Windows 2000平台之间移植。需要为每个Windows NT/Windows 2000平台分别配置循环计数器的最大值。而且,如果驱动程序是用优化非常好的编译器编译的,编译器可能会移除驱动程序的记数变量和增加计数器的那段循环。 如果驱动程序必须在设备硬件更新状态时停下等待,应遵照下列实现策略: 驱动程序可以在读设备寄存器之前调用KeStallExecutionProcessor。驱动程序应该最小化它的等待时间间隔,而且等待时间间隔一般应该不超过50毫秒。 KeStallExecutionProcessor时间间隔的单位为1毫秒。 如果设备更新状态的时间经常超过50毫秒,可以考虑在驱动程序中建立一个设备专用线程。 驱动程序线程 慢速设备或很少使用设备(如软盘控制器)的驱动程序可以通过创建一个设备专用的系统线程来解决很多等待问题。类似的,大多数文件系统驱动程序使用系统工作者线程,并提供工作者线程回调例程。线程可以调用KeDelayExecutionThread等待完整时间片长度或更长时间的间隔。 KeDelayExecutionThread等待时间间隔的单位大约是10毫秒。因为KeDelayExecutionThread是定时器驱动的例程,其等待间隔的单位会比10毫秒稍快或稍慢些,这取决于操作系统平台。然而,对此例程的调用是可移植的,因为指定的时间增量是常量。 如果设备驱动程序有自己的线程环境或运行于系统线程环境中,设备专用线程或最高层驱动程序的工作者线程回调例程,可以在驱动程序设备扩展的共享通信区中,同步Kernel定义的调度者对象(如事件或信号量)上的同步操作。当其设备没有使用时,设备专用线程可以在共享调度者对象上等待,例如通过用信号量调用KeWaitForSingleObject来等待。在调用这种设备驱动程序来执行I/O操作并将信号量设为Signaled状态之前,它的等待线程不占用CPU时间。 驱动程序可以通过调用KeSetBasePriorityThread来设置它用PsCreateSystemThread创建的驱动程序专用或设备专用线程的基优先级(base priority)。驱动程序应该将优先级指定为能避免在SMP机上运行时优先级倒置(runtime priority inversion)的值。将驱动程序创建的线程的基优先级设得过高,会延迟提交I/O请求给驱动程序的低优先级线程的执行。 管理内存的使用 许多驱动程序只是将分配给其设备对象的设备扩展的内存用作其全局存储区;只是将其在IRP中的I/O栈用作操作指定的本地存储区。然而,驱动程序可以按需要分配额外的系统空间内存,而且可以用内核栈在调用内部驱动程序例程时传递少量的数据。 使用系统内存 图16.2表示Windows NT/Windows 2000虚内存空间及它们与系统物理内存的关系。 图16.2 虚内存空间和物理内存 如图16.2所示,虚内存实际对应的是分页的物理内存,虚地址范围实际对应的是CPU中不邻接的页。用户空间虚内存和从页式存储池中分配的系统空间内存总是可分页的。也就是说,任何非当前处理及其数据都可以分页到辅助存储区中去,通常是磁盘上。 图16.2中的高位空间(hyperspace)是系统空间地址的专用区,内存管理器用它将当前处理的虚地址空间映射为CPU中的一系列物理页。注意:任何非当前处理的虚地址都是不可见的,因此它的内存空间是不可访问的。 访问用户空间内存的驱动程序 驱动程序不能分配用户空间的虚内存,因为它们运行在内核模式。此外,驱动程序不能通过用户模式的虚地址访问内存,除非它正运行在引起驱动程序当前I/O操作的用户模式线程环境中而且它正在使用此线程的虚地址。 只有最高层驱动程序(如FSD)可以保证它们的Dispatch例程会在这样的用户模式线程环境中被调用。最高层驱动程序可以在为低层驱动程序建立IRP之前调用MmProbeAdnLockPages以锁住(lock down)用户缓冲。 最低层驱动程序和为缓冲或直接I/O建立设备对象的中间层驱动程序,可以依赖I/O管理器或最高层驱动程序,来在IRP中传递对被锁用户缓冲或系统空间缓冲的合法访问。 为部分传输请求建立MDL 如果传输请求太大以致于下层设备驱动程序无法处理,那么高层驱动程序可以调用IoBuildPartialMdl,为下层设备驱动程序建立部分传输IRP队列。 如果最高层驱动程序不能在一台内存有限的计算机上用MmProbeAndLockPages锁住整个用户缓冲,初始请求也必须被分割成部分传输。对这种大传输请求,最高层驱动程序不能做下列事情: 调用IoBuildSynchronousFsdRequest来分配部分传输IRP并锁住用户缓冲的一部分。通常加锁区的大小要么是PAGESIZE的倍数,要么是下层设备的传输容量。 如果低层驱动程序返回STATUS_PENDING,就用部分传输IRP调用IoCallDriver,并调用KeWaitForSingleObject,以等待驱动程序建立与其部分传输IRP相关的事件对象。 当它重新获得控制时,重复步骤1和2,直到所有数据都被传输为止,然后完成初始IRP。 必须处理很大传输请求的最高层设备驱动程序可以使用前述技术,简单地用它分配的部分传输IRP调用它自己。除此之外,还有另一种方案,其中最高层设备驱动程序要做下列事情: 调用IoAllocateMdl来分配描述用户缓冲的一部分MDL。 调用MmProbeAndLockPages来锁住这部分用户缓冲。 给这部分用户缓冲传输数据。 调用MmUnlockPages,做下列事情之一: 如果驱动程序在步骤1中分配的MDL非常大,足够下次传输,调用MmPrepareMdlForReuse并重复步骤2到4。 否则,调用IoFreeMdl并重复步骤1到4。 所有数据都被传输后,调用MmUnlockPages和IoFreeMdl。 分配系统空间内存 图16.2所示的系统空间虚内存由有限个页式存储池和更少的非页式存储池组成。 非页式存储池总是常驻的。因此,运行在任何级别的IRQL时,它都可以被安全地访问。 对于驱动程序,页式存储池只有在下列条件下,才能被分配和访问: 使用对应的页式存储池虚地址的例程必须运行在低于APC_LEVEL的IRQL上。如果运行在高于APC_LEVEL的IRQL上时发生了页错误,那么它将是一个致命的错误。参见管理硬件优先级部分以了解更多关于IRQL的内容。 除了驱动程序或设备初始化,或者卸载(有时可能发生)以外,最低层和中间层驱动程序很少从页式存储池中分配内存,因为这些类型的驱动程序通常运行在高于APC_LEVEL的IRQL上。这样的驱动程序分配的任何可分页存储区只能被驱动程序创建的线程或DriverEntry、AddDevice、Reinitialize(如果有的话)和Unload(如果有的话)例程安全访问,这些线程或例程可以用页式存储池分配方式来存放只在驱动程序或设备初始化、或者卸载时需要的数据、对象和资源。 因为有些标准驱动程序例程运行在高于APC_LEVEL的IRQL上,所以从页式存储池中分配的内存对大多数中间层或设备驱动程序例程是不可访问的。例如,高层驱动程序的IoCompletion例程在专用线程环境中和(通常)DISPATCH_LEVEL上执行。这样的驱动程序决不应为将被IoCompletion例程访问的数据分配可分页存储区。请参见“管理硬件优先级”。 分配驱动程序缓冲空间 为了分配I/O缓冲空间,驱动程序可以调用MmAllocateNonCachedMemory、MmAllocateContiguousMemory、AllocateCommonBuffer(如果驱动程序的设备使用总线控制器DMA或系统DMA控制器的自动初始化模式)或ExAllocatePool。 系统运行时,非页式存储池往往会变成很多内存碎片,因此驱动程序的DriverEntry例程应该调用这些例程以建立驱动程序需要的长期I/O缓冲。这些例程(可能除了ExAllocatePool)均在处理器指定的边界(由处理器的数据缓存范围(data-cache-line)的大小决定)内分配内存以避免发生缓存及一致性问题。 驱动程序应尽可能节省地分配它们的内部I/O缓冲(如果有的话),因为非页式存储池是很有限的系统资源。一般,驱动程序应该避免重复调用这些支持例程来请求小于PAGE_SIZE的分配。 为了节省地分配I/O缓冲内存,记住下列事实: 每次调用MmAllocateNonCachedMemory至少占用非页式系统空间内存中的一整页,无论请求分配多大的存储区。对于小于一页的请求,页中余下的字节都被浪费掉了:调用MmAllocateNonCachedMemory的驱动程序不可访问它,它也不能被其他内核模式的程序使用。 如果指定的字节个数少于或等于一页,调用MmAllocateContiguousMemory分配至多一页的存储区。对于大于一页的请求,最后分配的页中剩余的字节被浪费:调用MmAllocateContiguousMemory的驱动程序不可访问它,它也不能被其他内核模式的程序使用。 调用AllocateCommonBuffer至少使用一个适配器对象映射寄存器,它至少映射1字节,最多映射一页。如果想对映射寄存器和使用公用缓冲有更多的了解,参见第3章中的“适配器对象和DMA部分”。 用ExAllocatePool 或ExAllocatePoolWithTag分配内存 驱动程序也可以调用ExAllocatePool 或ExAllocatePoolWithTag,将参数PoolType指定为下列系统定义的值之一: NonPagedPoolCacheAligned:驱动程序使用永久的I/O缓冲。如SCSI类驱动程序为请求检测(request-sense)数据开辟的缓冲 驱动程序应当调用MmAllocateNonCachedMemory或MmAllocateContiguousMemory分配永久I/O缓冲。 NonPagedPoolCacheAlignedMustS:临时但非常重要的I/O缓冲。如存放物理设备初始化数据的缓冲,这些数据在系统启动时要用。 NonPagedPool:没有存储在设备扩展或控制器扩展中的对象或资源,驱动程序可能会在运行IRQL高于APC_LEVEL时访问它们。 当PoolType取了这个值时,如果指定的NumberOfBytes小于或等于PAGE_SIZE,ExAllocatePool 或ExAllocatePoolWithTag就按需分配内存。否则,最后分配的页中剩余的字节被浪费:调用者不可访问它,它也不能被其他内核模式的程序使用。 例如,在x86机上,一个5K的分配请求会获得两个4K的页。第2页中余下的3K不能被调用者或其他调用者使用。为了避免浪费非页式存储池,驱动程序应该有效地分配多页。比如在这种情况下,驱动程序可以做两次分配,一次大小等于PAGE_SIZE,另一次等于1K,加起来共分配了5K。 NonPagedPoolMustSucceed:临时但非常重要的存储区,驱动程序会尽快释放它。如驱动程序用来修复错误的内存,否则错误会使系统瘫痪。 PagedPoolCacheAligned:文件系统的I/O缓冲。驱动程序将它锁住,然后下层海量存储设备驱动程序在请求DMA传输的IRP中传递它。 PagedPool:如果缓冲将在调用者返回之前释放,DriverEntry或Reinitialize例程可用此值开辟一个临时缓冲,用来保存初始化时必需的对象、数据或资源。此值也可用来开辟只能被一个或多个驱动程序创建的线程访问的存储区。 如果缓冲将在Unload例程返回控制之前被释放,那么驱动程序的Unload例程也可以从页式存储池中分配内存。 因为必须成功(must-succeed)存储池是非常有限的系统资源,驱动程序应该通过调用ExFreePool尽快释放分配的空间。大多数驱动程序不应该用值为NonPagedPoolMustSucceed或NonPagedPoolCacheAlignedMustS的PoolType参数调用ExAllocatePool 或ExAllocatePoolWithTag,除非是如果驱动程序的分配请求不成功,系统就不继续运行。如果PoolType参数指定为这些值,ExAllocatePool会在系统无法分配请求的内存时,使系统终止运行。 对其他PoolType参数值,如果不能分配请求的NumberOfBytes字节的内存,ExAllocatePool 或ExAllocatePoolWithTag返回NULL指针。驱动程序应该检查返回的指针。如果它的值为NULL,DriverEntry例程(或其他任何返回NTSTATUS的驱动程序例程)应该返回STATUS_INSUFFICIENT_RESOURCES或处理错误(可能的话)。参见“错误记录和NTSTATUS值”。 对于CacheAligned类的PoolType参数值,ExAllocatePool 或ExAllocatePoolWithTag在处理器指定的边界(由处理器的数据缓存范围的大小决定)内分配内存以避免发生缓存及一致性问题。 将总线相关的内存空间地址重新映射为虚地址 有些处理器有独立的内存和I/O地址空间,而有些没有。由于硬件平台上的这些差异,Windows 2000和WDM驱动程序用来访问常驻I/O或常驻内存的设备资源的机制因平台而异。 驱动程序请求设备I/O和内存资源来响应PnP管理器的IRP_MN_QUERY_RESOURCE_REQUIREMENTS的IRP。根据硬件结构的不同,HAL可以在I/O空间或内存空间分配I/O资源,也可以在I/O空间或内存空间分配内存资源。 如果HAL用总线相关内存空间来访问设备资源(如设备寄存器),驱动程序必须将I/O空间映射到虚内存,这样它就可以访问这些资源。驱动程序可以通过检查PnP管理器在设备启动时传给驱动程序的被映射资源,来确定资源是常驻I/O的,还是常驻内存的。如果HAL用I/O空间,不需要做映射。 具体地说,当驱动程序接收到一个IRP_MN_START_DEVICE请求时,它应该检查IrpSp->Parameters.StartDevice.AllocatedResources和 IrpSp->Parameters.StartDevice.AllocatedResourcesTranslated结构,它们分别描述了初始和映射后的PnP管理器分配给设备的资源。驱动程序应该在设备扩展中保存每个资源列表的拷贝,以供调试时辅助使用。 资源列表是成对的CM_RESOURCE_LIST结构,其中初始列表的每个元素都对应着转换后列表的相同元素。例如,如果AllocatedResources.List[0] 描述初始I/O端口范围,那么AllocatedResourcesTranslated.List[0]就描述了转换后的相同范围。每个被转换资源都包括物理地址和资源类型。 如果驱动程序被分配了一个转换的内存资源(CmResourceTypeMemory),它必须调用MmMapIoSpace将物理地址映射为可用来访问设备寄存器的虚地址。对以平台无关方式操作的驱动程序,如果需要的话,它应该检查每个返回的、转换后的资源并将其映射。 以下是每个驱动程序在响应IRP_MN_START_DEVICE以确保能访问所有设备资源时,都应该采取的步骤: 在设备扩展中复制IrpSp->Parameters.StartDevice.AllocatedResources。 在设备扩展中复制IrpSp->Parameters.StartDevice.AllocatedResourcesTranslated。 在循环里,检查AllocatedResourcesTranslated中的每个描述元素。如果描述资源类型是CmResourceTypeMemory,调用MmMapIoSpace,传递物理地址和转换后资源的长度。 当驱动程序收到来自PnP管理器的IRP_MN_STOP_DEVICE或IRP_MN_REMOVE_DEVICE请求时,它必须在类似循环中通过调用MmUnmapIoSpace释放映射。如果驱动程序必须拒绝IRP_MN_START_DEVICE请求,它也应该调用MmUnmapIoSpace。 初始资源类型表明驱动程序应当调用哪个HAL访问例程(READ_REGISTER_Xxx、WRITE_REGISTER_Xxx 、READ_PORT_Xxx、 WRITE_PORT_Xxx)。大多数驱动程序不需要检查初始资源列表以确定用这些例程中的哪一个,因为驱动程序本身已请求了这个资源,或者驱动程序开发者在已知设备硬件性质时已经确定了所需的类型。 对于I/O空间中的资源(CmResourceTypePort、CmResourceTypeInterrupt、CmResourceTypeDma),驱动程序应该用返回的物理地址的低32位访问设备资源(例如,通过HAL的READ_REGISTER_Xxx、WRITE_REGISTER_Xxx 、READ_PORT_Xxx、 WRITE_PORT_Xxx读写例程)。 使用内核栈 当驱动程序可以向其内部例程传递数据时,Windows 2000内核模式栈的大小约为两页。因此,驱动程序不能在内核栈上传送大量的数据。 为了避免用尽内核模式栈的空间,遵守以下设计规则: 避免从一个内部驱动程序例程中深度嵌套调用另一个,如果它们每个都要在内核栈上传送数据的话。 如果驱动程序设计中用到了递归例程,注意限制递归调用发生的次数。 也就是说,驱动程序的调用树结构应该比较平坦。由于非页式存储池也是有限的系统资源,因此驱动程序最好分配系统空间缓冲,而不是用尽内核栈空间。 Windows 2000内核模式栈是在缓存中,因此驱动程序不能用DMA在栈上传送数据。 为了避免DMA数据分配和/或数据完整性问题,遵守以下设计规则: 决不要试图用DMA在内核栈上传送数据。 DMA设备的驱动程序可以通过调用ExAllocatePool 或ExAllocatePoolWithTag获得一个NonPagedPoolCacheAligned类型的缓冲,从而缓冲要被传输的数据(如果有的话)。有些驱动程序可以通过使用公用缓冲DMA来做到这些。参见第3章中的“公用缓冲系统DMA或公共总线控制器DMA”。 使用后备列表 必须动态分配固定大小的缓冲以执行要求的I/O操作的Windows 2000和WDM驱动程序,可以使用Ex..LookasideList支持例程。在这样的驱动程序初始化了其后备列表后,OS会在驱动程序的后备列表中占有某些动态分配的、指定大小的缓冲,高效地为此驱动程序保留了一系列可重用的、固定大小的缓冲。驱动程序在其后备列表中的固定大小缓冲的格式和内容是由驱动程序决定的。 例如,必须为下层SCSI端口/微端口(miniport)驱动程序建立SCSI请求块(SRB)的存储类驱动程序使用了后备列表。这样的类驱动程序从它的后备列表中按需为SRB分配缓冲,并且只要SRB在完成的IRP中返回类驱动程序,就释放每个SRB缓冲到后备列表中。由于驱动程序上的I/O请求时多时少,存储类驱动程序无法预先确定某时刻它需要使用多少个SRB,因此在这样的驱动程序中后备列表是管理固定大小SRB的缓冲的分配与释放的一种便利且经济的方式。 OS维护所有当前正在使用的页式和非页式后备列表的状态,动态跟踪所有表中对分配和释放表项的请求,以及新表项的可用系统存储池。当分配请求很多时,OS增加它在每个后备列表中持有的表项个数。当请求又减少了,OS就将增加的后备表项释放回系统存储池。 在使用Ex..LookasideList例程的驱动程序中,遵守以下设计规则: 如果驱动程序本身或它传送后备列表表项的下层驱动程序可能以高于DISPATCH_LEVEL的IRQL或在专用线程环境中访问这些表项,用ExInitializeNPagedLookasideList建立一个非页式后备列表。 只有对驱动程序后备列表表项的访问不可能导致致命页错误时,才建立有页式表项的后备列表。 在非页式系统空间中为后备列表头提供常驻存储区,即使驱动程序用ExInitializePagedLookasideList建立了页式后备列表。 为了得到更好的性能,当调用ExInitialize(N)PagedLookasideList时为Allocate和Free传递NULL指针,除非这些可选的、驱动程序提供的例程除了为后备列表表项分配、释放内存之外还做其他事情(如维护驱动程序对动态分配缓冲的使用情况的状态信息)。 如果驱动程序提供Allocate例程,当此例程调用ExAllocatePoolWithTag时,在例程中使用给定的输入参数(PoolType、Tag和Size)。 对每个ExInitialize(N)PagedLookasideList调用,一旦先前分配的表项不再使用时,应尽快做逆调用ExFreeTo(N)PagedLookasideList。 对于页式后备列表,表项是从页式存储池中分配的,但是这样一个列表的头必须在常驻内存中。 Allocate和Free例程分别与调用ExAllocatePoolWithTag和ExFreePool的效果相同,提供它们会浪费CPU循环。ExAllocate(N)PagedLookasideList 和ExFreeTo(N)PagedLookasideList在驱动程序向ExInitialize(N)PagedLookasideList传递值为NULL的Allocate和Free指针时,会自动调用ExAllocatePoolWithTag和ExFreePool。 驱动程序提供的Allocate例程决不能从页式存储池中为将要记录在非页式后备列表中的表项分配内存,反之也一样。它还必须分配固定大小的表项,因为驱动程序对ExAllocate(N)PagedLookasideList后来的调用将返回当前记录在后备列表中的第一个表项,除非列表为空。也就是说,调用ExAllocate(N)PagedLookasideList只有在给定的当前后备列表为空的情况下,才会调用驱动程序提供的Allocate例程。因此,每次调用ExAllocate(N)PagedLookasideList,只有在后备列表中的所有表项都为一个固定的大小时,返回的表项才恰好是驱动程序需要的大小。驱动程序提供的Allocate例程也不应该改变驱动程序开始传给ExInitialize(N)PagedLookasideList的Tag值,因为对存储池标记的改变会使调试和跟踪驱动程序的内存使用情况变得非常困难。 调用ExFreeTo(N)PagedLookasideList将会返回先前分配的、将保存在后备列表中的表项,除非列表表项数已经达到系统决定的最大值。为了得到更好的性能,驱动程序应该尽快为每次ExAllocate(N)PagedLookasideList调用做其逆调用ExFreeTo(N)PagedLookasideList。当驱动程序迅速将表项释放回其后备列表后,此驱动程序对ExAllocate(N)PagedLookasideList的下次调用,就几乎不可能导致为另一个表项显式分配附加内存所引起的性能恶化了。 只读内存保护 Microsoft的Windows 2000增强了对标记为可写的页的只读访问。 只读内存在用户模式中总是被保护着。但是在Windows NT 4.0和早期版本中,它在内核模式下没有被保护。 如果Windows 2000内核模式驱动程序或应用程序试图写只读内存段,系统就发布错误检测(bug check)0xBE。(如果想了解对错误检测代码的描述,请参见使用Microsoft调试器文档) 截获(intercepting)系统调用 有些驱动程序通过重写驱动程序代码和插入跳转指令或其他修改来截获系统调用。这种技术会导致发布一个错误检测。 全局字符串 如果一个字符串将会被修改,那么决不能将它说明为指向常量值的指针: CHAR *myString=”This string cannot be modified.”; 在这种情况下,连接器可能会将此字符串放在只读内存段中,因此试图修改它会导致错误检测。 相反,这个字符串应该被显式地说明为L值(L-value)字符的队列: CHAR myString[]=”This string can be modified.”; 这样就能保证此字符串被放在可写内存中。 为DMA和PIO维护缓存的一致性 在Windows NT/Windows 2000计算机上,当驱动程序在系统内存和它的设备之间传送数据时,数据可以被缓存一个或多个处理器缓存中和/或系统DMA控制器的缓存中。使用DMA或PIO来为读/写IRP或任何需要DMA或PIO数据传送操作的设备I/O控制请求服务的驱动程序,应该保证传送操作期间可能缓存数据的完整性。有关这些内容将在以下几个小节中阐明。 在DMA操作期间刷新缓存数据 在有些平台上,处理器和系统DMA控制器(或总线控制器DMA适配器)表现出缓存一致性异常。 为了在DMA操作期间保持数据完整性,最低层驱动程序必须遵照下列规则: 在传送操作之前调用KeFlushIoBuffers,以保持可能被缓存在处理器中的数据和内存中数据之间的一致性。 如果驱动程序用值为TRUE的参数CacheEnabled调用AllocateCommonBuffer,驱动程序必须在向/从其缓冲进行传送操作之前调用KeFlushIoBuffers。 在每次设备传送操作完成时,调用FlushAdapterBuffers以保证系统DMA控制器缓冲中的所有剩余字节都已被写入内存或从属设备。 或在给定IRP的每次设备传送操作完成时,调用FlushAdapterBuffers以保证所有数据都已被读入系统内存或写入总线控制器DMA设备。 图16.3表明了,如果主处理器和DMA控制器不能自动维护缓存一致性,那么在使用DMA读或写之前刷新处理器缓存有多么重要。 图16.3 使用DMA的读写操作 异步DMA读或写操作访问内存中的数据,而不是处理器缓存中的数据。除非缓存已经在读操作之前通过调用KeFlushIoBuffers进行了刷新,否则如果处理器缓存稍后才刷新的话,DMA操作传送给系统内存的数据可能会被旧数据覆盖。除非缓存已经在写操作之前通过调用KeFlushIoBuffers进行了刷新,否则缓存中的数据可能比内存中的拷贝还要新。 如果处理器和DMA控制器可以自动保持缓存的一致性,就不需要使用KeFlushIoBuffers,因此在这种平台上调用此支持例程几乎没有任何开销(overhead)。 图16.3还表明,适配器对象代表的DMA控制器可以有内部缓冲。这样的DMA控制器可以以固定大小传送缓存数据,通常是一次8个或更多个字节。此外,这些DMA控制器可以在传送操作之前一直等待,直到它们的内部缓冲满了为止。 对于以可变大小或非系统DMA控制器缓存大小的整数倍的固定大小来使用从属DMA读数据的最低层驱动程序,除非这个驱动程序在每次设备传送完成后都调用FlushAdapterBuffers,否则它不能确定驱动程序请求的每个字节实际将在什么时候被传送。 总线控制器DMA设备的驱动程序也应该在IRP的每次设备传送完成后调用FlushAdapterBuffers,这样可以保证所有数据都已传送到了系统内存或传送出了设备。 FlushAdapterBuffers返回一个布尔量,指出请求的刷新操作是否成功。驱动程序可以用这个值在完成DMA读或写操作的IRP时,决定怎样设置I/O状态块。 在PIO操作期间刷新缓存数据 在有些平台上,处理器的指令和数据缓存在PIO读操作期间表现出缓存一致性异常。 为了在它们的读操作期间保持数据完整性,使用PIO的驱动程序必须遵守下列规则: 在每次读操作完成后调用KeFlushIoBuffers。 例如,从设备到系统内存做PIO传送的驱动程序应该在每次设备传送操作完成后调用KeFlushIoBuffers。又比如,将一类设备寄存器读入系统内存的驱动程序应该在读完每个类后调用KeFlushIoBuffers。否则在有些平台上,,这样的驱动程序可能会试图访问仍在处理器数据缓存中的数据,而不是系统内存中的。 如果处理器和DMA控制器可以自动保持缓存的一致性,就不需要使用KeFlushIoBuffers,因此在这种平台上调用此支持例程几乎没有任何开销。 错误记录和NTSTATUS值 Windows NT/Windows 2000的设计目标之一是在运行时错误方面比其他PC操作系统更强壮、更友好。也就是说,系统被设计做下列事情: 当发生错误时,能够继续运行,而不让一个组件(或线程)破坏其他组件的代码或数据。 无论何时发生错误,都能够继续运行,而不会发送大量含义模糊的信息来终止用户。 得承认有些I/O错误是用户引起的。例如,请求从可删除存储介质上的文件中读数据,可是用户提供了错误的磁盘、磁带或CD-ROM,这样就产生了一个用户引起的错误。如处理可删除存储介质部分所讨论的,这种错误很容易纠正,只要提示用户提供正确的介质就可以了。 其他I/O错误不能简单地通过终止用户操作来纠正。对于这种I/O错误,Windows NT/Windows 2000继续运行,并不强制用户意识到这些他们不可能立即解决的错误。相反,它提供了系统错误记录线程,在文件中将I/O错误信息作为表项格式化并保存。 Win32事件查看器可以读并显示这个错误记录文件。Windows NT/Windows 2000用户、系统管理员或技术支持人员可以用它来监视给定计算机上的硬件状态;如果需要的话,更换故障硬件;调整设备配置以获得更好的性能;如果发生硬件问题,调试这些问题。 调用IoAllocateErrorLogEntry 当驱动程序在处理IRP期间发现了一个I/O错误时,它应该如下调用IoAllocateErrorLogEntry: size=sizeof(IO_ERROR_LOG_PACKET)+(n*sizeof(ULONG)) +sizeof(InsertionStrings); //where n depends on how much //DumpData the driver will supply errorLogEntry=(PIO_ERROR_LOG_PACKET)IoAllocateErrorLogEntry( deviceExtension->DeviceObject, //target device for current operation size); 错误记录包的大小是有限制的。系统定义的限制适用于所有转储数据(dump data)和驱动程序提供给包的插入字符串。驱动程序可以用指定的EntrySize值(通常是ERROR_LOG_MAXIMUM_SIZE)调用IoAllocateErrorLogEntry。 IoAllocateErrorLogEntry返回一个指向错误记录包的指针。如果返回的指针为NULL,驱动程序就不需要记录错误。它应该只是继续运行,并保证如果同样的错误再次发生的话,能在那时记录下来。 填充错误记录包 错误记录包定义如下: typedef struct _IO_ERROR_LOG_PACKET{ UCHAR MajorFunctionCode; UCHAR RetryCount; USHORT DumpDataSize; USHORT NumberOfAtrings; USHORT StringOffset; USHORT EventCategory; NTSTATUS ErrorCode; ULONG UniqueErrorValue; NTSTATUS FianlStatus; ULONG SequenceNumber; ULONG IoControlCode; LARGE_INTEGER DeviceOffset; ULONG DumpData[1]; } IO_ERROR_LOG_PACKET,* PIO_ERROR_LOG_PACKET 驱动程序应当用下列数据填充错误记录包: MajorFunctionCode 指出当前IRP的驱动程序I/O栈中的IRP_MJ_XXX。 RetryCount 指出驱动程序重试操作和遇到此错误的次数。 RetryCount是个基于0的值。也就是说,驱动程序应该在当前IRP的第一次遇到错误时,将它设为0。 DumpDataSize 指出驱动程序将在包中设置的所有DumpData需要的字节数。 指定的值应该是sizeof(ULONG)的整数倍。 NumberOfStrings 支持驱动程序将提供给这个包的插入字符串的个数。对于不需要插入字符串的错误,驱动程序将此值设为0。 错误记录线程可以用这些驱动程序提供的、以0结尾的Unicode字符串填充写入Win32事件日志的信息,这些信息可以用Win32事件查看器查看。I/O管理器假定:初始插入字符串(如果有的话)要么是驱动程序的名字,要么是发生错误的设备的名字。 驱动程序提供的插入字符串应该是与语言无关的。记录错误并使用插入字符串的驱动程序应该使用从注册表中读出的字符串,或者使用语言无关或在任何语言中都相同的名字(如文件名)。 在大多数情况下,设备和中间层驱动程序可以仅仅记录I/O错误,而不需要为高层事件记录组件提供插入字符串,它们也可以不建立驱动程序指定的事件记录组件。在系统提供的驱动程序中,当前只有网络设备驱动程序在错误记录包里提供插入字符串。 StringOffset 就在DumpData之后,指出驱动程序提供的与插入字符串数据开始处的偏移量。 如果驱动程序提供了这个数据,每个字符串必须是以0结尾的Unicode字符串。 EventCategory 对将其自身作为事件记录组件保存在注册表中的驱动程序,这是一个驱动程序定义的值,它在驱动程序的分类信息文件(message file for categories)中指定。 ErrorCode 指出错误类型。 这是一个系统定义或驱动程序定义的常量,参见定义新的IO_ERR_XXX部分。 UniqueErrorValue 指出错误是在驱动程序中的什么地方检测到的。 FinalStatus 当IRP被完成时,指出此IRP的I/O状态块中设置的值;或指出驱动程序调用的支持例程返回的STATUS_XXX。 SequenceNumber 指出驱动程序分配给当前IRP的队列号,它在给定请求的生命期内应该是个常量。 IoControlCode 如果MajorFunctionCode是IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL,就指出当前IRP的驱动程序I/O栈中的I/O控制代码。否则,这个值应该是0。 如果想对I/O控制代码和设备I/O控制请求有更多的了解,请参见《Windows 2000驱动程序开发指南》第2卷第13章“IRP函数代码和IOCTL”。 DeviceOffset 指出设备中错误发生的偏移量。 DumpData 可以用来存放驱动程序指定的数据,如寄存器值或识别错误原因时要用到的其他有用信息。 任何驱动程序提供的插入字符串都必须紧跟在转储数据后面,从StringOffset处开始。 设置错误记录包中的NTSTATUS值 错误记录包中的ErrorCode和FinalStatus成员都是NTSTATUS类型的。图16.4表明了NTSTATUS值的格式。 图16.4 NTSTATUS格式 系统提供了一系列公共的IO_ERR_XXX常量来设置错误记录包中的ErrorCode。例如,驱动程序可以使用下列系统定义的常量: IO_ERR_RETRY_SUCCEEDED IO_ERR_INSUFFICIENT_RESOURCES IO_ERR_CONFIGURATION_ERROR IO_ERR_INCORRECT_IRQL IO_ERR_INBALID_IOBASE IO_ERR_DRIVER_ERROR IO_ERR_PARITY : : IO_ERR_OVERRUN_ERROR IO_ERR_TIMEOUT IO_ERR_CONTROLLER_ERROR IO_ERR_INTERNAL_ERROR 系统也提供了一系列公共的STATUS _XXX值,驱动程序可以用它们来设置错误记录包中的FinalStatus,它们可以从能返回NTSTATUS值的标准驱动程序例程返回。例如,驱动程序可以从其标准驱动程序例程中返回下列系统定义的常量,可以在IRP的I/O状态块中用下列值来进行设置,可以将错误记录包中的FinalStatus设为下列值: STATUS_SUCCESS STATUS_DEVICE_CONFIGURATION_ERROR STATUS_DRIVER_INTERNAL_ERROR STATUS_INVALID_DEVICE_STATE STATUS_IO_DEVICE_ERROR : : STATUS_DEVICE_BUSY STATUS_DEVICE_DOSE_NOT_EXIST STATUS_ADAPTER_HARDWARE_ERROR 公共IO_ERR_XXX和STATUS _XXX常量是系统资源。每个STATUS _XXX都由系统映射到相应的Win32常量。因为它们是内嵌在Windows NT/Windows 2000中的,所以只有在与Microsoft公司有合作的情况下,公共IO_ERR_XXX和STATUS _XXX常量才能被添加到系统。 调用IoWriteErrorLogEntry 当驱动程序已经向IoAllocateErrorLogEntry返回的包中填充了数据以后,它必须调用IoWriteErrorLogEntry,这样错误记录线程就可以在错误记录文件中写它的表项了。 只要可能,驱动程序都应该记录错误;如果需要的话,拒绝IRP;在遇到异常或未料到的I/O错误时,继续运行。 正在销售的驱动程序决不应该调用KeBugCheckEx(或KeBugCheck)使系统终止。然而,可以用KeBugCheckEx来调试正在开发的驱动程序。 定义新的IO_ERR_XXX 用户可以在驱动程序的资源文件xxxlog.mc中创建驱动程序指定的、NTSTATUS类型的私有IO_ERR_XXX代码。 如图16.4所示,对于这样的错误,NTSTATUS类型的Facility域必须被设成FACILITY_IO_ERROR_CODE。必须为每个新的IO_ERR_XXX提供Sev值和唯一的Code值。驱动程序也必须将其新IO_ERR_XXX中的C位设置成‘1’。 图16.4中的Sev域指出严重程度代码,它必须是下列系统定义的值之一: STATUS_SEVERITY_SUCCESS 指出错误记录包中的FinalStatus被设为STATUS_SUCCESS,而且ErrorCode被设为IO_ERR_RETRY_SUCCEEDED这样的值。 虽然STATUS_PENDING也属于STATUS_SEVERITY_SUCCESS类的值,驱动程序却不能在未决的IRP中记录错误。 STATUS_SEVERITY_INFORMATIONAL 表示提示消息的NTSTATUS值,如STATUS_SERIAL_MORE_WRITES。 STATUS_SEVERITY_WARNING 表示警告的NTSTATUS值,如STATUS_DEVICE_PAPER_EMPTY。 STATUS_SEVERITY_ERROR 表示错误的NTSTATUS值,如在错误记录包中,将FinalStatus值设为STATUS_INSUFFICIENT_RESOURCES,或将ErrorCode值设为IO_ERR_CONFIGURATION_ERROR。 绝大多数公共IO_ERR_XXX常量属于STATUS_SEVERITY_ERROR类。 为了使一系列驱动程序定义的错误对系统管理者可见,或为了通过Win32事件查看器终止用户,驱动程序必须在注册表中将它本身作为错误记录组件来建立。 定义私有NTSTATUS常量 一对新的驱动程序(如类/端口驱动程序或视频显示/微端口驱动程序)可以定义驱动程序指定的STATUS _XXX值,来交换有关私有定义的IRP_MJ_INTERNAL_DEVICE_CONTROL请求的信息,这个请求是从低层向高层驱动程序发的。 如果现存的高层驱动程序的IoCompletion例程可能为某个IRP调用,那么当类驱动程序完成此IRP时,它必须将所有私有STATUS _XXX值映射为系统定义的NTSTATUS值。 对于成对的显示和视频微端口驱动程序,视频端口驱动程序负责公共STATUS _XXX值和Win32定义的常量(由视频微端口驱动程序返回)之间的映射。如果想对视频微端口驱动程序有更多的了解,请参见《图形驱动程序设计指南》。 为一系列IRP_MJ_INTERNAL_DEVICE_CONTROL请求定义私有STATUS _XXX值的驱动程序必须做下列事情: 如图16.4所示,将Facility域设成适当的驱动程序定义的常量,这个值用来指出设备的类型。 设置用户代码标志(图16.4中用C标出)。 用适当的值设置Sev域。参见“定义新的IO_ERR_XXX部分”。 处理可删除存储介质 文件系统和可删除存储介质设备驱动程序必须:保证当文件在可删除存储介质设备上打开时,正确的介质已被安装;并且保证在访问介质的操作期间,正确的介质始终被安装着。位于文件系统和可删除存储介质设备驱动程序之间的中间层驱动程序也必须负责保证这一点。 因此,可删除存储介质设备的驱动程序应该能做到下列中的至少一件事情: 响应来自文件系统的验证请求(check-verify request) 通知文件系统可能的存储介质改变 检查DeviceObject->Flags 建立中间层驱动程序的IRP 响应来自文件系统的验证请求 文件系统可以随时向设备驱动程序的Dispatch入口点发送IRP,通过将I/O栈中的Parameters.DeviceIoControl.IoControlCode设为以下值,来提出IRP_MJ_ DEVICE_CONTROL请求: IOCTL_XXX_CHECK_VERIFY 其中XXX是设备类型,如DISK、TAPE或CDROM。 DISK类型包括不可分区(软盘)和可分区可删除存储介质设备。 如果下层设备驱动程序确定了存储介质没有改变,驱动程序应该完成IRP,用下列值返回IoStatus块: Status:设为STATUS_SUCCESS Information:设为0 此外,如果设备类型是DISK或CDROM且调用者指定了输出缓冲,驱动程序就返回缓冲中Irp->AssociatedSystemBuffer保存的存储介质改变记数,并且将IoStatus. Information设为sizeof(ULONG)。通过返回这个记数值,驱动程序使得调用者可以如实地确定存储介质是否已经改变。 如果下层设备驱动程序确定了存储介质已经改变,它会根据存储介质是否安装而执行不同的举措。如果安装了存储介质(VPB中的VPB_MOUNTED标志被置为1),驱动程序应该做下列事情: 将DeviceObject中的Flags与DO_VERIFY_VOLUME进行“或”运算,得到新的Flags值。 将IRP中的IoStatus块设为下列值: 将Status设为STATUS_VERIFY_REQUIRED 将Information设为0 用输入IRP调用IoCompleteRequest。 如果没有安装存储介质,驱动程序决不能将DO_VERIFY_VOLUME置为1。驱动程序应该将IoStatus. Status设为STATUS_IO_DEVICE_ERROR,将IoStatus. Information设为0,然后用输入IRP调用IoCompleteRequest。 通知文件系统可能的存储介质改变 可删除存储介质设备驱动程序必须保证:驱动程序处理请求向/从存储介质发送的IRP或驱动程序处理会影响到存储介质的设备I/O控制操作时,DeviceObject(每个被发送IRP的驱动程序例程的输入)代表的设备的存储介质没有改变。如果物理设备总是向驱动程序通告状态变化,那么对改变介质最好的检查时间就是在从无介质存在状态向介质存在状态转化之后。 如果在驱动程序开始I/O操作之前或操作期间,物理设备指出存储介质的状态可能已经改变了,驱动程序就必须做下列事情: 通过检查VPB中的VPB_MOUNTED标志确保安装了存储介质。(如果没有安装存储介质,驱动程序决不能将DO_VERIFY_VOLUME置为1。驱动程序应该将IoStatus. Status设为STATUS_IO_DEVICE_ERROR,将IoStatus. Information设为0,然后用此IRP调用IoCompleteRequest。) 将DeviceObject中的Flags与DO_VERIFY_VOLUME进行“或”运算,得到新的Flags值。 将IRP中的IoStatus块设为下列值: 将Status设为STATUS_VERIFY_REQUIRED 将Information设为0 在完成IoStatus块中的Status域值不是STATUS_SUCCESS的那些IRP之前,驱动程序必须调用IoIsErrorUserInduced,当Status域为下列值时,它返回TRUE: STATUS_BERIFY_REQUIRED STATUS_NO_MEDIA_IN_DEVICE STATUS_WRONG_VOLUME STATUS_UNRECOGNIAED_MEDIA STATUS_MEDIA_WRITE_PROTECTED STATUS_IO_TIMEOUT STATUS_DEVICE_NOT_READY 如果IoIsErrorUserInduced返回TRUE,驱动程序必须调用IoSetHardErrorOrVerifyDevice,这样FSD就可以向用户发送提供了正确存储介质的弹出菜单;或重试初始请求;或撤消请求的操作。 检查设备对象中的标志 对每个请求向/从可删除存储介质进行I/O操作的IRP,可删除存储介质设备驱动程序必须确定其DeviceObject->Flags 是否被设为DO_VERIFY_VOLUME。如果是,驱动程序必须做下列事情: 对于IRP_MJ_READ、IRP_MJ_WRITE和某些IRP_MJ_DEVICE_CONTROL请求,检查I/O栈位置Flags的值是否为SL_OVERRIDE_VERIFY_VOLUME。如果是,继续请求的操作。 当IFS安装或重新安装可删除存储介质时,返回下层存储介质逻辑结构信息的设备控制请求将I/O栈位置Flags的值设为SL_OVERRIDE_ VERIFY_VOLUME。 否则,驱动程序必须在DeviceObject->Flags 为DO_VERIFY_VOLUME时,拒绝为相应驱动器、设备或分区执行I/O操作。如前面几个小节所述,可删除存储介质设备的驱动程序必须拒绝发送给相应设备的IRP,对每个IRP重复步骤2和3,直到FSD清除了DeviceObject->Flags中的DO_VERIFY_VOLUME值为止。 如果可删除存储介质设备驱动程序在设置了DO_VERIFY_VOLUME且没有设置SL_OVERRIDE_VERIFY_VOLUME时,没有拒绝IRP,那么文件系统既不能保持缓存文件数据的完整性,又不能提示用户重新安装这个已经打开了文件的存储介质。 在中间层驱动程序中建立IRP 位于文件系统和可删除存储介质设备驱动程序之间的中间层驱动程序必须在IRP中建立低一层驱动程序的I/O栈位置。当中间层驱动程序为下层驱动程序建立I/O栈位置时,它必须从输入的IRP_MJ_READ、IRP_MJ_WRITE和IRP_MJ_DEVICE_CONTROL请求中,将自己的I/O栈位置Flags复制到低一层驱动程序的I/O栈位置。 如果中间层驱动程序为下层可删除存储介质驱动程序分配了新的IRP,它必须按以下所述建立这些IRP: 对于传输请求,它必须根据初始IRP中Tail.Overlay.Thread的值,在每个分配给驱动程序的IRP中建立线程环境。 对于IRP_MJ_READ、IRP_MJ_WRITE和IRP_MJ_DEVICE_CONTROL请求,它必须从初始IRP中将I/O栈位置Flags复制到每个分配给驱动程序的IRP。 否则,文件系统既不能保持缓存文件数据的完整性,又不能提示用户重新安装这个已经打开了文件的存储介质。 使设备对应用程序和驱动程序可用 对于任何用户模式代码可以直接递交I/O请求的设备,无论它是物理的、逻辑的或虚拟的,其驱动程序都必须为它的用户模式客户提供某种名字。用这个名字,用户模式应用程序(或其他系统组件)就可以识别出请求I/O的那个设备。 在以前的操作系统版本中,驱动程序命名它们的设备对象,并在注册表中为这些名字和用户可见的Win32逻辑名建立符号连接。 然而,Windows 2000和WDM驱动程序并不命名设备对象,而是为每个设备对象注册并使一个用户模式I/O请求可被发送到的设备接口可用。设备接口是向其他系统组件(包括其他驱动程序和用户模式应用程序)输出设备和驱动程序功能的一种方式。 设备对象被划分成类,每一类都与一个GUID相关。系统在设备指定的头文件中为公共设备接口类定义GUID。当驱动程序注册了一个设备接口,I/O管理器将其设备接口类GUID与一个符号连接名相关。连接名保存在注册表中,它在系统启动期间一直存在。使用此设备接口的应用程序可以查找它的符号连接名,并将它保存用作I/O请求的目标。 注册设备接口 Windows 98和Windows 2000提供了两种注册设备接口的方法: 对于内核模式组件,像大多数驱动程序一样,使用I/O管理器例程。这一节描述了如何使用这些例程。 对于用户模式代码,使用SetupDiXxx函数。如果想对这些函数有更多的了解,请在在线DDK中参见设备接口函数部分。 Windows 2000和WDM驱动程序并不命名其设备对象。相反,当驱动程序调用IoCreateDevice以创建一个设备对象时,它应该将设备名指定为NULL字符串。总线驱动程序应该将FILE_AUTOGENERATED_DEVICE_NAME标志设为1。所有PnP函数、过滤器和总线驱动程序都应该将FILE_DEVICE_SECURE_OPEN标志设为1。相应地,系统为此PDO选择一个唯一的设备名。 在创建了设备对象并将它连接到设备栈之后,一个驱动程序调用IoRegisterDeviceInterface。I/O管理器对这个例程的定义如下: NTSTATUS IoRegisterDeviceInterface( IN PDEVICE_OBJECT PhysicalDebiceObject, IN CONST GUID *InterfaceClassGuid, IN PUNICODE_STRING ReferenceString, OPTIONAL OUT PUNICODE_STRING SymbolicLinkName ); 通常,函数驱动程序从它的AddDevice例程中做此调用,但有时过滤器驱动程序注册这个接口。 调用者用PhysicalDeviceObject传递指向设备PDO的指针。InterfaceClassGuid用来标识正被注册的接口。大多数函数和所有过滤器驱动程序应该在ReferenceString中传递NULL字符串,这个参数为总线驱动程序提供了给按要求即时创建的软件设备定义接口的方法。 注册过的接口在操作系统启动期间一直存在。如果指定的接口已经被注册过了,I/O管理器就在SymbolicLinkName中传送它的名字,并返回提示成功状态STATUS_OBJECT_NAME_EXISTS。 如果接口还没有被注册过,I/O管理器就为这个设备接口创建一个注册键,并在分配给调用者的Unicode字符串结构中返回隐式的SymbolicLinkName。驱动程序使此设备接口可用或不可用时传递此连接名。它也用这个名字访问注册键,在这个注册键中它可以存放为设备接口指定的信息。(参见IoOpenDeviceInterfaceRegistryKey)应用程序用这个连接名打开设备。 在兼容设备接口类下,驱动程序可以按需要任意多次地调用IoRegisterDeviceInterface来注册附加的设备接口。 其他系统组件在驱动程序使设备接口可用之前,不能使用它。参见“使设备接口可用和不可用”。 使设备接口可用和不可用 在成功地启动了设备后,注册了接口的驱动程序调用IoSetDeviceInterfaceState使此接口可用。I/O管理器对这个例程的定义如下: NTSTATUS IoSetDebiceINterfaceState( IN PUNICODE_STRING SymbolicLinkName, IN BOOLEAN Enable ); 驱动程序传递由IoRegisterDeviceInterface返回的SymbolicLinkName和值为TRUE的Enable参数来使此接口可用。 如果驱动程序能成功地启动其设备,它应该在处理PnP管理器的IRP_MN_START_DEVICE请求时调用这个例程。 在IRP_MN_START_DEVICE请求完成后,PnP管理器向所有请求了它们的内核模式和用户模式组件发设备接口返回通知。参见在线DDK上的“Registering for Device Interface Change Notification(设备接口注册改变通知)”。 为了使设备接口不可用,驱动程序调用IoSetDeviceInterfaceState,传递由IoRegisterDeviceInterface返回的SymbolicLinkName和值为FALSE的Enable参数。 当驱动程序为设备处理IRP_MN_SURPRISE_REMOVAL或IRP_MN_REMOVE_DEVICE时,它应该使设备的接口不可用。当设备被停止(IRP_MN_STOP_DEVICE)或处于休眠状态时,驱动程序不因该使接口不可用,而应该让所有设备接口可用并对I/O排队。 使用设备接口 设备接口可以被内核模式组件和用户模式应用程序使用。用户模式的代码必须用SetupDiXxx函数找出注册了的、可用的设备接口。参见在线DDK中的“Device Interface Functions(设备接口函数)”。 在内核模式组件可以使用指定的设备或文件对象之前,它必须做下列事情: 确定所需的设备接口是否是注册过并可用的。 驱动程序在注册表中注册时,可以配置成能够通知PnP管理器何时设备接口可用或不可用。为了注册,组件调用IoRegisterPlugPlayNotification。无论何时为指定设备类将设备接口设置成可用或不可用,IoRegisterPlugPlayNotification例程都会调用驱动程序提供的回调函数。参见《即插即用、电源管理和安装设计指南》第4章中的“使用PnP设备接口改变通知”。 驱动程序或其他内核模式组件也可以调用IoGetDeviceInterface来获得指定设备接口类的所有注册过并可用的设备接口的列表。返回的列表中有指向标识设备接口的Unicode字符串的指针。 获得代表所需设备接口类的符号连接的Unicode字符串。 IoGetDeviceInterface返回指向SymbolicLinkList参数中字符串的指针。如果驱动程序注册为可通知PnP管理器,上述例程的回调(callback)可以从DEVICE_INTERFACE_CHANGE_NOTIFICATION结构中取回这个字符串。 用这个Unicode字符串得到指向相应设备或文件对象的指针。 为了访问指定的设备对象,驱动程序必须调用IoGetDeviceObjectPointer,ObjectName参数中放的是所需接口的Unicode字符串。为了访问文件对象,驱动程序必须用ObjectName参数传递这个Unicode字符串,调用InitializeObjectAttributes,然后调用ZwCreateFile传递成功初始化了的属性结构。 可分页代码和数据 用户可以使某些驱动程序的全部或部分可分页。对驱动程序代码分页减小了驱动程序加载映像的大小,从而为其他应用节省了系统空间。这对突发使用(sporadically-used)设备(如调制解调器和CD-ROM)的驱动程序或部分很少被调用的驱动程序非常有效。 在频繁使用的驱动程序中,决定性能好坏的代码段不应该是可分页的。按需要随时对代码分页会对驱动程序和系统的性能产生负面影响。 为了确定驱动程序的某部分是否可以设成可分页的,记住下列事实: 如果下列情况之一为真,驱动程序代码就可以是可分页的: 它访问页式存储池。 它调用另一个可分页例程。 它在用户线程环境中引用用户缓冲。 下列驱动程序代码必须是常驻的,不能是可分页的: 运行于或高于DISPATCH_LEVEL的IRQL上的代码 获得自旋锁的代码 调用Knernel的对象支持例程的代码。如Wait参数被设为TRUE的KeReleaseMutex或KeReleaseSemaphore例程。如果Knernel在Wait值为TRUE的情况下被调用,调用返回时IRQL为DISPATCH_LEVEL,而且发送者数据库被锁。 一般,如果所有可分页代码(或数据)的总量至少为4K,那么使此段可分页是可行的。应该尽可能地将纯可分页代码(或数据)从必须可分页或按要求随时可被锁的代码中分离出来,放到单独的一段中。将纯可分页代码和按要求随时可被锁的代码混在一起会使一些不需要被锁的系统空间被锁住。 然而,如果驱动程序的可分页代码(或数据)少于4K,可以将这些代码和按要求随时可被锁的代码混在一起,放在同一段中,从而节省系统空间。 如果想对此要更深入的了解,请参见下列: 16.9.1 使驱动程序代码可分页 16.9.2 锁住可分页代码或数据 16.9.3 对整个驱动程序分页 使驱动程序代码可分页 为了使驱动程序例程可分页,必须保证它运行在低于DISPATCH_LEVEL的IRQL上,而且还要保证它不获得任何自旋锁。 为了检测出运行在高于DISPATCH_LEVEL的IRQL上的代码,可以使用PAGED_CODE()宏。在调试模式下,如果代码运行在高于DISPATCH_LEVEL的IRQL上,这个宏就生成信息。将此宏放在例程的第一句处,就将整个例程标记为分页式代码,如下面例子所示: NTSTATUS MyDriverXxx( IN OUT PVOID ParseContext OPTIONAL, OUT PHANDLE Handle ) { NTSTATUS Status; PAGED_CODE(); . . . } 为了保证能正确执行,在已编好的驱动程序中选中“Force IRQL Checking”选项运行“Driver Verifier”。这个选项使系统可以在每次驱动程序将IRQL提高到DISPATCH_LEVEL或更高时,自动将可分页代码调出内存。使用“Driver Verifier”可以在这个区内快速查找驱动程序错误。否则,这些错误一般只能通过用户来查找,而它们通常很难重现。 使用了自旋锁的例程不能被分页。然而,在有些情况下,可以将不在可分页段中的单独例程中的那些需要自旋锁的操作分离出来。 例如,对于以下代码段: //PAGED_CODE(); KeInitializeEvent(&event,NotificationEvent,FALSE); irp=IoBuildDeviceIoControlRequest(IRP_MJ_DEVICE_CONTROL, DeviceObject, (PVOID)NULL, 0, (PVOID)NULL, 0, FALSE, &event, &ioStatus); if(irp){ irpSp=IoGetNextIrpStackLocation(irp); irpSp->MajorFunction=IRP_MJ_FILE_SYSTEM_CONTROL; irpSp->MinorFunction=IRP_MN_LOAD_FILE_SYSTEM; status=IoCallDriver(DeviceObject,irp); if(status= =STATUS_PENDING){ (VOID)KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, (PLARGE_INTEGER)NULL); } } SPINLOCKUSE ! ExAcquireSpinLock(&IopDatabaseLock,&irql); //Code inside spin lock DeviceObject->ReferenceCount--; if(!DeviceObject->ReferenceCount&& !DeviceObject->AttachedDevice){ //Unload the driver . . . }else{ ExReleaseSpinLock(&IopDatabaseLock,irql); } 可以通过将引用自旋锁的几行代码移到一个单独的例程中,使前面的例程可分页(大约节省160个字节)。 此外,记住:如果驱动程序调用了Wait参数值为TRUE的KeXxx支持例程(如KeReleaseMutex或KeReleaseSemaphore),那么它的代码决不能标记为可分页的。这种调用返回时,IRQL为DISPATCH_LEVEL,而且发送者数据库被锁。 锁住可分页代码或数据 非页式代码是内存常驻的,它运行在等于或高于DISPATCH_LEVEL的IRQL上,而且永远不会引起页错误。页式代码运行在低于DISPATCH_LEVEL的IRQL上,当对它的引用不会导致系统瘫痪或负面影响驱动程序操作时,它可以被页调入(page in)。 某些驱动程序(如串行和并行驱动程序)不需要常驻内存,除非它们管理的设备是打开的。只有有一个活动的连接或端口,管理此端口的驱动程序代码的某部分就必须常驻来为设备服务。然而,当端口或连接没有被使用时,驱动程序代码就不需要了。与此相反,存放系统代码、应用程序代码或系统分页文件的磁盘的驱动程序必须总是常驻内存的,因为这个驱动程序要经常在其设备和系统之间传输数据。 突发使用设备(如调制解调器)的驱动程序可以在设备处于非活动状态时释放系统空间。如果在单独的段中有必须常驻以服务活动设备的代码,这一段可以设计成可分页的。当驱动程序的设备被打开时,操作系统就将可分页段放入内存。 系统CD音频驱动程序代码也有这个特征。驱动程序代码根据CD设备制造商来分类到个可分页段中。某些品牌可能永远不会出现在给定系统上。此外,即使系统上有CD-ROM,它可能很少被访问。因此按CD类型将代码分类到个可分页段中可以保证某个机器上没有的设备的代码永远不会被加载,而当设备被访问时,系统为适当的CD设备加载代码。然后如下面所述的,驱动程序调用MmLockPagableCodeSection,在其设备正被使用时,将它的代码锁进内存。 为了将可分页代码分离到一个已命名的程序段中,用下列编译器指令对它进行标记。所有与Windows NT/Windows 2000兼容的编译器都支持这条指令: #pragma alloc_text(PAGEXxxx,RoutineName) 这个程序段的名字必须以PAGE开头,而且必须以1到4个唯一标识驱动程序可分页段的字符完成。段名是区分大小写的,也就是说,PAGE必须大写。RoutineName标识了将包括在可分页段中的入口点。 例如,下列代码将RdrCreateConnection表示为PAGEELK段内部的入口点: #ifdef ALLOC_PRAGMA #pragma alloc_text(PAGELK,RdrCreateConnection) #endif 为了使可分页驱动程序代码常驻并被锁住,驱动程序调用MmLockPagableCodeSection,传递在可分页代码段中的一个地址(通常是驱动程序例程的入口点)。MmLockPagableCodeSection在整个代码段中加了锁,包括在此调用中引用的例程。也就是说,它使每个与同一个PAGEXxxx标识符相关的例程都常驻并被锁。 只有在被传递的符号地址没有在可分页段中时,对MmLockPagableCodeSection的调用才会失败。 MmLockPagableCodeSection返回一个句柄,这个句柄将在对程序段解锁时(MmUnlockPagableImageSection),或驱动程序必须从其代码的其他地方锁程序段时用到。 驱动程序也可以将很少使用的数据视为可分页的,这样就可以将它的页调出内存,直到它支持的设备活动为止。例如,系统混频器(mixer)驱动程序使用可分页数据。混频器设备没有与它相关的异步I/O,因此这个驱动程序可以使其数据可分页。 为了创建可分页数据段,在数据模块开始处使用下列编译器指令: #pragma data_seg(“PAGE”) 在模块末尾,用下列指令: #pragma data_seg() 关键词PAGE是区分大小写的,因此它必须大写。 为了使数据段常驻并被锁,驱动程序调用MmLockPagableDataSection,传递出现在可分页数据段中的数据项。MmLockPagableDataSection返回一个将在后续加锁或解锁请求中用到的句柄。 为了恢复被锁段的可分页状态,调用MmUnlockPagableImageSection,传递由MmLockPagableCodeSection或MmLockPagableDataSection返回的句柄值。驱动程序的Unload例程必须调用MmUnlockPagableImageSection释放它为可锁代码和数据段获得的每个句柄。 对段加锁是一项开销很大的操作,因为内存管理器必须在将页锁进内存之前检索它的加载模块列表。如果驱动程序在其代码的许多地方都要加锁,它应该在第一次调用MmLockPagableXxxxSection之后,使用更有效的MmLockPagableSectionByHandle。 传递给MmLockPagableSectionByHandle的句柄是由先前调用MmLockPagableCodeSection或MmLockPagableDataSection返回的句柄。 内存管理器为每个段句柄维护一个记数值。每次驱动程序为这个段调用MmLockPagableXxx,内存管理器就将此记数值加1。调用MmUnlockPagableImageSection使此记数值减1。当段句柄的记数值非零时,这个段在内存中保持被锁状态。 段句柄是有效的,只要其驱动程序已被加载。因此,驱动程序应该只调用MmLockPagableXxxxSection一次。如果驱动程序需要其他的加锁调用,它应该使用MmLockPagableSectionByHandle。 如果驱动程序为已经加锁的段调用MmLockPagableXxxxSection例程,内存管理器只是将此段的引用记数值加1。如果当调用加锁例程时,此段对应的页已被调出,内存管理器将此页调入,并将它的引用记数值设为1。 用这种技术最小化了驱动程序对系统资源的影响。当驱动程序运行时,它可以将需要常驻的代码和数据锁进内存。当对设备没有未完成的I/O请求时,(也就是说,当设备被关闭或如果设备从未被打开时),驱动程序可以对同一段代码或数据开锁,使它的页可以被调出。 然而,在驱动程序连接了中断后,在中断处理期间可被调用的驱动程序代码必须是常驻内存的。有些设备驱动程序可以按需要随时使其可分页或锁进内存,而这样的驱动程序的代码和数据的某些核心部分必须在系统空间中永久常驻。 对代码或数据段加锁时,应考虑下列实现策略: Mm(Un)LockXxx的基本用途是:使一般情况下被视为非页式的代码或数据成为可分页的,并作为非页式代码或数据被引入(bring in)。像串行或并行驱动程序这样的驱动程序是很好的例子:如果这种驱动程序管理的设备没有打开的句柄,部分代码是不需要的,可以将其页调出。换向器(redirector)和服务器也是可以使用这种技术的好例子。当没有活动连接时,这些组件的页都可以调出。 整个可分页段被锁进内存。 每个驱动程序中,一段给代码、一段给数据是很有效的。一般,使用许多命名的可分页段效率比较低。 保存纯可分页段并将它分页,但是要与按要求随时可被锁的代码分开。 记住:MmLockPagableCodeSection 和MmLockPagableDataSection不应该频繁地被调用。当内存管理器加载段时,这些例程会使I/O负载过重。如果驱动程序必须在其代码的几处对段加锁,它应该使用MmLockPagableSectionByHandle。 使用MmLockPagableCodeSection和MmUnlockPagableImageSection减少了每个驱动程序加载映像的大小,但同时影响了驱动程序和系统性能。一般,对系统性能有决定性影响的驱动程序(如键盘和鼠标驱动程序)和经常使用设备的驱动程序(如磁盘驱动程序)不应该有可分页段。不值得以性能损失为代价换来驱动程序映像大小暂时的减少。 对整个驱动程序分页 使用MmLockPagableXxx支持例程并指定分页和可丢弃段的驱动程序,是由非页式段、页式段和驱动程序初始化后就丢弃的INIT段组成的。 在设备驱动程序为它管理的设备连接了中断之后,驱动程序的中断处理路径必须常驻在系统空间。中断处理代码必须是页不能被调出的驱动程序段的一部分,以防中断发生。 两个附加的内存管理器例程MmPageEntireDriver和MmResetDriverPaging可用来设置组成驱动程序映像的所有段的可分页或不可分页属性。这些例程允许驱动程序在其设备没有被使用和不能产生中断时整个被调出。 完全可分页系统驱动程序的例子有win32.sys驱动程序、串行驱动程序、信件槽(mailslot)驱动程序、蜂鸣器(beep)驱动程序和零位驱动程序(null driver)。 典型的串行驱动程序是被间歇使用的。串行驱动程序可以整个被调出,直到它管理的端口打开为止。端口只要一打开,串行驱动程序中必须常驻内存的那部分就必须被放入非页式系统空间。驱动程序的其他部分可以仍是可分页的。 能整个被调出的驱动程序应该在中断被连接之前的驱动程序初始化期间调用MmPageEntireDriver。PnP驱动程序不应该使用这个例程。 当被调出的驱动程序所管理的设备收到一个打开请求时,驱动程序页被调入。然后,驱动程序必须在连接中断之前调用MmResetDriverPaging。调用MmResetDriverPaging使内存管理器根据编译链接阶段获得的属性来处理驱动程序段。非页式段(如文本段)将被调入非页式系统内存,可分页段将在它们被引用时调入。 这样的驱动程序必须保存其设备打开句柄的引用记数值。每当有设备打开请求时,驱动程序增加这个记数值;每当有关闭请求时,驱动程序减少这个记数值。当此记数值为0时,驱动程序应当断开中断,然后调用MmPageEntireDriver。如果驱动程序管理的设备超过一个,在所有设备的记数值都为0以后,驱动程序才能调用MmPageEntireDriver。 驱动程序要做在改变引用记数值时同步所需的所有工作,并且在驱动程序可分页状态改变时,它要防止引用记数值也改变。也就是说,在SMP机上,驱动程序必须保证:当一个处理器上的打开调用导致中断被连接且引用记数值被增加时,另一个处理器上不能正在运行MmPageEntireDriver。 常见的驱动程序可靠性问题 以内核模式执行的驱动程序占用了大量代码基地址(base)。因此,要改善系统可靠性就必须考虑到这个庞大的代码基地址。 可用来保证驱动程序可靠性的最重要的工具是Driver Verifier。它可以检查许多常见的驱动程序问题,它们中的一部分将在这一节中讨论。 这一节为下列常见的驱动程序问题提出了预防策略: 缓冲I/O 用户空间地址 直接I/O 输入数据和设备状态 Dispatch例程 多处理器环境 处理IRP 对上述问题的讨论分布在以下各节,其中有很多节在讲述时附带了程序片段。这些小程序段说明了典型的错误以及怎样更正它们。(为了简明,已经对这些代码做了适当的修改) 缓冲I/O中的错误 没有检查缓冲大小可能是最常见的驱动程序问题。这个问题可以在许多环境中发生,但是在下述情况下尤为麻烦。 例如,假设下列代码出现在被Dispatch例程调用的一个例程中,并假设驱动程序没有验证IRP中传递的缓冲大小的有效性: switch(ControlCode) case IOCTL_NEW_ADDRESS:{ tNEW_ADDRESS *pNewAddress= pIrp->AssociatedIrp.SystemBuffer; pDeviceContext->Addr=ntohl(pNewAddress->Address); 这个例子没有在分配语句(粗体)之前检查缓冲大小。结果,若输入缓冲大小不足以放置tNEW_ADDRESS结构,则第二行中的pNewAddress->Address引用就是错的。 下列代码检查了缓冲大小,避免了可能出现的问题: case IOCTL_NEW_ADDRESS:{ tNEW_ADDRESS *pNewAddress= pIrp->AssociatedIrp.SystemBuffer; if(pIrpSp->Parameters.DeviceIoControl.InputBufferLength>= sizeof(tNEW_ADDRESS)){ pDeviceContext->Addr=ntohl(pNewAddress->Address); 处理其他缓冲I/O的代码(如使用可变大小缓冲的WMI请求)可能有类似的错误。 为缓冲IOCTL和FSCTL输出请求检查缓冲大小 输出缓冲问题与输入缓冲问题相似。它们很容易破坏存储池,而且用户模式的调用者不易察觉它们的发生。 在下面的例子中,驱动程序没有检查SystemBuffer的大小: case IOCTL_GET_INFO:{ Info=Irp->AssociatedIrp.SystemBuffer; Info->NumIF= NumIF; Irp->IoStatus.Information= NumIF*sizeof(GET_INFO_ITEM)+ sizeof(ULONG); Irp->IoStatus.Status=ntStatus; } 假定系统缓冲的NumIF域表示输入项的个数,这个例子可以将IoStatus.Information设为大于输出缓冲的值,因此向用户模式代码返回了过多信息。如果应用程序编得不好,用过小的输出缓冲调用,前面的代码段会因在系统缓冲外写而破坏存储池。 记住:I/O管理器假定Information域中的值是有效的。当调用者为输出缓冲传递了一个无效的内核模式地址和零字节大小,如果驱动程序没有检查输出缓冲的大小而过后发现错误,就会产生严重问题。 以缓冲I/O路径向调用者返回未初始化数据 驱动程序应该在将输出缓冲数据返回给调用者之前,把所有输出缓冲数据都初始化为0。没有初始化缓冲会导致垃圾数据,其个数为未初始化字节数。 在下面的例子中,驱动程序返回无用的垃圾数据: case IOCTL_GET_NAME:{ outputBufferLength= ioStack->Parameters.DeviceControl.OutputBufferLength; outputBufferLength=(PGET_NAME)Irp->AssociatedIrp.SystemBuffer; if(outputBufferLength>=sizeof(GET_NAME)){ length= outputBufferLength-sizeof(GET_NAME); ntStatus=IoGetDeviceProperty( DeviceExtension->PhysicalDeviceObject, DevicePropertyDriverKeyName, length, outputBuffer->DriverKeyName, &length); outputBuffer->ActualLength= length+sizeof(GET_NAME); Irp->IoStatus.Information=outputBufferLength; }else{ ntStatus=STATUS_BUFFER_TOO_SMALL; } 将IoStatus.Information的值设为输出缓冲大小使得整个输出缓冲都被返回给调用者。I/O管理器没有初始化超过输入缓冲大小的数据(由于缓冲要求,输入和输出缓冲是重叠的)。因为系统支持例程IoGetDeviceProperty没有写整个缓冲,这个IOCTL向调用者返回了未初始化的数据。 有些驱动程序用Information域返回提供I/O请求更多细节的代码。在这样做之前,这样的驱动程序应该检查IRP标志以确保这个IRP_INPUT_OPERATION没有被置1。当此标志位没有被置1时,IOCTL或FSCTL没有输出缓冲,因此Information域不需要提供缓冲大小。在这种情况下,驱动程序可以安全地使用Information域返回它自己的代码。 验证可变长度缓冲有效性的错误(会引起整数下溢或上溢) 驱动程序经常采用有固定大小头部和可变长度数据的输入缓冲,如下面例子所示: typedef struct_WAIT_FOR_BUFFER{ LARGE_INTEGER Timeout; ULONG NameLength; BOOLEAN TimeoutSpecified; WCHAR Name[1]; } WAIT_FOR_BUFFER,*PWAIT_FOR_BUFFER; if(InputBufferLength<sizeof(WAIT_FOR_BUFFER)){ IoCompleteRequest(Irp,STATUS_INVALID_PARAMETER); Return(STATUS_INVALID_PARAMETER); } WaitBuffer=Irp->AssociatedIrp.SystemBuffer; if(FIELD_OFFSET(WAIT_FOR_BUFFER,Name[0])+ WaitBuffer->NameLength>InputBufferLength){ IoCompleteRequest(Irp,STATUS_INVALID_PARAMETER); Return(STATUS_INVALID_PARAMETER); } 如果WaitBuffer->NameLength是一个很大的ULONG值,把它加到偏移量(offset)上会引起整数上溢。相反,驱动程序应该从InputBufferLength中减去偏移量,将结果与WaitBuffer->NameLength相比较,如下面例子所示: if(InputBufferLength<sizeof(WAIT_FOR_BUFFER)){ IoCompleteRequest(Irp,STATUS_INVALID_PARAMETER); Return(STATUS_INVALID_PARAMETER); } WaitBuffer=Irp->AssociatedIrp.SystemBuffer; if((InputBufferLength FIELD_OFFSET(WAIT_FOR_BUFFER,Name[0])> WaitBuffer->NameLength){ IoCompleteRequest(Irp,STATUS_INVALID_PARAMETER); Return(STATUS_INVALID_PARAMETER); } 前面的减运算不会下溢,因为第一个if语句保证了InputBufferLength大于或等于WAIT_FOR_BUFFER的大小。 下面例子是一个更复杂的下溢问题: case IOCTL_SET_VALUE: dwSize=sizeof(SET_VALUE); if(inputBufferLength<dwSize){ ntStatus=STATUS_BUFFER_TOO_SMALL; break; } dwSize=FIELD_OFFSET(SET_VALUE,pInfo[0])+ pSetValue->NumEntries*sizeof(SET_VALUE_INFO); if(inputBufferLength<dwSize){ ntStatus=STATUS_BUFFER_TOO_SMALL; break; } 在这个例子中,乘运算时会发生整数上溢。如果SET_VALUE_INFO结构的大小是2的倍数,像0x80000000这样的NumEntries值在做乘运算位左移时就会上溢。然而,缓冲大小仍然可以通过有效性验证,因为上溢使dwSize显得非常小。为了避免这个问题,像前一个例子那样减去长度,用sizeof(SET_VALUE_INFO)去除,然后将结果与NumEntries相比较。 引用用户空间地址时的错误 驱动程序应该在使用任何用户空间中的地址之前,验证它的有效性。I/O管理器既不验证这种地址的有效性,也不验证缓冲中传递给驱动程序的指针的有效性。 没有验证在Type3(METHOD_NEITHER)IOCTL和FSCTL中被传递的地址的有效性 I/O管理器不为METHOD_NEITHER的IOCTL和FSCTL做任何有效性验证。为了保证用户空间地址是有效的,驱动程序必须使用ProbeForRead和ProbeForWrite例程,将所有缓冲引用嵌入try/except块中。 在下面的例子中,驱动程序假定Type3InputBuffer中传递的值是一个有效的地址: case IOCTL_GET_HANDLER:{ PULONG EntryPoint; EntryPoint= IrpSp->Parameters.DeviceIoControl.Type3InputBuffer; * EntryPoint=(ULONG)DriverEntryPoint; 下面的代码避免了这个问题: case IOCTL_GET_HANDLER:{ PULONG_PTR EntryPoint; EntryPoint= IrpSp->Parameters.DeviceIoControl.Type3InputBuffer; try{ if(Irp->RequestorMode!=KernelMode){ ProbeForWrite(EntryPoint, sizeof(ULONG_PTR), TYPE_ALIGNMENT(ULONG_PTR)); } *EntryPoint=(ULONG_PTR)DriverEntryPoint; }except(EXCEPTION_EXECUTE_HANDLER){ 应注意:正确的代码将DriverEntryPoint强制转换为ULONG_PTR类型。这个转换在64位Windows环境下可以做更进一步的应用。 没有验证嵌在缓冲I/O请求中的指针的有效性 通常驱动程序会在缓冲请求中嵌入指针,如下面例子所示: struct ret_buf{ void *arg; //Pointer embedded in request int rval; }; pBuf=Irp->AssociatedIrp.SystemBuffer; … arg=pBuf->arg; //Fetch the embedded pointer … //If the pointer is invalid, //this statement can corrupt the system. RtlMoveMemoru(arg,&info,sizeof(info)); 在这个例子中,驱动程序应该使用放在try/except块中的ProbeXxx例程来验证嵌入指针的有效性,像在前面验证METHOD_NEITHER IOCTL的有效性一样。虽然嵌入指针使得驱动程序可以返回更多的信息,但是驱动程序可以通过使用有关的偏移量或可变长度缓冲来更有效地达到这个目的。 直接I/O中的错误 最常见的直接I/O问题是不能正确地处理零长度缓冲。因为I/O管理器不为零长度传输创建MDL,所以零长度缓冲使得Irp->MdlAddress的值为NULL。 为了映射地址空间,Windows 2000驱动程序应该使用MmGetSystemAddressForMdlSafe,它在映射失败时返回NULL。如果驱动程序传递了一个值为NULL的MdlAddress,它也会返回NULL。在Windows 98上,MmGetSystemAddressForMdlSafe在错误时返回NULL。驱动程序应该在每次试图使用返回的地址之前,先检查返回的是不是NULL。 直接I/O包括用户地址空间与系统地址缓冲之间的双向映射,这样两个不同的虚地址就有了同一个物理地址。双向映射会引起下列结果,有时它们会引起问题: 用户地址虚页的偏移量变成了系统页的偏移量。 超出系统缓冲边界的访问可能长时间没有被通知,这个时间取决于映射的页粒度。如果分配给调用者的缓冲距离页边界较远,那么写在缓冲边界外的数据仍然会出现在缓冲中,调用者不会意识到已经发生了错误。如果缓冲边界恰好与页边界重合,在边界之外的系统虚地址可能会指向其他内容或无效。这样的问题很难发现。 如果调用进程有修改的另一个线程,在用户内存映射改变时,系统缓冲的内容也会改变。 在这种情况下,使用系统缓冲来保存过期(scratch)数据会带来问题。两个从相同内存单元取数的操作可能会得到不同的数值。 下列代码段在一次直接I/O请求中接收到一个字符串,然后试图把这个字符串转换成大写的: PWCHAR PortName=NULL; PortName=(PWCHAR)MmGetSystemAddressFromMdl(irp->MdlAddress); // //Null-terminate the PortName so that RtlInitUnicodeString will not //be invalid. // PortName[Size/sizepf(WCHAR)-1]=UNICODE_NULL; RtlInitUnicodeString(&AdapterName,PortName); 因为缓冲可能没有所要求的正确格式,所以代码试图将Unicode的NULL强制作为最后一个缓冲字符。然而,如果下层物理内存同时被映射为用户模式和内核模式地址,一旦写操作完成,这个进程的另一个线程就会写覆盖掉缓冲。 相反地,如果没有NULL,那么对RtlInitUnicodeString的调用就会超出缓冲的范围。并且如果它处于系统映射之外的话,可能引起错误检测。 如果驱动程序创建并映射它自己的MDL,那么它应该保证它只是用自己已经验证过的方法来访问这个MDL。也就是说,当驱动程序调用MmProbeAndLockPages时,它指定一种访问方法(IoReadAccess、IoWriteAccess或IoModifyAccess)。如果驱动程序指定了IoReadAccess,它就决不能再试图通过MmGetSystemAddressForMdl或MmGetSystemAddressForMdlSafe向系统缓冲写。 调用者输入和设备状态的错误 驱动程序应该验证所有来自调用者的输入的有效性。因为驱动程序不能保证调用者传递了正确的数据类型或个数,也不能保证调用者只是在设备或驱动程序处于正确状态时才进行调用。 没有验证设备状态的有效性 下面例子的驱动程序在调试版(checked build)中使用了ASSERT宏检查设备是否处于正确状态,而在发行版(free build)中没有检查设备的状态: case IOCTL_WAIT_FOR_EVENT: ASSERT((!Extension->WaitEventIrp)); Extension->WaitEventIrp=Irp; IoMarkIrpPending(Irp); status=STATUS_PENDING; 在调试版中,如果驱动程序已经使IRP保持在未决状态,系统就会发通知(assert)。然而,在发行版中,驱动程序没有检查这种错误。对相同IOCTL的两个调用会使系统失去对IRP的跟踪。 在多处理器系统中,这个代码段可能会带来其他问题。假定这个例程在入口点有这个IRP的所有权(操纵的权利)。当例程把Irp指针保存在全局结构Extension->WaitEventIrp中时,另一个线程可以从这个全局结构中得到此IRP地址,然后对这个IRP进行操作。为了防止这种问题的发生,驱动程序应该在保存IRP之前将此IRP标记为未决,并且应该在互锁的队列中包括对IoMarkIrpPending的调用和分配。驱动程序可能还需要为此IRP建立Cancel例程,请参见第12章“Cancel例程”。 收到不明设备对象的意外I/O请求 很多驱动程序通过调用IoCreateDevice创建了一种以上的设备对象。有些驱动程序甚至在驱动程序创建FDO之前,在它们的DriverEntry例程中创建了控制设备对象,使得应用程序可以与驱动程序通信。例如,文件系统驱动程序在用IoRegisterFileSystem将自己注册为文件系统时,创建设备对象来处理文件系统通知。 驱动程序应该能够在它自己创建的任何设备上处理Create请求。在成功地完成了Create请求之后,它应该准备在创建的文件对象上接收任何用户可访问I/O请求。因此,创建了一个以上设备对象的驱动程序必须检查每个I/O请求指定的是哪个设备对象。 例如,驱动程序可能期望I/O请求为给定设备指定FDO,而实际上请求指定了它的控制设备对象。如果驱动程序没有像在其他设备对象中那样,在此控制设备对象的设备扩展中初始化相同的域,当尝试使用来自此控制设备对象的设备扩展信息时,驱动程序可能会崩溃。 没有验证句柄的有效性 有些驱动程序必须操纵调用者传给它们的对象,或必须在同一时间处理两个文件对象。例如,调制解调器驱动程序可能收到一个事件对象的句柄,或网络驱动程序可能收到两个不同文件对象的句柄。驱动程序必须验证这些句柄的有效性。因为它们是由调用者传来的,而不是通过I/O管理器,I/O管理器不能做任何有效性验证。 例如,在下面的代码段中,驱动程序被传递了句柄AscInfo->AddressHandle,却没有在调用ObReferenceObjectByHandle前验证它: // //This handle is embedded in a buffered request. // status=ObReferenceObjectByHandle( AscInfo->AddressHandle, 0, NULL, KernelMode, &fileObject, NULL); if(NT_SUCCESS(status)){ if((fileObject->DeviceObject= = DeviceObject)&& (fileObject->FsContext2= =TRANSPORT_SOCK)){ 虽然对的ObReferenceObjectByHandle调用成功了,代码却不能保证返回的指针引用了一个文件对象,它相信调用者传递了正确的信息。 即使调用ObReferenceObjectByHandle的所有参数都是正确的,而且调用成功了,如果文件对象不是驱动程序想要的,那么驱动程序仍然会得到无法预料的结果。在下面的代码段中,驱动程序假定成功调用返回了它想要的指向文件对象的指针: status=ObReferenceObjectByHandle( AscInfo->Handle, 0L, DesiredAccess, *IoFileObjectType, Irp->RequestorMode, (PVOID *)&AcpEndpointFileObject, NULL); if(!NT_SUCCESS(status)){ goto complete; } AcpEndpoint= AcpEndpointFileObject->FsContext; if(AcpEndpoint->Type!=BlockTypeEndpoint) 虽然ObReferenceObjectByHandle返回了指向文件对象的指针,驱动程序还是不能保证指针引用的是它想要的文件对象。在这种情况下,驱动程序应该在访问驱动程序指定的AcpEndpointFileObject->FsContext数据之前,验证这个指针的有效性。 为了预防这样的问题,驱动程序应该检查以得到有效的数据,如下所示: 检查对象类型以保证它是驱动程序想要的。 保证请求的访问适合于这种对象类型和所需的任务。例如,如果驱动程序执行快速文件复制,就要保证句柄可以做读访问。 一定要指定正确的访问模式(UserMode或KernelMode),要保证这种访问模式与请求的访问兼容。 如果驱动程序需要它自己创建的文件对象的句柄,按照设备对象或驱动程序验证此句柄的有效性。然而,注意不要破坏了想不明设备发送I/O请求的过滤器。 如果驱动程序支持多种类型的文件对象(如控制通道;地址对象;TDI驱动程序或文件系统的Volume、Directory、File对象的连接),要保证能区分它们。 Dispatch例程中的错误 有些驱动程序没有区分DispatchCleanup例程和DispatchClose例程中要求的任务。当文件对象的最后一个句柄被关闭时,I/O管理器调用DispatchCleanup例程。而当最后一个被从这个文件对象中释放时,调用DispatchClose例程。驱动程序不应该试图在它的DispatchCleanup例程中释放资源,因为它们正被连接在一个文件对象上,可能被其他Dispatch例程使用。 调用发送例程时,I/O管理器为正常I/O调用保持一个对文件对象的引用。结果,驱动程序可以在其DispatchCleanup例程被调用之后,而DispatchClose例程被调用之前,收到对文件对象的I/O请求。例如,在来自另一个线程的I/O管理器请求正被处理时,用户模式调用者可能关闭这个文件句柄。如果驱动程序在I/O管理器调用其DispatchClose例程之前,已经删除或释放了必需的资源,就会发生无效的指针引用或其他问题。 合并公共IOCTL和私有IOCTL路径 一般,驱动程序不应该有私有(内部)和公共IOCTL的合并执行路径。驱动程序无法仅凭IOCTL代码确定IOCTL来源于内核模式还是用户模式。因此以同一路径处理二者会使驱动程序的安全得不到保障。如果某个私有IOCTL是授权的,那么知道了这个IOCTL代码的未授权用户就可能能够访问它。因此,如果驱动程序创建了私有IOCTL,那么要保证将这些IOCTL与它必须支持的公共IOCTL分开处理。 多处理器环境中的错误 在Windows NT/Windows 2000系统中,驱动程序是多线程的,它们可以同时从不同的线程收到多个I/O请求。在设计驱动程序时,必须保证它能在SMP系统上运行,而且要采取适当的措施以确保数据完整性。 具体地说就是,无论何时驱动程序改变了全局或文件对象数据,它必须用锁或互锁队列防止竞态条件的产生。 引用全局或文件对象指定的数据时产生了竞态条件 在下面的代码段中,当驱动程序访问Data.LpcInfo中的全局数据时,会产生竞态条件: PLPC_INFO pLpcInfo=&Data.LpcInfo; //Pointer to global data … … //This saved pointer may be overwritten by another thread, pLpcInfo->LpcPort.Buffer=ExAllocatePool( PagedPool, arg->PortName.Length); 作为IOCTL调用结果的多个进入此代码段的线程会使内存泄漏,因为指针被写覆盖了。为了避免这个问题,驱动程序应该在改变全局数据时,使用ExInterlockedXxx例程或某类锁。驱动程序的需求决定了锁的类型。细节请参见“使用自旋锁”、“调度者对象”和ExAcquireResourceLite。 下面的例子试图重新分配一个文件指定的缓冲(Endpoint->LocalAddress)来保存终点(endpoint)地址: Endpoint=FileObject->FsContext; if(Endpoint->LocalAddress!=NULL&& Endpoint->LocalAddressLength< ListenEndpoint->LocalAddressLength){ FREE_POOL(Endpoint->LocalAddress, LOCAL_ADDRESS_POOL_TAG ); Endpoint->LocalAddress=NULL; } if(Endpoint->LocalAddress= =NULL){ Endpoint->LocalAddress=ALLOCATE_POOL(NonFagedPool, ListenEndpoint->LocalAddressLength, LOCAL_ADDRESS_POOL_TAG); } 在这个例子中,竞态条件会在访问文件对象时发生。因为驱动程序没有持有任何锁,所以对同一文件对象的两个请求可以进入这个函数。结果可能是内存被释放、多个试图释放相同内存或内存泄漏。为了避免这些错误,两个if语句应该被放在一个自旋锁中。 处理IRP时的错误 下列是在驱动程序处理IRP时常会犯的错误: 丢失或完成两次的IRP 这些问题和忘记调用I/O管理器例程(如IoStartNextPacket)通常发生在错误处理路径上。快速检查驱动程序路径可以发现这样的问题。 错误地复制栈位置 将IRP下传到栈中时,使用标准函数IoSkipCurrentIrpStackLocation和IoCopyCurrentIrpStackLocationToNext,而不要编写驱动程序决定的代码来复制栈位置。使用这些标准例程能保证驱动程序没有复制位于其上的驱动程序的Cancel例程。 完成驱动程序没有处理的IRP时返回成功 驱动程序决不能为它没有处理的IRP返回STATUS_SUCCESS。例如,有些驱动程序尽管没有实现所要求的功能,仍以成功状态不正确地完成了查询IRP。这样做很容易使系统崩溃或瘫痪,特别是在像文件名查找这样的操作期间,如果I/O管理器或其他组件试图使用Dispatch例程没有初始化的数据的话。除非某个IRP在文档中有另外说明,否则驱动程序应该为它没有处理的所有IRP都返回STATUS_NOT_SUPPORTED。 使用撤消自旋锁时引起死锁 I/O管理器无论何时调用驱动程序的Cancel例程都持有撤消自旋锁。如果驱动程序的Cancel例程中获得了第二个自旋锁,就会发生死锁。当驱动程序内的另一个线程以相反顺序获得这个锁时,死锁也会发生。 第三卷 内核模式驱动程序设计指南 1 第一部分 一般内核模式 2 第1章 Windows 2000和WDM驱动程序 3 1.1 Windows 2000组件一览 3 1.2 Windows 2000中的驱动程序种类 4 1.3 Windows 2000和WDM驱动程序的设计目标 5 1.3.1 移植性 6 1.3.2 可配置性 6 1.3.3 永远抢占优先和永远中断 7 1.3.4 多处理器安全 8 1.3.5 基于对象 9 1.3.6 带有可复用IRP的包驱动I/O 10 1.3.7 支持异步I/O 11 第2章 分层的I/O、IRP和I/O对象 12 2.1 Windows 2000 I/O模型概述 12 2.2 终端用户I/O请求和Windows 2000文件对象 13 2.2.1 用户I/O请求的注意事项 14 2.3 IRP和驱动程序指定的I/O栈位置 14 2.3.1 IRP处理的注意事项 18 2.4 驱动程序对象和标准驱动程序例程 19 2.4.1 对象的不透明性 19 2.4.2 标准驱动程序对象入口点 20 2.4.3 其他标准驱动程序例程 21 2.4.4 标准驱动程序例程的注意事项 22 2.5 设备配置和分层驱动程序 23 2.5.1 样例设备和驱动程序配置 24 2.5.2 添加驱动程序的注意事项 24 2.6 设备对象和分层驱动程序 24 2.6.1 针对样本配置的设备对象 25 2.6.2 设备对象的注意事项 26 第3章 系统定义的对象和对驱动程序的支持 27 3.1 系统组成部分和分层内核模式驱动程序 28 3.1.1 操纵系统定义的对象的支持例程 28 3.1.2 存储系统定义的对象 30 3.2 设备对象和设备扩展 31 3.2.1 定义设备扩展 31 3.2.2 创建设备对象和设备扩展 32 3.2.3 初始化驱动程序特定的设备对象和设备扩展 33 3.2.4 设置用户缓冲区的访问 35 3.2.4.1 使用缓冲I/O 35 3.2.4.2 使用直接I/O 36 3.2.4.3 使用非直接也非缓冲的I/O 38 3.3 适配器对象和DMA 39 3.3.1 映射寄存器 40 3.3.2 获取适配器对象 41 3.3.3 拆分传输请求 42 3.3.4 使用系统DMA 43 3.3.4.1 基于包的系统DMA 43 3.3.4.1.1 为基于包的DMA分配适配器通道 44 3.3.4.1.2 为基于包的DMA设置系统DMA控制器 45 3.3.4.2 公用缓冲区系统DMA 47 3.3.4.2.1 为公用缓冲区系统DMA分配适配器通道 48 3.3.4.2.2 为公用缓冲区DMA设置系统DMA控制器 49 3.3.5 使用总线控制器DMA 49 3.3.5.1 基于包的总线控制器DMA 50 3.3.5.1.1 分配总线控制器适配器对象 51 3.3.5.1.2 设置传输操作 52 3.3.5.2 公用缓冲区的总线控制器DMA 54 3.3.6 执行发散/收集DMA 55 3.4 控制器对象 56 3.4.1 创建带有控制器扩展的控制器对象 57 3.4.2 I/O操作分配控制器对象 58 3.5 中断对象 59 3.5.1 获得系统指定的中断向量、DIRQL和处理器掩码 60 3.5.2 注册ISR 60 3.6 DPC对象 61 3.6.1 注册和排队DpcForIsr例程 62 3.6.2 注册和排队CustomDpc例程 63 3.7 带有相关的DPC的定时器对象 64 3.7.1 注册和启用IoTimer例程 64 3.7.2 注册和排队CustomTimerDpc例程 65 3.8 设备队列对象和互锁队列 66 3.8.1 设置设备队列对象和排队IRP 67 3.8.2 设置一个互锁队列和排队IRP 68 3.9 内核调度者对象 69 3.9.1 定时器对象 71 3.9.2 事件对象 72 3.9.3 信号量对象 74 3.9.4 互斥体对象 75 3.9.5 线程在调度者对象上等待的报警和APC的处理 78 3.10 回调对象 79 3.10.1 定义回调对象 79 3.10.2 使用驱动程序定义的回调对象 80 3.10.3 使用系统定义的回调对象 80 第4章 驱动程序基本结构 82 4.1 标准驱动程序例程 82 4.2 最低层设备驱动程序的分段IRP处理 83 4.3 中间层驱动程序的分段IRP处理 86 4.4 设计和开发一个驱动程序 88 4.4.1 设备对象命名和设备接口注册 88 4.4.2 为驱动程序例程选择名字 88 4.4.3 开始设计 89 4.4.4 开始开发 91 第5章 DriverEntry 和 Reinitialize 例程 93 5.1 DriverEntry例程需求 93 5.1.1 DriverEntry例程语法与参数 93 5.1.2 DriverEnty返回值 94 5.2 DriverEntry例程功能 94 5.2.1 DriverEntry例程必备功能 95 5.2.2 DrierEntry可选功能 95 5.3 Reinitialize例程需求与功能 96 第6章 Dispatch例程 98 6.1 Dispatch例程需求 98 6.1.1 Dispatch例程一般实现 99 6.1.2 Dispatch例程条件实现 100 6.2 Dispatch例程基本功能 101 6.2.1 何时检查I/O栈位置 101 6.2.2 如何在一个Dispatch例程中完成一个IRP 102 6.2.3 何时在Dispatch例程中完成一个IRP 103 6.2.4 如何从一个Dispatch例程传送带有效参数的IRP 104 6.2.5 为较低层驱动程序分配IRP 105 6.2.6 处理异步传输请求 105 6.3 Dispatch例程设计 106 6.3.1 DispatchCreate和DispatchClose功能 106 6.3.1.1 基本DispatchCreateClose例程 107 6.3.1.2 实现DispatchCreateClose的注意事项 107 6.3.2 DispatchCleanup功能 108 6.3.3 DispatchRead或DispatchWrite功能 109 6.3.3.1 使用缓冲I/O的DispatchReadWrite 110 6.3.3.2 使用直接I/O的DispatchReadWrite 111 6.3.3.3 较高层驱动程序的DispatchReadWrite 111 6.3.3.4 实现DispatchReadWrite的注意事项 112 6.3.4 Dispatch(Internal)DeviceControl功能 113 6.3.4.1 最低层驱动程序中的DispatchDeviceControl 114 6.3.4.2 较高层驱动程序中的DispatchDeviceControl 114 6.3.4.3 类/端口驱动程序中的Dispatc(Internal)DeviceControl 115 6.3.4.4 实现Dispatch(Internal)DeviceControl的注意事项 116 第7章 StartIo和队列管理例程 118 7.1 排队IRP 118 7.2 StarIo例程需求 119 7.2.1 设备驱动程序中的StartIo例程 120 7.2.2 StartIo例程功能 121 7.2.2.1 使用缓冲I/O的设备驱动程序StartIo例程 122 7.2.2.2 使用直接I/O的设备驱动程序StartIo例程 122 7.2.2.3 在StartIo中处理设备控制请求 123 7.2.3 I/O操作设备编程 123 7.2.4 较高层驱动程序中的StartIo例程 124 7.2.5 实现StartIo的注意事项 124 7.3 使用互锁队列或设备队列的需求 125 7.3.1 驱动程序管理的队列的存储需求 126 7.3.2 建立驱动程序管理的队列 126 7.3.3 驱动程序管理的队列中排队的IRP 126 7.3.4 用驱动程序创建的线程管理互锁队列 127 7.3.5 管理补充设备队列 128 第8章 中断服务例程 131 8.1 ISR需求 131 8.1.1 ISR性能 131 8.1.2 附加的需求的驱动程序例程 132 8.1.3 建立一个ISR 132 8.2 ISR基本功能 132 8.3 ISR重叠I/O操作功能 133 第9章 DpcForIsr例程和CustomDpc例程 135 9.1 DpcForIsr和CustomDpc例程需求 135 9.1.1 DpcForIsr例程与CustomDpc例程 135 9.1.2 DpcForIsr例程基本需求 136 9.1.3 CustomDpc例程基本需求 136 9.1.4 其他需要的驱动程序例程 137 9.1.5 建立DpcForIsr和CustomDpc例程 137 9.2 DpcForIsr和CustomDpc功能 138 9.2.1 使用缓冲I/O和满足设备控制IRP 139 9.2.2 直接I/O使用 139 9.2.3 实现一个DpcForIsr或CustomDpc例程的注意事项 140 第10章 SynchCritSection例程 142 10.1 SynchCritSection例程需求 142 10.1.1 使用SynchCritSection例程 142 10.1.2 运行SynchCritSection例程 143 10.1.3 SynchCritSection例程性能需求 143 10.2 SynchCritSection例程功能 143 10.2.1 为I/O操作对设备编程 143 10.2.2 维护有关中断驱动I/O操作的状态 144 第11章 AdapterControl和ControllerControl例程 146 11.1 驱动程序控制例程 146 11.1.1 驱动程序控制例程语法和参数 146 11.1.2 从DriverControl例程返回 147 11.1.3 设计有DriverControl例程驱动程序 147 11.1.4 存储需求 148 11.1.5 建立AdapterControl和ControllerControl例程 148 11.2 AdapterControl例程需求 149 11.3 ControllerControl例程需求 150 第12章 Cancel例程 151 12.1 Cancel例程需求 151 12.2 处理可撤消IRP 153 12.2.1 对传递IRP的驱动程序例程中可撤消IRP的处理 153 12.2.2 对处理请求的驱动程序例程中可撤消IRP的处理 154 12.2.3 使用系统撤消自旋锁 154 12.2.4 使用驱动程序提供的自旋锁和Cancel例程 155 12.3 Cancel例程功能 159 12.3.1 带有StartIo例程的驱动程序中的Cancel例程 159 12.3.2 没有StartIo例程的驱动程序中的Cancel例程 160 12.4 处理可撤消IRP时的注意事项 160 第13章 IoCompletion例程 164 13.1 使用IoCompletion例程 164 13.1.1 建立IoCompletion例程 164 13.1.2 调用IoCompletion例程 165 13.1.3 为低层驱动程序分配IRP 167 13.2 IoCompletion例程需要的功能 168 第14章 IoTimer和CustomTimerDpc例程 170 14.1 定时器例程需求 170 14.2 IoTimer例程功能 170 14.2.1 建立IoTimer例程 171 14.2.2 为IoTimer例程建立环境 171 14.2.3 使用IoTimer例程 171 14.3 CustomTimerDpc例程功能 173 14.3.1 设置CustomTimerDpc例程 173 14.3.2 为CustomTimerDpc例程设置环境 174 14.3.3 使用CustomTimerDpc例程 174 第15章 Unload例程 177 15.1 Unload例程需求 177 15.2 Unload例程环境 177 15.3 Unload例程功能 178 15.3.1 PnP驱动程序的Unload功能 178 15.3.2 非PnP驱动程序的Unload功能 179 15.3.2.1 释放分配给驱动程序的资源 179 15.3.2.2 释放设备和控制器对象 180 第16章 常见的驱动程序设计问题 182 16.1 管理硬件优先级 183 16.2 使用自旋锁 185 16.2.1 为自旋锁和被保护数据提供存储空间 186 16.2.2 初始化自旋锁 187 16.2.3 调用使用了自旋锁的支持例程 187 16.2.4 快速释放自旋锁 188 16.2.5 使用自旋锁时防止错误或死锁的出现 189 16.3 轮询设备 190 16.3.1 驱动程序线程 191 16.4 管理内存的使用 191 16.4.1 使用系统内存 192 16.4.1.1 访问用户空间内存的驱动程序 192 16.4.1.2 为部分传输请求建立MDL 192 16.4.1.3 分配系统空间内存 193 16.4.1.4 将总线相关的内存空间地址重新映射为虚地址 195 16.4.2 使用内核栈 196 16.4.3 使用后备列表 196 16.4.4 只读内存保护 197 16.5 为DMA和PIO维护缓存的一致性 198 16.5.1 在DMA操作期间刷新缓存数据 198 16.5.2 在PIO操作期间刷新缓存数据 199 16.6 错误记录和NTSTATUS值 199 16.6.1 调用IoAllocateErrorLogEntry 200 16.6.2 填充错误记录包 200 16.6.3 设置错误记录包中的NTSTATUS值 202 16.6.4 调用IoWriteErrorLogEntry 202 16.6.5 定义新的IO_ERR_XXX 203 16.6.6 定义私有NTSTATUS常量 203 16.7 处理可删除存储介质 204 16.7.1 响应来自文件系统的验证请求 204 16.7.2 通知文件系统可能的存储介质改变 205 16.7.3 检查设备对象中的标志 205 16.7.4 在中间层驱动程序中建立IRP 206 16.8 使设备对应用程序和驱动程序可用 206 16.8.1 注册设备接口 207 16.8.2 使设备接口可用和不可用 207 16.8.3 使用设备接口 208 16.9 可分页代码和数据 209 16.9.1 使驱动程序代码可分页 209 16.9.2 锁住可分页代码或数据 211 16.9.3 对整个驱动程序分页 213 16.10 常见的驱动程序可靠性问题 214 16.10.1 缓冲I/O中的错误 214 16.10.2 引用用户空间地址时的错误 217 16.10.3 直接I/O中的错误 219 16.10.4 调用者输入和设备状态的错误 220 16.10.5 Dispatch例程中的错误 222 16.10.6 多处理器环境中的错误 222 16.10.7 处理IRP时的错误 223