有关驱动内存分配的说明

转载自:http://hi.baidu.com/zhanghuikl/blog/item/845478096f6878c53bc763ae.html

 

 程序设计涉及的一个重大的方面是分配存储单元。不幸的是,驱动程序不能简单的调用malloc和 free,或者new和delete。要确定分配正确类型的存储器,使用完毕后必须释放分配的存储器,因为内核模式代码没有自动清除机制。

驱动程序可用的存储器

  驱动程序有三种分配存储器的选择。标准的分配存储器的选择依靠持续事件,大小,IRQL来确定。可用的选择是:

1. 内核堆栈:内核模式堆栈在驱动程序例程执行期间给局部变量提供有限数量的非分页存储空间。

2. 分页池: 运行在DISPATCH_LEVEL IRQL以下的例程可以有一个称作分页池的堆。这个区域的存储器是可分页的,当它访问的时候可能产生缺页故障。

3. 非分页池:运行在提高IRQL的例程需要从一个称作非分页池的堆空间中分配暂时的存储空间。系统保证非分页池中的虚空间总是在物理存储器空间中。I/O管理器创建的设备和控制器Extension就存储在这个区域。

因为驱动程序必须是可重入的,所以除了只读数据之外从不分配全局变量。否则,一个线程尝试存储数据到的全局变量,与另一个线程的读写将可能是同一个数据。

当然,驱动程序的局部静态变量同样糟糕。驱动程序的状态信息必须存储在其它的地方,例如像以前介绍的设备Extension。

使用内核堆栈

  在80X86的平台上,内核堆栈的大小仅仅是12Kbyte,在其它的平台上,内核堆栈的大小是16Kbyte。因此,内核堆栈是宝贵的资源,内核堆栈溢出将导致异常。遵循以下指导方针可以避免内核堆栈溢出:

1. 不要设计有很深的嵌套的内部例程,保持调用树尽量平坦。

2. 应尽量避免递归,必须的时候应当限制递归的深度。驱动程序不是进行Fibonacci级数运算的地方。

3. 不要使用内核堆栈来创建大的数据结构,大的数据结构应该在池中创建。

另一个内核堆栈的特性是它存在于高速缓冲存储器中,所以它不能用作DMA操作,DMA的缓冲区应该在非分页池中。

使用缓冲池

  使用内核例程ExAllocatePool和ExFreePool在缓冲池中分配存储空间。

  这些函数允许分配下列存储空间:

1. NonPagedPool:IRQL高于或者等于DISPATCH_LEVEL的驱动程序例程可用的存储器。

2. NonPagedPoolMustSucceed:驱动程序继续操作的重要的临时存储空间。在紧急的情况下使用这种存储空间,使用后要尽快释放。实际上,如果分配失败将会产生异常。

3. NonPagedPoolCacheAligned:保证CPU高速缓存线的自然边界对齐的存储器,驱动程序使用这中存储器作为永久的I/O缓冲区。

4. NonPagedPoolCacheAlignedMustS:对驱动程序重要的操作的临时缓冲区。末尾的S表示成功。像之前的MustSucceed操作这个请求大概从没有被使用过。

 5. PagedPool:   有被IRQL低于DISPATCH_LEVEL的例程使用。通常,这些包括驱动程序的初始化,清除,派遣例程和任何内核模式的线程。

6. PagedPoolCacheAligned:它是文件系统使用的I/O缓冲区存储空间,

使用系统存储器的时候,记得下列几点:

1. 缓冲池是珍贵的系统资源,不要太奢侈,尤其是非分页区域。

2. 当分配或者释放非分页存储器驱动程序必须在或者高于DISPATCH_LEVEL的IRQL上执行。驱动程序必须执行在或者低于below APC_LEVEL的IRQL上分配或者释放分页存储器。

3. 尽快释放不再使用的存储器,否则系统在缺少存储器的情况下运行效率会变低。特别的,要确认在卸载驱动程序的时候归还空间给缓冲池。

存储器再分配

  一般地,驱动程序应当避免常常分配和释放小于PAGE_SIZE bytes的缓冲池,这样使缓冲池中产生碎片而不能被其它内核模式代码使用。如果必须这样的话,就分配一个大块的存储区域和提供一个再分配子程序去分配它们。

实际上,一个C程序员可能编写自己的在一个大的缓冲池中分配和释放存储空间的子程序,一个C++程序员可能重载new和delete操作。

一些驱动程序需要管理一些小的固定尺寸的存储块。例如,SCSI驱动程序必须提供一个SCSI请求块(SRBs),它被用来发送命令给SCSI设备。内核提供两个不同的处理在分配的机制:

区域缓冲区

  区域缓冲区是一块驱动程序分配的缓冲池。Executive例程提供管理分页或者非分页存储器中的固定尺寸的存储块。

使用区域缓冲区时要注意同步,特别的,如果一个中断服务,DPC,派遣例程都需要访问统一个区域缓冲区,这时要使用一个Executive自旋锁来同步。如果所有的访问操作在统一个IRQL水平,可以使用一个互斥体代替。

在安装区域缓冲区之前,必须了解ZONE_HEADER数据结构。区域缓冲区或者快速互斥体对象必须声明和初始化。下面是管理区域缓冲区的步骤:

         1. 调用ExAllocatePool请求区域缓冲区,然后使用ExInitializeZone初始化它,这个步骤常常在DriverEntry例程中执行。

         2. 调用ExAllocateFromZone或者ExInterlockedAllocateFromZone从区域缓冲区分配一个块,后者使用一个自旋锁去同步访问区域缓冲区。前者的同步工作留给了驱动程序代码。

3. 调用ExFreeToZone或者InterlockedFreeToZone释放分配的块。

         4. 在驱动程序的Unload例程,使用ExFreePool释放整个区域缓冲区的存储空间。在释放区域缓冲区的时候,必须确定区域缓冲区中的块都被释放。

一个区域缓冲区应该不大于必要的空间,MmQuerySystemSize可以得到可用的系统存储空间的总数,另一个Executive函数 MmIsThisAnNTAsSystem是用来检查当前的平台是否是WIN2000的服务器版本,运行在服务器版本的驱动程序可以分配稍微大一点的空间。

如果在区域缓冲区中分配存储块失败,驱动程序将使用标准的缓冲池去得到请求的块,这个策略需要一个清楚的结构去指出分配是来自区域缓冲区还是缓冲池。这样才能调用合适的例程释放这个块。

通过ExExtendZone或者ExInterlockedExtendZone可以使存在的区域缓冲区的更大,但是这些函数很少被使用,系统好象不能正确的分配额外的区域缓冲区,实际上,微软有考虑过废除整个区域缓冲区的抽象。WIN2000提供一个更加有效的监视列表构架。

监视列表

  监视列表(Lookaside List)是一个固定尺寸存储块的一个连接列表,不像区域缓冲区,根据系统状态的不同监视列表可以动态的增大或者减小。因此合适大小的监视列表可能较少的浪废存储空间。

于区域缓冲区相比,在监视列表上使用同步机制更加有效,如果CPU构建中有一个8-byte比较交换指令,Executive使用它去使访问监视列表的操作连续。在没有这个指令的平台上,对于非分页池就使用一个自旋锁,对于分页池就使用一个互斥体。

在使用监视列表之前,必须声明一个NPAGED_LOOKASIDE_LIST或者PAGED_LOOKASIDE_LIST(依赖于存储空间是否分页)结构,下面介绍监视列表的管理过程:

1. 使用ExInitializeNPagedLookasideList或者ExInitializePagedLookasideList函数初始化列表的头结构,通常DriverEntry或者AddDevice例程执行这个任务。

2. 调用ExAllocateNPagedLookasideList或者ExAllocatePagedLookasideList从监控列表分配一个块,可以在驱动程序的任何地方调用这两个例程。

3. 调用ExFreeTonPageLookasideList或者ExFreeToPageLookasideList释放块。

        4. 调用ExDeleteNPagedLookasideList或者ExDeletePagedLookasideList释放任何于监控列表相关联的资源,这个函数通常在驱动程序的Unload或者RemoveDevice例程中执行。

监控列表初始化函数简单的设置列表头,它们不实际的分配存储器给列表。初始化函数请求列表可以拥有的最大数量(称作列表的深度)的块。

当使用分配函数的时候,系统才分配需要数量的存储空间,当块被释放,他们被添加到监控列表,直到最大允许深度。任何块的释放将导致存储空间被释放到系统,这样一段时间之后,在监控列表中的块数量将趋近于列表深度附近。

要小心控制监控列表的深度,如果太浅,系统会常常执行昂贵的分配和释放的动作,如果太深,会造成存储空间的浪费。列表头结构的统计数字可以帮助决定列表深度。

猜你喜欢

转载自blog.csdn.net/wwwgeyang777/article/details/7683103