常见的驱动程序设计问题[转]

常见的驱动程序设计问题 这一章介绍了所有驱动程序开发者都会感兴趣的一些内容,主要包括以下几部分: § 总结了标准驱动程序例程运行的缺省硬件优先级(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时的错误 1.1 管理硬件优先级 特定设备或中间层驱动程序例程运行的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参见“使用自旋锁”部分和例程相应的参考部分。 1.2 使用自旋锁 自旋锁是由内核定义的内核模式仅有(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使用自旋锁时防止错误或死锁的出现 1.2.1 为自旋锁和被保护数据提供存储空间 作为设备启动工作的一部分,驱动程序必须在下列各处之一为所有自旋锁保护的数据或资源以及相应的自旋锁分配常驻存储空间: § 驱动程序通过调用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,那么它还必须为与其中断对象相关的另一个自旋锁提供存储空间。 1.2.2 初始化自旋锁 在调用需要访问调用者提供的执行自旋锁的支持例程之前,驱动程序必须调用KeInitializeSpinLock来初始化相应的执行自旋锁。需要初始化执行自旋锁的支持例程如下: § KeAcquireSpinLock和随后的KeRelaeseSpinLock § KeAcquireSpinLockAtDpcLevel和随后的KeRelaeseSpinLockFromDpcLevel § ExInterlockedXxx例程 在调用IoConnectInterrupt和KeSynchronizeExecution之前,最低层驱动程序必须调用KeInitializeSpinLock以初始化由它提供存储空间的中断自旋锁。 1.2.3 调用使用了自旋锁的支持例程 调用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。 1.2.4 快速释放自旋锁 将驱动程序持有自旋锁的时间最小化可以很明显地改善驱动程序和系统整体的性能。例如,图16.1表示了中断自旋锁怎样保护SMP机器上必须被ISR和StartIo及DpcForIsr例程共享的设备确定的数据。 图16.1 使用中断自旋锁 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中断、时钟中断和电源错误中断仍可以在两个处理器中的任一个上发生。 2. 当ISR 将驱动程序的DpcForIsr排队并返回时,第二个处理器上的AccessDevice运行在等于相关中断对象SynchronizeIrql值的 IRQL上,而且访问了SynchronizeContext。同时,另一个处理器上的DpcForIsr运行在DISPATCH_LEVEL。 DpcForIsr也已准备好访问SynchronizeContext,因此它调用KeSynchronizeExecution,调用参数与步骤1中StartIo例程的参数相同。 当KeSynchronizeExecution获得自旋锁并代表StartIo 例程运行AccessDevice时,驱动程序提供的同步例程AccessDevice可以排他地访问SynchronizeContext。因为 AccessDevice运行在SynchronizeIrql值指定的IRQL上,所以驱动程序的ISR直到自旋锁被释放时,才能获得此自旋锁并访问相同的存储区,否则,即使AccessDevice正在运行时另一个处理器上发生了设备中断也不行。 3. 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例程会动态改变它的值。 1.2.5 使用自旋锁时防止错误或死锁的出现 驱动程序持有自旋锁时,只要它引起了硬件或软件异常,系统性能就会下降。这也就是说,驱动程序的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机器上,很难充分地调试并检测嵌套自旋锁。此外,使用嵌套自旋锁会极大地降低驱动程序和系统的性能。 1.3 轮询设备 除非不得已,否则设备驱动程序应该尽量避免轮询(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毫秒,可以考虑在驱动程序中建立一个设备专用线程。 1.3.1 驱动程序线程 慢速设备或很少使用设备(如软盘控制器)的驱动程序可以通过创建一个设备专用的系统线程来解决很多等待问题。类似的,大多数文件系统驱动程序使用系统工作者线程,并提供工作者线程回调例程。线程可以调用KeDelayExecutionThread等待完整时间片长度或更长时间的间隔。 KeDelayExecutionThread等待时间间隔的单位大约是10毫秒。因为KeDelayExecutionThread是定时器驱动的例程,其等待间隔的单位会比10毫秒稍快或稍慢些,这取决于操作系统平台。然而,对此例程的调用是可移植的,因为指定的时间增量是常量。 如果设备驱动程序有自己的线程环境或运行于系统线程环境中,设备专用线程或最高层驱动程序的工作者线程回调例程,可以在驱动程序设备扩展的共享通信区中,同步Kernel定义的调度者对象(如事件或信号量)上的同步操作。当其设备没有使用时,设备专用线程可以在共享调度者对象上等待,例如通过用信号量调用KeWaitForSingleObject来等待。在调用这种设备驱动程序来执行I/O操作并将信号量设为Signaled状态之前,它的等待线程不占用CPU时间。 驱动程序可以通过调用KeSetBasePriorityThread来设置它用PsCreateSystemThread创建的驱动程序专用或设备专用线程的基优先级(base priority)。驱动程序应该将优先级指定为能避免在SMP机上运行时优先级倒置(runtime priority inversion)的值。将驱动程序创建的线程的基优先级设得过高,会延迟提交I/O请求给驱动程序的低优先级线程的执行。 1.4 管理内存的使用 许多驱动程序只是将分配给其设备对象的设备扩展的内存用作其全局存储区;只是将其在IRP中的I/O栈用作操作指定的本地存储区。然而,驱动程序可以按需要分配额外的系统空间内存,而且可以用内核栈在调用内部驱动程序例程时传递少量的数据。 1.4.1 使用系统内存 图16.2表示Windows NT/Windows 2000虚内存空间及它们与系统物理内存的关系。 图16.2 虚内存空间和物理内存 如图16.2所示,虚内存实际对应的是分页的物理内存,虚地址范围实际对应的是CPU中不邻接的页。用户空间虚内存和从页式存储池中分配的系统空间内存总是可分页的。也就是说,任何非当前处理及其数据都可以分页到辅助存储区中去,通常是磁盘上。 图16.2中的高位空间(hyperspace)是系统空间地址的专用区,内存管理器用它将当前处理的虚地址空间映射为CPU中的一系列物理页。注意:任何非当前处理的虚地址都是不可见的,因此它的内存空间是不可访问的。 1.4.1.1 访问用户空间内存的驱动程序 驱动程序不能分配用户空间的虚内存,因为它们运行在内核模式。此外,驱动程序不能通过用户模式的虚地址访问内存,除非它正运行在引起驱动程序当前I/O操作的用户模式线程环境中而且它正在使用此线程的虚地址。 只有最高层驱动程序(如FSD)可以保证它们的Dispatch例程会在这样的用户模式线程环境中被调用。最高层驱动程序可以在为低层驱动程序建立IRP之前调用MmProbeAdnLockPages以锁住(lock down)用户缓冲。 最低层驱动程序和为缓冲或直接I/O建立设备对象的中间层驱动程序,可以依赖I/O管理器或最高层驱动程序,来在IRP中传递对被锁用户缓冲或系统空间缓冲的合法访问。 1.4.1.2 为部分传输请求建立MDL 如果传输请求太大以致于下层设备驱动程序无法处理,那么高层驱动程序可以调用IoBuildPartialMdl,为下层设备驱动程序建立部分传输IRP队列。 如果最高层驱动程序不能在一台内存有限的计算机上用MmProbeAndLockPages锁住整个用户缓冲,初始请求也必须被分割成部分传输。对这种大传输请求,最高层驱动程序不能做下列事情: 1. 调用IoBuildSynchronousFsdRequest来分配部分传输IRP并锁住用户缓冲的一部分。通常加锁区的大小要么是PAGESIZE的倍数,要么是下层设备的传输容量。 2. 如果低层驱动程序返回STATUS_PENDING,就用部分传输IRP调用IoCallDriver,并调用KeWaitForSingleObject,以等待驱动程序建立与其部分传输IRP相关的事件对象。 3. 当它重新获得控制时,重复步骤1和2,直到所有数据都被传输为止,然后完成初始IRP。 必须处理很大传输请求的最高层设备驱动程序可以使用前述技术,简单地用它分配的部分传输IRP调用它自己。除此之外,还有另一种方案,其中最高层设备驱动程序要做下列事情: 1. 调用IoAllocateMdl来分配描述用户缓冲的一部分MDL。 2. 调用MmProbeAndLockPages来锁住这部分用户缓冲。 3. 给这部分用户缓冲传输数据。 4. 调用MmUnlockPages,做下列事情之一: § 如果驱动程序在步骤1中分配的MDL非常大,足够下次传输,调用MmPrepareMdlForReuse并重复步骤2到4。 § 否则,调用IoFreeMdl并重复步骤1到4。 5. 所有数据都被传输后,调用MmUnlockPages和IoFreeMdl。 1.4.1.3 分配系统空间内存 图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在处理器指定的边界(由处理器的数据缓存范围的大小决定)内分配内存以避免发生缓存及一致性问题。 1.4.1.4 将总线相关的内存空间地址重新映射为虚地址 有些处理器有独立的内存和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以确保能访问所有设备资源时,都应该采取的步骤: 1. 在设备扩展中复制IrpSp->Parameters.StartDevice.AllocatedResources。 2. 在设备扩展中复制IrpSp->Parameters.StartDevice.AllocatedResourcesTranslated。 3. 在循环里,检查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读写例程)。 1.4.2 使用内核栈 当驱动程序可以向其内部例程传递数据时,Windows 2000内核模式栈的大小约为两页。因此,驱动程序不能在内核栈上传送大量的数据。 为了避免用尽内核模式栈的空间,遵守以下设计规则: 避免从一个内部驱动程序例程中深度嵌套调用另一个,如果它们每个都要在内核栈上传送数据的话。 如果驱动程序设计中用到了递归例程,注意限制递归调用发生的次数。 也就是说,驱动程序的调用树结构应该比较平坦。由于非页式存储池也是有限的系统资源,因此驱动程序最好分配系统空间缓冲,而不是用尽内核栈空间。 Windows 2000内核模式栈是在缓存中,因此驱动程序不能用DMA在栈上传送数据。 为了避免DMA数据分配和/或数据完整性问题,遵守以下设计规则: 决不要试图用DMA在内核栈上传送数据。 DMA设备的驱动程序可以通过调用ExAllocatePool 或ExAllocatePoolWithTag获得一个NonPagedPoolCacheAligned类型的缓冲,从而缓冲要被传输的数据(如果有的话)。有些驱动程序可以通过使用公用缓冲DMA来做到这些。参见第3章中的“公用缓冲系统DMA或公共总线控制器DMA”。 1.4.3 使用后备列表 必须动态分配固定大小的缓冲以执行要求的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的下次调用,就几乎不可能导致为另一个表项显式分配附加内存所引起的性能恶化了。 1.4.4 只读内存保护 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.”; 这样就能保证此字符串被放在可写内存中。 1.5 为DMA和PIO维护缓存的一致性 在Windows NT/Windows 2000计算机上,当驱动程序在系统内存和它的设备之间传送数据时,数据可以被缓存一个或多个处理器缓存中和/或系统DMA控制器的缓存中。使用DMA或 PIO来为读/写IRP或任何需要DMA或PIO数据传送操作的设备I/O控制请求服务的驱动程序,应该保证传送操作期间可能缓存数据的完整性。有关这些内容将在以下几个小节中阐明。 1.5.1 在DMA操作期间刷新缓存数据 在有些平台上,处理器和系统DMA控制器(或总线控制器DMA适配器)表现出缓存一致性异常。 为了在DMA操作期间保持数据完整性,最低层驱动程序必须遵照下列规则: 1. 在传送操作之前调用KeFlushIoBuffers,以保持可能被缓存在处理器中的数据和内存中数据之间的一致性。 如果驱动程序用值为TRUE的参数CacheEnabled调用AllocateCommonBuffer,驱动程序必须在向/从其缓冲进行传送操作之前调用KeFlushIoBuffers。 2. 在每次设备传送操作完成时,调用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状态块。 1.5.2 在PIO操作期间刷新缓存数据 在有些平台上,处理器的指令和数据缓存在PIO读操作期间表现出缓存一致性异常。 为了在它们的读操作期间保持数据完整性,使用PIO的驱动程序必须遵守下列规则: § 在每次读操作完成后调用KeFlushIoBuffers。 § 例如,从设备到系统内存做PIO传送的驱动程序应该在每次设备传送操作完成后调用KeFlushIoBuffers。又比如,将一类设备寄存器读入系统内存的驱动程序应该在读完每个类后调用KeFlushIoBuffers。否则在有些平台上,,这样的驱动程序可能会试图访问仍在处理器数据缓存中的数据,而不是系统内存中的。 如果处理器和DMA控制器可以自动保持缓存的一致性,就不需要使用KeFlushIoBuffers,因此在这种平台上调用此支持例程几乎没有任何开销。 1.6 错误记录和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用户、系统管理员或技术支持人员可以用它来监视给定计算机上的硬件状态;如果需要的话,更换故障硬件;调整设备配置以获得更好的性能;如果发生硬件问题,调试这些问题。 1.6.1 调用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,驱动程序就不需要记录错误。它应该只是继续运行,并保证如果同样的错误再次发生的话,能在那时记录下来。 1.6.2 填充错误记录包 错误记录包定义如下: 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处开始。 1.6.3 设置错误记录包中的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常量才能被添加到系统。 1.6.4 调用IoWriteErrorLogEntry 当驱动程序已经向IoAllocateErrorLogEntry返回的包中填充了数据以后,它必须调用IoWriteErrorLogEntry,这样错误记录线程就可以在错误记录文件中写它的表项了。 只要可能,驱动程序都应该记录错误;如果需要的话,拒绝IRP;在遇到异常或未料到的I/O错误时,继续运行。 正在销售的驱动程序决不应该调用KeBugCheckEx(或KeBugCheck)使系统终止。然而,可以用KeBugCheckEx来调试正在开发的驱动程序。 1.6.5 定义新的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事件查看器终止用户,驱动程序必须在注册表中将它本身作为错误记录组件来建立。 1.6.6 定义私有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部分”。 1.7 处理可删除存储介质 文件系统和可删除存储介质设备驱动程序必须:保证当文件在可删除存储介质设备上打开时,正确的介质已被安装;并且保证在访问介质的操作期间,正确的介质始终被安装着。位于文件系统和可删除存储介质设备驱动程序之间的中间层驱动程序也必须负责保证这一点。 因此,可删除存储介质设备的驱动程序应该能做到下列中的至少一件事情: 响应来自文件系统的验证请求(check-verify request) 通知文件系统可能的存储介质改变 检查DeviceObject->Flags 建立中间层驱动程序的IRP 1.7.1 响应来自文件系统的验证请求 文件系统可以随时向设备驱动程序的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),驱动程序应该做下列事情: 1. 将DeviceObject中的Flags与DO_VERIFY_VOLUME进行“或”运算,得到新的Flags值。 2. 将IRP中的IoStatus块设为下列值: § 将Status设为STATUS_VERIFY_REQUIRED § 将Information设为0 3. 用输入IRP调用IoCompleteRequest。 如果没有安装存储介质,驱动程序决不能将DO_VERIFY_VOLUME置为1。驱动程序应该将IoStatus. Status设为STATUS_IO_DEVICE_ERROR,将IoStatus. Information设为0,然后用输入IRP调用IoCompleteRequest。 1.7.2 通知文件系统可能的存储介质改变 可删除存储介质设备驱动程序必须保证:驱动程序处理请求向/从存储介质发送的IRP或驱动程序处理会影响到存储介质的设备I/O控制操作时,DeviceObject(每个被发送IRP的驱动程序例程的输入)代表的设备的存储介质没有改变。如果物理设备总是向驱动程序通告状态变化,那么对改变介质最好的检查时间就是在从无介质存在状态向介质存在状态转化之后。 如果在驱动程序开始I/O操作之前或操作期间,物理设备指出存储介质的状态可能已经改变了,驱动程序就必须做下列事情: 1. 通过检查VPB中的VPB_MOUNTED标志确保安装了存储介质。(如果没有安装存储介质,驱动程序决不能将DO_VERIFY_VOLUME置为1。驱动程序应该将IoStatus. Status设为STATUS_IO_DEVICE_ERROR,将IoStatus. Information设为0,然后用此IRP调用IoCompleteRequest。) 2. 将DeviceObject中的Flags与DO_VERIFY_VOLUME进行“或”运算,得到新的Flags值。 3. 将IRP中的IoStatus块设为下列值: § 将Status设为STATUS_VERIFY_REQUIRED § 将Information设为0 4. 在完成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就可以向用户发送提供了正确存储介质的弹出菜单;或重试初始请求;或撤消请求的操作。 1.7.3 检查设备对象中的标志 对每个请求向/从可删除存储介质进行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,那么文件系统既不能保持缓存文件数据的完整性,又不能提示用户重新安装这个已经打开了文件的存储介质。 1.7.4 在中间层驱动程序中建立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。 否则,文件系统既不能保持缓存文件数据的完整性,又不能提示用户重新安装这个已经打开了文件的存储介质。 1.8 使设备对应用程序和驱动程序可用 对于任何用户模式代码可以直接递交I/O请求的设备,无论它是物理的、逻辑的或虚拟的,其驱动程序都必须为它的用户模式客户提供某种名字。用这个名字,用户模式应用程序(或其他系统组件)就可以识别出请求I/O的那个设备。 在以前的操作系统版本中,驱动程序命名它们的设备对象,并在注册表中为这些名字和用户可见的Win32逻辑名建立符号连接。 然而,Windows 2000和WDM驱动程序并不命名设备对象,而是为每个设备对象注册并使一个用户模式I/O请求可被发送到的设备接口可用。设备接口是向其他系统组件(包括其他驱动程序和用户模式应用程序)输出设备和驱动程序功能的一种方式。 设备对象被划分成类,每一类都与一个GUID 相关。系统在设备指定的头文件中为公共设备接口类定义GUID。当驱动程序注册了一个设备接口,I/O管理器将其设备接口类GUID与一个符号连接名相关。连接名保存在注册表中,它在系统启动期间一直存在。使用此设备接口的应用程序可以查找它的符号连接名,并将它保存用作I/O请求的目标。 1.8.1 注册设备接口 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来注册附加的设备接口。 其他系统组件在驱动程序使设备接口可用之前,不能使用它。参见“使设备接口可用和不可用”。 1.8.2 使设备接口可用和不可用 在成功地启动了设备后,注册了接口的驱动程序调用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排队。 1.8.3 使用设备接口 设备接口可以被内核模式组件和用户模式应用程序使用。用户模式的代码必须用SetupDiXxx函数找出注册了的、可用的设备接口。参见在线DDK中的“Device Interface Functions(设备接口函数)”。 在内核模式组件可以使用指定的设备或文件对象之前,它必须做下列事情: 1. 确定所需的设备接口是否是注册过并可用的。 驱动程序在注册表中注册时,可以配置成能够通知PnP管理器何时设备接口可用或不可用。为了注册,组件调用IoRegisterPlugPlayNotification。无论何时为指定设备类将设备接口设置成可用或不可用,IoRegisterPlugPlayNotification例程都会调用驱动程序提供的回调函数。参见《即插即用、电源管理和安装设计指南》第4章中的“使用PnP设备接口改变通知”。 驱动程序或其他内核模式组件也可以调用IoGetDeviceInterface来获得指定设备接口类的所有注册过并可用的设备接口的列表。返回的列表中有指向标识设备接口的Unicode字符串的指针。 2. 获得代表所需设备接口类的符号连接的Unicode字符串。 IoGetDeviceInterface返回指向SymbolicLinkList参数中字符串的指针。如果驱动程序注册为可通知PnP管理器,上述例程的回调(callback)可以从DEVICE_INTERFACE_CHANGE_NOTIFICATION结构中取回这个字符串。 3. 用这个Unicode字符串得到指向相应设备或文件对象的指针。 为了访问指定的设备对象,驱动程序必须调用IoGetDeviceObjectPointer,ObjectName参数中放的是所需接口的Unicode字符串。为了访问文件对象,驱动程序必须用ObjectName参数传递这个Unicode字符串,调用InitializeObjectAttributes,然后调用ZwCreateFile传递成功初始化了的属性结构。 1.9 可分页代码和数据 用户可以使某些驱动程序的全部或部分可分页。对驱动程序代码分页减小了驱动程序加载映像的大小,从而为其他应用节省了系统空间。这对突发使用(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 对整个驱动程序分页 1.9.1 使驱动程序代码可分页 为了使驱动程序例程可分页,必须保证它运行在低于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,而且发送者数据库被锁。 1.9.2 锁住可分页代码或数据 非页式代码是内存常驻的,它运行在等于或高于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减少了每个驱动程序加载映像的大小,但同时影响了驱动程序和系统性能。一般,对系统性能有决定性影响的驱动程序(如键盘和鼠标驱动程序)和经常使用设备的驱动程序(如磁盘驱动程序)不应该有可分页段。不值得以性能损失为代价换来驱动程序映像大小暂时的减少。 1.9.3 对整个驱动程序分页 使用MmLockPagableXxx支持例程并指定分页和可丢弃段的驱动程序,是由非页式段、页式段和驱动程序初始化后就丢弃的INIT段组成的。 在设备驱动程序为它管理的设备连接了中断之后,驱动程序的中断处理路径必须常驻在系统空间。中断处理代码必须是页不能被调出的驱动程序段的一部分,以防中断发生。 两个附加的内存管理器例程MmPageEntireDriver和MmResetDriverPaging可用来设置组成驱动程序映像的所有段的可分页或不可分页属性。这些例程允许驱动程序在其设备没有被使用和不能产生中断时整个被调出。 完全可分页系统驱动程序的例子有win32.sys驱动程序、串行驱动程序、信件槽(mailslot)驱动程序、蜂鸣器(beep)驱动程序和零位驱动程序(null driver)。 典型的串行驱动程序是被间歇使用的。串行驱动程序可以整个被调出,直到它管理的端口打开为止。端口只要一打开,串行驱动程序中必须常驻内存的那部分就必须被放入非页式系统空间。驱动程序的其他部分可以仍是可分页的。 能整个被调出的驱动程序应该在中断被连接之前的驱动程序初始化期间调用MmPageEntireDriver。PnP驱动程序不应该使用这个例程。 当被调出的驱动程序所管理的设备收到一个打开请求时,驱动程序页被调入。然后,驱动程序必须在连接中断之前调用MmResetDriverPaging。调用MmResetDriverPaging使内存管理器根据编译链接阶段获得的属性来处理驱动程序段。非页式段(如文本段)将被调入非页式系统内存,可分页段将在它们被引用时调入。 这样的驱动程序必须保存其设备打开句柄的引用记数值。每当有设备打开请求时,驱动程序增加这个记数值;每当有关闭请求时,驱动程序减少这个记数值。当此记数值为0时,驱动程序应当断开中断,然后调用MmPageEntireDriver。如果驱动程序管理的设备超过一个,在所有设备的记数值都为0以后,驱动程序才能调用MmPageEntireDriver。 驱动程序要做在改变引用记数值时同步所需的所有工作,并且在驱动程序可分页状态改变时,它要防止引用记数值也改变。也就是说,在SMP机上,驱动程序必须保证:当一个处理器上的打开调用导致中断被连接且引用记数值被增加时,另一个处理器上不能正在运行MmPageEntireDriver。 1.10 常见的驱动程序可靠性问题 以内核模式执行的驱动程序占用了大量代码基地址(base)。因此,要改善系统可靠性就必须考虑到这个庞大的代码基地址。 可用来保证驱动程序可靠性的最重要的工具是Driver Verifier。它可以检查许多常见的驱动程序问题,它们中的一部分将在这一节中讨论。 这一节为下列常见的驱动程序问题提出了预防策略: § 缓冲I/O § 用户空间地址 § 直接I/O § 输入数据和设备状态 § Dispatch例程 § 多处理器环境 § 处理IRP 对上述问题的讨论分布在以下各节,其中有很多节在讲述时附带了程序片段。这些小程序段说明了典型的错误以及怎样更正它们。(为了简明,已经对这些代码做了适当的修改) 1.10.1 缓冲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(InputBufferLengthAssociatedIrp.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(InputBufferLengthAssociatedIrp.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(inputBufferLengthNumEntries*sizeof(SET_VALUE_INFO); if(inputBufferLengthParameters.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的有效性一样。虽然嵌入指针使得驱动程序可以返回更多的信息,但是驱动程序可以通过使用有关的偏移量或可变长度缓冲来更有效地达到这个目的。 1.10.3 直接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向系统缓冲写。 1.10.4 调用者输入和设备状态的错误 驱动程序应该验证所有来自调用者的输入的有效性。因为驱动程序不能保证调用者传递了正确的数据类型或个数,也不能保证调用者只是在设备或驱动程序处于正确状态时才进行调用。 没有验证设备状态的有效性 下面例子的驱动程序在调试版(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对象的连接),要保证能区分它们。 1.10.5 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分开处理。 1.10.6 多处理器环境中的错误 在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语句应该被放在一个自旋锁中。 1.10.7 处理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例程中获得了第二个自旋锁,就会发生死锁。当驱动程序内的另一个线程以相反顺序获得这个锁时,死锁也会发生。

你可能感兴趣的:(System)