全面介紹Windows記憶體管理機制及C++記憶體分配例項
本文基本上是windows via c/c++上的內容,筆記做得不錯。。
本文背景:
在程式設計中,很多Windows或C++的記憶體函式不知道有什麼區別,更別談有效使用;根本的原因是,沒有清楚的理解作業系統的記憶體管理機制,本文企圖通過簡單的總結描述,結合例項來闡明這個機制。
本文目的:
對Windows記憶體管理機制瞭解清楚,有效的利用C++記憶體函式管理和使用記憶體。
1. 程序地址空間
1.1地址空間
· 32|64位的系統|CPU
作業系統執行在硬體CPU上,32位作業系統運行於32位CPU上,64位作業系統運行於64位CPU上;目前沒有真正的64位CPU。
32位CPU一次只能操作32位二進位制數;位數多CPU設計越複雜,軟體設計越簡單。
軟體的程序運行於32位系統上,其定址位也是32位,能表示的空間是232=4G,範圍從0x0000 0000~0xFFFF FFFF。
· NULL指標分割槽
範圍:0x0000 0000~0x0000 FFFF
作用:保護記憶體非法訪問
例子:分配記憶體時,如果由於某種原因分配不成功,則返回空指標0x0000 0000;當用戶繼續使用比如改寫資料時,系統將因為發生訪問違規而退出。
那麼,為什麼需要那麼大的區域呢,一個地址值不就行了嗎?我在想,是不是因為不讓8或16位的程式運行於32位的系統上呢?!因為NULL分割槽剛好範圍是16的程序空間。
· 獨享使用者分割槽
範圍:0x0001 0000~0x7FFE FFFF
作用:程序只能讀取或訪問這個範圍的虛擬地址;超越這個範圍的行為都會產生違規退出。
例子:
程式的二進位制程式碼中所用的地址大部分將在這個範圍,所有exe和dll檔案都載入到這個。每個程序將近2G的空間是獨享的。
注意:如果在boot.ini上設定了/3G,這個區域的範圍從2G擴大為3G:0x0001 0000~0xBFFE FFFF。
· 共享核心分割槽
範圍:0x8000 0000~0xFFFF FFFF
作用:這個空間是供作業系統核心程式碼、裝置驅動程式、裝置I/O快取記憶體、非頁面記憶體池的分配、程序目表和頁表等。
例子:
這段地址各程序是可以共享的。
注意:如果在boot.ini上設定了/3G,這個區域的範圍從2G縮小為1G:0xC000 0000~0xFFFF FFFF。
通過以上分析,可以知道,如果系統有n個程序,它所需的虛擬空間是:2G*n+2G (核心只需2G的共享空間)。
1.2地址對映
· 區域
區域指的是上述地址空間中的一片連續地址。區域的大小必須是粒度(64k) 的整數倍,不是的話系統自動處理成整數倍。不同CPU粒度大小是不一樣的,大部分都是64K。
區域的狀態有:空閒、私有、對映、映像。
在你的應用程式中,申請空間的過程稱作保留(預訂),可以用VirtualAlloc;刪除空間的過程為釋放,可以用VirtualFree。
在程式裡預訂了地址空間以後,你還不可以存取資料,因為你還沒有付錢,沒有真實的RAM和它關聯。
這時候的區域狀態是私有;
預設情況下,區域狀態是空閒;
當exe或DLL檔案被對映進了程序空間後,區域狀態變成映像;
當一般資料檔案被對映進了程序空間後,區域狀態變成對映。
· 物理儲存器
Windows各系列支援的記憶體上限是不一樣的,從2G到64G不等。理論上32位CPU,硬體上只能支援4G記憶體的定址;能支援超過4G的記憶體只能靠其他技術來彌補。順便提一下,Windows個人版只能支援最大2G記憶體,Intel使用Address Windows Extension (AWE) 技術使得定址範圍為236=64G。當然,也得作業系統配合。
記憶體分配的最小單位是4K或8K,一般來說,根據CPU不同而不同,後面你可以看到可以通過系統函式得到區域粒度和頁面粒度。
· 頁檔案
頁檔案是存在硬碟上的系統檔案,它的大小可以在系統屬性裡面設定,它相當於實體記憶體,所以稱為虛擬記憶體。事實上,它的大小是影響系統快慢的關鍵所在,如果實體記憶體不多的情況下。
每頁的大小和上述所說記憶體分配的最小單位是一樣的,通常是4K或8K。
· 訪問屬性
物理頁面的訪問屬性指的是對頁面進行的具體操作:可讀、可寫、可執行。CPU一般不支援可執行,它認為可讀就是可執行。但是,作業系統提供這個可執行的許可權。
PAGE_NOACCESS
PAGE_READONLY
PAGE_READWRITE
PAGE_EXECUTE
PAGE_EXECUTE_READ
PAGE_EXECUTE_READWRITE
這6個屬性很好理解,第一個是拒絕所有操作,最後一個是接受收有操作;
PAGE_WRITECOPY
PAGE_EXECUTE_WRITECOPY
這兩個屬性在運行同一個程式的多個例項時非常有用;它使得程式可以共享程式碼段和資料段。一般情況下,多個程序只讀或執行頁面,如果要寫的話,將會Copy頁面到新的頁面。通過對映exe檔案時設定這兩個屬性可以達到這個目的。
PAGE_NOCACHE
PAGE_WRITECOMBINE
這兩個是開發裝置驅動的時候需要的。
PAGE_GUARD
當往頁面寫入一個位元組時,應用程式會收到堆疊溢位通知,線上程堆疊時有用。
· 對映過程
程序地址空間的地址是虛擬地址,也就是說,當取到指令時,需要把虛擬地址轉化為實體地址才能夠存取資料。這個工作通過頁目和頁表進行。
從圖中可以看出,頁目大小為4K,其中每一項(32位)儲存一個頁表的實體地址;每個頁表大小為4K,其中每一項(32位)儲存一個物理頁的實體地址,一共有1024個頁表。利用這4K+4K*1K=4.4M的空間可以表示程序的1024*1024* (一頁4K) =4G的地址空間。
程序空間中的32位地址如下:
高10位用來找到1024個頁目項中的一項,取出頁表的實體地址後,利用中10位來得到頁表項的值,根據這個值得到物理頁的地址,由於一頁有4K大小,利用低12位得到單元地址,這樣就可以訪問這個記憶體單元了。
每個程序都有自己的一個頁目和頁表,那麼,剛開始程序是怎麼找到頁目所在的物理頁呢?答案是CPU的CR3暫存器會儲存當前程序的頁目實體地址。
當程序被建立時,同時需要建立頁目和頁表,一共需要4.4M。在程序的空間中,0xC030 0000~0xC030 0FFF是用來儲存頁目的4k空間。0xC000 0000~0xC03F FFFF是用來儲存頁表的4M空間。也就是說程式裡面訪問這些地址你是可以讀取頁目和頁表的具體值的(要工作在核心方式下)。有一點我不明白的是,頁表的空間包含了頁目的空間!
至於說,頁目和頁表是儲存在實體記憶體還是頁檔案中,我覺得,頁目比較常用,應該在實體記憶體的概率大點,頁表需要時再從頁檔案匯入實體記憶體中。
頁目項和頁表項是一個32位的值,當頁目項第0位為1時,表明頁表已經在實體記憶體中;當頁表項第0位為1時,表明訪問的資料已經在記憶體中。還有很多資料是否已經被改變,是否可讀寫等標誌。另外,當頁目項第7位為1時,表明這是一個4M的頁面,這值已經是物理頁地址,用虛擬地址的低22位作為偏移量。還有很多:資料是否已經被改變、是否可讀寫等標誌。
1.3 一個例子
· 編寫生成軟體程式exe
軟體描述如下:
Main ()
{
1:定義全域性變數
2:處理函式邏輯(Load 所需DLL庫,呼叫方法處理邏輯)
3:定義並實現各種方法(方法含有區域性變數)
4:程式結束
}
將程式編譯,生成exe檔案,附帶所需的DLL庫。
· exe檔案格式
exe檔案有自己的格式,有若干節(section):.text用來放二進位制程式碼(exe或dll);.data用來放各種全域性資料。
.text
指令1:move a, b
指令2:add a, b
…
.data
資料1:a=2
資料2:b=1
…
這些地址都是虛擬地址,也就是程序的地址空間。
· 執行exe程式
建立程序:執行這個exe程式時,系統會建立一個程序,建立程序控制塊PCB,生成程序頁目和頁表,放到PCB中。
資料對齊:資料的記憶體地址除以資料的大小,餘數為0時說明資料是對齊的。現在的編譯器編譯時就考慮資料對齊的問題,生成exe檔案後,資料基本上是對齊的,CPU執行時,暫存器有標誌標識CPU是否能夠自動對齊資料,如果遇到不能對齊的情況,或者通過兩次訪問記憶體,或者通知作業系統處理。
要注意的是,如果資料沒有對齊,CPU處理的效率是很低的。
檔案對映:系統不會將整個exe檔案和所有的DLL檔案裝載進實體記憶體中,同時它也不會裝載進頁面檔案中。相反,它會建立檔案對映,也就是利用exe本身當作頁面檔案。系統將部分二進位制程式碼裝載進記憶體,分配頁面給它。
假設分配了一個頁面,實體地址為0x0232 FFF1。其中裝載的一個指令虛擬地址為0x4000 1001=0100 0000 00 0000 0000 01 0000 0000 0001。一個頁面有4K,系統會將指令儲存在低12位0x0001的地址處。同時,系統根據高10位0x0100找到頁目項,如果沒有關聯的頁表,系統會生成一個頁表,分配一個物理頁;然後,根據中10位0x0001找到表項,將實體地址0x0232 FFF1存進去。
執行過程:
執行時,當系統拿到一個虛擬地址,就根據頁目和頁表找到資料的地址,根據頁目上的值可以判斷頁表是在頁檔案中還是在記憶體中;
如果在頁檔案中,會將頁面匯入記憶體,更新頁目項。讀取頁表項的值後,可以判斷資料頁檔案中還是在實體記憶體中;如果在頁檔案中,會匯入到記憶體中,更新頁表項。最終,拿到了資料。
在分配物理頁的過程中,系統會根據記憶體分配的狀況適當淘汰暫時不用的頁面,如果頁面內容改變了(通過頁表項的標誌位),儲存到頁檔案中,系統會維護記憶體與頁檔案的對應關係。
由於將exe檔案當作記憶體對映檔案,當需要改變資料,如更改全域性變數的值時,利用Copy-On-Write的機制,重新生成頁檔案,將結果儲存在這個頁檔案中,原來的頁檔案還是需要被其他程序例項使用的。
在清楚了指令和資料是如何匯入記憶體,如何找到它們的情況下,剩下的就是CPU不斷的取指令、執行、儲存資料的過程了,當程序結束後,系統會清空之前的各種結構、釋放相關的實體記憶體和刪除頁檔案。
2. 記憶體狀態查詢函式
2.1系統資訊
Windows 提供API可以查詢系統記憶體的一些屬性,有時候我們需要獲取一些頁面大小、分配粒度等屬性,在分配記憶體時用的上。
請看以下C++程式:
SYSTEM_INFO sysInfo;
GetSystemInfo(&sysInfo);
cout<<"機器屬性:"<<endl;
cout<<"頁大小="<<sysInfo.dwPageSize<<endl;
cout<<"分配粒度="<<sysInfo.dwAllocationGranularity<<endl;
cout<<"使用者區最小值="<<sysInfo.lpMinimumApplicationAddress<<endl;
cout<<"使用者區最大值="
<<sysInfo.lpMaximumApplicationAddress<<endl<<endl;
結果如下:
可以看出,頁面大小是4K,區域分配粒度是64K,程序使用者區是0x0001 0000~0x7FFE FFFF。
2.2記憶體狀態
· 記憶體狀態可以獲取總記憶體和可用記憶體,包括頁檔案和實體記憶體。
請看以下C++程式:
MEMORYSTATUS memStatus;
GlobalMemoryStatus(&memStatus);
cout<<"記憶體初始狀態:"<<endl;
cout<<"記憶體繁忙程度="<<memStatus.dwMemoryLoad<<endl;
cout<<"總實體記憶體="<<memStatus.dwTotalPhys<<endl;
cout<<"可用實體記憶體="<<memStatus.dwAvailPhys<<endl;
cout<<"總頁檔案="<<memStatus.dwTotalPageFile<<endl;
cout<<"可用頁檔案="<<memStatus.dwAvailPageFile<<endl;
cout<<"總程序空間="<<memStatus.dwTotalVirtual<<endl;
cout<<"可用程序空間="<<memStatus.dwAvailVirtual<<endl<<endl;
結果如下:
可以看出,總實體記憶體是1G,可用實體記憶體是510兆,總頁檔案是2.5G,這個是包含實體記憶體的頁檔案;可用頁檔案是1.9G。這裡還標識了總程序空間,還有可用的程序空間,程式只用了22兆的記憶體空間。這裡說的都是大約數。
記憶體繁忙程式是標識當前系統記憶體管理的繁忙程式,從0到100,其實用處不大。
· 在函式裡面靜態分配一些記憶體後,看看究竟發生什麼
char stat[65536];
MEMORYSTATUS memStatus1;
GlobalMemoryStatus(&memStatus1);
cout<<"靜態分配空間:"<<endl;
printf("指標地址=%x/n",stat);
cout<<"減少實體記憶體="<<memStatus.dwAvailPhys-memStatus1.dwAvailPhys<<endl;
cout<<"減少可用頁檔案="<<memStatus.dwAvailPageFile-memStatus1.dwAvailPageFile<<endl;
cout<<"減少可用程序空間="<<memStatus.dwAvailVirtual-
memSta tus1.dwAvailVirtual<<endl<<endl;
結果如下:
可以看出,實體記憶體、可用頁檔案和程序空間都沒有損耗。因為區域性變數是分配線上程堆疊裡面的,每個執行緒系統都會建立一個預設1M大小的堆疊給執行緒函式呼叫使用。如果分配超過1M,就會出現堆疊溢位。
· 在函式裡面動態分配300M的記憶體後,看看究竟發生什麼
char *dynamic=new char[300*1024*1024];
MEMORYSTATUS memStatus2;
GlobalMemoryStatus(&memStatus2);
cout<<"動態分配空間:"<<endl;
printf("指標地址=%x/n",dynamic);
cout<<"減少實體記憶體="<<memStatus.dwAvailPhys-memStatus2.dwAvailPhys<<endl;
cout<<"減少可用頁檔案="<<memStatus.dwAvailPageFile-memStatus2.dwAvailPageFile<<endl;
cout<<"減少可用程序空間="<<memStatus.dwAvailVirtual-memStatus2.dwAvailVirtual<<endl<<endl;
結果如下:
動態分配情況下,系統分配直到記憶體頁檔案使用完為止,當然,系統要留一下系統使用的頁面。
2.3 程序區域地址查詢
在給定一個程序空間的地址後,可以查詢它所在區域和相鄰頁面的狀態,包括頁面保護屬性、儲存器型別等。
· C++靜態分配了兩次記憶體,一次是4K大一點,一個是900K左右。
char arrayA[4097];
char arrayB[900000];
第一次查詢:
long len=sizeof(MEMORY_BASIC_INFORMATION);
MEMORY_BASIC_INFORMATION mbiA;
VirtualQuery(arrayA,&mbiA,len);
cout<<"靜態記憶體地址屬性:"<<endl;
cout<<"區域基地址="<<mbiA.AllocationBase<<endl;
cout<<"區域鄰近頁面狀態="<<mbiA.State<<endl;
cout<<"區域保護屬性="<<mbiA.AllocationProtect<<endl;
cout<<"頁面基地址="<<mbiA.BaseAddress<<endl;
printf("arrayA指標地址=%x/n",arrayA);
cout<<"從頁面基地址開始的大小="<<mbiA.RegionSize<<endl;
cout<<"鄰近頁面物理儲存器型別="<<mbiA.Type<<endl;
cout<<"頁面保護屬性="<<mbiA.Protect<<endl<<endl;
第二次查詢:
MEMORY_BASIC_INFORMATION mbiB;
VirtualQuery(arrayB,&mbiB,len);
cout<<"靜態記憶體地址屬性:"<<endl;
cout<<"區域基地址="<<mbiB.AllocationBase<<endl;
cout<<"區域鄰近頁面狀態="<<mbiB.State<<endl;
cout<<"區域保護屬性="<<mbiB.AllocationProtect<<endl;
cout<<"頁面基地址="<<mbiB.BaseAddress<<endl;
printf("arrayB指標地址=%x/n",arrayB);
cout<<"從頁面基地址開始的大小="<<mbiB.RegionSize<<endl;
cout<<"鄰近頁面物理儲存器型別="<<mbiB.Type<<endl;
cout<<"頁面保護屬性="<<mbiB.Protect<<endl<<endl;
說明:區域基地址指的是給定地址所在的程序空間區域;
鄰近頁面狀態指的是與給定地址所在頁面狀態相同頁面的屬性:MEM_FREE(空閒=65536)、MEM_RESERVE(保留=8192)和MEM_COMMIT(提交=4096)。
區域保護屬性指的是區域初次被保留時被賦予的保護屬性:PAGE_READONLY(2)、PAGE_READWRITE(4)、PAGE_WRITECOPY(8)和PAGE_EXECUTE_WRITECOPY(128)等等。
頁面基地址指的是給定地址所在頁面的基地址。
從頁面基地址開始的區域頁面的大小,指的是與給定地址所在頁面狀態、保護屬性相同的頁面。
鄰近頁面物理儲存器型別指的是與給定地址所在頁面相同的儲存器型別,包括:MEM_PRIVATE(頁檔案=131072)、MEM_MAPPED(檔案對映=262144)和MEM_IMAGE(exe映像=16777216)。
頁面保護屬性指的是頁面被指定的保護屬性,在區域保護屬性指定後更新。
結果如下:
如前所說,這是在堆疊區域0x0004 0000裡分配的,後分配的地址arrayB反而更小,符合堆疊的特性。arrayA和arrayB它們處於不同的頁面。頁面都受頁檔案支援,並且區域都是提交的,是系統線上程建立時提交的。
· C++動態分配了兩次記憶體,一次是1K大一點,一個是64K左右。所以應該不會在一個區域。
char *dynamicA=new char[1024];
char *dynamicB=new char[65467];
VirtualQuery(dynamicA,&mbiA,len);
cout<<"動態記憶體地址屬性:"<<endl;
cout<<"區域基地址="<<mbiA.AllocationBase<<endl;
cout<<"區域鄰近頁面狀態="<<mbiA.State<<endl;
cout<<"區域保護屬性="<<mbiA.AllocationProtect<<endl;
cout<<"頁面基地址="<<mbiA.BaseAddress<<endl;
printf("dynamicA指標地址=%x/n",dynamicA);
cout<<"從頁面基地址開始的大小="<<mbiA.RegionSize<<endl;
cout<<"鄰近頁面物理儲存器型別="<<mbiA.Type<<endl;
cout<<"頁面保護屬性="<<mbiA.Protect<<endl<<endl;
VirtualQuery(dynamicB,&mbiB,len);
cout<<"動態記憶體地址屬性:"<<endl;
cout<<"區域基地址="<<mbiB.AllocationBase<<endl;
cout<<"區域鄰近頁面狀態="<<mbiB.State<<endl;
cout<<"區域保護屬性="<<mbiB.AllocationProtect<<endl;
cout<<"頁面基地址="<<mbiB.BaseAddress<<endl;
printf("dynamicB指標地址=%x/n",dynamicB);
cout<<"從頁面基地址開始的大小="<<mbiB.RegionSize<<endl;
cout<<"鄰近頁面物理儲存器型別="<<mbiB.Type<<endl;
cout<<"頁面保護屬性="<<mbiB.Protect<<endl;
結果如下:
這裡是動態分配,dynamicA和dynamicB處於兩個不同的區域;同樣,頁面都受頁檔案支援,並且區域都是提交的。
第二個區域是比64K大的,由分配粒度可知,區域至少是128K。那麼,剩下的空間也是提交的嗎,如果是的話那就太浪費了。看看就知道了:0x00E2 1000肯定在這個空間裡,所以查詢如下:
VirtualQuery((char*)0xE23390,&mbiB,len);
cout<<"動態記憶體地址屬性:"<<endl;
cout<<"區域基地址="<<mbiB.AllocationBase<<endl;
cout<<"區域鄰近頁面狀態="<<mbiB.State<<endl;
cout<<"區域保護屬性="<<mbiB.AllocationProtect<<endl;
cout<<"頁面基地址="<<mbiB.BaseAddress<<endl;
printf("dynamicB指標地址=%x/n",0xE21000);
cout<<"從頁面基地址開始的大小="<<mbiB.RegionSize<<endl;
cout<<"鄰近頁面物理儲存器型別="<<mbiB.Type<<endl;
cout<<"頁面保護屬性="<<mbiB.Protect<<endl;
結果如下:
可以看出,鄰近頁面狀態為保留,還沒提交,預料之中;0x00E1 0000 這個區域的大小可以計算出來:69632+978944=1024K。系統動態分配了1M的空間,就為了64K左右大小的空間。可能是為了使得下次有要求分配時時不用再分配了。
3. 記憶體管理機制--虛擬記憶體 (VM)
· 虛擬記憶體使用場合
虛擬記憶體最適合用來管理大型物件或資料結構。比如說,電子表格程式,有很多單元格,但是也許大多數的單元格是沒有資料的,用不著分配空間。也許,你會想到用動態連結串列,但是訪問又沒有陣列快。定義二維陣列,就會浪費很多空間。
它的優點是同時具有陣列的快速和連結串列的小空間的優點。
· 分配虛擬記憶體
如果你程式需要大塊記憶體,你可以先保留記憶體,需要的時候再提交物理儲存器。在需要的時候再提交才能有效的利用記憶體。一般來說,如果需要記憶體大於1M,用虛擬記憶體比較好。
· 保留
用以下Windows 函式保留記憶體塊
VirtualAlloc (PVOID 開始地址,SIZE_T 大小,DWORD 型別,DWORD 保護屬性)
一般情況下,你不需要指定“開始地址”,因為你不知道程序的那段空間是不是已經被佔用了;所以你可以用NULL。“大小”是你需要的記憶體位元組;“型別”有MEM_RESERVE(保留)、MEM_RELEASE(釋放)和MEM_COMMIT(提交)。“保護屬性”在前面章節有詳細介紹,只能用前六種屬性。
如果你要保留的是長久不會釋放的記憶體區,就保留在較高的空間區域,這樣不會產生碎片。用這個型別標誌可以達到:
MEM_RESERVE|MEM_TOP_DOWN。
C++程式:保留1G的空間
LPVOID pV=VirtualAlloc(NULL,1000*1024*1024,MEM_RESERVE|MEM_TOP_DOWN,PAGE_READWRITE);
if(pV==NULL)
cout<<"沒有那麼多虛擬空間!"<<endl;
MEMORYSTATUS memStatusVirtual1;
GlobalMemoryStatus(&memStatusVirtual1);
cout<<"虛擬記憶體分配:"<<endl;
printf("指標地址=%x/n",pV);
cout<<"減少實體記憶體="<<memStatusVirtual.dwAvailPhys-memStatusVirtual1.dwAvailPhys<<endl;
cout<<"減少可用頁檔案="<<memStatusVirtual.dwAvailPageFile-memStatusVirtual1.dwAvailPageFile<<endl;
cout<<"減少可用程序空間="
<<memStatusVirtual.dwAvailVirtual-memStatusVirtual1.dwAvailVirtual<<endl<<endl;
結果如下:
可見,程序空間減少了1G;減少的實體記憶體和可用頁檔案用來管理頁目和頁表。但是,現在訪問空間的話,會出錯的:
int * iV=(int*)pV;
//iV[0]=1;現在訪問會出錯,出現訪問違規
· 提交
你必須提供一個初始地址和提交的大小。提交的大小系統會變成頁面的倍數,因為只能按頁面提交。指定型別是MEM_COMMIT。保護屬性最好跟區域的保護屬性一致,這樣可以提高系統管理的效率。
C++程式:提交100M的空間
LPVOID pP=VirtualAlloc(pV,100*1024*1024,MEM_COMMIT,PAGE_READWRITE);
if(pP==NULL)
cout<<"沒有那麼多物理空間!"<<endl;
int * iP=(int*)pP;
iP[0]=3;
iP[100/sizeof(int)*1024*1024-1]=5;//這是能訪問的最後一個地址
//iP[100/sizeof(int)*1024*1024]=5;訪問出錯
· 保留&提交
你可以用型別MEM_RESERVE|MEM_COMMIT一次全部提交。但是這樣的話,沒有有效地利用記憶體,和使用一般的C++動態分配記憶體函式一樣了。
· 更改保護屬性
更改已經提交的頁面的保護屬性,有時候會很有用處,假設你在訪問資料後,不想別的函式再訪問,或者出於防止指標亂指改變結構的目的,你可以更改資料所處的頁面的屬性,讓別人無法訪問。
VirtualProtect (PVOID 基地址,SIZE_T 大小,DWORD 新屬性,DWORD 舊屬性)
“基地址”是你想改變的頁面的地址,注意,不能跨區改變。
C++程式:更改一頁的頁面屬性,改為只讀,看看還能不能訪問
DWORD protect;
iP[0]=8;
VirtualProtect(pV,4096,PAGE_READONLY,&protect);
int * iP=(int*)pV;
iP[1024]=9;//可以訪問,因為在那一頁之外
//iP[0]=9;不可以訪問,只讀
//還原保護屬性
VirtualProtect(pV,4096,PAGE_READWRITE,&protect);
cout<<"初始值="<<iP[0]<<endl;//可以訪問
· 清除物理儲存器內容
清除頁面指的是,將頁面清零,也就是說當作頁面沒有改變。假設資料存在實體記憶體中,系統沒有RAM頁面後,會將這個頁面暫時寫進虛擬記憶體頁檔案中,這樣來回的倒騰系統會很慢;如果那一頁資料已經不需要的話,系統可以直接使用。當程式需要它那一頁時,系統會分配另一頁給它。
VirtualAlloc (PVOID 開始地址,SIZE_T 大小,DWORD 型別,DWORD 保護屬性)
“大小”如果小於一個頁面的話,函式會執行失敗,因為系統使用四捨五入的方法;“型別”是MEM_RESET。
有人說,為什麼需要清除呢,釋放不就行了嗎?你要知道,釋放了後,程式就無法訪問了。現在只是因為不需要結構的內容了,順便提高一下系統的效能;之後程式仍然需要訪問這個結構的。
C++程式:
清除1M的頁面:
PVOID re=VirtualAlloc(pV,1024*1024,MEM_RESET,PAGE_READWRITE);
if(re==NULL)
cout<<"清除失敗!"<<endl;
這時候,頁面可能還沒有被清零,因為如果系統沒有RAM請求的話,頁面記憶體儲存不變的,為了看看被清零的效果,程式人為的請求大量頁面:
C++程式:
VirtualAlloc((char*)pV+100*1024*1024+4096,memStatus.dwAvailPhys+10000000,MEM_COMMIT,PAGE_READWRITE);//沒訪問之前是不給實體記憶體的。
char* pp=(char*)pV+100*1024*1024+4096;
for(int i=0;i<memStatus.dwAvailPhys+10000000;i++)
pp[i]='V';//逼他使用實體記憶體,而不使用頁檔案
GlobalMemoryStatus(&memStatus);
cout<<"記憶體初始狀態:"<<endl;
cout<<"長度="<<memStatus.dwLength<<endl;
cout<<"記憶體繁忙程度="<<memStatus.dwMemoryLoad<<endl;
cout<<"總實體記憶體="<<memStatus.dwTotalPhys<<endl;
cout<<"可用實體記憶體="<<memStatus.dwAvailPhys<<endl;
cout<<"總頁檔案="<<memStatus.dwTotalPageFile<<endl;
cout<<"可用頁檔案="<<memStatus.dwAvailPageFile<<endl;
cout<<"總程序空間="<<memStatus.dwTotalVirtual<<endl;
cout<<"可用程序空間="<<memStatus.dwAvailVirtual<<end;
cout<<"清除後="<<iP[0]<<endl;
結果如下:
當記憶體所剩無幾時,系統將剛清除的記憶體頁面分配出去,同時不會把頁面的記憶體寫到虛擬頁面檔案中。可以看見,原先是8的值現在是0了。
· 虛擬記憶體的關鍵之處
虛擬記憶體存在的優點是,需要的時候才真正分配記憶體。那麼程式必須決定何時才提交記憶體。
如果訪問沒有提交記憶體的資料結構,系統會產生訪問違規的錯誤。提交的最好方法是,當你程式需要訪問虛擬記憶體的資料結構時,假設它已經是分配記憶體的,然後異常處理可能出現的錯誤。對於訪問違規的錯誤,就提交這個地址的記憶體。
· 釋放
可以釋放整個保留的空間,或者只釋放分配的一些實體記憶體。
釋放特定分配的實體記憶體:
如果不想釋放所有空間,可以只釋放某些實體記憶體。
“開始地址”是頁面的基地址,這個地址不一定是第一頁的地址,一個竅門是提供一頁中的某個地址就行了,因為系統會做頁邊界處理,取該頁的首地址;“大小”是頁面的要釋放的位元組數;“型別”是MEM_DECOMMIT。
C++程式:
//只釋放實體記憶體
VirtualFree((int*)pV+2000,50*1024*1024,MEM_DECOMMIT);
int* a=(int*)pV;
a[10]=2;//可以使用,沒有釋放這一頁
MEMORYSTATUS memStatusVirtual3;
GlobalMemoryStatus(&memStatusVirtual3);
cout<<"實體記憶體釋放:"<<endl;
cout<<"增加實體記憶體="<<memStatusVirtual3.dwAvailPhys-memStatusVirtual2.dwAvailPhys<<endl;
cout<<"增加可用頁檔案="<<memStatusVirtual3.dwAvailPageFile-memStatusVirtual2.dwAvailPageFile<<endl;
cout<<"增加可用程序空間="
<<memStatusVirtual3.dwAvailVirtual-memStatusVirtual2.dwAvailVirtual<<endl<<endl;
結果如下:
可以看見,只釋放實體記憶體,沒有釋放程序的空間。
釋放整個保留的空間:
VirtualFree (LPVOID 開始地址,SIZE_T 大小,DWORD 型別)
“開始地址”一定是該區域的基地址;“大小”必須是0,因為只能釋放整個保留的空間;“型別”是MEM_RELEASE。
C++程式:
VirtualFree(pV,0,MEM_RELEASE);
//a[10]=2;不能使用了,程序空間也釋放了
MEMORYSTATUS memStatusVirtual4;
GlobalMemoryStatus(&memStatusVirtual4);
cout<<"虛擬記憶體釋放:"<<endl;
cout<<"增加實體記憶體="<<memStatusVirtual4.dwAvailPhys-memStatusVirtual3.dwAvailPhys <<endl;
cout<<"增加可用頁檔案="<<memStatusVirtual4.dwAvailPageFile-memStatusVirtual3.dwAvailPageFile<<endl;
cout<<"增加可用程序空間="
<<memStatusVirtual4.dwAvailVirtual-memStatusVirtual3.dwAvailVirtual<<endl<<endl;
結果如下:
整個分配的程序區域被釋放了,包括所佔的實體記憶體和頁檔案。
· 何時釋放
如果陣列的元素大小是小於一個頁面4K的話,你需要記錄哪些空間不需要,哪些在一個頁面上,可以用一個元素一個Bit來記錄;另外,你可以建立一個執行緒定時檢測無用單元。
· 擴充套件地址AWE
AWE是記憶體管理器功能的一套應用程式程式設計介面 (API) ,它使程式能夠將實體記憶體保留為非分頁記憶體,然後將非分頁記憶體部分動態對映到程式的記憶體工作集。此過程使記憶體密集型程式(如大型資料庫系統)能夠為資料保留大量的實體記憶體,而不必交換分頁檔案以供使用。相反,資料在工作集中進行交換,並且保留的記憶體超過 4 GB 範圍。
對於實體記憶體小於2G程序空間時,它的作用是:不必要在實體記憶體和虛擬頁檔案中交換。
對於實體記憶體大於2G程序空間時,它的作用是:應用程式能夠訪問的實體記憶體大於2G,也就相當於程序空間超越了2G的範圍;同時具有上述優點。
3GB
當在boot.ini 上加上 /3GB 選項時,應用程式的程序空間增加了1G,也就是說,你寫程式時,可以分配的空間又增大了1G,而不管實體記憶體是多少,反正有虛擬記憶體的頁檔案,大不了慢點。
PAE
當在boot.ini上加上 /PAE 選項時,作業系統可以支援大於4G的實體記憶體,否則,你加再多記憶體作業系統也是不認的,因為管理這麼大的記憶體需要特殊處理。所以,你記憶體小於4G是沒有必要加這個選項的。注意,當要支援大於16G的實體記憶體時,不能使用/3G選項,因為,只有1G的系統空間是不能管理超過16G的記憶體的。
AWE
當在boot.ini上加上 /AWE選項時,應用程式可以為自己保留實體記憶體,直接的使用實體記憶體而不通過頁檔案,也不會被頁檔案交換出去。當記憶體大於3G時,就顯得特別有用。因為可以充分利用實體記憶體。
當實體記憶體大於4G時,需要/PAE的支援。
以下是一個boot.ini的例項圖,是我機器上的:
要使用AWE,需要使用者具有Lock Pages in Memory許可權,這個在控制面板中的本地計算機政策中設定。
第一,分配程序虛擬空間:
VirtualAlloc (PVOID 開始地址,SIZE_T 大小,DWORD 型別,DWORD 保護屬性)
“開始地址”可以是NULL,由系統分配程序空間;“型別”是MEM_RESERVE|MEM_PHYSICAL;“保護屬性”只能是
PAGE_READWRITE。
MEM_PHYSICAL指的是區域將受物理儲存器的支援。
第二,你要計算出分配的頁面數目PageCount:
利用本文第二節的GetSystemInfo可以計算出來。
第三,分配實體記憶體頁面:
AllocateUserPhysicalPages (HANDLE 程序控制代碼,SIZE_T 頁數,ULONG_PTR 頁面指標陣列)
程序控制代碼可以用GetCurrentProcess()獲得;頁數是剛計算出來的頁數PageCount;頁面陣列指標unsigned long* Array[PageCount]。
系統會將分配結果存進這個陣列。
第四,將實體記憶體與虛擬空間進行對映:
MapUserPhysicalPages (PVOID 開始地址,SIZE_T 頁數,ULONG_PTR 頁面指標陣列)
“開始地址”是第一步分配的空間;
這樣的話,虛擬地址就可以使用了。
如果“頁面指標陣列”是NULL,則取消對映。
第五,釋放物理頁面
FreeUserPhysicalPages (HANDLE 程序控制代碼,SIZE_T 頁數,ULONG_PTR 頁面指標陣列)
這個除了釋放物理頁面外,還會取消物理頁面的對映。
第六,釋放程序空間
VirtualFree (PVOID 開始地址,0,MEM_RELEASE)
C++程式:
首先,在登入使用者有了Lock Pages in Memory許可權以後,還需要呼叫Windows API啟用這個許可權。
BOOL VirtualMem::LoggedSetLockPagesPrivilege ( HANDLE hProcess,BOOL bEnable)
{
struct {
DWORD Count;//陣列的個數
LUID_AND_ATTRIBUTES Privilege [1];} Info;
HANDLE Token;
//開啟本程序的許可權控制代碼
BOOL Result = OpenProcessToken ( hProcess,
TOKEN_ADJUST_PRIVILEGES,
& Token);
If (Result!= TRUE )
{
printf( "Cannot open process token./n" );
return FALSE;
}
//我們只改變一個屬性
Info.Count = 1;
//準備啟用
if( bEnable )
Info.Privilege[0].Attributes = SE_PRIVILEGE_ENABLED;
else
Info.Privilege[0].Attributes = 0;
//根據許可權名字找到LGUID
Result = LookupPrivilegeValue ( NULL,
SE_LOCK_MEMORY_NAME,
&(Info.Privilege[0].Luid));
if( Result != TRUE )
{
printf( "Cannot get privilege for %s./n", SE_LOCK_MEMORY_NAME );
return FALSE;
}
// 啟用Lock Pages in Memory許可權
Result = AdjustTokenPrivileges ( Token, FALSE,(PTOKEN_PRIVILEGES) &Info,0, NULL, NULL);
if( Result != TRUE )
{
printf ("Cannot adjust token privileges (%u)/n", GetLastError() );
return FALSE;
}
else
{
if( GetLastError() != ERROR_SUCCESS )
{
printf ("Cannot enable the SE_LOCK_MEMORY_NAME privilege; ");
printf ("please check the local policy./n");
return FALSE;
}
}
CloseHandle( Token );