1. 程式人生 > >windows虛擬內存管理(轉)

windows虛擬內存管理(轉)

sys 們的 導出 重置 getc 只讀 min 一個數 一段時間

內存管理是操作系統非常重要的部分,處理器每一次的升級都會給內存管理方式帶來巨大的變化,向早期的8086cpu的分段式管理,到後來的80x86 系列的32位cpu推出的保護模式和段頁式管理。在應用程序中我們無時不刻不在和內存打交道,我們總在不經意間的進行堆內存和棧內存的分配釋放,所以內存是我們進行程序設計必不可少的部分。

CPU的內存管理方式

段寄存器怎麽消失了?

在學習8086匯編語言時經常與寄存器打交道,其中8086CPU采用的內存管理方式為分段管理的方式,尋址時采用:短地址 * 16 + 偏移地址的方式,其中有幾大段寄存器比如:CS、DS、SS、ES等等,每個段的偏移地址最大為64K,這樣總共能尋址到2M的內存。但是到32位CPU之後偏移地址變成了32位這樣每個段就可以有4GB的內存空間,這個空間已經足夠大了,這個時候在編寫相應的匯編程序時我們發現沒有段寄存器的身影了,是不是在32位中已經沒有段寄存器了呢,答案是否定了,32位CPU中不僅有段寄存器而且它們的作用比以前更大了。 在32位CPU中段寄存器不再作為段首地址,而是作為段選擇子,CPU為了管理內存,將某些連續的地址內存作為一頁,利用一個數據結構來說明這頁的屬性,比如是否可讀寫,大小,起始地址等等,這個數據結構叫做段描述符,而多個段描述符則組成了一個段描述符表,而段寄存器如今是用來找到對應的段描述符的,叫做段選擇子。段寄存器仍然是16位其中高13位表示段描述符表的索引,第二位是區分LDT(局部描述符表)和GDT(全局描述符表),全局描述符表是系統級的而LDT是每個進程所獨有的,如果第二位表示的是LDT,那麽首先要從GDT中查詢到LDT所在位置,然後才根據索引找到對應的內存地址,所以現在尋址采用的是通過段選擇子查表的方式得到一個32位的內存地址。由於這些表都是由系統維護,並且不允許用戶訪問及修改所以在普通應用程序中沒有必要也不能使用段寄存器。通過上面的說明,我們可以推導出來32位機器最多可以支持2^(13 + 1 + 32) = 64T內存。

段頁式管理

通過查表方式得到的32位內存地址是否就是真實的物理內存的地址呢,這個也是不一定的,這個還要看系統是否開啟了段頁式管理。如果沒有則這個就是真實的物理地址,如果開啟了段頁式管理那麽這個只是一個線性地址,還需要通過頁表來尋址到真實的物理內存。 32位CPU專門新贈了一個CR3寄存器用來完成分頁式管理,通過CR3寄存器可以尋址到頁目錄表,然後再將32位線性地址的高10位作為頁目錄表的索引,通過這個索引可以找到相應的頁表,再將中間10為作為頁表的索引,通過這個索引可以尋址到對應物理內存的起始地址,最後通過這個其實地址和最後低12位的偏移地址找到對應真實內存。下面是這個過程的一個示例圖: 技術分享

為什麽要使用分頁式管理,直接讓那個32位線性地址對應真實的內存不可以嗎。當然可以,但是分頁式管理也有它自身的優點:

1. 可以實現頁面的保護:系統通過設置相關屬性信息來指定特權級別和其他狀態

2. 可以實現物理內存的共享:從上面的圖中可以看出,不同的線性地址是可以映射到相同的物理內存上的,只需要更改頁表中對應的物理地址就可以實現不同的線性地址對應相同的物理內存實現內存共享。

3. 可以方便的實現虛擬內存的支持:在系統中有一個pagefile.sys的交互頁面文件,這個是系統用來進行內存頁面與磁盤進行交互,以應對內存不夠的情況。系統為每個內存頁維護了一個值,這個值表示該頁面多久未被訪問,當頁面被訪問這個值被清零,否則每過一段時間會累加一次。當這個值到達某個閾值時,系統將頁面中的內容放入磁盤中,將這塊內存空余出來以便保存其他數據,同時將之前的線性地址做一個標記,表名這個線性地址沒有對應到具體的內存中,當程序需要再次訪問這個線性地址所對應的內存時系統會再次將磁盤中的數據寫入到內存中。雖說這樣做相當於擴大了物理內存,但是磁盤相對於內存來說是一個慢速設備,在內存和磁盤間進行數據交換總是會耗費大量的時間,這樣會拖慢程序運行,而采用SSD硬盤會顯著提高系統運行效率,就在於SSD提高了與內存進行數據交換的效率。如果想顯著提高效率,最好的辦法是加內存畢竟在內存和硬盤間倒換數據是要話費時間的。

保護模式

在以前的16位CPU中采用的多是實模式,程序中使用的地址都是真實的物理地址,這樣如果內存分配不合理,會造成一個程序將另外一個程序所在的內存覆蓋這樣對另外一個程序將造成嚴重影響,但是在32位保護模式下,不再會產生這種問題,保護模式將每個進程的地址空間隔離開來,還記得上面的LDT嗎,在不同的程序中即使采用的是相同的地址,也會被LDT映射到不同的線性地址上。

保護模式主要體現在這樣幾個方面:

1.同一進程中,使用4個不同訪問級別的內存段,對每個頁面的訪問屬性做了相應的規定,防止錯誤訪問的情況,同時為提供了4中不同代碼特權,0特權的代碼可以訪問任意級別的內存,1特權能任意訪問1…3級內存,但不能訪問0級內存,依次類推。通常這些特權級別叫做ring0-ring3。

2. 對於不同的進程,將他們所用到的內存等資源隔離開來,一個進程的執行不會影響到另一個進程。

windows系統的內存管理

windows內存管理器

我們將系統中實際映射到具體的實際內存上的頁面稱為工作集。當進程想訪問多余實際物理內存的內存時,系統會啟用虛擬內存管理機制(工作集管理),將那些長時間未訪問的物理頁面復制到硬盤緩沖文件上,並釋放這些物理頁面,映射到虛擬空間的其它頁面上;

系統的內存管理器主要由下面的幾個部分組成:

1. 工作集管理器(優先級16):這個主要負責記錄每個頁面的年齡,也就有多久未被訪問,當頁面被訪問這個年齡被清零,否則每過一段時間就進行累加1的操作。

2. 進程/棧交換器(優先級23):主要用於在進行進程或者線程切換時保存寄存器中的相關數據用以保存相關環境。

3. 已修改頁面寫出器(優先級17):當內存映射的內容發生改變時將這個改變及時的寫入到硬盤中,防止由於程序意外終止而造成數據丟失

4. 映射頁面寫出器(優先級17):當頁面的年齡達到一定的閾值時,將頁面內容寫入到硬盤中

5. 解引用段線程(優先級18):釋放以寫入到硬盤中的空閑頁面

6. 零頁面線程(優先級0):將空閑頁面清零,以便程序下次使用,這個線程保證了新提交的頁面都是幹凈的零頁面

進程虛擬地址空間的布局

windows為每個進程提供了平坦的4GB的線性地址空間,這個地址空間被分為用戶分區和內核分區,他們各占2GB大小,其中內核分區在高地址位,用戶分區在低地址位,下面是內存分布的一個表格:

分區地址範圍
NULL指針區 0x00000000-0x0000FFFF
用戶分區 0x00010000-0x7FFEFFFF
64K禁入區 0x7FFF0000-0x7FFFFFFF
內核分區 0x80000000-0xFFFFFFFF

從上面的圖中可以看出,系統的內核分區是2GB而用戶可用的分區並沒有2GB,在用戶分區的頭64K和尾部的64K不允許用戶使用。 另外我們可以壓縮內核分區的大小,以便使用戶分區占更多的內存,這就是/3GB方式,下面是這種方式的具體內存分布:

分區地址範圍
NULL指針區 0x00000000-0x0000FFFF
用戶分區 0x00010000-0xBFFEFFFF
64K禁入區 0xBFFF0000-0xBFFFFFFF
內核分區 0xC0000000-0xFFFFFFFF

windows虛擬內存管理函數

VirtualAlloc

VirtualAlloc函數主要用於提交或者保留一段虛擬地址空間,通過該函數提交的頁面是經過0頁面線程清理的幹凈的頁面。

LPVOID VirtualAlloc(
  LPVOID lpAddress, //虛擬內存的地址
  DWORD dwSize, //虛擬內存大小
  DWORD flAllocationType,//要對這塊的虛擬內存做何種操作 
  DWORD flProtect //虛擬內存的保護屬性
); 

我們可以指定第一個參數來告知系統,我們希望操作哪塊內存,如果這個地址對應的內存已經被保留了那麽將向下偏移至64K的整數倍,如果這塊內存已經被提交,那麽地址將向下偏移至4K的整數倍,也就是說保留頁面的最小粒度是64K,而提交的最小粒度是一頁4K。 第三個參數是指定分配的類型,主要有以下幾個值

含義
MEM_COMMIT 提交,也就是說將虛擬地址映射到對應的真實物理內存中,這樣這塊內存就可以正常使用
MEM_RESERVE 保留,告知系統以這個地址開始到後面的dwSize大小的連續的虛擬內存程序要使用,進程其他分配內存的操作不得使用這段內存。
MEM_TOP_DOWN 從高端地址保留空間(默認是從低端向高端搜索)
MEM_LARGE_PAGES 開啟大頁面的支持,默認一個頁面是4K而大頁面是2M(這個視具體系統而定)
MEM_WRITE_WATCH 開啟頁面寫入監視,利用GetWriteWatch可以得到寫入頁面的統計情況,利用ResetWriteWatch可以重置起始計數
MEM_PHYSICAL 用於開啟PAE

第四個參數主要是頁面的保護屬性,參數可取值如下:

含義
PAGE_READONLY 只讀
PAGE_READWRITE 可讀寫
PAGE_EXECUTE 可執行
PAGE_EXECUTE_READ 可讀可執行
PAGE_EXECUTE_READWRITE 可讀可寫可執行
PAGE_NOACCESS 不可訪問
PAGE_GUARD 將該頁設置為保護頁,如果試圖對該頁面進行讀寫操作,會產生一個STATUS_GUARD_PAGE 異常

下面是該函數使用的幾個例子:

1. 頁面的提交/保留與釋放

//保留並提交
    LPVOID pMem = VirtualAlloc(NULL, 4 * 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
    srand((unsigned int)time(NULL));

    float* pfMem = (float*)pMem;
    for (int i = 0; i < 4 * 4096 / sizeof(float); i++)
    {
        pfMem[i] = rand();
    }

    //釋放
    VirtualFree(pMem, 4 * 4096, MEM_RELEASE);

    //先保留再提交
    LPBYTE pByte = (LPBYTE)VirtualAlloc(NULL, 1024 * 1024, MEM_RESERVE, PAGE_READWRITE);
    VirtualAlloc(pByte + 4 * 4096, 4096, MEM_COMMIT, PAGE_READWRITE);
    pfMem = (float*)(pByte + 4 * 4096);
    for (int i = 0; i < 4096/sizeof(float); i++)
    {
        pfMem[i] = rand();
    }

    //釋放
    VirtualFree(pByte + 4 * 4096, 4096, MEM_DECOMMIT);
    VirtualFree(pByte, 1024 * 1024, MEM_RELEASE);

  1. 大頁面支持
//獲得大頁面的尺寸
DWORD dwLargePageSize = GetLargePageMinimum();
LPVOID pBuffer = VirtualAlloc(NULL, 64 * dwLargePageSize, MEM_RESERVE, PAGE_READWRITE);
//提交大頁面
VirtualAlloc(pBuffer, 4 * dwLargePageSize, MEM_COMMIT | MEM_LARGE_PAGES, PAGE_READWRITE);
VirtualFree(pBuffer, 4 * dwLargePageSize, MEM_DECOMMIT);
VirtualFree(pBuffer, 64 * dwLargePageSize, MEM_RELEASE);

VirtualProtect

VirtualProtect用來設置頁面的保護屬性,函數原型如下:

BOOL VirtualProtect( 
  LPVOID lpAddress, //虛擬內存地址
  DWORD dwSize, //大小
  DWORD flNewProtect, //保護屬性
  PDWORD lpflOldProtect //返回原來的保護屬性
); 

這個保護屬性與之前介紹的VirtualAlloc中的保護屬性相同,另外需要註意的一點是一般返回原來的屬性的話,這個指針可以為NULL,但是這個函數不同,如果第四個參數為NULL,那麽函數調用將會失敗

LPVOID pBuffer = VirtualAlloc(NULL, 4 * 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
float *pfArray = (float*)pBuffer;
for (int i = 0; i < 4 * 4096 / sizeof(float); i++)
{
    pfArray[i] = 1.0f * rand();
}

//將頁面改為只讀屬性
DWORD dwOldProtect = 0;
VirtualProtect(pBuffer, 4 * 4096, PAGE_READONLY, &dwOldProtect);
//寫入數據將發生異常
pfArray[9] = 0.1f;
VirtualFree(pBuffer, 4 * 4096, MEM_RELEASE);

VirtualQuery

這個函數用來查詢某段虛擬內存的屬性信息,這個函數原型如下:

DWORD VirtualQuery(
  LPCVOID lpAddress,//地址 
  PMEMORY_BASIC_INFORMATION lpBuffer, //用於接收返回信息的指針
  DWORD dwLength //緩沖區大小,上述結構的大小
); 

結構MEMORY_BASIC_INFORMATION的定義如下:

typedef struct _MEMORY_BASIC_INFORMATION {
    PVOID BaseAddress; //該頁面的起始地址
    PVOID AllocationBase;//分配給該頁面的首地址
    DWORD AllocationProtect;//頁面的保護屬性
    DWORD RegionSize; //頁面大小
    DWORD State;//頁面狀態
    DWORD Protect;//頁面的保護類型

    DWORD Type;//頁面類型
} MEMORY_BASIC_INFORMATION; 
typedef MEMORY_BASIC_INFORMATION *PMEMORY_BASIC_INFORMATION; 

AllocationProtect與Protect所能取的值與之前的保護屬性的值相同。 State的取值如下: MEM_FREE:空閑 MEM_RESERVE:保留 MEM_COMMIT:已提交 Type的取值如下: MEM_IMAGE:映射類型,一般是映射到地址控件的可執行模塊如DLL,EXE等 MEM_MAPPED:文件映射類型 MEM_PRIVATE:私有類型,這個頁面的數據為本進程私有數據,不能與其他進程共享 下面是這個的使用例子:

#include<windows.h>
#include <stdio.h>
#include <tchar.h>
#include <atlstr.h>

CString GetMemoryInfo(MEMORY_BASIC_INFORMATION *pmi);
int _tmain(int argc, TCHAR *argv[])
{
    SYSTEM_INFO sm = {0};
    GetSystemInfo(&sm);
    LPVOID dwMinAddress = sm.lpMinimumApplicationAddress;
    LPVOID dwMaxAddress = sm.lpMaximumApplicationAddress;

    MEMORY_BASIC_INFORMATION mbi = {0};
    _putts(_T("BaseAddress\tAllocationBase\tAllocationProtect\tRegionSize\tState\tProtect\tType\n"));

    for (LPVOID pAddress = dwMinAddress; pAddress <= dwMaxAddress;)
    {
        if (VirtualQuery(pAddress, &mbi, sizeof(MEMORY_BASIC_INFORMATION)) == 0)
        {
            break;
        }

        _putts(GetMemoryInfo(&mbi));
        //一般通過BaseAddress(頁面基地址) + RegionSize(頁面長度)來尋址到下一個頁面的的位置
        pAddress = (BYTE*)mbi.BaseAddress + mbi.RegionSize;
    }

}

CString GetMemoryInfo(MEMORY_BASIC_INFORMATION *pmi)
{
    CString lpMemoryInfo = _T("");

    int iBaseAddress = (int)(pmi->BaseAddress);
    int iAllocationBase = (int)(pmi->AllocationBase);

    CString szProtected = _T("\0");
    if (pmi->Protect & PAGE_READONLY)
    {
        szProtected = _T("R");
    }else if (pmi->Protect & PAGE_READWRITE)
    {
        szProtected = _T("RW");
    }else if (pmi->Protect & PAGE_WRITECOPY)
    {
        szProtected = _T("WC");
    }else if (pmi->Protect & PAGE_EXECUTE)
    {
        szProtected = _T("X");
    }else if (pmi->Protect & PAGE_EXECUTE_READ)
    {
        szProtected = _T("RX");
    }else if (pmi->Protect & PAGE_EXECUTE_READWRITE)
    {
        szProtected = _T("RWX");
    }else if (pmi->Protect & PAGE_EXECUTE_WRITECOPY)
    {
        szProtected = _T("WCX");
    }else if (pmi->Protect & PAGE_GUARD)
    {
        szProtected = _T("GUARD");
    }else if (pmi->Protect & PAGE_NOACCESS)
    {
        szProtected = _T("NOACCESS");
    }else if (pmi->Protect & PAGE_NOCACHE)
    {
        szProtected = _T("NOCACHE");
    }else
    {
        szProtected = _T(" ");
    }

    CString szAllocationProtect = _T("\0");
    if (pmi->AllocationProtect & PAGE_READONLY)
    {
        szProtected = _T("R");
    }else if (pmi->AllocationProtect & PAGE_READWRITE)
    {
        szProtected = _T("RW");
    }else if (pmi->AllocationProtect & PAGE_WRITECOPY)
    {
        szProtected = _T("WC");
    }else if (pmi->AllocationProtect & PAGE_EXECUTE)
    {
        szProtected = _T("X");
    }else if (pmi->AllocationProtect & PAGE_EXECUTE_READ)
    {
        szProtected = _T("RX");
    }else if (pmi->AllocationProtect & PAGE_EXECUTE_READWRITE)
    {
        szProtected = _T("RWX");
    }else if (pmi->AllocationProtect & PAGE_EXECUTE_WRITECOPY)
    {
        szProtected = _T("WCX");
    }else if (pmi->AllocationProtect & PAGE_GUARD)
    {
        szProtected = _T("GUARD");
    }else if (pmi->AllocationProtect & PAGE_NOACCESS)
    {
        szProtected = _T("NOACCESS");
    }else if (pmi->AllocationProtect & PAGE_NOCACHE)
    {
        szProtected = _T("NOCACHE");
    }else
    {
        szProtected = _T(" ");
    }

    DWORD dwRegionSize = pmi->RegionSize;
    CString strState = _T("");
    if (pmi->State & MEM_FREE)
    {
        strState = _T("Free");
    }else if (pmi->State & MEM_RESERVE)
    {
        strState = _T("Reserve");
    }else if (pmi->State & MEM_COMMIT)
    {
        strState = _T("Commit");
    }else 
    {
        strState = _T(" ");
    }

    CString strType = _T("");
    if (pmi->Type & MEM_IMAGE)
    {
        strType = _T("Image");
    }else if (pmi->Type & MEM_MAPPED)
    {
        strType = _T("Mapped");
    }else if (pmi->Type & MEM_PRIVATE)
    {
        strType = _T("Private");
    }

    lpMemoryInfo.Format(_T("%08X %08X %s %d %s %s %s\n"), iBaseAddress, iAllocationBase, szAllocationProtect, dwRegionSize, strState, szProtected, strType);
    return lpMemoryInfo;
}

VirtualLock和VirtualUnlock

這兩個函數用於鎖定和解鎖頁面,前面說過操作系統會將長時間不用的內存中的數據放入到系統的磁盤文件中,需要的時候再放回到內存中,這樣來回倒騰,必定會造成程序效率的底下,為了避免這中效率底下的操作,可以使用VirtualLock將頁面鎖定在內存中,防止頁面交換,但是不用了的時候需要使用VirtualUnlock來解鎖,不然一直鎖定而不解鎖會造成真實內存的不足。 另外需要註意的是,不能一次操作超過工作集規定的最大虛擬內存,這樣會造成程序崩潰,我們可以通過函數SetProcessWorkingSetSize來設置工作集規定的最大虛擬內存的大小。下面是一個使用例子:

SetProcessWorkingSetSize(GetCurrentProcess(), 1024 * 1024, 2 * 1024 * 1024);
LPVOID pBuffer = VirtualAlloc(NULL, 4 * 4096, MEM_RESERVE, PAGE_READWRITE);
//不能鎖定超過進程工作集大小的虛擬內存
VirtualLock(pBuffer, 3 * 1024 * 1024);
//不能一次提交超過進程工作集大小的虛擬內存
VirtualAlloc(pBuffer, 3 * 1024 * 1024, MEM_COMMIT, PAGE_READWRITE);
float *pfArray = (float*)pBuffer;
for (int i = 0; i < 4096 / sizeof(float); i++)
{
    pfArray[i] = 1.0f * rand();
}

VirtualUnlock(pBuffer, 4096);
VirtualFree(pBuffer, 4096, MEM_DECOMMIT);
VirtualFree(pBuffer, 4 * 4096, MEM_RELEASE);

VirtualFree

VirtualFree用於釋放申請的虛擬內存。這個函數支持反提交和釋放,這兩個操作由第三個參數指定: MEM_DECOMMIT:反提交,這樣這個線性地址就不再映射到具體的物理內存,但是這個地址仍然是保留地址。 MEM_RELEASE:釋放,這個範圍的地址不再作為保留地址

windows虛擬內存管理(轉)