[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對象。

相關文章
相關標籤/搜索