全面介紹Windows內存管理機制及C++內存分配實例
十分感謝MS社區的帖子,講得很好~
http://social.technet.microsoft.com/Forums/zh-CN/2219/thread/afc1269f-fe08-4dc7-bb94-c395d607e536
(一):進程空間
在編程中,很多Windows或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 );
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;
結果如下:
可以看出,第二次映射的值沒有覆蓋第一次映射的值,也就是說,用同一個進程空間地址可以取出兩份數據,這樣的話,相當於進程的地址空間增大了。
(四):內存映射文件
4. 內存管理機制--內存映射文件 (Map)
和虛擬內存一樣,內存映射文件可以用來保留一個進程地址區域;但是,與虛擬內存不同,它提交的不是物理內存或是虛擬頁文件,而是硬盤上的文件。
· 使用場合
它有三個主要用途:
系統加載EXE和DLL文件
操作系統就是用它來加載exe和dll文件建立進程,運行exe。這樣可以節省頁文件和啟動時間。
訪問大數據文件
如果文件太大,比如超過了進程用戶區2G,用fopen是不能對文件進行操作的。這時,可用內存映射文件。對於大數據文件可以不必對文件執行I/O操作,不必對所有文件內容進行緩存。
進程共享機制
內存映射文件是多個進程共享數據的一種較高性能的有效方式,它也是操作系統進程通信機制的底層實現方法。RPC、COM、OLE、DDE、窗口消息、剪貼板、管道、Socket等都是使用內存映射文件實現的。
· 系統加載EXE和DLL文件
ü EXE文件格式
每個EXE和DLL文件由許多節(Section)組成,每個節都有保護屬性:READ,WRITE,EXECUTE和SHARED(可以被多個進程共享,關閉頁面的COPY-ON-WRITE屬性)。
以下是常見的節和作用:
節名 |
作用 |
.text |
.exe和.dll文件的代碼 |
.data |
已經初始化的數據 |
.bss |
未初始化的數據 |
.reloc |
重定位表(裝載進程的進程地址空間) |
.rdata |
運行期只讀數據 |
.CRT |
C運行期只讀數據 |
.debug |
調試信息 |
.xdata |
異常處理表 |
.tls |
線程的本地化存儲 |
.idata |
輸入文件名表 |
.edata |
輸出文件名表 |
.rsrc |
資源表 |
.didata |
延遲輸入文件名表 |
ü 加載過程
1. 系統根據exe文件名建立進程內核對象、頁目和頁表,也就是建立了進程的虛擬空間。
2. 讀取exe文件的大小,在默認基地址0x0040 0000上保留適當大小的區域。可以在鏈接程序時用/BASE 選項更改基地址(在VC工程屬性\鏈接器\高級上設置)。提交時,操作系統會管理頁目和頁表,將硬盤上的文件映射到進程空間中,頁表中保存的地址是exe文件的頁偏移。
3. 讀取exe文件的.idata節,此節列出exe所用到的所有dll文件。然后和
exe文件一樣,將dll文件映射到進程空間中。如果無法映射到基地址,系統會重新定位。
4. 映射成功后,系統會把第一頁代碼加載到內存,然后更新頁目和頁
表。將第一條指令的地址交給線程指令指針。當系統執行時,發現代碼沒有在內存中,會將exe文件中的代碼加載到內存中。
ü 第二次加載時(運行多個進程實例)
1. 建立進程、映射進程空間都跟前面一樣,只是當系統發現這個exe已
經建立了內存映射文件對象時,它就直接映射到進程空間了;只是當
系統分配物理頁面時,根據節的保護屬性賦予頁面保護屬性,對於代碼
節賦予READ屬性,全局變量節賦予COPY-ON-WRITE屬性。
2. 不同的實例共享代碼節和其他的節,當實例需要改變頁面內容時,會
拷貝頁面內容到新頁面,更新頁目和頁表。
3. 對於不同進程實例需要共享的變量,exe文件有一
個默認的節, 給這個節賦予SHARED屬性。
4. 你也可以創建自己的SHARED節
#pragma data_seg(“節名”)
Long instCount;
#pragma data_seg()
然后,你需要在鏈接程序時告訴編譯器節的默認屬性。
/SECTION: 節名,RWS
或者,在程序里用以下表達式:
#pragma comment(linker,“/SECTION:節名,RWS”)
這樣的話編譯器會創建.drective節來保存上述命令,然后鏈接時會用它改變節屬性。
注意,共享變量有可能有安全隱患,因為它可以讀到其他進程的數據。
C++程序:多個進程共享變量舉例
*.cpp開始處:
#pragma data_seg(".share")
long shareCount=0;
#pragma data_seg()
#pragma comment(linker,"/SECTION:.share,RWS")
ShareCount++;
注意,同一個exe文件產生的進程會共享shareCount,必須是處於同一個位置上的exe。
· 訪問大數據文件
ü 創建文件內核對象
使用CreateFile(文件名,訪問屬性,共享模式,…) API可以創建。
其中,訪問屬性有:
0 不能讀寫 (用它可以訪問文件屬性)
GENERIC_READ
GENERIC_WRITE
GENERIC_READ|GENERIC_WRITE;
共享模式:
0 獨享文件,其他應用程序無法打開
FILE_SHARE_WRITE
FILE_SHARE_READ|FILE_SHARE_WRITE
這個屬性依賴於訪問屬性,必須和訪問屬性不沖突。
當創建失敗時,返回INVALID_HANDLE_VALUE。
C++程序如下:
試圖打開一個1G的文件:
MEMORYSTATUS memStatus;
GlobalMemoryStatus(&memStatus);
HANDLE hn=CreateFile(L"D:\\1G.rmvb",GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ|FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
if(hn==INVALID_HANDLE_VALUE)
cout<<"打開文件失敗!"<<endl;
FILE *p=fopen("D:\\1G.rmvb","rb");
if(p==NULL)
cout<<"用fopen不能打開大文件!"<<endl;
MEMORYSTATUS memStatus2;
GlobalMemoryStatus(&memStatus2);
cout<<"打開文件后的空間:"<<endl;
cout<<"減少物理內存="<<memStatus.dwAvailPhys-memStatus2.dwAvailPhys<<endl;
cout<<"減少可用頁文件="<<memStatus.dwAvailPageFile-memStatus2.dwAvailPageFile<<endl;
cout<<"減少可用進程空間="
<<memStatus.dwAvailVirtual-memStatus2.dwAvailVirtual<<endl<<endl;
結果如下:
可見,系統需要一些內存來管理內核對象,每一次運行的結果都不一樣,但差別不會太大。
用c語言的fopen不能打開這么大的文件。理論上,32位系統能支持232字節,但是,進程空間只有2G,它只能表示那么大的空間。
ü 創建文件映射內核對象
API如下:
HANDLE CreateFileMapping(Handle 文件,PSECURITY_ATTRIBUTES 安全屬性,DWORD 保護屬性,DWORD 文件大小高32位,DWORD 文件大小低32位,PCTSTR 映射名稱)
“文件”是上面創建的句柄;
“安全屬性”是內核對象需要的,NULL表示使用系統默認的安全屬性;“保護屬性”是當將存儲器提交給進程空間時,需要的頁面屬性:PAGE_READONLY, PAGE_READWRITE和PAGE_WRITECOPY。這個屬性不能和文件對象的訪問屬性沖突。除了這三個外,還有兩個屬性可以和它們連接使用(|)。當更新文件內容時,不提供緩存,直接寫入文件,可用SEC_NOCACHE;當文件是可執行文件時,系統會根據節賦予不同的頁面屬性,可用SEC_IMAGE。另外,SEC_RESERVE和SEC_COMMIT用於稀疏提交的文件映射,詳細介紹請參考下文。
“文件大小高32位”和“文件大小低32位”聯合起來告訴系統,這個映射所能支持的文件大小(操作系統支持264B文件大小);當這個值大於實際的文件大小時,系統會擴大文件到這個值,因為系統需要保證進程空間能完全被映射。值為0默認為文件的大小,這時候如果文件大小為0,創建失敗。
“映射名稱”是給用戶標識此內核對象,供各進程共享,如果為NULL,則不能共享。
對象創建失敗時返回NULL。
創建成功后,系統仍未為文件保留進程空間。
C++程序:
MEMORYSTATUS memStatus2;
GlobalMemoryStatus(&memStatus2);
HANDLE hmap=CreateFileMapping(hn,NULL,PAGE_READWRITE,0,0,L"Yeming-Map");
if(hmap==NULL)
cout<<"建立內存映射對象失敗!"<<endl;
MEMORYSTATUS memStatus3;
GlobalMemoryStatus(&memStatus3);
cout<<"建立內存映射文件后的空間:"<<endl;
cout<<"減少物理內存="<<memStatus2.dwAvailPhys-memStatus3.dwAvailPhys<<endl;
cout<<"減少可用頁文件="<<memStatus2.dwAvailPageFile-memStatus3.dwAvailPageFile<<endl;
cout<<"減少可用進程空間="
<<memStatus2.dwAvailVirtual-memStatus3.dwAvailVirtual<<endl<<endl;
結果如下:
默認內存映射的大小是1G文件。沒有損失內存和進程空間。它所做的是建立內核對象,收集一些屬性。
ü 文件映射內核對象映射到進程空間
API如下:
PVOID MAPViewOfFile(HANDLE 映射對象,DWORD訪問屬性,DWORD 偏移量高32位,DWORD 偏移量低32位,SIZE_T 字節數)
“映射對象”是前面建立的對象;
“訪問屬性”可以是下面的值:FILE_MAP_WRITE(讀和寫)、FILE_MAP_READ、FILE_MAP_ALL_ACCESS(讀和寫)、FILE_MAP_COPY。當使用FILE_MAP_COPY時,系統分配虛擬頁文件,當有寫操作時,系統會拷貝數據到這些頁面,並賦予PAGE_READWRITE屬性。
可以看到,每一步都需要設置這類屬性,是為了可以多點控制,試想,如果在這一步想有多種不同的屬性操作文件的不同部分,就比較有用。
“偏移高32位”和“偏移低32位”聯合起來標識映射的開始字節(地址是分配粒度的倍數);
“字節數”指映射的字節數,默認0為到文件尾。
當你需要指定映射到哪里時,你可以使用:
PVOID MAPViewOfFile(HANDLE 映射對象,DWORD訪問屬性,DWORD 偏移量高32位,DWORD 偏移量低32位,SIZE_T 字節數,PVOID 基地址)
“基地址”是映射到進程空間的首地址,必須是分配粒度的倍數。
C++程序:
MEMORYSTATUS memStatus3;
GlobalMemoryStatus(&memStatus3);
LPVOID pMAP=MapViewOfFile(hmap,FILE_MAP_WRITE,0,0,0);
cout<<"映射內存映射文件后的空間:"<<endl;
if(pMAP==NULL)
cout<<"映射進程空間失敗!"<<endl;
else
printf("首地址=%x\n",pMAP);
MEMORYSTATUS memStatus4;
GlobalMemoryStatus(&memStatus4);
cout<<"減少物理內存="<<memStatus3.dwAvailPhys-memStatus4.dwAvailPhys<<endl;
cout<<"減少可用頁文件="<<memStatus3.dwAvailPageFile-memStatus4.dwAvailPageFile<<endl;
cout<<"減少可用進程空間="
<<memStatus3.dwAvailVirtual-memStatus4.dwAvailVirtual<<endl<<endl;
結果如下:
進程空間減少了1G,系統同時會開辟一些內存來做文件緩存。
ü 使用文件
1. 對於大文件,可以用多次映射的方法達到訪問的目的。有點像AWE技術。
2. Windows只保證同一文件映射內核對象的多次映射的數據一致性,比如,當有兩次映射同一對象到二個進程空間時,一個進程空間的數據改變后,另一個進程空間的數據也會跟着改變;不保證不同映射內核對象的多次映射的一致性。所以,使用文件映射時,最好在CreateFile時將共享模型設置為0獨享,當然,對於只讀文件沒這個必要。
C++程序:使用1G的文件
MEMORYSTATUS memStatus4;
GlobalMemoryStatus(&memStatus4);
cout<<"讀取1G文件前:"<<endl;
cout<<"可用物理內存="<<memStatus4.dwAvailPhys<<endl;
cout<<"可用頁文件="<<memStatus4.dwAvailPageFile<<endl;
cout<<"可用進程空間="<<memStatus4.dwAvailVirtual<<endl<<endl;
int* pInt=(int*)pMAP;
cout<<"更改前="<<pInt[1000001536/4-1]<<endl;//文件的最后一個整數
for(int i=0;i<1000001536/4-1;i++)
pInt[i]++;
pInt[1000001536/4-1]=10;
pInt[100]=90;
pInt[101]=100;
cout<<"讀取1G文件后:"<<endl;
MEMORYSTATUS memStatus5;
GlobalMemoryStatus(&memStatus5);
cout<<"可用物理內存="<<memStatus5.dwAvailPhys<<endl;
cout<<"可用頁文件="<<memStatus5.dwAvailPageFile<<endl;
cout<<"可用進程空間="<<memStatus5.dwAvailVirtual<<endl<<endl;
結果如下:
程序將1G文件的各個整型數據加1,從上圖看出內存損失了600多兆,但有時候損失不過十幾兆,可能跟系統當時的狀態有關。
不管怎樣,這樣你完全看不到I/O操作,就像訪問普通數據結構一樣方便。
ü 保存文件修改
為了提高速度,更改文件時可能只更改到了系統緩存,這時,需要強制保存更改到硬盤,特別是撤銷映射前。
BOOL FlushViewOfFile(PVOID 進程空間地址,SIZE_T 字節數)
“進程空間地址”指的是需要更改的第一個字節地址,系統會變成頁面的地址;
“字節數”,系統會變成頁面大小的倍數。
寫入磁盤后,函數返回,對於網絡硬盤,如果希望寫入網絡硬盤后才返回的話,需要將FILE_FLAG_WRITE_THROUGH參數傳給CreateFile。
當使用FILE_MAP_COPY建立映射時,由於對數據的更改只是對虛擬頁文件的修改而不是硬盤文件的修改,當撤銷映射時,會丟失所做的修改。如果要保存,怎么辦?
你可以用FILE_MAP_WRITE建立另外一個映射,它映射到進程的另外一段空間;掃描第一個映射的PAGE_READWRITE頁面(因為屬性被更改),如果頁面改變,用MoveMemory或其他拷貝函數將頁面內容拷貝到第二次映射的空間里,然后再調用FlushViewOfFile。當然,你要記錄哪個頁面被更改。
ü 撤銷映射
用以下API可以撤銷映射:
BOOL UnmapViewOfFile(PVOID pvBaseAddress)
這個地址必須與MapViewOfFile返回值相同。
ü 關閉內核對象
在不需要內核對象時,盡早將其釋放,防止內存泄露。由於它們是內核對象,調用CloseHandle(HANDLE)就可以了。
在CreateFileMapping后馬上關閉文件句柄;
在MapViewOfFile后馬上關閉內存映射句柄;
最后再撤銷映射。
· 進程共享機制
ü 基於硬盤文件的內存映射
如果進程需要共享文件,只要按照前面的方式建立內存映射對象,然后按照名字來共享,那么進程就可以映射這個對象到自己的進程空間中。
C++程序如下:
HANDLE mapYeming=OpenFileMapping(FILE_MAP_WRITE,true,L"Yeming-Map");
if(mapYeming==NULL)
cout<<"找不到內存映射對象:Yeming-Map!"<<endl;
MEMORYSTATUS memStatus3;
GlobalMemoryStatus(&memStatus3);
LPVOID pMAP=MapViewOfFile(mapYeming,FILE_MAP_WRITE,0,0,100000000);
cout<<"建立內存映射文件后的空間:"<<endl;
if(pMAP==NULL)
cout<<"映射進程空間失敗!"<<endl;
else
printf("首地址=%x\n",pMAP);
MEMORYSTATUS memStatus4;
GlobalMemoryStatus(&memStatus4);
cout<<"減少物理內存="<<memStatus3.dwAvailPhys-memStatus4.dwAvailPhys<<endl;
cout<<"減少可用頁文件="<<memStatus3.dwAvailPageFile-memStatus4.dwAvailPageFile<<endl;
cout<<"減少可用進程空間="<<memStatus3.dwAvailVirtual-memStatus4.dwAvailVirtual<<endl<<endl;
int* pInt=(int*)pMAP;
cout<<pInt[100]<<endl;
結果如下:
在2.exe中打開之前1.exe創建的內存映射對象(當然,1.exe得處於運行狀態),然后映射進自己的進程空間,當1.exe改變文件的值時,2.exe的文件對應值也跟着改變,Windows保證同一個內存映射對象映射出來的數據是一致的。可以看見,1.exe將值從90改為91,2.exe也跟着改變,因為它們有共同的緩沖頁。
ü 基於頁文件的內存映射
如果只想共享內存數據時,沒有必要創建硬盤文件,再建立映射。可以直
接建立映射對象:
只要傳給CreateFileMapping一個文件句柄INVALID_HANDLE_VALUE就行了。所以,CreateFile時,一定要檢查返回值,否則會建立一個基於頁文件的內存映射對象。接下來就是映射到進程空間了,這時,系統會分配頁文件給它。
C++程序如下:
HANDLE hPageMap=CreateFileMapping(INVALID_HANDLE_VALUE,NULL,PAGE_READWRITE,0,
100000000,L"Yeming-Map-Page");
if(hPageMap==NULL)
cout<<"建立基於頁文件的內存映射對象失敗!"<<endl;
MEMORYSTATUS memStatus6;
GlobalMemoryStatus(&memStatus6);
cout<<"建立基於頁文件的內存映射文件后的空間:"<<endl;
cout<<"減少物理內存="<<memStatus5.dwAvailPhys-memStatus6.dwAvailPhys<<endl;
cout<<"減少可用頁文件="<<memStatus5.dwAvailPageFile-memStatus6.dwAvailPageFile<<endl;
cout<<"減少可用進程空間="<<memStatus5.dwAvailVirtual-memStatus6.dwAvailVirtual<<endl<<endl;
LPVOID pPageMAP=MapViewOfFile(hPageMap,FILE_MAP_WRITE,0,0,0);
結果如下:
可見,和基於數據文件的內存映射不同,現在剛建立內核對象時就分配了所要的100M內存。好處是,別的進程可以通過這個內核對象共享這段內存,只要它也做了映射。
ü 稀疏內存映射文件
在虛擬內存一節中,提到了電子表格程序。虛擬內存解決了表示很少單元格有數據但必須分配所有內存的內存浪費問題;但是,如果想在多個進程之間共享這個電子表格結構呢?
如果用基於頁文件的內存映射,需要先分配頁文件,還是浪費了空間,沒有了虛擬內存的優點。
Windows提供了稀疏提交的內存映射機制。
當使用CreateFileMapping時,保護屬性用SEC_RESERVE時,其不提交物理存儲器,使用SEC_COMMIT時,其馬上提交物理存儲器。注意,只有文件句柄為INVALID_HANDLE_VALUE時,才能使用這兩個參數。
按照通常的方法映射時,系統只保留進程地址空間,不會提交物理存儲器。
當需要提交物理內存時才提交,利用通常的VirtualAlloc函數就可以提交。
當釋放內存時,不能調用VirtualFree函數,只能調用UnmapViewOfFile來撤銷映射,從而釋放內存。
C++程序如下:
HANDLE hVirtualMap=CreateFileMapping(INVALID_HANDLE_VALUE,NULL,PAGE_READWRITE|SEC_RESERVE,0,100000000,L"Yeming-Map-Virtual");
if(hPageMap==NULL)
cout<<"建立基於頁文件的稀疏內存映射對象失敗!"<<endl;
MEMORYSTATUS memStatus8;
GlobalMemoryStatus(&memStatus8);
cout<<"建立基於頁文件的稀疏內存映射文件后的空間:"<<endl;
cout<<"減少物理內存="<<memStatus7.dwAvailPhys-memStatus8.dwAvailPhys<<endl;
cout<<"減少可用頁文件="<<memStatus7.dwAvailPageFile-memStatus8.dwAvailPageFile<<endl;
cout<<"減少可用進程空間="<<memStatus7.dwAvailVirtual-memStatus8.dwAvailVirtual<<endl<<endl;
LPVOID pVirtualMAP=MapViewOfFile(hVirtualMap,FILE_MAP_WRITE,0,0,0);
cout<<"內存映射進程后的空間:"<<endl;
if(pVirtualMAP==NULL)
cout<<"映射進程空間失敗!"<<endl;
else
printf("首地址=%x\n",pVirtualMAP);
MEMORYSTATUS memStatus9;
GlobalMemoryStatus(&memStatus9);
cout<<"減少物理內存="<<memStatus8.dwAvailPhys-memStatus9.dwAvailPhys<<endl;
cout<<"減少可用頁文件="<<memStatus8.dwAvailPageFile-memStatus9.dwAvailPageFile<<endl;
cout<<"減少可用進程空間="<<memStatus8.dwAvailVirtual-memStatus9.dwAvailVirtual<<endl<<endl;
結果如下:
用了SEC_RESERVE后,只是建立了一個內存映射對象,和普通的一樣;不同的是,它映射完后,得到了一個虛擬進程空間。現在,這個空間沒有分配任何的物理存儲器給它,你可以用VirtualAlloc 提交存儲器給它,詳細請參考上一篇<虛擬內存(VM)>。
注意,你不可以用VirtualFree來釋放了,只能用UnmapViewOfFile來。
C++程序如下:
LPVOID pP=VirtualAlloc(pVirtualMAP,100*1000*1000,MEM_COMMIT,PAGE_READWRITE);
MEMORYSTATUS memStatus10;
GlobalMemoryStatus(&memStatus10);
cout<<"減少物理內存="<<memStatus9.dwAvailPhys-memStatus10.dwAvailPhys<<endl;
cout<<"減少可用頁文件="<<memStatus9.dwAvailPageFile-memStatus10.dwAvailPageFile<<endl;
cout<<"減少可用進程空間="<<memStatus9.dwAvailVirtual-memStatus10.dwAvailVirtual<<endl<<endl;
bool result=VirtualFree(pP,100000000,MEM_DECOMMIT);
if(!result)
cout<<"釋放失敗!"<<endl;
result=VirtualFree(pP,100000000,MEM_RELEASE);
if(!result)
cout<<"釋放失敗!"<<endl;
CloseHandle(hVirtualMap);
MEMORYSTATUS memStatus11;
GlobalMemoryStatus(&memStatus11);
cout<<"增加物理內存="<<memStatus11.dwAvailPhys-memStatus10.dwAvailPhys<<endl;
cout<<"增加可用頁文件="<<memStatus11.dwAvailPageFile-memStatus10.dwAvailPageFile<<endl;
cout<<"增加可用進程空間="<<memStatus11.dwAvailVirtual-memStatus10.dwAvailVirtual<<endl<<endl;
result=UnmapViewOfFile(pVirtualMAP);
if(!result)
cout<<"撤銷映射失敗!"<<endl;
MEMORYSTATUS memStatus12;
GlobalMemoryStatus(&memStatus12);
cout<<"增加物理內存="<<memStatus12.dwAvailPhys-memStatus11.dwAvailPhys<<endl;
cout<<"增加可用頁文件="<<memStatus12.dwAvailPageFile-memStatus11.dwAvailPageFile<<endl;
cout<<"增加可用進程空間="
<<memStatus12.dwAvailVirtual-memStatus11.dwAvailVirtual<<endl<<endl;
結果如下:
可以看見,用VirtualFree是不能夠釋放這個稀疏映射的;最后用UnmapViewOfFile得以釋放進程空間和物理內存。
(五):堆
5. 內存管理機制--堆 (Heap)
· 使用場合
堆是進程創建時在進程空間建立的區域,由堆管理器來管理。一個進程可以有很多個堆。進程有一個默認堆為1M,可以動態的擴大。
當程序需要管理很多小對象時,適合用堆;當需要的空間大於1M時,最好用虛擬內存來管理。
堆的優點是,有堆管理器來替它管理,不需管理具體的事情如頁面邊界
和分配粒度等問題,你可以從調用函數看的出來,比VirtualAlloc的參數少了
不少。
堆的缺點是分配和釋放的速度比前幾種機制要慢,所以最好不要超過
1M;不像虛擬內存那樣隨時提交和釋放,因為它是由堆管理器決定的。如果
用堆分配1G的空間,需要1分種,而用虛擬內存,則感覺不到任何延遲。
· 默認堆
進程默認堆是供所有線程使用的,每當線程需要從堆中分配釋放內存區時,系
統會同步堆,所以訪問速度較慢。
它的默認大小是1M,同樣的,你可以通過以下鏈接命令改變其大小:
#pragma comment(linker,"/HEAP:102400000,1024000")
第一個值是堆的保留空間,第二個值是堆開始時提交的物理內存大小。本文將堆改變為100M。
當你在程序中擴大了堆提交的物理內存時,進程運行時,物理內存將減少擴大的數量。但是,默認堆總是可以擴大的,不能限制它的最大值。
當你在程序中擴大了堆保留的空間時,進程運行時,可用進程空間將會減少擴大的數量。
每次你用New操作符分配內存時,進程空間會相應的減少,物理內存也會相應的減少。
一個重要的提示,本文經過測試,如果你需要的內存塊大部分都超過512K,那么,建堆時給它的初始大小不應該很大,因為,如果你所需內存塊大於512K的話,它不是從堆中分配的,也就是說不用堆中默認的空間,但其仍然屬於堆管理。
默認堆的一個用處是系統函數需要利用它運行。比如,Windows2000的字符集是UNICODE的,如果調用ANSI版本的函數,系統需要利用堆來從ANSI到UNICODE的轉換,調用UNICODE版本的函數。
· 自建堆
ü 使用場合
保護數據結構:
將不同的數據結構存在不同的堆中,可以防止不同的結構之間由於指針誤操作而破壞了它們。
消除內存碎片:
將大小不同的結構保存在一個堆中,會導致碎片的產生,比如釋放一個小結構時,大結構也不能利用它。
獨享堆的快速:
如果用默認堆的話,線程之間是同步訪問,速度慢;如果創建獨享堆,則系統可以不需同步,比較快。
第二個快速體現在釋放的快速,默認堆中,你只能釋放某個內存塊,而不能釋放整個堆;而獨享堆可以一次釋放堆,也就是釋放了所有的內存塊。
ü 開始使用
建立堆:
使用以下API
HANDLE HeapCreate(DWORD 選項,SIZE_T 初始大小,SIZE_T 最大值)
“選項” 取值為0 ,不是以下任意一個
HEAP_NO_SERIALIZE,系統無需同步堆
HEAP_GENERATE_EXCEPTIONS,當創建失敗或分配失敗時產生異常。
“初始大小”是堆的大小,系統會規整到頁面的整數倍,如0~4096的任何數都為4096;但是,進程空間至少要64K。
“最大值”是堆允許的最大值;為0則無限。
使用HEAP_NO_SERIALIZE需確定只有單線程訪問這個堆,否則有可能破壞堆;或程序有同步代碼來同步堆。
C++程序如下:
pHeap=(char*)GetProcessHeap();
printf("默認堆地址=%x\n",pHeap);
MEMORYSTATUS memStatus2;
GlobalMemoryStatus(&memStatus2);
HANDLE hHeap=HeapCreate(HEAP_NO_SERIALIZE|HEAP_GENERATE_EXCEPTIONS,1024*1024*50,0);
char* pHeap=(char*)hHeap;
printf("新建堆1地址=%x\n",pHeap);
if(hHeap==NULL)
{
cout<<"創建堆失敗!"<<endl;
}
MEMORYSTATUS memStatus3;
GlobalMemoryStatus(&memStatus3);
cout<<"建立堆后:"<<endl;
cout<<"減少物理內存="<<memStatus2.dwAvailPhys-memStatus3.dwAvailPhys<<endl;
cout<<"減少可用頁文件="<<memStatus2.dwAvailPageFile-memStatus3.dwAvailPageFile<<endl;
cout<<"減少可用進程空間="<<memStatus2.dwAvailVirtual-memStatus3.dwAvailVirtual<<endl<<endl;
HANDLE hHeap2=HeapCreate(HEAP_NO_SERIALIZE|HEAP_GENERATE_EXCEPTIONS,1024*1024*10,0);
char* pHeap2=(char*)hHeap2;
printf("新建堆2地址=%x\n",pHeap2);
結果如下:
當建立堆1時,它分配了50M的物理內存給堆使用;當建立堆2時,堆2的地址是0x04bc 0000=0x019c 0000+50*1024*1024.
分配內存:
使用以下API
PVOID HeapAlloc(HANDLE 堆句柄,DWORD 選項,SIZE_T 字節數)
“選項”可以是,
HEAP_ZERO_MEMORY,所有字節初始化為0
HEAP_NO_SERIALIZE,堆這個內存區獨享
HEAP_GENERATE_EXCEPTIONS,產生異常。如果創建堆有了它就不用再設了。異常可能為:STATUS_NO_MEMOR(無足夠內存)和STATUS_ACCESS_VIOLATION(堆被破壞,分配失敗)。
C++程序如下:
GlobalMemoryStatus(&memStatus3);
PVOID pV=HeapAlloc(hHeap,
HEAP_ZERO_MEMORY|HEAP_NO_SERIALIZE|HEAP_GENERATE_EXCEPTIONS,1024*507);
if(pV==NULL)
{
cout<<"分配堆內存失敗!"<<endl;
}
char * pC=(char*)pV;
printf("第一次分配地址=%x\n",pC);
MEMORYSTATUS memStatus4;
GlobalMemoryStatus(&memStatus4);
cout<<"第一次堆分配后:"<<endl;
cout<<"減少物理內存="<<memStatus3.dwAvailPhys-memStatus4.dwAvailPhys<<endl;
cout<<"減少可用頁文件="<<memStatus3.dwAvailPageFile-memStatus4.dwAvailPageFile<<endl;
cout<<"減少可用進程空間="<<memStatus3.dwAvailVirtual-memStatus4.dwAvailVirtual<<endl<<endl;
PVOID pV2=HeapAlloc(hHeap,
HEAP_ZERO_MEMORY|HEAP_NO_SERIALIZE|HEAP_GENERATE_EXCEPTIONS,1024*508);
if(pV2==NULL)
{
cout<<"分配堆內存失敗!"<<endl;
}
char * pC2=(char*)pV2;
printf("第二次分配地址=%x\n",pC2);
MEMORYSTATUS memStatus5;
GlobalMemoryStatus(&memStatus5);
cout<<"第二次堆分配后:"<<endl;
cout<<"減少物理內存="<<memStatus4.dwAvailPhys-memStatus5.dwAvailPhys<<endl;
cout<<"減少可用頁文件="<<memStatus4.dwAvailPageFile-memStatus5.dwAvailPageFile<<endl;
cout<<"減少可用進程空間="<<memStatus4.dwAvailVirtual-memStatus5.dwAvailVirtual<<endl<<endl;
for(int i=0;i<200*1024;i++)
pC2[i]=9;
MEMORYSTATUS memStatus10;
GlobalMemoryStatus(&memStatus10);
cout<<"第二次堆使用一半后:"<<endl;
cout<<"減少物理內存="<<memStatus5.dwAvailPhys-memStatus10.dwAvailPhys<<endl;
cout<<"減少可用頁文件="<<memStatus5.dwAvailPageFile-memStatus10.dwAvailPageFile<<endl;
cout<<"減少可用進程空間="
<<memStatus5.dwAvailVirtual-memStatus10.dwAvailVirtual<<endl<<endl;
結果如下:
可以看出,第一次分配507K的地址為0x04ad d650<0x04bc 0000,它是在堆中分配的;第二次分配508K的地址為0x055c 0020>0x04bc 0000,它是在堆外分配的;無論在多大的堆中,只要分配內存塊大於507K時,都會在堆外分配,但是,它像在堆中一樣,存在堆的鏈接表中,受堆管理。分配時,系統使用的是虛擬頁文件;只有在真正使用時,才會分配物理內存。
至於為什么分配大於507K會在堆外分配而不直接使用堆中的內存,目前仍然不清楚。
改變大小:
PVOID HeapReAlloc(HANDLE 堆句柄,DWORD 選項,PVOID 舊內存塊地址,SIZE_T 新內存塊大小)
“選項”除了以上三個外,還有HEAP_REALLOC_IN_PLACE_ONLY,指定不能移動原有內存塊的地址。
C++程序如下:
GlobalMemoryStatus(&memStatus4);
PVOID pV2New=HeapReAlloc(hHeap,0,pV2,1024*1024*2);
if(pV2New!=NULL)
{
char * pC2New=(char*)pV2New;
printf("改變分配地址=%x\n",pC2New);
cout<<pC2New[0]<<endl;
//cout<<pC2[0]<<endl;出現訪問違規
SIZE_T lenNew=HeapSize(hHeap,0,pV2New);
cout<<"改變后大小="<<lenNew<<endl;
}
GlobalMemoryStatus(&memStatus5);
cout<<"改變分配后:"<<endl;
cout<<"減少物理內存="<<memStatus4.dwAvailPhys-memStatus5.dwAvailPhys<<endl;
cout<<"減少可用頁文件="<<memStatus4.dwAvailPageFile-memStatus5.dwAvailPageFile<<endl;
cout<<"減少可用進程空間="
<<memStatus4.dwAvailVirtual-memStatus5.dwAvailVirtual<<endl<<endl;
結果如下:
可以看出,新內存塊緊接着原來內存塊結束的地方開始創建,大小為2M;原來的內存塊的內容被銷毀和釋放,所以新內存塊只減少了增加的內存量。一個缺點就是,新內存塊居然不保留原來內存的內容!另外,如果采用HEAP_REALLOC_IN_PLACE_ONLY的話,出現Not Enough Quote異常。也就是說,當前內存的狀況是,必須移動才可以擴大此內存塊。
查詢內存:
可以查詢堆中一個內存塊的大小。
SIZE_T HeapSize(HANDLE 堆句柄,DWORD 選項,LPVOID 內存塊地址)
“選項”可為0或HEAP_NO_SERIALIZE。
參考以上例子。
釋放內存塊:
BOOL HeapFree(HANDLE 堆句柄,DWORD 選項,PVOID 內存塊地址)
“選項”可為0或HEAP_NO_SERIALIZE。
C++程序如下:
GlobalMemoryStatus(&memStatus5);
HeapFree(hHeap,0,pV2New);
MEMORYSTATUS memStatus6;
GlobalMemoryStatus(&memStatus6);
cout<<"第二次堆分配釋放后:"<<endl;
cout<<"增加物理內存="<<memStatus6.dwAvailPhys-memStatus5.dwAvailPhys<<endl;
cout<<"增加可用頁文件="<<memStatus6.dwAvailPageFile-memStatus5.dwAvailPageFile<<endl;
cout<<"增加可用進程空間="<<memStatus6.dwAvailVirtual-memStatus5.dwAvailVirtual<<endl<<endl;
結果如下:
內存空間釋放了原來的2M空間。
釋放堆:
BOOL HeapDestroy(HANDLE 堆句柄)
不能用它釋放默認堆,系統忽略它的處理。
這一次,我們先在堆1中分配了70M的內存,由於它很大,所以,堆在堆外給它分配了內存,所以,堆1一共有50M+70M=120M。釋放程序如下:
PVOID pV4=HeapAlloc(hHeap,HEAP_ZERO_MEMORY|HEAP_NO_SERIALIZE|HEAP_GENERATE_EXCEPTIONS)
,1024*1024*70);
if(pV4==NULL)
{
cout<<"分配堆內存失敗!"<<endl;
}
char * pC4=(char*)pV4;
printf("第四次堆分配=%x\n",pC4);
MEMORYSTATUS memStatus9;
GlobalMemoryStatus(&memStatus9);
cout<<"分配堆內存后:"<<endl;
cout<<"減少物理內存="<<memStatus7.dwAvailPhys-memStatus9.dwAvailPhys<<endl;
cout<<"減少可用頁文件="<<memStatus7.dwAvailPageFile-memStatus9.dwAvailPageFile<<endl;
cout<<"減少可用進程空間="<<memStatus7.dwAvailVirtual-memStatus9.dwAvailVirtual<<endl<<endl;
SIZE_T len=HeapSize(hHeap,0,pV4);
cout<<"len="<<len<<endl;
bool re=HeapDestroy(hHeap);
if(re==false)
{
cout<<"釋放堆失敗!"<<endl;
}
MEMORYSTATUS memStatus8;
GlobalMemoryStatus(&memStatus8);
cout<<"釋放堆后:"<<endl;
cout<<"增加物理內存="<<memStatus8.dwAvailPhys-memStatus9.dwAvailPhys<<endl;
cout<<"增加可用頁文件="<<memStatus8.dwAvailPageFile-memStatus9.dwAvailPageFile<<endl;
cout<<"增加可用進程空間="<<memStatus8.dwAvailVirtual-memStatus9.dwAvailVirtual<<endl<<endl;
結果如下:
如所猜想一樣,釋放了120M內存。
獲取所有堆:
DWORD GetProcessHeaps(DWORD 數量,PHANDLE 句柄數組)
“數量”是你想獲取的堆數目;
“句柄數組”是獲得的堆句柄。
默認堆也可以獲取。
HANDLE handles[10];
memset(handles,0,sizeof(handles));
GetProcessHeaps(10,handles);
for(int i=0;i<10;i++)
cout<<"堆"<<i+1<<"="<<handles[i]<<endl;
結果如下:
可以看見,一共有8個堆,堆1是默認堆,堆7和堆8是本文建立的堆。另外5個不知來源。
驗證堆:
BOOL HeapValidate(HANDLE 堆句柄,DWORD 選項,LPVOID 內存塊地址)
“選項” 可為0或HEAP_NO_SERIALIZE;
“內存塊地址”為NULL時,驗證所有內存塊。
C++程序如下:
HANDLE handles[10];
memset(handles,0,sizeof(handles));
GetProcessHeaps(10,handles);
for(int i=0;i<10;i++)
{
cout<<"堆"<<i+1<<"="<<handles[i]<<" ";
if(HeapValidate(handles[i],0,NULL))
cout<<"驗證堆成功!"<<endl;
else
cout<<endl;
}
結果如下:
合並內存塊:
UINT HeapCompact(HANDLE 堆句柄,DWORD 選項)
“選項” 可為0或HEAP_NO_SERIALIZE;
此函數可以合並空閑內存塊。
其他函數:
HeapLock和HeapUnlock 通常是系統使用的;
HeapWalk可以遍歷堆內存,需要以上兩個函數。
· C++內存函數
Malloc和Free
這是C語言使用的函數,只能從默認堆中分配內存,並且只是分配內存,不能調用構造函數,且只是按字節分配,不能按類型分配。
New 和Delete
這是C++語言使用的函數,默認情況下從默認堆中分配內存,但是也可以通過重載New函數,從自建堆中按類型分配;同時可以執行構造函數和析構函數。它底層是通過HeapAlloc和HeapFree實現的。 依賴於編譯器的實現。
GlobalAlloc 和GlobalFree
這是比HeapAlloc和HeapFree更慢的函數,但是也沒有比它們更好的優點,只能在默認堆中分配;16位操作系統下利用它們分配內存。
LocalAlloc和LocalFree
在WindowsNT 內核里,和GlobalAlloc、GlobalFree是一樣的。
· 一個例子
默認情況下,New關鍵字是利用HeapAlloc在默認堆上建立對象。本文重載了類的New方法,使得類在自己的堆中存放,這樣可以與外面的對象隔離,以免重要的數據結構被意外破壞。由於類中的成員變量是在堆中存放,因此不局限於線程堆棧的1M空間。
C++程序如下:
class AllocateInOtherHeap
{
public:
AllocateInOtherHeap(void);
~AllocateInOtherHeap(void);
void* operator new(size_t size);
static HANDLE heap;
public:
//類對象唯一所需的空間
int iArray[1024*1024*10];
AllocateInOtherHeap::AllocateInOtherHeap(void)
{
cout<<"AllocateInOtherHeap()"<<endl;
//如果New函數沒有分配夠空間,那么此處會出現訪問違規
memset(iArray,0,sizeof(AllocateInOtherHeap));
iArray[1024]=8;
}
void* AllocateInOtherHeap::operator new(size_t size)
{
if(heap==NULL)
heap=HeapCreate(HEAP_NO_SERIALIZE|HEAP_GENERATE_EXCEPTIONS,1024*1024*10,0);
//分配足夠這個類對象的空間
void* p=HeapAlloc(heap,0,sizeof(AllocateInOtherHeap));
cout<<"堆的大小="<<HeapSize(heap,0,p)<<endl;
printf("AllocateInOtherHeap堆地址=%x\n",heap);
printf("AllocateInOtherHeap返回地址=%x\n",p);
return p;
}
AllocateInOtherHeap::~AllocateInOtherHeap(void)
{
cout<<"~AllocateInOtherHeap"<<endl;
}
void AllocateInOtherHeap::operator delete(void* p)
{
HeapFree(heap,0,p);
HeapDestroy(heap);
cout<<"delete()"<<endl;
}
};
結果如下:
可見,new函數先分配夠空間,然后才能初始化對象變量;而delete函數得先做析構,才能釋放空間。對象保存在堆外,因為大於512K;對象大小剛好是iArray變量的大小。
注意,如果沒有分配足夠的空間,雖然你可以得到對象指針,但是你訪問數據時可能會出現訪問違規,如果沒出現,那更慘,意味着你讀寫了別人的數據。
(六):堆棧
6. 內存管理機制--堆棧 (Stack)
· 使用場合
操作系統為每個線程都建立一個默認堆棧,大小為1M。這個堆棧是供函數調用時使用,線程內函數里的各種靜態變量都是從這個默認堆棧里分配的。
· 堆棧結構
默認1M的線程堆棧空間的結構舉例如下,其中,基地址為0x0004 0000,剛開始時,CPU的堆棧指針寄存器保存的是棧頂的第一個頁面地址0x0013 F000。第二頁面為保護頁面。這兩頁是已經分配物理存儲器的可用頁面。
隨着函數的調用,系統將需要更多的頁面,假設需要另外5頁,則給這5頁提交內存,刪除原來頁面的保護頁面屬性,最后一頁賦予保護頁面屬性。
當分配倒數第二頁0x0004 1000時,系統不再將保護屬性賦予它,相反,它會產生堆棧溢出異常STATUS_STACK_OVERFLOW,如果程序沒有處理它,則線程將退出。最后一頁始終處於保留狀態,也就是說可用堆棧數是沒有1M的,之所以不用,是防止線程破壞棧底下面的內存(通過違規訪問異常達到目的)。
當程序的函數里分配了臨時變量時,編譯器把堆棧指針遞減相應的頁數目,堆棧指針始終都是一個頁面的整數倍。所以,當編譯器發現堆棧指針位於保護頁面之下時,會插入堆棧檢查函數,改變堆棧指針及保護頁面。這樣,當程序運行時,就會分配物理內存,而不會出現訪問違規。
· 使用例子
改變堆棧默認大小:
有兩個方法,一是在CreateThread()時傳一個參數進去改變;
二是通過鏈接命令:
#pragma comment(linker,"/STACK:102400000,1024000")
第一個值是堆棧的保留空間,第二個值是堆棧開始時提交的物理內存大小。本文將堆棧改變為100M。
堆棧溢出處理:
如果出現堆棧異常不處理,則導致線程終止;如果你只做了一般處理,內 存
結構已經處於破壞狀態,因為已經沒有保護頁面,系統沒有辦法再拋出堆棧溢
出異常,這樣的話,當再次出現溢出時,會出現訪問違規操作
STATUS_ACCESS_VIOLATION,這是線程將被系統終止。解決辦法是,恢復
堆棧的保護頁面。請看以下例子:
C++程序如下:
bool handle=true;
static MEMORY_BASIC_INFORMATION mi;
LPBYTE lpPage;
//得到堆棧指針寄存器里的值
_asm mov lpPage, esp;
// 得到當前堆棧的一些信息
VirtualQuery(lpPage, &mi, sizeof(mi));
//輸出堆棧指針
printf("堆棧指針=%x\n",lpPage);
// 這里是堆棧的提交大小
printf("已用堆棧大小=%d\n",mi.RegionSize);
printf("堆棧基址=%x\n",mi.AllocationBase);
for(int i=0;i<2;i++)
{
__try
{
__try
{
__try
{
cout<<"**************************"<<endl;
//如果是這樣靜態分配導致的堆棧異常,系統默認不拋出異常,捕獲不到
//char a[1024*1024];
//動態分配棧空間,有系統調用Alloca實現,自動釋放
Add(1000);
//系統可以捕獲違規訪問
int * p=(int*)0xC00000000;
*p=3;
cout<<"執行結束"<<endl;
}
__except(GetExceptionCode()==STATUS_ACCESS_VIOLATION ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
cout<<"Excpetion 1"<<endl;
}
}
__except(GetExceptionCode()==STATUS_STACK_OVERFLOW ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
cout<<"Exception 2"<<endl;
if(handle)
{
//做堆棧破壞狀態恢復
LPBYTE lpPage;
static SYSTEM_INFO si;
static MEMORY_BASIC_INFORMATION mi;
static DWORD dwOldProtect;
// 得到內存屬性
GetSystemInfo(&si);
// 得到堆棧指針
_asm mov lpPage, esp;
// 查詢堆棧信息
VirtualQuery(lpPage, &mi, sizeof(mi));
printf("壞堆棧指針=%x\n",lpPage);
// 得到堆棧指針對應的下一頁基址
lpPage = (LPBYTE)(mi.BaseAddress)-si.dwPageSize;
printf("已用堆棧大小=%d\n",mi.RegionSize);
printf("壞堆棧基址=%x\n",mi.AllocationBase);
//釋放准保護頁面的下面所有內存
if (!VirtualFree(mi.AllocationBase,
(LPBYTE)lpPage - (LPBYTE)mi.AllocationBase,
MEM_DECOMMIT))
{
exit(1);
}
// 改頁面為保護頁面
if (!VirtualProtect(lpPage, si.dwPageSize,
PAGE_GUARD | PAGE_READWRITE,
&dwOldProtect))
{
exit(1);
}
}
printf("Exception handler %lX\n", _exception_code());
}
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
cout<<"Default handler"<<endl;
}
}
cout<<"正常執行"<<endl;
//分配空間,耗用堆棧
char c[1024*800];
printf("c[0]=%x\n",c);
printf("c[1024*800]=%x\n",&c[1024*800-1]);
}
void ThreadStack::Add(unsigned long a)
{
//深遞歸,耗堆棧
char b[1000];
if(a==0)
return;
Add(a-1);
}
程序運行結果如下:
可以看見,在執行遞歸前,堆棧已被用了800多K,這些是在編譯時就靜態決定了。它們不再占用進程空間,因為堆棧占用了默認的1M進程空間。分配是從棧頂到棧底的順序。
當第一次遞歸調用后,系統捕獲到了它的溢出異常,然后堆棧指針自動恢復到原來的指針值,並且在異常處理里,更改了保護頁面,確保第二次遞歸調用時不會出現訪問違規而退出線程,但是,它仍然會導致堆棧溢出,需要動態的增加堆棧大小,本文沒有對這個進行研究,但是試圖通過分配另外內存區,改變堆棧指針,但是沒有奏效。
注意:在一個線程里,全局變量加上任何一個函數里的臨時變量,如果超過堆棧大小,當調用這個函數時,都會出現堆棧溢出,這種溢出系統不會拋出堆棧溢出異常,而直接導致線程退出。
對於函數1調用函數2,而函數n-1又調用函數n的嵌套調用,每層調用不算臨時變量將損失240字節,所以默認線程最多有1024*(1024-2)/240=4360次調用。加上函數本身有變量,這個數目會大大減少。
至此,內存管理機制完全介紹完畢! 謝謝光顧!