全面介紹Windows內存管理機制及C++內存分配實例

轉自:http://blog.csdn.net/yeming81/article/details/2046193

本文基本上是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 );

            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得以釋放進程空間和物理內存。


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次調用。加上函數本身有變量,這個數目會大大減少。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章