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提供的例程時,能夠指定某個例程和某個全局變量是載入分頁內存仍是非分頁內存,須要作以下定義:
//
#define PAGEDCODE code_seg("PAGE")
#define LOCKEDCODE code_seg()
#define INITCODE code_seg("INIT")
#define PAGEDDATA code_seg("PAGE")
#define LOCKEDDATA code_seg()
#define INITDATA code_seg("INIT")
//
若是將某個函數載入到分頁內存中,咱們須要在函數的實現中加入以下代碼:
//
#pragma PAGEDCODE
VOID SomeFunction()
{
PAGED_CODE();
// Do any other things ....
}
//
其中,PAGED_CODE()是WDK提供的宏,只在check版本中生效。他會檢測這個函數是否運行低於DISPATCH_LEVEL的中斷請求級,若是等於或高於這個中斷請求級,將產生一個斷言。
若是讓函數加載到非分頁內存中,須要在函數的實現中加入以下代碼:
//
#pragma LOCKEDCODE
VOID SomeFunction()
{
// Do any other things ....
}
//
還有一些特殊的狀況,當某個例程在初始化的時候載入內存,而後就能夠從內存中卸載掉。這種狀況特指在調用DriverEntry的時候。尤爲是NT式驅動,它會很長,佔用很大的空間,爲了節省內存,須要及時的從內存中卸載掉。代碼以下:
//
#pragma INITCODE
extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegistryPath)
{
// Do any other things ....
}
//
6. 分配內核內存
Windows驅動程序使用的內存資源很是珍貴,分配內存時要儘可能節約。和應用程序同樣,局部變量是存放在棧(Stack)空間中的。可是棧空間不會像應用程序那麼大,因此驅動程序不適合遞歸調用或者局部變量是大型結構體。若是須要大型結構體,須要在堆(Heap)中申請。
堆中申請內存的函數有如下幾個:
//
NTKERNELAPI
PVOID
ExAllocatePool(
__drv_strictTypeMatch(__drv_typeExpr) __in POOL_TYPE PoolType,
__in SIZE_T NumberOfBytes
);
NTKERNELAPI
PVOID
NTAPI
ExAllocatePoolWithTag(
__in __drv_strictTypeMatch(__drv_typeExpr) POOL_TYPE PoolType,
__in SIZE_T NumberOfBytes,
__in ULONG Tag
);
NTKERNELAPI
PVOID
ExAllocatePoolWithQuota(
__drv_strictTypeMatch(__drv_typeExpr) __in POOL_TYPE PoolType,
__in SIZE_T NumberOfBytes
);
NTKERNELAPI
PVOID
ExAllocatePoolWithQuotaTag(
__in __drv_strictTypeMatch(__drv_typeExpr) POOL_TYPE PoolType,
__in SIZE_T NumberOfBytes,
__in ULONG Tag
);
//
● PoolType:枚舉變量。若是爲NonPagedPool,則分配非分頁內存。若是爲PagedPool,則分配分頁內存。
● NumberOfBytes:分配內存的大小。注:最好是4的倍數。
● 返回值:分配內存的地址,必定是內核模式地址。若是返回0則表明分配失敗。
以上四個函數功能相似。以WithQuota結尾的函數表明分配的時候按配額分配。以WithTag結尾的函數和ExAllocatePool功能相似,惟一不一樣的是多了一個tag參數,系統在要求的內存外額外地多分配了4字節的標籤。在調試的時候,能夠找到是否有標有這個標籤的內存沒有被釋放。
以上4個函數都須要指定PoolType,分別能夠指定以下幾種:
● NonPagedPool:指定要求分配非分頁內存。
● PagedPool:指定要求分配分頁內存。
● NonPagedPoolMustSucceed:指定分配非分頁內存,必須成功。
● DontUseThisType:未指定。
● NonPagedPoolCacheAligned:指定要求分配非分頁內存,並且必須內存對齊。
● PagedPoolCacheAligned:指定分配分頁內存,並且必須內存對齊。
● NonPagedPoolCacheAlignedMustS:指定分配非分頁內存,並且必須對齊,且必須成功。
將分配的內存進行回收的函數是ExFreePool和ExFreePoolWithTag,他們的原型是:
//
NTKERNELAPI
VOID
ExFreePoolWithTag(
__in __drv_freesMem(Mem) PVOID P, // 要釋放的地址
__in ULONG Tag
);
#define ExFreePool(a) ExFreePoolWithTag(a,0)
//
2、在驅動中使用鏈表
WDK提供了兩種鏈表:單向鏈表、雙向鏈表。
單項鍊表每一個元素有一個Next指針指向下一個元素。雙向鏈表每隔元素有兩個指::BLINK指向前一個元素,FLINK指向下一個元素。
1. 鏈表結構
// WDK中定義的雙向鏈表數據結構
//
// Doubly linked list structure. Can be used as either a list head, or
// as link words.
//
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;
//
// Singly linked list structure. Can be used as either a list head, or
// as link words.
//
typedef struct _SINGLE_LIST_ENTRY {
struct _SINGLE_LIST_ENTRY *Next;
} SINGLE_LIST_ENTRY, *PSINGLE_LIST_ENTRY;
//
2. 鏈表初始化
初始化鏈表頭用InitializeListHead宏實現。讓雙向鏈表的兩個指針都指向本身。
判斷鏈表是否爲空,只用判斷鏈表指針是否指向本身便可。WDK提供了一個IsListEmpty。
程序員須要本身定義鏈表每一個元素的數據類型,並將LIST_ENTRY結構做爲自動以結構的一個子域。LIST_ENTRY的做用是將自定義的數據結構串成一個鏈表。
//
typedef struct _MYDATASTRUCT{
// List Entry要做爲_MYDATASTRUCT結構體的一部分
LIST_ENTRY ListEntry;
// 本身定義的數據
ULONG x;
ULONG y;
};
//
3. 從首部插入鏈表
在頭部插入鏈表使用語句InsertHeadList。
//
InsertHeadList(&head, &mydata->ListEntry);
//
head是LIST_ENTRY結構的鏈表頭,mydata是用戶定義的數據結構,它的子域ListEntry是包含其中的LIST_ENTRY數據結構。
4. 從尾部插入鏈表
在尾部插入鏈表使用語句InsertTailList。
//
InsertTailList(&head, &mydata->ListEntry);
//
head是LIST_ENTRY結構的鏈表頭,mydata是用戶定義的數據結構,它的子域ListEntry是包含其中的LIST_ENTRY數據結構。
5. 從鏈表刪除
從鏈表刪除元素也是分兩種。一種是從鏈表頭部刪除,一種是從鏈表尾部刪除。分別隊形RemoveHeadList和RemoveTailList函數。
//
PLIST_ENTRY pEntry = RemoveHeadList(&head);
PLIST_ENTRY pEntry = RemoveTailList(&tail);
//
head是鏈表頭,pEntry是從鏈表刪除下來的元素中的ListEntry。
若是用戶自定義的數據結構第一個字段是LIST_ENTRY時,返回的指針能夠強制轉換爲用戶的數據結構指針。
若是第一個字段不是LIST_ENTRY時,須要減去偏移量。爲了簡化操做WDK提供了宏CONTAINING_RECORD,其用法以下:
//
PLIST_ENTRY pEntry = RemoveHeadList(&head);
PIRP pIrp = CONTAINING_RECORD(pEntry, MYDATASTRUCT, ListEntry);
//
ListEntry爲自定義的數據結構指針。
3、 Lookaside結構
頻繁申請和回收內存,會致使在內存上產生大量內存「空洞」,致使沒法申請新的內存。WDK爲程序員提供了Lookaside結構來解決此問題。
1. 頻繁申請內存的弊端
頻繁的申請與釋放內存,會致使內存產生大量碎片。即便內存中有大量的可用內存,也會致使沒有足夠的連續內存空間而致使申請內存失敗。在操做系統空閒的時候,系統會整理內存中的碎片,將碎片合併。
2. 使用Lookaside
Lookaside對象能夠理解成一個內存容器。在初始的時候,它先向Windows申請量一塊比較大的內存。之後程序員每次申請的時候就不直接向Windows申請內存了,而是直接向Lookaside對象申請呢村。Lookaside對象智能的避免產生內存碎片。
若是Lookaside內部內存不夠用時它會向操做系統申請更多的內存。當Lookaside有大量內存未被使用時,它會讓Windows回收部份內存。使用Lookaside申請內存效率要高於直接向Windows申請內存。
Lookaside通常在如下狀況使用:
a. 程序員每次申請固定大小的內存;
b. 申請和回收操做很是頻繁。
使用Lookaside對象,首先要進行初始化:
// WDK提供的Lookaside初始化函數
VOID ExInitializeNPagedLookasideList(
IN PNPAGED_LOOKASIDE_LIST Lookaside,
IN PALLOCATE_FUNCTION Allocate OPTIONAL,
IN PFREE_FUNCTION Free OPTIONAL,
IN ULONG Flags,
IN SIZE_T Size,
IN ULONG Tag,
IN USHORT Depth);
VOID ExInitializePagedLookasideList(
IN PPAGED_LOOKASIDE_LIST Lookaside,
IN PALLOCATE_FUNCTION Allocate OPTIONAL,
IN PFREE_FUNCTION Free OPTIONAL,
IN ULONG Flags,
IN SIZE_T Size,
IN ULONG Tag,
IN USHORT Depth);
//
這兩個函數分別是對非分頁內存和分頁內存的申請。內存回收可用如下函數
//
VOID
ExFreeToNPagedLookasideList(
IN PNPAGED_LOOKASIDE_LIST Lookaside,
IN PVOID Entry);
VOID
ExFreeToPagedLookasideList(
IN PPAGED_LOOKASIDE_LIST Lookaside,
IN PVOID Entry);
//
它們是用於回收非分頁內存與分頁內存。
在使用完Lookaside對象後,須要刪除Lookaside對象,有如下兩個函數:
//
VOID ExDeleteNPagedLookasideList(IN PNPAGED_LOOKASIDE_LIST Lookaside);
VOID ExDeletePagedLookasideList(IN PPAGED_LOOKASIDE_LIST Lookaside);
//
這兩個函數分別刪除非分頁與分頁的Lookaside對象。