常见的驱动程序设计问题_第1页
常见的驱动程序设计问题_第2页
常见的驱动程序设计问题_第3页
常见的驱动程序设计问题_第4页
常见的驱动程序设计问题_第5页
已阅读5页,还剩15页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、常见的驱动程序设计问题 这一章介绍了所有驱动程序开发者都会感兴趣的一些内容,主要包括以下几部分:           总结了标准驱动程序例程运行的缺省硬件优先级(IRQL)以及在适当的IRQL上调用支持例程的一些策略           关于使用自旋锁的一般策略,这些自旋锁用来同步对驱动程序例程共享的数据或资源的访问      

2、     关于用内核栈和后备列表分配系统空间内存的一般策略。           驱动程序应该怎样处理I/O错误,以及NTSTATUS值是怎样定义的           怎样使所有或部分驱动程序映像可分页           怎样注册设备接口以使其他内核模式和

3、用户模式的代码可以访问设备           怎样避免会影响驱动程序可靠性的的常见问题这一章还讨论了设备类型决定或设计决定的设计问题,包括下列内容:           对最低层设备驱动程序,是驱动程序轮询设备,还是建立一个等待Kernel定义的调度者对象的线程,即是用时间还是用信号量         &#

4、160; 对于DMA或PIO驱动程序,怎样在传输操作期间维护缓存的一致性和数据的完整性           对于可删除存储介质设备(removable-media)的驱动程序,怎样处理用户引起的错误(如提供了错误的存储介质或移除了在其上有文件打开的存储介质)这一章的目录如下:16.1 管理硬件优先级16.2 使用自旋锁16.2.1 为自旋锁和被保护数据提供存储空间16.2.2 初始化自旋锁16.2.3 调用使用了自旋锁的支持例程16.2.4 快速释放自旋锁16.2.5 使用自旋锁时防止错误或死锁

5、的出现16.3 轮询设备16.4 管理内存的使用16.4.1 使用系统内存16.4.1.1 访问用户空间内存的驱动程序16.4.1.2 为部分传输请求建立MDL16.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 调用IoAllocateErrorLogEntry16.6.

6、2 填充错误记录包16.6.3 设置错误记录包中的NTSTATUS值16.6.4 调用IoWriteErrorLogEntry16.6.5  定义新的IO_ERR_XXX16.6.6 定义私有NTSTATUS常量16.7 处理可删除存储介质16.7.1 响应来自文件系统的验证(Check-Verify)请求16.7.2 通知文件系统可能的存储介质改变16.7.3 检查设备对象中的标志16.7.4 在中间层驱动程序中建立IRP16.8 使设备对应用程序和驱动程序可用16.8.1 注册设备接口16.8.2 使设备接口可用和不可用16.8.3 使用设备接口16.9 可分页代码和数据16.9

7、.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上。其他例程在调用者运行在提高的(

8、raised)IRQL(即高于PASSIVE_LEVEL的IRQL)时不能被安全地调用。表16.1列出了最常见的标准驱动程序例程被调用的缺省IRQL以及Kernel定义的IRQL值(由低到高)。 表16.1   驱动程序例程的缺省IRQLIRQL(由低到高) 屏蔽掉的中断 运行在此IRQL的支持例程 PASSIVE_LEVEL 无 Dispatch、DriverEntry、AddDevice、Reinitialize、Unload例程、驱动程序创建的线程、工作者线程(work-thread)回调、文件系统驱动程序 DISPATCH_LEVEL DISPATCH_LEVEL和

9、APC_LEVEL中断被屏蔽掉了。设备、时钟和电源错误中断仍可发生 StartIo、AdapterControl、AdapterListControl、ControllerControl、IoTimer、Cancel(持有撤消自旋锁时)、DpcForIsr、CustomTimerDpc、CustomDpc例程 DIRQL 驱动程序中断对象中所有IRQL<=DIRQL的中断。时钟和电源错误中断仍可发生 ISR、SyncCritSection例程 当运行在下列三种IRQL之一时,由最低层驱动程序处理IRP:       &

10、#160;   PASSIVE_LEVEL:没有处理器中断被屏蔽掉,在驱动程序的Dispatch例程中。DriverEntry、AddDevice、Reinitialize和Unload例程也运行在PASSIVE_LEVEL,此外还有驱动程序创建的系统线程           DISPATCH_LEVEL:处理器的DISPATCH_LEVEL和APC_LEVEL中断被屏蔽掉了,在StartIo例程中。AdapterControl、AdapterListControl、Cont

11、rollerControl、IoTimer、Cancel(持有撤消自旋锁时)、DpcForIsr、CustomTimerDpc和CustomDpc例程也都运行在DISPATCH_LEVEL。           Device IRQL(DIRQL):处理器上所有低于或等于驱动程序中断对象的SynchronizeIrql的中断都被屏蔽掉了,在ISR和SyncCritSection例程中。当运行在下列两种IRQL时,由更高层驱动程序处理IRP:    

12、0;      PASSIVE_LEVEL:没有处理器中断被屏蔽掉,在驱动程序的Dispatch例程中。DriverEntry、AddDevice、Reinitialize和Unload例程也运行在PASSIVE_LEVEL,此外还有驱动程序创建的系统线程、工作者线程回调或文件系统驱动程序。           DISPATCH_LEVEL:处理器的DISPATCH_LEVEL和APC_LEVEL中断被屏蔽掉了,在驱动程序的IoCompletio

13、n例程中。IoTimer、Cancel和CustomTimerDpc例程也都运行在DISPATCH_LEVEL。有时,海量存储设备的中间层和最低层驱动程序在等于APC_LEVEL的IRQL上被调用。特别是,这种情况会在文件系统驱动程序向低层驱动程序发送IRP_MJ_READ请求导致页错误时发生。大多数标准驱动程序例程运行在仅能使它们调用适当的支持例程的IRQL上。例如,当设备驱动程序运行在等于DISPATCH_LEVEL的IRQL上时,它必须调用AllocateAdapter或IoAllocateController。由于多数设备驱动程序从StartIo例程中调用这些例程,因此它们通常运行在D

14、ISPATCH_LEVEL。应注意的是,对于没有StartIo例程的设备驱动程序,因为它建立并管理自己的IRP队列,所以当它应该调用AllocateAdapter(或IoAllocateController)时,不一定非要运行在等于DISPATCH_LEVEL的IRQL上。这样的驱动程序必须在调用KeRaiseIrql和调用KeLowerIrql之间调用AllocateAdapter,于是当它调用AllocateAdapter时,就能运行在要求的IRQL上,而且当调用例程重新获得控制时,能够恢复初始IRQL。为了能在适当的IRQL调用支持例程并能在驱动程序中成功地管理硬件优先级,应当注意下列情

15、况:           用低于当前IRQL的输入NewIrql值调用KeRaiseIrql会导致一个致命错误。调用KeLowerIrql以期望恢复初始IRQL(也就是,在调用KeRaiseIrql之后)也会导致一个致命错误。           当运行在提高的IRQL上时,用Kernel定义的调度者对象调用KeWaitForSingleObject或KeWaitForMultipleObject

16、s以在非零时间段中等待会导致一个致命错误。只有运行在非任意线程和PASSIVE_LEVEL的驱动程序例程(如驱动程序创建的线程、DriverEntry例程和Reinitialize例程、或像大多数设备I/O控制请求那样的同步I/O操作的Dispatch例程)能在非零时间段中安全地等待时间、信号量、互斥体或定时器。           即使运行在PASSIVE_LEVEL上,可分页代码也决不能在输入Wait参数为TRUE的情况下,用它调用KeSetEvent、KeReleaseSemaphore或

17、KeReleaseMutex。这样的调用会导致一个致命的页错误。           运行在高于APC_LEVEL的IRQL上的例程既不能从页式存储池中分配内存,也不能安全地访问页式存储池中的内存。如果这样的例程引起了一个页错误,这个错误将是致命的。           当驱动程序调用KeAcquireSpinLockAtDpcLevel和KeRelaeseSpinLockFromDpcLevel

18、时,它必须运行在DISPATCH_LEVEL上。当驱动程序调用KeAcquireSpinLock时,它可以运行在低于DISPATCH_LEVEL的IRQL上,但是它必须通过调用KeRelaeseSpinLock释放这个自旋锁。也就是说,通过调用KeRelaeseSpinLockFromDpcLevel释放由调用KeAcquireSpinLock获得的自旋锁是编程错误。当驱动程序运行在高于DISPATCH_LEVEL的IRQL上时,它绝对不能调用KeAcquireSpinLockAtDpcLevel、KeRelaeseSpinLockFromDpcLevel、KeAcquireSpinLock或

19、KeRelaeseSpinLock。           如果调用者还没有运行在这些提高后的IRQL上,调用使用了自旋锁的支持例程(如ExInterlockedXxx)会将当前处理器上的IRQL提高到DISPATCH_LEVEL或DIRQL。           运行在提高IRQL上的驱动程序代码应该尽快执行。为了获得好的整体性能,例程运行的IRQL越高,就越应该将例程执行速度调得尽可能快。例如,

20、调用KeRaiseIrql的驱动程序应当尽快地做逆调用KeLowerIrql。请使用在线DDK参见“使用自旋锁”部分和例程相应的参考部分。1.2 使用自旋锁自旋锁是由内核定义的内核模式仅有(kernel-mode-only)的一种同步机制,它以一种不透明类型KSPIN_LOCK向外界输出。当在Windows NT/Windows 2000 SMP机器上同时执行并运行在提高IRQL上的例程同时访问共享数据或资源时,自旋锁用来保护这些共享数据或资源。包括驱动程序在内的许多组件(component)都使用了自旋锁。任何类型的驱动程序可能都要使用一个或多个执行自旋锁。例如,大多数文件系统在FSD的设备

21、扩展中使用一个互锁的工作队列,来保存由文件系统的工作者线程回调例程和FSD处理的IRP。互锁工作队列用执行自旋锁来保护,这个锁可以解决FSD中一个试图将IRP插入队列,而同时有其他线程要将IRP移出队列时所引起的问题。又如,系统软盘控制器驱动程序用两个执行自旋锁。一个保护与驱动程序设备专用线程共享的互锁工作队列,另一个用来保护三个驱动程序例程共享的定时器对象。每个有ISR的驱动程序都使用一个中断自旋锁来保护被其ISR和其SynchCritSection例程(通常在驱动程序的StartIo和DpcForIsr例程中调用它)共享的数据或硬件。中断自旋锁与驱动程序调用IoConnectInterru

22、pt时创建的中断对象集相关,在注册ISR部分对此有详尽的阐明。在驱动程序中使用自旋锁时,应遵守下列规则:           在常驻系统空间内存(非页式存储池,如图16.3所示)中,为自旋锁保护的所有数据或资源和相应的自旋锁提供存储空间。驱动程序必须为它使用的所有执行自旋锁提供存储空间。然而,设备驱动程序不需要为中断自旋锁提供存储空间,除非它有多重矢量(multivector)ISR或者有一个以上的ISR,在注册ISR部分对此有详尽的阐明。    

23、0;      在使用驱动程序提供存储空间的每个自旋锁,以同步对被保护的共享数据或资源的访问之前,先要调用KeInitializeSpinLock来初始化这些自旋锁。           在适当的IRQL上调用每个使用了自旋锁的支持例程。一般,对于执行自旋锁,IRQL<=DISPATCH_LEVEL;对于与驱动程序中断对象相关的中断自旋锁,IRQL<=DIRQL。      

24、     实现例程时,应使其在持有自旋锁时尽快地执行。所有例程持有自旋锁的时间都不应超过25毫秒。           实现例程时注意,当它持有自旋锁时一定要避免做下列事情:            引起硬件异常或软件异常           

25、; 试图访问可分页内存            做可能引起死锁或自旋锁持有时间超过25毫秒的递归调用            试图获得另一个自旋锁(这样做可能会导致死锁)            调用一个违反了上述任一条规则的外部例程参见下列部分以更深入地了解这些规则:

26、60;          16.2.1 为自旋锁和被保护数据提供存储空间           16.2.2初始化自旋锁           16.2.3调用使用了自旋锁的支持例程           16.2.

27、4快速释放自旋锁           16.2.5使用自旋锁时防止错误或死锁的出现1.2.1 为自旋锁和被保护数据提供存储空间作为设备启动工作的一部分,驱动程序必须在下列各处之一为所有自旋锁保护的数据或资源以及相应的自旋锁分配常驻存储空间:           驱动程序通过调用IoCreateDevice建立的设备对象的设备扩展      

28、;     驱动程序通过调用IoCreateController建立的控制器对象的控制器扩展           驱动程序通过调用ExAllocatePool获得的非页式系统空间内存当持有自旋锁时,如果试图访问可分页数据而这一页不在内存中,就会导致一个致命的页错误。引用无效自旋锁(原来被保存在可分页内存中,而现在它所在的页已被调出内存(paged-out)也会导致一个致命的页错误。驱动程序必须为下列各种可能用到的执行自旋锁提供存储空间: 

29、0;         调用了KeAcquireSpinLock和KeRelaeseSpinLock 或者调用了KeAcquireSpinLockAtDpcLevel和KeRelaeseSpinLockFromDpcLevel的非ISR驱动程序用来同步对驱动程序定义数据的访问的所有自旋锁           通过调用资源确定的ExInterlockedXxx例程集来同步对驱动程序分配资源的访问的所有自旋锁驱动程序可以从

30、其ISR或SynchCritSection例程调用ExInterlocked.List例程,然而当它运行在高于DISPATCH_LEVEL的IRQL上时,它不能调用KeAcquireSpinLock和KeRelaeseSpinLock 或者KeAcquireSpinLockAtDpcLevel和KeRelaeseSpinLockFromDpcLevel。因此,所有在调用Ke.SoinLock和ExInterlockedXxx时重用了自旋锁的驱动程序,都必须在运行的IRQL低于DISPATCH_LEVEL时做每个调用。驱动程序可以将相同的自旋锁传递给ExInterlockedInsertHead

31、List,就像传递给另一个ExInterlockedXxx例程一样,这样做的前提是两个例程在相同的IRQL上使用此自旋锁。如果想深入了解自旋锁的使用对性能有何影响,请参见快速释放自旋锁一节。除了为其执行自旋锁提供存储空间以外,如果设备驱动程序有多重矢量ISR或一个以上的ISR,那么它还必须为与其中断对象相关的另一个自旋锁提供存储空间。1.2.2  初始化自旋锁在调用需要访问调用者提供的执行自旋锁的支持例程之前,驱动程序必须调用KeInitializeSpinLock来初始化相应的执行自旋锁。需要初始化执行自旋锁的支持例程如下:     

32、;      KeAcquireSpinLock和随后的KeRelaeseSpinLock           KeAcquireSpinLockAtDpcLevel和随后的KeRelaeseSpinLockFromDpcLevel           ExInterlockedXxx例程在调用IoConnectInterrupt和KeSynchr

33、onizeExecution之前,最低层驱动程序必须调用KeInitializeSpinLock以初始化由它提供存储空间的中断自旋锁。1.2.3  调用使用了自旋锁的支持例程调用KeAcquireSpinLock可以将当前处理器上的IRQL设为DISPATCH_LEVEL,直到用对应的KeRelaeseSpinLock调用将此IRQL恢复到改变前的值为止。因此,当驱动程序调用KeAcquireSpinLock时,它必须在低于DISPATCH_LEVEL的IRQL上执行。KeAcquireSpinLockAtDpcLevel和KeRelaeseSpinLockFromDpcLevel的

34、调用者运行得更快一些,因为它们已经运行在DISPATCH_LEVEL上,所以这些支持例程不需要将当前处理器上的IRQL重新设置。因此,在大多数Windows NT/Windows 2000平台上,当运行在低于DISPATCH_LEVEL的IRQL上时,调用KeAcquireSpinLockAtDpcLevel是个致命的错误。通过调用KeRelaeseSpinLockFromDpcLevel来释放用KeAcquireSpinLock获得的自旋锁也是一个致命的错误,因为没有恢复调用者的初始IRQL。持有执行自旋锁的例程(如ExInterlockedXxx)通常运行在DISPATCH_LEVEL上,

35、直到它们释放了这个自旋锁并向调用者返回控制为止。然而,只要传给ExInterlockedXxx集的自旋锁是被驱动程序的ISR和SynchCritSection例程排他地使用,这个ISR这个SynchCritSection例程(运行在DIRQL)就可以调用其中的某个ExInterlockedXxx例程(如ExInterlocked.List例程)。持有中断自旋锁的例程运行在相关中断对象集的DIRQL上。因此,驱动程序绝对不能从它的ISR或SynchCritSection例程中调用KeAcquireSpinLock和KeRelaeseSpinLock例程,也不能调用其他任何使用了执行自旋锁的例程。

36、这种调用会导致系统死锁,需要用户重新启动他的计算机。还应注意,如果驱动程序的ISR或SynchCritSection例程调用了ExInterlocked.List例程,那么此驱动程序就不能在它调用Ke.SpinLock或Ke.SpinLock.DpcLevel时重用它传给ExInterlocked.List例程的自旋锁。如果驱动程序有多重向量ISR或多个ISR,当运行IRQL高于相关中断对象指定的SynchronizeIrql值时,它可以调用KeSynchronizeExecution。请参见“管理硬件优先级”。如果想对管理支持例程确定的IRQL需求有更多的了解,请参见在线DDK。1.2.4

37、 快速释放自旋锁将驱动程序持有自旋锁的时间最小化可以很明显地改善驱动程序和系统整体的性能。例如,图16.1表示了中断自旋锁怎样保护SMP机器上必须被ISR和StartIo及DpcForIsr例程共享的设备确定的数据。图16.1   使用中断自旋锁1.       驱动程序的ISR运行在一个处理器的DIRQL上,而它的StartIo例程运行在第二个处理器的DISPATCH_LEVEL上。内核中断处理者在驱动程序的设备扩展中持有此驱动程序ISR的InterruptSpinLock,它用来访问设备确定的被保护数

38、据,如设备寄存器(SynchronizeContext)的状态或指针。已准备好访问SynchronizeContext的StartIo例程调用KeSynchronizeExecution,传递指向相关中断对象的指针、共享SynchronizeContext和驱动程序的SynchCritSection例程(图16.1中的AccessDevice)。KeSynchronizeExecution在第二个处理器上一直循环以防止AccessDevice访问SynchronizeContext,直到ISR返回(从而释放了驱动程序的InterruptSpinLock)时为止。然而,KeSynchronize

39、Execution还提高了第二个处理器上的IRQL,使它等于中断对象的SynchronizeIrql的值,从而防止了在这个处理器上发生其他设备中断,因此ISR一返回,AccessDevice就可以运行在DIRQL上。不过,其他设备的更高级的DIRQL中断、时钟中断和电源错误中断仍可以在两个处理器中的任一个上发生。2.       当ISR将驱动程序的DpcForIsr排队并返回时,第二个处理器上的AccessDevice运行在等于相关中断对象SynchronizeIrql值的IRQL上,而且访问了SynchronizeContext。

40、同时,另一个处理器上的DpcForIsr运行在DISPATCH_LEVEL。DpcForIsr也已准备好访问SynchronizeContext,因此它调用KeSynchronizeExecution,调用参数与步骤1中StartIo例程的参数相同。当KeSynchronizeExecution获得自旋锁并代表StartIo例程运行AccessDevice时,驱动程序提供的同步例程AccessDevice可以排他地访问SynchronizeContext。因为AccessDevice运行在SynchronizeIrql值指定的IRQL上,所以驱动程序的ISR直到自旋锁被释放时,才能获得此自旋锁

41、并访问相同的存储区,否则,即使AccessDevice正在运行时另一个处理器上发生了设备中断也不行。3.       AccessDevice返回时释放自旋锁。StartIo例程继续在第二个处理器的DISPATCH_LEVEL上运行。现在KeSynchronizeExecution在第三个处理器上运行AccessDevice,因此它可以代表DpcForIsr访问SynchronizeContext。然而,如果设备中断在第2步中DpcForIsr调用KeSynchronizeExecution之前就发生了,那么此ISR可能会在KeSyn

42、chronizeExecution获得自旋锁并在第三个处理器上运行AccessDevice之前在另一个处理器上运行。如图16.1所示,当一个处理器上运行的例程持有自旋锁时,其他每个试图获得此自旋锁的例程都无法成功。每个试图获得已占用自旋锁的例程都在其当前处理器上循环,直到持锁者释放了这个自旋锁为止。一个自旋锁被释放后,有且只有一个例程能够获得它,没有获得此自旋锁的其他各例程将继续循环。任何自旋锁的持锁者都运行在提高IRQL上,对于执行自旋锁,在DISPATCH_LEVEL;对于中断自旋锁,在DIRQL。KeAcquireSpinLock的调用者运行在DISPATCH_LEVEL上,直到它们调用

43、KeRelaeseSpinLock为止。KeSynchronizeExecution的调用者自动将当前处理器上的IRQL提高为中断对象的SynchronizeIrql值,直到调用者提供的SynchCritSection例程退出且KeSynchronizeExecution返回控制为止。请参见调用使用自旋锁的支持例程。记住下列使用自旋锁的规则:           在被自旋锁持有者占用或其他例程占用的处理器集合上,运行在低级IRQL上试图获得相同自旋锁的代码将无法实现其目的。因此,最小化驱动程序持

44、锁时间可以极大地改善驱动程序的性能和系统的整体性能。如图16.1所示,在多处理器机上,Knernel中断处理者按“先到先服务”的原则执行那些在相同IRQL上运行的例程。Knernel还要做下列事情:           当驱动程序例程调用KeSynchronizeExecution时,Knernel使驱动程序的SynchCritSection例程运行在调用KeSynchronizeExecution的处理器上。(见步骤1和3)      

45、;     当驱动程序的ISR将其DpcForIsr排队时,Knernel使DPC运行在IRQL低于DISPATCH_LEVEL的第一个可用的处理器上。它不一定是IoRequestDpc调用发生的那个处理器。(见步骤2)在单处理器机上,驱动程序中断驱动的I/O操作可能需要串行化。但是在SMP机上,同样的操作完全可以真正异步实现。如图16.1所示,在驱动程序的DpcForIsr开始处理那些ISR已经为其处理设备在CPU1上中断的IRP之前,此驱动程序的ISR可以运行在SMP机中的CPU4上。也就是说,在DpcForIsr例程或CustomDpc例程运行之前

46、,中断自旋锁不能阻止:ISR在运行于一个处理器上时保存的操作指定数据,在另一个处理器上发生设备中断时被此ISR写覆盖。尽管驱动程序可以试着将所有中断驱动的I/O操作串行化以保存ISR收集的数据,但是这个驱动程序在SMP机器上的运行不会比在单处理器机上快多少。在保持Windows NT/Windows 2000单处理器和多处理器平台之间可移植性的前提下,为了获得尽可能好的性能,驱动程序应该用其他技术保存那些由ISR获得的以供DpcForIsr随后处理的操作指定数据。例如,ISR可以在它传给DpcForIsr的IRP中保存操作指定的数据。对这种方法的一种改进是:将DpcForIsr实现为可以查询I

47、SR增加的计数值(ISR-augmented count),用ISR提供的数据来处理计数值代表的IRP个数,然后在返回前将计数值重置为0。当然,必须用驱动程序的中断自旋锁来保护这个计数值,因为驱动程序的ISR和SynchCritSection例程会动态改变它的值。1.2.5  使用自旋锁时防止错误或死锁的出现驱动程序持有自旋锁时,只要它引起了硬件或软件异常,系统性能就会下降。这也就是说,驱动程序的ISR和驱动程序在调用KeSynchronizeExecution时提供的任何SynchCritSection例程,都不能引起页错误或算法异常这样的错误或陷阱,也不能引起软件异常。调用KeA

48、cquireSpinLock的例程在释放了它的执行自旋锁而且不再运行在DISPATCH_LEVEL上之前,也不能引起硬件或软件异常。可分页数据和支持例程 持有自旋锁时,驱动程序决不能调用任何访问可分页数据的例程。记住:驱动程序可以访问某些访问可分页数据的支持例程,当且仅当此调用发生时驱动程序运行在低于DISPATCH_LEVEL的IRQL上。对IRQL的这个限定使得驱动程序在持有自旋锁时不可能调用这些支持例程。如果想对某个具体的支持例程的IRQL需求有更多了解,请在在线DDK上参见此例程的相应参考部分。递归 试图递归地获得自旋锁必然会引起死锁:递归例程的持有实例在第二个实例循环,以试图获得相同

49、自旋锁时,不会释放此自旋锁。在递归例程中使用自旋锁应遵守下列策略:递归例程决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。当递归例程持有自旋锁时,如果递归可能导致死锁或可能使调用者的持锁时间超过25毫秒,那么另一个驱动程序例程决不能调用这个递归例程。如果想对递归驱动程序例程有更多的了解,请参见“使用内核栈”。获得嵌套自旋锁 当持有自旋锁时,试图获得第二个自旋锁也会导致死锁或很差的驱动程序性能。在实现持有自旋锁的驱动程序时,应该遵守下列策略:           驱动程序

50、决不能调用使用自旋锁的支持例程,除非能保证不会发生死锁。           即使不会死锁,驱动程序也不应该调用使用自旋锁的支持例程,除非替代它的程序技术无法提供同等的驱动程序性能和功能。           如果驱动程序做了嵌套调用以获得自旋锁,它必须以相反的顺序释放那些自旋锁。也就是说,如果驱动程序在获得自旋锁B之前获得了自旋锁A,那么它必须先释放B,后释放A。一般情况下,应避免使用嵌套自旋锁

51、来保护重叠的共享数据和资源的子集或离散集(discrete set)。应当考虑:如果驱动程序使用两个执行自旋锁来保护离散资源(比如,可能由不同驱动程序例程来单独或共同设置的一对定时器对象),那么可能会发生什么情况。在SMP机上,当两个各持有一个自旋锁的例程中的一个试图获得对方的自旋锁时,驱动程序会间歇地发生死锁。即使能够设计出不会死锁的驱动程序来使用嵌套自旋锁,它也很难成功地实现。在Windows NT/Windows 2000 SMP机器上,很难充分地调试并检测嵌套自旋锁。此外,使用嵌套自旋锁会极大地降低驱动程序和系统的性能。1.3 轮询设备除非不得已,否则设备驱动程序应该尽量避免轮询(pu

52、lling)其设备,而且设备驱动程序不该用整时间片轮询。轮询设备是一项开销很大的操作,它使操作系统在做轮询的驱动程序内受计算限制(compute-bound)。要做很多轮询的设备驱动程序与其他设备上的I/O操作相冲突,从而使系统变得很慢,甚至对用户不做响应。现在开发的设备和运行Windows NT/Windows 2000的处理器一样,它们技术先进,很少需要驱动程序轮询它的设备以确保设备已准备好启动I/O操作或操作完成。不过,有些仍在使用的设备是以前设计的,它们和数据总线窄、时钟速率慢的老式处理器一起协同工作。老式处理器上的操作系统执行同步I/O,而且是单用户单任务的。这样的设备可能需要轮询或

53、用其他方式等待设备更新它的寄存器,特别是对Windows NT/Windows 2000来说,因为它们是被设计为在具有宽数据总线和快速时钟速率的新型处理器上做异步I/O的。虽然通过编写一个增加计数器的简单循环来解决慢速设备的问题似乎可行(这样可以在设备更新寄存器时,“浪费”少量的时间),但这样的驱动程序往往不能在Windows NT/Windows 2000平台之间移植。需要为每个Windows NT/Windows 2000平台分别配置循环计数器的最大值。而且,如果驱动程序是用优化非常好的编译器编译的,编译器可能会移除驱动程序的记数变量和增加计数器的那段循环。如果驱动程序必须在设备硬件更新状

54、态时停下等待,应遵照下列实现策略:           驱动程序可以在读设备寄存器之前调用KeStallExecutionProcessor。驱动程序应该最小化它的等待时间间隔,而且等待时间间隔一般应该不超过50毫秒。KeStallExecutionProcessor时间间隔的单位为1毫秒。如果设备更新状态的时间经常超过50毫秒,可以考虑在驱动程序中建立一个设备专用线程。1.3.1 驱动程序线程慢速设备或很少使用设备(如软盘控制器)的驱动程序可以通过创建一个设备专用的系统线程来解决很多等待问题。

55、类似的,大多数文件系统驱动程序使用系统工作者线程,并提供工作者线程回调例程。线程可以调用KeDelayExecutionThread等待完整时间片长度或更长时间的间隔。KeDelayExecutionThread等待时间间隔的单位大约是10毫秒。因为KeDelayExecutionThread是定时器驱动的例程,其等待间隔的单位会比10毫秒稍快或稍慢些,这取决于操作系统平台。然而,对此例程的调用是可移植的,因为指定的时间增量是常量。如果设备驱动程序有自己的线程环境或运行于系统线程环境中,设备专用线程或最高层驱动程序的工作者线程回调例程,可以在驱动程序设备扩展的共享通信区中,同步Kernel定义

56、的调度者对象(如事件或信号量)上的同步操作。当其设备没有使用时,设备专用线程可以在共享调度者对象上等待,例如通过用信号量调用KeWaitForSingleObject来等待。在调用这种设备驱动程序来执行I/O操作并将信号量设为Signaled状态之前,它的等待线程不占用CPU时间。驱动程序可以通过调用KeSetBasePriorityThread来设置它用PsCreateSystemThread创建的驱动程序专用或设备专用线程的基优先级(base priority)。驱动程序应该将优先级指定为能避免在SMP机上运行时优先级倒置(runtime priority inversion)的值。将驱动

57、程序创建的线程的基优先级设得过高,会延迟提交I/O请求给驱动程序的低优先级线程的执行。1.4 管理内存的使用许多驱动程序只是将分配给其设备对象的设备扩展的内存用作其全局存储区;只是将其在IRP中的I/O栈用作操作指定的本地存储区。然而,驱动程序可以按需要分配额外的系统空间内存,而且可以用内核栈在调用内部驱动程序例程时传递少量的数据。1.4.1 使用系统内存图16.2表示Windows NT/Windows 2000虚内存空间及它们与系统物理内存的关系。图16.2   虚内存空间和物理内存如图16.2所示,虚内存实际对应的是分页的物理内存,虚地址范围实际对应的是CPU中不邻接

58、的页。用户空间虚内存和从页式存储池中分配的系统空间内存总是可分页的。也就是说,任何非当前处理及其数据都可以分页到辅助存储区中去,通常是磁盘上。图16.2中的高位空间(hyperspace)是系统空间地址的专用区,内存管理器用它将当前处理的虚地址空间映射为CPU中的一系列物理页。注意:任何非当前处理的虚地址都是不可见的,因此它的内存空间是不可访问的。1.4.1.1 访问用户空间内存的驱动程序驱动程序不能分配用户空间的虚内存,因为它们运行在内核模式。此外,驱动程序不能通过用户模式的虚地址访问内存,除非它正运行在引起驱动程序当前I/O操作的用户模式线程环境中而且它正在使用此线程的虚地址。只有最高层驱

59、动程序(如FSD)可以保证它们的Dispatch例程会在这样的用户模式线程环境中被调用。最高层驱动程序可以在为低层驱动程序建立IRP之前调用MmProbeAdnLockPages以锁住(lock down)用户缓冲。最低层驱动程序和为缓冲或直接I/O建立设备对象的中间层驱动程序,可以依赖I/O管理器或最高层驱动程序,来在IRP中传递对被锁用户缓冲或系统空间缓冲的合法访问。1.4.1.2 为部分传输请求建立MDL如果传输请求太大以致于下层设备驱动程序无法处理,那么高层驱动程序可以调用IoBuildPartialMdl,为下层设备驱动程序建立部分传输IRP队列。如果最高层驱动程序不能在一台内存有限

60、的计算机上用MmProbeAndLockPages锁住整个用户缓冲,初始请求也必须被分割成部分传输。对这种大传输请求,最高层驱动程序不能做下列事情:1.       调用IoBuildSynchronousFsdRequest来分配部分传输IRP并锁住用户缓冲的一部分。通常加锁区的大小要么是PAGESIZE的倍数,要么是下层设备的传输容量。2.       如果低层驱动程序返回STATUS_PENDING,就用部分传输IRP调用IoCallDriver,并调用KeWaitFo

61、rSingleObject,以等待驱动程序建立与其部分传输IRP相关的事件对象。3.       当它重新获得控制时,重复步骤1和2,直到所有数据都被传输为止,然后完成初始IRP。必须处理很大传输请求的最高层设备驱动程序可以使用前述技术,简单地用它分配的部分传输IRP调用它自己。除此之外,还有另一种方案,其中最高层设备驱动程序要做下列事情:1.       调用IoAllocateMdl来分配描述用户缓冲的一部分MDL。2.    &#

62、160;  调用MmProbeAndLockPages来锁住这部分用户缓冲。3.       给这部分用户缓冲传输数据。4.       调用MmUnlockPages,做下列事情之一:            如果驱动程序在步骤1中分配的MDL非常大,足够下次传输,调用MmPrepareMdlForReuse并重复步骤2到4。  

63、0;         否则,调用IoFreeMdl并重复步骤1到4。5.       所有数据都被传输后,调用MmUnlockPages和IoFreeMdl。1.4.1.3 分配系统空间内存图16.2所示的系统空间虚内存由有限个页式存储池和更少的非页式存储池组成。非页式存储池总是常驻的。因此,运行在任何级别的IRQL时,它都可以被安全地访问。对于驱动程序,页式存储池只有在下列条件下,才能被分配和访问:    

64、0;      使用对应的页式存储池虚地址的例程必须运行在低于APC_LEVEL的IRQL上。如果运行在高于APC_LEVEL的IRQL上时发生了页错误,那么它将是一个致命的错误。参见管理硬件优先级部分以了解更多关于IRQL的内容。除了驱动程序或设备初始化,或者卸载(有时可能发生)以外,最低层和中间层驱动程序很少从页式存储池中分配内存,因为这些类型的驱动程序通常运行在高于APC_LEVEL的IRQL上。这样的驱动程序分配的任何可分页存储区只能被驱动程序创建的线程或DriverEntry、AddDevice、Reinitialize(如果有的话)

65、和Unload(如果有的话)例程安全访问,这些线程或例程可以用页式存储池分配方式来存放只在驱动程序或设备初始化、或者卸载时需要的数据、对象和资源。因为有些标准驱动程序例程运行在高于APC_LEVEL的IRQL上,所以从页式存储池中分配的内存对大多数中间层或设备驱动程序例程是不可访问的。例如,高层驱动程序的IoCompletion例程在专用线程环境中和(通常)DISPATCH_LEVEL上执行。这样的驱动程序决不应为将被IoCompletion例程访问的数据分配可分页存储区。请参见“管理硬件优先级”。分配驱动程序缓冲空间 为了分配I/O缓冲空间,驱动程序可以调用MmAllocateNonCach

66、edMemory、MmAllocateContiguousMemory、AllocateCommonBuffer(如果驱动程序的设备使用总线控制器DMA或系统DMA控制器的自动初始化模式)或ExAllocatePool。系统运行时,非页式存储池往往会变成很多内存碎片,因此驱动程序的DriverEntry例程应该调用这些例程以建立驱动程序需要的长期I/O缓冲。这些例程(可能除了ExAllocatePool)均在处理器指定的边界(由处理器的数据缓存范围(data-cache-line)的大小决定)内分配内存以避免发生缓存及一致性问题。驱动程序应尽可能节省地分配它们的内部I/O缓冲(如果有的话),因

67、为非页式存储池是很有限的系统资源。一般,驱动程序应该避免重复调用这些支持例程来请求小于PAGE_SIZE的分配。为了节省地分配I/O缓冲内存,记住下列事实:           每次调用MmAllocateNonCachedMemory至少占用非页式系统空间内存中的一整页,无论请求分配多大的存储区。对于小于一页的请求,页中余下的字节都被浪费掉了:调用MmAllocateNonCachedMemory的驱动程序不可访问它,它也不能被其他内核模式的程序使用。   

68、60;       如果指定的字节个数少于或等于一页,调用MmAllocateContiguousMemory分配至多一页的存储区。对于大于一页的请求,最后分配的页中剩余的字节被浪费:调用MmAllocateContiguousMemory的驱动程序不可访问它,它也不能被其他内核模式的程序使用。           调用AllocateCommonBuffer至少使用一个适配器对象映射寄存器,它至少映射1字节,最多映射一页。如果想对映射

69、寄存器和使用公用缓冲有更多的了解,参见第3章中的“适配器对象和DMA部分”。用ExAllocatePool 或ExAllocatePoolWithTag分配内存 驱动程序也可以调用ExAllocatePool 或ExAllocatePoolWithTag,将参数PoolType指定为下列系统定义的值之一:           NonPagedPoolCacheAligned:驱动程序使用永久的I/O缓冲。如SCSI类驱动程序为请求检测(request-sense)数据开辟的缓冲 驱动程序应当调用MmAllocateNonCachedMemory或MmAllocateContiguousMemory分配永久I/O缓冲。     

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论