ai3000
驱动牛犊
驱动牛犊
  • 注册日期2007-01-08
  • 最后登录2011-01-18
  • 粉丝2
  • 关注1
  • 积分10分
  • 威望140点
  • 贡献值1点
  • 好评度75点
  • 原创分1分
  • 专家分0分
阅读:1577回复:4

常见的驱动程序设计问题

楼主#
更多 发布于:2007-01-26 22:38
  这一章介绍了所有驱动程序开发者都会感兴趣的一些内容,主要包括以下几部分:

§           总结了标准驱动程序例程运行的缺省硬件优先级(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

最新喜欢:

TOMG2004TOMG20...
magichere
驱动小牛
驱动小牛
  • 注册日期2007-01-24
  • 最后登录2008-05-07
  • 粉丝0
  • 关注0
  • 积分1000分
  • 威望137点
  • 贡献值0点
  • 好评度136点
  • 原创分0分
  • 专家分0分
沙发#
发布于:2007-01-28 15:30
顶!
创造美好的未来生活!!!
xiabl
驱动牛犊
驱动牛犊
  • 注册日期2005-10-24
  • 最后登录2010-05-20
  • 粉丝0
  • 关注0
  • 积分221分
  • 威望77点
  • 贡献值0点
  • 好评度71点
  • 原创分0分
  • 专家分0分
板凳#
发布于:2007-01-29 18:26
collection!
小桥流水人家
firabc
驱动牛犊
驱动牛犊
  • 注册日期2004-10-10
  • 最后登录2007-10-20
  • 粉丝0
  • 关注0
  • 积分410分
  • 威望42点
  • 贡献值0点
  • 好评度42点
  • 原创分0分
  • 专家分0分
地板#
发布于:2007-01-29 19:45
支持
magichere
驱动小牛
驱动小牛
  • 注册日期2007-01-24
  • 最后登录2008-05-07
  • 粉丝0
  • 关注0
  • 积分1000分
  • 威望137点
  • 贡献值0点
  • 好评度136点
  • 原创分0分
  • 专家分0分
地下室#
发布于:2007-01-30 13:30
turecrypt 怎么才能做到 加密,保留原来的数据!????
创造美好的未来生活!!!
游客

返回顶部