[Windows驱动开发](四)内存管理

1、内存管理概念程序员

1. 物理内存概念(Physical Memory Address)数据结构

    PC上有三条总线,分别是数据总线、地址总线和控制总线。32位CPU的寻址能力为4GB(2的32次方)个字节。用户最多可使用4GB的真实物理内存。PC中不少设备都提供了本身的设备内存。这部份内存会映射到PC的物理内存上,也就是读写这段物理地址,其实读写的是设备内存地址,而不是物理内存地址。ide

 

2. 虚拟内存概念函数

    虽然能够寻址4GB的内存,可是PC中每每没有如此多的真实物理内存。操做系统和硬件(主要是CPU中的内存管理单元MMU)为使用者提供了虚拟内存的概念。Windows的全部程序能够操做的都是虚拟内存。对虚拟内存的全部操做最终都会被转换成对真实物理内存的操做。操作系统

    CPU中有一个重要的寄存器CR0,它是一个32位寄存器,其中的PG位负责标记是否分页。Windows在启动前会将它设置为1,即容许分页。WDK中有一个宏PAGE_SIZE记录分页大小,通常为4KB。4GB的虚拟内存会被分割成1M个分页单元。线程

    其中,有一部分单元会和物理内存对应起来,即虚拟内存中第N个分页单元对应着物理内存的第M个分页单元。这种对应不是一一对应,而是多对一的映射,多个虚拟内存页能够映射同一个物理内存页。还有一部分单元会被映射成磁盘上的一个文件,并被标记为“脏的(Dirty)”。读取这段虚拟内存的时候,系统会发出一个异常,此时会触发异常处理函数,异常处理函数会将这个页的磁盘文件读入内存,并将其标记设置为“不脏”。让常常不读写的内存页交换(Swap)成文件,并将此页设置为“脏”。还有一部分单元什么也没有对应,为空。设计

    Windows如此设计是由于如下两种缘由:指针

        a. 虚拟的增长了内存的大小。调试

        b. 使不一样进程的虚拟内存互不干扰。code

 

3. 用户态地址和内核态地址

    虚拟地址在0~0x7fffffff范围内的虚拟内存,即低2GB的虚拟地址,被称为用户态地址。而0x80000000~0xffffffff范围内的虚拟内存,即高2GB的虚拟内存,被称为内核态地址。Windows规定运行在用户态(Ring3层)的程序只能访问用户态地址,而运行在内核态(Ring0层)的程序能够访问整个4GB的虚拟内存。

    Windows的核心代码和Windows的驱动程序加载的位置都是在高2GB的内核地址中。Windows操做系统在进程切换时,保持内核态地址是彻底相同的,即全部进程的内核地址映射彻底一致,进程切换时只改变用户模式地址的映射。

 

4. Windows驱动程序和进程的关系

    驱动程序相似于一个DLL,被应用程序加载到虚拟内存中,只不过加载地址是内核地址。它能访问的只是这个进程的虚拟内存,不能访问其余进程的虚拟地址。Windows驱动程序里的不一样例程运行在不一样的进程中。DriverEntry例程和AddDevice例程是运行在系统(System)进程中的。这个进程是Windows第一个运行的进程。当须要加载的时候,这个进程中会有一个线程将驱动程序加载到内核模式地址空间内,并调用DriverEntry例程。

    其余的例程,如IRP的派遣函数会运行于应用程序的“上下文”中。“上下文”是指运行于某个进程的环境中,所能访问的虚拟地址是这个进程的虚拟地址。

    在内核态经过调用PsGetCurrentProcess()函数获得当前IO活动的进程,它是EPROCESS的结构体,其中包含了进程的相关信息。因为微软没有公开EPROCESS结构体,因此不一样的系统须要使用Windbg查看其具体的值。在Win XP SP2中这个结构的0x174偏移处记录了一个字符串指针,表示的是进程的映像名称。

 

5. 分页与非分页内存

    Windows规定有些虚拟内存页面是能够交换到文件中的,这类内存被称为分页内存。而有些虚拟内存页永远也不会交换到文件中,这些内存被称为非分页内存。

    当程序的中断请求级在DISPATCH_LEVEL之上时(包括DISPATCH_LEVEL层),程序只能使用非分页内存,不然将致使系统蓝屏死机。

    在编译WDK提供的例程时,能够指定某个例程和某个全局变量是载入分页内存仍是非分页内存,须要作以下定义:

 

 
  1. //

  2.  
  3. #define PAGEDCODE code_seg("PAGE")

  4. #define LOCKEDCODE code_seg()

  5. #define INITCODE code_seg("INIT")

  6.  
  7. #define PAGEDDATA code_seg("PAGE")

  8. #define LOCKEDDATA code_seg()

  9. #define INITDATA code_seg("INIT")

  10.  
  11. //

 

    若是将某个函数载入到分页内存中,咱们须要在函数的实现中加入以下代码:

 

 
  1. //

  2.  
  3. #pragma PAGEDCODE

  4. VOID SomeFunction()

  5. {

  6. PAGED_CODE();

  7. // Do any other things ....

  8. }

  9.  
  10. //

 

    其中,PAGED_CODE()是WDK提供的宏,只在check版本中生效。他会检测这个函数是否运行低于DISPATCH_LEVEL的中断请求级,若是等于或高于这个中断请求级,将产生一个断言。

    若是让函数加载到非分页内存中,须要在函数的实现中加入以下代码:

 

 
  1. //

  2.  
  3. #pragma LOCKEDCODE

  4. VOID SomeFunction()

  5. {

  6. // Do any other things ....

  7. }

  8.  
  9. //

 

    还有一些特殊的状况,当某个例程在初始化的时候载入内存,而后就能够从内存中卸载掉。这种状况特指在调用DriverEntry的时候。尤为是NT式驱动,它会很长,占用很大的空间,为了节省内存,须要及时的从内存中卸载掉。代码以下:

 

 
  1. //

  2.  
  3. #pragma INITCODE

  4. extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegistryPath)

  5. {

  6. // Do any other things ....

  7. }

  8.  
  9. //

 

6. 分配内核内存

    Windows驱动程序使用的内存资源很是珍贵,分配内存时要尽可能节约。和应用程序同样,局部变量是存放在栈(Stack)空间中的。可是栈空间不会像应用程序那么大,因此驱动程序不适合递归调用或者局部变量是大型结构体。若是须要大型结构体,须要在堆(Heap)中申请。

    堆中申请内存的函数有如下几个:

 

 
  1. //

  2.  
  3. NTKERNELAPI

  4. PVOID

  5. ExAllocatePool(

  6. __drv_strictTypeMatch(__drv_typeExpr) __in POOL_TYPE PoolType,

  7. __in SIZE_T NumberOfBytes

  8. );

  9.  
  10. NTKERNELAPI

  11. PVOID

  12. NTAPI

  13. ExAllocatePoolWithTag(

  14. __in __drv_strictTypeMatch(__drv_typeExpr) POOL_TYPE PoolType,

  15. __in SIZE_T NumberOfBytes,

  16. __in ULONG Tag

  17. );

  18.  
  19. NTKERNELAPI

  20. PVOID

  21. ExAllocatePoolWithQuota(

  22. __drv_strictTypeMatch(__drv_typeExpr) __in POOL_TYPE PoolType,

  23. __in SIZE_T NumberOfBytes

  24. );

  25.  
  26. NTKERNELAPI

  27. PVOID

  28. ExAllocatePoolWithQuotaTag(

  29. __in __drv_strictTypeMatch(__drv_typeExpr) POOL_TYPE PoolType,

  30. __in SIZE_T NumberOfBytes,

  31. __in ULONG Tag

  32. );

  33.  
  34. //

 

    ● PoolType:枚举变量。若是为NonPagedPool,则分配非分页内存。若是为PagedPool,则分配分页内存。

    ● NumberOfBytes:分配内存的大小。注:最好是4的倍数。

    ● 返回值:分配内存的地址,必定是内核模式地址。若是返回0则表明分配失败。

 

    以上四个函数功能相似。以WithQuota结尾的函数表明分配的时候按配额分配。以WithTag结尾的函数和ExAllocatePool功能相似,惟一不一样的是多了一个tag参数,系统在要求的内存外额外地多分配了4字节的标签。在调试的时候,能够找到是否有标有这个标签的内存没有被释放。

    以上4个函数都须要指定PoolType,分别能够指定以下几种:

    ● NonPagedPool:指定要求分配非分页内存。

    ● PagedPool:指定要求分配分页内存。

    ● NonPagedPoolMustSucceed:指定分配非分页内存,必须成功。

    ● DontUseThisType:未指定。

    ● NonPagedPoolCacheAligned:指定要求分配非分页内存,并且必须内存对齐。

    ● PagedPoolCacheAligned:指定分配分页内存,并且必须内存对齐。

    ● NonPagedPoolCacheAlignedMustS:指定分配非分页内存,并且必须对齐,且必须成功。

 

    将分配的内存进行回收的函数是ExFreePool和ExFreePoolWithTag,他们的原型是:

 

 
  1. //

  2.  
  3. NTKERNELAPI

  4. VOID

  5. ExFreePoolWithTag(

  6. __in __drv_freesMem(Mem) PVOID P, // 要释放的地址

  7. __in ULONG Tag

  8. );

  9.  
  10. #define ExFreePool(a) ExFreePoolWithTag(a,0)

  11.  
  12. //

 

2、在驱动中使用链表

 

    WDK提供了两种链表:单向链表、双向链表。

    单项链表每一个元素有一个Next指针指向下一个元素。双向链表每隔元素有两个指::BLINK指向前一个元素,FLINK指向下一个元素。

 

1. 链表结构

    

 
  1. // WDK中定义的双向链表数据结构

  2.  
  3. //

  4. // Doubly linked list structure. Can be used as either a list head, or

  5. // as link words.

  6. //

  7.  
  8. typedef struct _LIST_ENTRY {

  9. struct _LIST_ENTRY *Flink;

  10. struct _LIST_ENTRY *Blink;

  11. } LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

  12.  
  13. //

  14. // Singly linked list structure. Can be used as either a list head, or

  15. // as link words.

  16. //

  17.  
  18. typedef struct _SINGLE_LIST_ENTRY {

  19. struct _SINGLE_LIST_ENTRY *Next;

  20. } SINGLE_LIST_ENTRY, *PSINGLE_LIST_ENTRY;

  21.  
  22. //

 

2. 链表初始化

    初始化链表头用InitializeListHead宏实现。让双向链表的两个指针都指向本身。

    判断链表是否为空,只用判断链表指针是否指向本身便可。WDK提供了一个IsListEmpty。

    程序员须要本身定义链表每一个元素的数据类型,并将LIST_ENTRY结构做为自动以结构的一个子域。LIST_ENTRY的做用是将自定义的数据结构串成一个链表。

 

 
  1. //

  2.  
  3. typedef struct _MYDATASTRUCT{

  4. // List Entry要做为_MYDATASTRUCT结构体的一部分

  5. LIST_ENTRY ListEntry;

  6.  
  7. // 本身定义的数据

  8. ULONG x;

  9. ULONG y;

  10. };

  11.  
  12. //

 

3. 从首部插入链表

    在头部插入链表使用语句InsertHeadList。

 

 
  1. //

  2.  
  3. InsertHeadList(&head, &mydata->ListEntry);

  4.  
  5. //

 

    head是LIST_ENTRY结构的链表头,mydata是用户定义的数据结构,它的子域ListEntry是包含其中的LIST_ENTRY数据结构。

 

4. 从尾部插入链表

    在尾部插入链表使用语句InsertTailList。

 

 
  1. //

  2.  
  3. InsertTailList(&head, &mydata->ListEntry);

  4.  
  5. //

 

    head是LIST_ENTRY结构的链表头,mydata是用户定义的数据结构,它的子域ListEntry是包含其中的LIST_ENTRY数据结构。

 

5. 从链表删除

    从链表删除元素也是分两种。一种是从链表头部删除,一种是从链表尾部删除。分别队形RemoveHeadList和RemoveTailList函数。

 

 
  1. //

  2.  
  3. PLIST_ENTRY pEntry = RemoveHeadList(&head);

  4. PLIST_ENTRY pEntry = RemoveTailList(&tail);

  5.  
  6. //

 

    head是链表头,pEntry是从链表删除下来的元素中的ListEntry。

    若是用户自定义的数据结构第一个字段是LIST_ENTRY时,返回的指针能够强制转换为用户的数据结构指针。

    若是第一个字段不是LIST_ENTRY时,须要减去偏移量。为了简化操做WDK提供了宏CONTAINING_RECORD,其用法以下:

 

 
  1. //

  2.  
  3. PLIST_ENTRY pEntry = RemoveHeadList(&head);

  4. PIRP pIrp = CONTAINING_RECORD(pEntry, MYDATASTRUCT, ListEntry);

  5.  
  6. //

 

ListEntry为自定义的数据结构指针。

 

3、 Lookaside结构

 

    频繁申请和回收内存,会致使在内存上产生大量内存“空洞”,致使没法申请新的内存。WDK为程序员提供了Lookaside结构来解决此问题。

 

1. 频繁申请内存的弊端

    频繁的申请与释放内存,会致使内存产生大量碎片。即便内存中有大量的可用内存,也会致使没有足够的连续内存空间而致使申请内存失败。在操做系统空闲的时候,系统会整理内存中的碎片,将碎片合并。

 

2. 使用Lookaside

    Lookaside对象能够理解成一个内存容器。在初始的时候,它先向Windows申请量一块比较大的内存。之后程序员每次申请的时候就不直接向Windows申请内存了,而是直接向Lookaside对象申请呢村。Lookaside对象智能的避免产生内存碎片。

    若是Lookaside内部内存不够用时它会向操做系统申请更多的内存。当Lookaside有大量内存未被使用时,它会让Windows回收部份内存。使用Lookaside申请内存效率要高于直接向Windows申请内存。

    Lookaside通常在如下状况使用:

    a. 程序员每次申请固定大小的内存;

    b. 申请和回收操做很是频繁。

 

    使用Lookaside对象,首先要进行初始化:

 

 
  1. // WDK提供的Lookaside初始化函数

  2.  
  3. VOID ExInitializeNPagedLookasideList(

  4. IN PNPAGED_LOOKASIDE_LIST Lookaside,

  5. IN PALLOCATE_FUNCTION Allocate OPTIONAL,

  6. IN PFREE_FUNCTION Free OPTIONAL,

  7. IN ULONG Flags,

  8. IN SIZE_T Size,

  9. IN ULONG Tag,

  10. IN USHORT Depth);

  11.  
  12. VOID ExInitializePagedLookasideList(

  13. IN PPAGED_LOOKASIDE_LIST Lookaside,

  14. IN PALLOCATE_FUNCTION Allocate OPTIONAL,

  15. IN PFREE_FUNCTION Free OPTIONAL,

  16. IN ULONG Flags,

  17. IN SIZE_T Size,

  18. IN ULONG Tag,

  19. IN USHORT Depth);

  20.  
  21. //

 

    这两个函数分别是对非分页内存和分页内存的申请。内存回收可用如下函数

 

 
  1. //

  2.  
  3. VOID

  4. ExFreeToNPagedLookasideList(

  5. IN PNPAGED_LOOKASIDE_LIST Lookaside,

  6. IN PVOID Entry);

  7.  
  8. VOID

  9. ExFreeToPagedLookasideList(

  10. IN PPAGED_LOOKASIDE_LIST Lookaside,

  11. IN PVOID Entry);

  12.  
  13. //

 

    它们是用于回收非分页内存与分页内存。

    在使用完Lookaside对象后,须要删除Lookaside对象,有如下两个函数:

 

 
  1. //

  2.  
  3. VOID ExDeleteNPagedLookasideList(IN PNPAGED_LOOKASIDE_LIST Lookaside);

  4.  
  5. VOID ExDeletePagedLookasideList(IN PPAGED_LOOKASIDE_LIST Lookaside);

  6.  
  7. //

 

    这两个函数分别删除非分页与分页的Lookaside对象。

相关文章
相关标签/搜索