全面介绍Windows内存管理机制及C++内存分配实例(三):虚拟内存
本文背景:
在編程中,很多Windows或C++的內存函數不知道有什么區別,更別談有效使用;根本的原因是,沒有清楚的理解操作系統的內存管理機制,本文企圖通過簡單的總結描述,結合實例來闡明這個機制。
本文目的:
對Windows內存管理機制了解清楚,有效的利用C++內存函數管理和使用內存。
本文內容:
本文一共有六節,由于篇幅較多,故按節發表。其他章節請看本人博客的Windows內存管理及C++內存分配實例(一)(二)(四)(五)和(六)。
?
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 );
????????????return?TRUE;
}
?
分配100M虛擬空間:
PVOID pVirtual=VirtualAlloc(NULL,100*1024*1024,MEM_RESERVE|MEM_PHYSICAL,PAGE_READWRITE);
????????????if(pVirtual==NULL)
????????????????????????cout<<"沒有那么大連續進程空間!"<<endl;
?
????????????MEMORYSTATUS memStatusVirtual5;
????????????GlobalMemoryStatus(&memStatusVirtual5);
????????????cout<<"虛擬內存分配:"<<endl;
cout<<"減少物理內存="<<memStatusVirtual4.dwAvailPhys-memStatusVirtual5.dwAvailPhys<<endl
cout<<"減少可用頁文件="<<memStatusVirtual4.dwAvailPageFile-memStatusVirtual5.dwAvailPageFile<<endl;
???cout<<"減少可用進程空間="
<<memStatusVirtual4.dwAvailVirtual-memStatusVirtual5.dwAvailVirtual<<endl<<endl;
結果如下:
?
可以看見,只分配了進程空間,沒有分配物理內存。
?
分配物理內存:
ULONG_PTR pages=(ULONG_PTR)100*1024*1024/sysInfo.dwPageSize;
????????????ULONG_PTR *frameArray=new?ULONG_PTR[pages];
????????????//如果沒激活權限,是不能調用這個方法的,可以調用,但是返回FALSE
BOOL flag=AllocateUserPhysicalPages(GetCurrentProcess(),
&pages,frameArray);
????????????if(flag==FALSE)
????????????????????????cout<<"分配物理內存失敗!"<<endl;
????????????MEMORYSTATUS memStatusVirtual6;
????????????GlobalMemoryStatus(&memStatusVirtual6);
????????????cout<<"物理內存分配:"<<endl;
cout<<"減少物理內存="<<memStatusVirtual5.dwAvailPhys-memStatusVirtual6.dwAvailPhys<<endl
cout<<"減少可用頁文件="<<memStatusVirtual5.dwAvailPageFile-memStatusVirtual6.dwAvailPageFile<<endl;
cout<<"減少可用進程空間="<<memStatusVirtual5.dwAvailVirtual-memStatusVirtual6.dwAvailVirtual<<endl<<endl;
??結果如下:
?
分配了物理內存,可能分配時需要進程空間管理。
?
物理內存映射進程空間:
int* pVInt=(int*)pVirtual;
????????????//pVInt[0]=10;這時候訪問會出錯
????????????flag=MapUserPhysicalPages(pVirtual,1,frameArray);
????????????if(flag==FALSE)
????????????????????????cout<<"映射物理內存失敗!"<<endl;
????????????MEMORYSTATUS memStatusVirtual7;
????????????GlobalMemoryStatus(&memStatusVirtual7);
????????????cout<<"物理內存分配:"<<endl;
cout<<"減少物理內存="<<memStatusVirtual6.dwAvailPhys-memStatusVirtual7.dwAvailPhys<<endl
cout<<"減少可用頁文件="<<memStatusVirtual6.dwAvailPageFile-memStatusVirtual7.dwAvailPageFile<<endl;
cout<<"減少可用進程空間="
<<memStatusVirtual6.dwAvailVirtual-memStatusVirtual7.dwAvailVirtual<<endl<<endl;
結果如下:
?
這個過程沒有損失任何東西。
?
看看第一次映射和第二次映射的值:
pVInt[0]=10;
????????????cout<<"第一次映射值="<<pVInt[0]<<endl;
????????????????????????flag=MapUserPhysicalPages(pVirtual,1,frameArray+1);
????????????if(flag==FALSE)
????????????????????????cout<<"映射物理內存失敗!"<<endl;
????????????pVInt[0]=21;
????????????cout<<"第二次映射值="<<pVInt[0]<<endl;
????????????flag=MapUserPhysicalPages(pVirtual,1,frameArray);
????????????if(flag==FALSE)
????????????????????????cout<<"映射物理內存失敗!"<<endl;
????????????cout<<"再現第一次映射值="<<pVInt[0]<<endl;
結果如下:
?
可以看出,第二次映射的值沒有覆蓋第一次映射的值,也就是說,用同一個進程空間地址可以取出兩份數據,這樣的話,相當于進程的地址空間增大了。
?
總結
以上是生活随笔為你收集整理的全面介绍Windows内存管理机制及C++内存分配实例(三):虚拟内存的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【BZOJ】3139: [Hnoi201
- 下一篇: 关于 mybatis-generator