1. 程式人生 > >WINDOWS核心程式設計——Windows記憶體管理

WINDOWS核心程式設計——Windows記憶體管理

想要了解Windows記憶體體系結構首先要對系統的記憶體的分段分頁和程序隔離機制要有所瞭解。系統為了對程序進行隔離,使得每個程序只能訪問自己申請的記憶體而不能訪問其他程序的記憶體資源,對每個程序的記憶體使用線性地址編制,在通過記憶體的分頁機制在程序需要訪問實體記憶體時通過程序的頁表找到世界的實體記憶體的地址通過系統讀寫記憶體中的資料。在早期匯流排(20位定址1M)大於暫存器(16位定址64k)的情況下為了表示更多的實體記憶體地址採用了分段技術,現在已經不需要分段技術了(32位的內表示4GB,64位內表示16EB)採用平坦模型。

32位的系統支援4GB的記憶體,線性地址的各個區間有不同的作用:


1.空指標賦值分割槽:用來給空指標賦值的,這個分割槽不可操作,操作就報錯。

2.使用者模式分割槽:使用者程式碼在這裡跑,堆疊都在這裡,使用者可以隨便用,一般出錯都在這裡。

3.64kb禁入分割槽:不知道幹什麼用的,估計就是為了區隔核心模式跟使用者模式的。

4.核心模式分割槽:系統執行的空間,所有程序共用的,使用者模式的的程式碼不能訪問這部分程式碼,若要訪問續的通過系統提供的API進入到核心態。

windows的記憶體體系結構基於虛擬的線性的地址和分頁機制。對於線性地址的分配也是以頁為單位進行的,實體地址的管理更是以頁為單位。我們可以呼叫函式從地址空間中預定一塊記憶體,在實際使用的時候再從實體記憶體中調撥,相當於C語言中的宣告與定義,當不再需要記憶體的時候可以還給系統,先將一塊記憶體標記為可用的(標記線性空間中的地址空閒可用),當積攢夠了一定的空閒記憶體是在取消提交(把實體記憶體歸還給作業系統)。對於實體記憶體而言,在暫時不用或者記憶體緊張的情況下可以被交換到磁碟上的頁交換檔案中,在需要的時候(CPU缺頁中斷)再從也交換檔案中載入到記憶體中,這樣就提高了記憶體的使用效率。頁交換檔案的使用當然需要一定的代價,頻繁的在磁碟與記憶體將交換頁會導致系統性能下降(硬碟顛簸),一般而言採用增加記憶體的辦法比提升CPU對系統的效能改善更大。對於程式的資料可以採用交換頁的技術來擴充套件記憶體以提高實體記憶體的使用效率,對於一些相對於資料的內容多變而且大小不可預計的記憶體使用方式而言交換頁確實能提高效率,但是對於可以預知整塊記憶體大小且需要連續的空間而言如檔案映象,固定大小的資料檔案等使用記憶體對映檔案是效率更高的方式。分頁記憶體機制調配記憶體的過程可以粗略的描述如下:


系統在對記憶體訪問的安全性方面做的不只是按區段來控制記憶體的訪問,也可以對每一個記憶體頁指定保護屬性:


我們將整個4GB的線性地址空間稱為虛擬記憶體(地址稱為邏輯地址),我們所有的記憶體操作只在邏輯地址上完成,系統會幫我們處理實體地址對映,缺頁等所有的情況。系統的記憶體的狀態也主要是通過虛擬記憶體的狀態來表現的,主要通過如下介面獲得記憶體的狀態:

//獲取系統資訊 64位系統要通過GetNativeSystemInfo
void WINAPI GetSystemInfo(
    LPSYSTEM_INFO lpSystemInfo  
);
typedef struct _SYSTEM_INFO {  
  union {  
    DWORD  dwOemId;  
    struct {  
      WORD wProcessorArchitecture;  //處理器體系結構  
      WORD wReserved;  //保留
    } ;  
  } ;  
  DWORD     dwPageSize;   //分頁大小
  LPVOID    lpMinimumApplicationAddress;  //程序最小定址空間
  LPVOID    lpMaximumApplicationAddress; //程序最大定址空間  
  DWORD_PTR dwActiveProcessorMask;  //處理器掩碼; 0..31 表示不同的處理器
  DWORD     dwNumberOfProcessors;  //CPU數量  
  DWORD     dwProcessorType;  //處理器型別
  DWORD     dwAllocationGranularity;  //虛擬記憶體空間的粒度
  WORD      wProcessorLevel;  //處理器等級
  WORD      wProcessorRevision;  //處理器版本
} SYSTEM_INFO;  

//獲取當前系統中關係記憶體使用情況
BOOL WINAPI GlobalMemoryStatusEx(
    LPMEMORYSTATUSEX lpBuffer  
);
typedef struct _MEMORYSTATUSEX {  
  DWORD     dwLength;  // sizeof (MEMORYSTATUSEX)
  DWORD     dwMemoryLoad; //已使用記憶體數量  
  DWORDLONG ullTotalPhys;  //系統實體記憶體總量  
  DWORDLONG ullAvailPhys;  //空閒的實體記憶體  
  DWORDLONG ullTotalPageFile;//頁交換檔案大小  
  DWORDLONG ullAvailPageFile;//空閒的頁交換空間  
  DWORDLONG ullTotalVirtual;  //程序可使用虛擬機器地址空間大小  
  DWORDLONG ullAvailVirtual;  //空閒的虛擬地址空間大小  
  DWORDLONG ullAvailExtendedVirtual;  //ullAvailExtendedVirtual保留欄位
} MEMORYSTATUSEX, *LPMEMORYSTATUSEX  

//獲取當前程序的記憶體使用情況
BOOL WINAPI GetProcessMemoryInfo(
    HANDLE Process, //程序控制代碼
    PPROCESS_MEMORY_COUNTERS ppsmemCounters, //返回記憶體使用情況的結構
    DWORD cb  //結構的大小
); 
typedef struct _PROCESS_MEMORY_COUNTERS_EX {  
  DWORD  cb;  //結構的大小
  DWORD  PageFaultCount; //發生的頁面錯誤  
  SIZE_T PeakWorkingSetSize;  //使用過的最大工作集  
  SIZE_T WorkingSetSize;      //目前的工作集  
  SIZE_T QuotaPeakPagedPoolUsage;//使用過的最大分頁池大小  
  SIZE_T QuotaPagedPoolUsage;  //分頁池大小  
  SIZE_T QuotaPeakNonPagedPoolUsage;//非分頁池使用過的  
  SIZE_T QuotaNonPagedPoolUsage;  //非分頁池大小  
  SIZE_T PagefileUsage; //頁交換檔案使用大小  
  SIZE_T PeakPagefileUsage; //歷史頁交換檔案使用  
  SIZE_T PrivateUsage;  //程序執行過程中申請的記憶體大小  
} PROCESS_MEMORY_COUNTERS_EX, *PPROCESS_MEMORY_COUNTERS_EX  

//查詢當前程序虛擬地址空間的某個地址所屬的塊資訊
SIZE_T WINAPI VirtualQuery(
    LPCVOID                   lpAddress, //查詢記憶體的地址
    PMEMORY_BASIC_INFORMATION lpBuffer, //接收記憶體資訊
    SIZE_T                    dwLength //結構的大小
);
//查詢程序虛擬地址空間的某個地址所屬的塊資訊
DWORD VirtualQueryEx(
    HANDLE hProcess, //程序控制代碼
    LPCVOID lpAddress, //查詢記憶體的地址
    PMEMORY_BASIC_INFORMATION lpBuffer, //接收記憶體資訊
    DWORD dwLength //結構的大小
);
typedef struct _MEMORY_BASIC_INFORMATION {  
  PVOID  BaseAddress;  //區域基地址  
  PVOID  AllocationBase;//使用VirtualAlloc分配的基地址  
  DWORD  AllocationProtect; //保護屬性  
  SIZE_T RegionSize;    //區域大小  
  DWORD  State;     //頁屬性  
  DWORD  Protect;  //區域屬性  
  DWORD  Type;  //區域型別  
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;  
程式不能直接操作實體記憶體的,所有的資料都需要儲存線上性的虛擬記憶體(邏輯地址)中。使用虛擬記憶體主要使用函式VirtualAlloc來預定和提交記憶體,使用VirtualFree來歸還或取消提交記憶體。

虛擬記憶體的操作以頁為粒度,適合用來管理大型物件陣列或大型結構陣列。對於存在頁交換檔案的記憶體頁若我們能確定整頁的記憶體資料不會改變,或者放棄在記憶體中的改變,下回直接從頁交換檔案中重新載入,則稱該記憶體頁為可重設的,不需要被交換到頁檔案中,直接覆蓋其中的內容,在需要的時候重新從也檔案中載入。預定提交重設用的同一個函式說明如下:

//預定虛擬記憶體和調撥實體記憶體,失敗返回NULL,成功返回lpAddress的取整的值
LPVOID VirtualAlloc{
     LPVOID lpAddress, // 要分配的記憶體區域的地址,按分配粒度向上取整,為NULL則由系統決定
     DWORD dwSize, // 分配的大小,分配粒度的整數倍
     DWORD flAllocationType, // 分配的型別
     DWORD flProtect // 該記憶體的初始保護屬性
};
對函式VirtualAlloc中的型別和保護屬性說明如下:


VirtualAlloc的逆向操作為VirtualFree用於釋放和清理虛擬記憶體:

BOOL WINAPI VirtualFree(
    LPVOID lpAddress, //釋放(取消預定或提交)的頁的首地址
    SIZE_T dwSize,  //大小
    DWORD dwFreeType  //MEM_DECOMMIT 取消VirtualAlloc提交的頁, MEM_RELEASE 釋放指定頁
    //當釋放整個區域時 dwFreeType 設定為MEM_RELEASE,lpAddress設定為區域的起始地址,dwSize設定為0,
);
對於VirtualAlloc時指定的保護方式可以通過函式VirtualProtect來更改:
BOOL VirtualProtect(
    LPVOID lpAddress, // 目標地址起始位置
    DWORD dwSize, // 大小
    DWORD flNewProtect, // 請求的保護方式
    PDWORD lpflOldProtect // 儲存老的保護方式
);
為了允許一個32位程序分配和訪問更多的實體記憶體,突破這一受限地址空間所能表達的記憶體範圍,Windows提供了一組函式,稱為地址視窗擴充套件(AWE , Address  Windowing  Extensions)。用到的不多可以稍微瞭解下。

而更常見的在有限的地址空間中處理大資料量(大到4GB的地址空間無法容納所有資料)是,我們通常採用記憶體對映檔案的辦法一段段的處理資料。所謂對映就是把一段邏輯地址與檔案的一段內容一一對應起來(同一段地址可以多次對應不同的檔案內容)。對映原理如下(圖片摘自網路如有版權問題請聯絡刪除):

正是由於記憶體對映檔案的這幾個特性所以特別合適用來處理下列事情:

1:系統使用記憶體對映檔案來將exe或是dll檔案本身作為後備儲存器,而非系統頁交換檔案,這大大節省了系統頁交換空間,由於不需要將exe或是dll檔案載入到頁系統交換檔案,也提高了啟動速度。由於是對映到各自的邏輯地址的所以每個程序儲存自己的副本,所有的變數之間也互不共享,但是可以通過DLL的資料段在使用同一DLL的不同程序間共享變數。
2:使用記憶體對映檔案來將磁碟上的檔案對映到程序的空間區域,使得開發人員操作檔案就像操作記憶體資料一樣,將對檔案的操作交由作業系統來管理,簡化了開發人員的工作。這是最常用的方式,使用方式如下:

1.建立或開啟一個檔案核心物件
HANDLE WINAPI CreateFile(
    LPCTSTR lpFileName,
    DWORD dwDesiredAccess,
    DWORD dwShareMode,
    LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    DWORD dwCreationDisposition,
    DWORD dwFlagsAndAttributes,
    HANDLE hTemplateFile
);
2.建立一個檔案對映核心物件
HANDLE WINAPI CreateFileMapping(
    HANDLE hFile,  //檔案控制代碼
    LPSECURITY_ATTRIBUTES lpAttributes, //安全屬性
    DWORD flProtect, //保護屬性
    DWORD dwMaximumSizeHigh, //檔案對映的最大長度的高32位
    DWORD dwMaximumSizeLow, //檔案對映的最大長度的低32位
    LPCTSTR lpName //核心檔案命名
);
5.關閉檔案物件
CloseHandle(hFile);

3.將檔案對映物件對映到程序地址空間
LPVOID WINAPI MapViewOfFile(
    HANDLE hFileMappingObject, //檔案控制代碼
    DWORD dwDesiredAccess, //檔案資料的訪問方式要與CreateFileMapping()的保護屬性相匹配
    DWORD dwFileOffsetHigh, //表示檔案對映起始偏移的高32位
    DWORD dwFileOffsetLow, //表示檔案對映起始偏移的低32位
    SIZE_T dwNumberOfBytesToMap //指定對映檔案的位元組數
);

6.關閉檔案對映物件
CloseHandle(hFileMapping);

4.從程序的地址空間中撤消檔案資料的映像
BOOL UnmapViewOfFile(
    PVOID pvBaseAddress //pvBaseAddress由MapViewOfFile函式返回
);

//可以按以上順序執行或者看情況執行4,5,6
//對於修改過的資料的一部分或全部強制重新寫入磁碟映像中
BOOL FlushViewOfFile(
   PVOID pvAddress, //記憶體對映檔案中的檢視的一個位元組的地址
   SIZE_T dwNumberOfBytesToFlush //想要重新整理的位元組數
);
對於一些引數的說明如下:

使用fdwProtect 引數設定的部分保護屬性

dwDesiredAccess用於標識如何訪問該資料



3:windows提供了多種程序間通訊的方法,但他們都是基於記憶體對映檔案來實現的。

對於程序間通訊只要在不同程序中映射了同一個檔案內容,當其中一個對映被改變時(就算還沒有儲存到磁碟上)其他程序自動會獲取到改變。

windows的程序除了直接向系統申請記憶體之外還可以使用執行時庫提供的記憶體堆和棧,簡單的有如下說明:

http://blog.csdn.net/pokeyode/article/details/53303029

http://blog.csdn.net/pokeyode/article/details/53336826
雖然執行時庫提供的堆足以滿足我們的需要,但我們還是會基於一下原因來建立自己的堆(引用自):

一:對資料保護。建立兩個或多個獨立的堆,每個堆儲存不同的結構,對兩個堆分別操作,可以使問題區域性化。
二:更有效的記憶體管理。建立額外的堆,管理同樣大小的物件。這樣在釋放一個空間後可以剛好容納另一個物件。
三:記憶體訪問區域性化。將需要同時訪問的資料放在相鄰的區域,可以減少缺頁中斷的次數。
四:避免執行緒同步開銷。預設堆的訪問是依次進行的。堆函式必須執行額外的程式碼來保證執行緒安全性。通過建立額外的堆可以避免同步開銷。
五:快速釋放。我們可以直接釋放整個堆而不需要手動的釋放每個記憶體塊。這不但極其方便,而且還可以更快的執行。
要建立並管理自己的堆,需要使用以下介面,首先要建立堆:

HANDLE HeapCreate(
    DWORD fdwOptions, //如何操作堆
    SIZE_T dwInitilialize, //一開始要調撥給堆的位元組數向上取整到CPU頁面大小的整數倍
    SIZE_T dwMaximumSize //堆所能增長到的最大大小,即預定的地址空間的最大大小。若為0,那麼堆可增長到用盡所有的物理儲存器為止。
); 

fdwOptions表示對堆的操作該如何進行
HEAP_NO_SERIALIZE標誌使得多個執行緒可以同時訪問一個堆,這使得堆中的資料可能會遭到破壞,因此應該避免使用。
HEAP_GENERATE_EXCEPTIONS標誌告訴系統,每當在堆中分配或者重新分配記憶體塊失敗的時候,丟擲一個異常。
HEAP_CREATE_ENABLE_EXECUTE標誌告訴系統,我們想在堆中存放可執行程式碼。如果不設定這個標誌,那麼當我們試圖在來自堆的記憶體塊中執行程式碼時,系統會丟擲EXCEPTION_ACCESS_VIOLATION異常。

有了堆之後從堆中分配記憶體時要:

1.遍歷已分配的記憶體的連結串列和閒置記憶體的連結串列。
2.找到一塊足夠大的閒置記憶體塊。
3.分配一塊新的記憶體,將2找到的記憶體塊標記為已分配。
4.將新分配的記憶體塊新增到已分配的連結串列中。

呼叫函式來從堆中分配並在需要時調整記憶體大小:

PVOID HeapAlloc(
    HANDLE hHeap, //堆控制代碼,表示要從哪個堆分配記憶體
    DWORD fdwFlags, //堆分配時的可選引數
    SIZE_T dwBytes //要分配堆的位元組數
);  
PVOID HeapReAlloc(
    HANDLE hHeap, //堆控制代碼
    DWORD fdwFlags, //HeapAlloc的fdwFlags一樣
    PVOID pvMem, //指定要調整大小的記憶體塊
    SIZE_T dwBytes //指定記憶體塊的新大小
);  
fdwFlags說明如下:

HeapReAlloc的fdwFlags特別的有HEAP_REALLOC_IN_PLACE_ONLY 如果HeapReAlloc函式能在不移動記憶體塊的前提下就能讓它增大,那麼函式會返回原來的記憶體塊地址。另一方面,如果HeapReAlloc必須移動記憶體塊的地址,那麼函式將返回一個指向一塊更大記憶體塊的新地址。如果一個記憶體塊是連結串列或者樹的一部分,那麼需要指定這個標誌。因為連結串列或者樹的其他節點可能包含指向當前節點的指標,把節點移動到堆中其他的地方會破壞連結串列或樹的完整性。

若成功則返回記憶體地址若失敗則返回NULL,若指定了HEAP_GENERATE_EXCEPTIONS報異常:


在不需要記憶體是把記憶體歸還給堆:

BOOL HeapFree( 
    HANDLE hHeap, //堆控制代碼
    DWORD fdwFlags,
    PVOID pvMem //指定要調整大小的記憶體塊
); 
在不需要堆時銷燬堆
BOOL HeapDestroy(HANDLE hHeap);