windows底層內存管理技術

1.1. 物理地址

在物理存儲器上的內存地址,一般由內核管理,應用程序無法直接得到。

1.2. 虛擬地址

在進程私有空間中的地址,即應用程序指針所指向的地址值。

1.3. 尋址空間

進程所能夠範圍的地址空間範圍,跟指針的位數有關,指針的位數取決於cpu字長,32位指針的地址空間範圍爲4GB,64位指針的地址空間範圍爲1 6 E B。

2. windows內存結構

2.1. 虛擬地址空間的管理

對於32位多任務的windows操作系統來說,每個進程都在自己的私有地址空間(虛擬地址空間)運行,因此當進程中的一個線程正在運行時,該線程可以訪問只屬於它的進程的內存。屬於所有其他進程的內存則隱藏着,並且不能被正在運行的線程訪問。

在win2k中,屬於內核的內存也是隱藏的,正在運行的線程無法訪問。這意味着線程不能直接訪問內核的數據。如果要想訪問內核數據,則必須通過系統調用(系統win32 api)來操作,否則會引發一個內存錯誤異常。

Win98中,屬於操作系統的內存是不隱藏的,正在運行的線程可以訪問。因此,正在運行的線程常常可以訪問操作系統的數據,也可以破壞操作系統(從而有可能導致操作系統崩潰)。

在Win98中,一個進程的線程不可能訪問屬於另一個進程的內存,與win2k相同。

建議無論在win98還是win2k中,都採用系統調用來訪問內核(操作系統內存)。

2.2. 虛擬地址空間的劃分

雖然32位的應用程序理論上可以訪問4GB的地址空間,但是真正可以使用的地址空間並沒有那麼多。

每個進程的虛擬地址空間都要劃分成各個分區。地址空間的分區是根據操作系統的基本實現方法來進行的。不同的Windows內核,其分區也略有不同。winxp的內存結構與win2k相同。

進程的地址空間分區表

分區

32位Windows 2000(x86和Alpha處理器)

32位Windows 2000(x86w/3GB用戶方式)

64位Windows 2000(Alpha和IA-64處理器)

Windows 98

N U L L指針分配的分區

0 x 0 0 0 0 0 0 0 0 0 x 0 0 0 0 F F F F

0 x 0 0 0 0 0 0 0 0 0 x 0 0 0 0 F F F F

0x00000000 00000000 0x00000000 0000FFFF

0 x 0 0 0 0 0 0 0 0 0 x 0 0 0 0 0 F F F

DOS/16位Windows應用程序兼容分區

0 x 0 0 0 0 0 1 0 0 0 0 x 0 0 3 F F F F F

用戶方式

0 x 0 0 0 1 0 0 0 0 0 x 7 F F E F F F F

0 x 0 0 0 1 0 0 0 0 0 x B F F E F F F F F

0x00000000 00010000 0x000003FF FFFEFFFF

0 x 0 0 4 0 0 0 0 0 0 x 7 F F F F F F F

64-KB

0 x 7 F F F 0 0 0 0

0 x B F F F 0 0 0 0

0 x 0 0 0 0 0 3 F F F F F F 0 0 0 0

禁止進入

0 x 7 F F F F F F F

0 x B F F F F F F F

0 x 0 0 0 0 0 3 F F F F F F F F F F

共享的MMF分區

0 x 8 0 0 0 0 0 0 0

文件(MMF)內核方式

0 x 8 0 0 0 0 0 0 0 0 0 x F F F F F F F F

0 x C 0 0 0 0 0 0 0 0 x F F F F F F F F

0x00000400 00000000 0xFFFFFFFFF FFFFFFF

0 x B F F F F F F F 0 x C 0 0 0 0 0 0 0 0 x F F F F F F F F

3 2位Windows 2000的內核與6 4位Windows 2000的內核擁有大體相同的分區,差別在於分區的大小和位置有所不同。另一方面,可以看到Windows 98下的分區有着很大的不同。

NULL指針分配的分區:爲了幫助程序員掌握N U L L指針的分配情況。如果你的進程中的線程試圖讀取該分區的地址空間的數據,或者將數據寫入該分區的地址空間,那麼C P U就會引發一個訪問違規。保護這個分區是極其有用的,它可以幫助你發現N U L L指針的分配情況。一般的c/c++編譯器都把NULL設置爲0,落在這個分區中。

MS-DOS/16Windows應用程序兼容分區(僅適用Win98):進程地址空間的這個4MB分區是Windows 98需要的,目的是維護MS - DOS應用程序與16位應用程序之間的兼容性。不應該試圖從32位應用程序來讀取該分區的數據,或者將數據寫入該分區。在理想的情況下,如果進程中的線程訪問該內存, CPU應該產生一個訪問違規,但是由於技術上的原因, Microsoft無法保護這個4MB的地址空間。

在Windows 2000中,16位MS-DOS與16位Windows應用程序是在它們自己的地址空間(其實是在虛擬機中)中運行的,32位應用程序不會對它們產生任何影響。

16位DOS程序的虛擬機就是cmd,16位windows程序使用的是系統虛擬機。

用戶方式分區:這個分區是進程的私有(非共享)地址空間所在的地方。

在Windows 2000中,所有的. e x e和DLL模塊均加載這個分區。每個進程可以將這些D L L加載到該分區的不同地址中(不過這種可能性很小)。系統還可以在這個分區中映射該進程可以訪問的所有內存映射文件。

在Windows 98中,主要的Win32系統DLL(Kernel32.dll,AdvAPI32.dll,User32.dll和GDI32.dll)均加載共享內存映射文件分區中。. e x e和所有其他D L L模塊則加載到這個用戶方式分區中。多個進程的共享D L L均位於相同的虛擬地址中,但是其他DLL可以將這些D L L加載到用戶方式分區的不同地址中(不過這種可能性不大)。另外,在Windows 98中,用戶方式分區中決不會出現內存映射文件。

在32位windows中,用戶分區的最大尋址空間大約爲2G,內核尋址空間爲3G。M i crosof t允許x 8 6的Windows 2000 Advanced Server版本和Windows 2000 Data Center版本將用戶方式分區擴大爲3 G B,內核分區壓縮爲1G。若要使所有進程都能夠使用3 G B用戶方式分區和1 G B內核方式分區,必須將/ 3 G B開關附加到系統的BOOT. INI文件的有關項目中。

在x86w/3GB和64位的windows中,若要使用2GB以上的用戶空間,該應用程序必須使用/ LARGEADDRESSAWARE 鏈接開關來創建。

64KB禁止進入的分區(適用於win2k):這個位於用戶方式分區上面的64 KB分區是禁止進入的,訪問該分區中的內存的任何企圖均將導致訪問違規。

共享的MMF分區(適用於win98):存放系統DLL、進程共享數據和內存映射文件。

內核方式分區:存放內核代碼。用於線程調度、內存管理、文件系統支持、網絡支持和所有設備驅動程序的代碼全部在這個分區加載。駐留在這個分區中的一切均可被所有進程共享。

在Windows 2000中,這些組件是完全受到保護的。如果你試圖訪問該分區中的內存地址,你的線程將會產生訪問違規,導致系統向用戶顯示一個消息框,並關閉你的應用程序。

在Windows 98中該分區中的數據是不受保護的。任何應用程序都可以從該分區讀取數據,也可以寫入數據,因此有可能破壞操作系統。

2.3. 地址空間的區域

當進程被創建並被賦予它的地址空間時,該可用地址空間的主體是空閒的,即未分配的。若要使用該地址空間的各個部分,必須通過調用VirtualAlloc函數來分配它裏邊的各個區域。對一個地址空間的區域進行分配的操作稱爲保留( reserving )。

每當你保留地址空間的一個區域時,系統要確保該區域從一個分配粒度的邊界開始。對於不同的CPU平臺來說,分配粒度是各不相同的。幾乎所有的CPU平臺(x86、32位Alpha、64位Alpha和IA-64)都使用64 KB這個相同的分配粒度。

當你保留地址空間的一個區域時,系統還要確保該區域的大小是系統的頁面大小的倍數。頁面是系統在管理內存時使用的一個內存單位。與分配粒度一樣,不同的C P U,其頁面大小也是不同的。x86使用的頁面大小是4 KB,而A l p h a使用的頁面大小則是8 KB。IA-64也使用8KB的頁面。但是,如果測試顯示使用更大的頁面能夠提高系統的總體性能,那麼Microsoft可以切換到更大的頁面(16KB或更大)。

系統有時會直接代表進程保留一些區域,比如用來存放進程環境塊PEB和線程環境塊TEB。

由於內核會做區域和頁面管理,所以它給應用程序保留的區域邊界可能不是64k邊界。

如果保留區域大小不是頁面大小的整數倍,則會圓整到比它大的最近的頁面倍數。比如,在x86平臺上頁面大小爲4K,申請保留10k內存時,系統會保留12K內存給你。

不再使用保留區域時,應該調用VirtualFree來釋放。

保留區域並不真正分配物理內存,只是佔用進程的地址空間而已。

如果要分配物理頁面,必須通過調用VirtualAlloc函數來提交保留區域。

2.4. 物理內存與頁文件

Windows虛擬內存是映射到磁盤上的頁文件。頁文件對應用程序透明。頁面調度算法在內核中實現。

虛擬內存的管理需要cpu和內核配合,cpu會判斷內存頁面是否在RAM中,否則會引發一個缺頁中斷通知操作系統內核,內核再進行頁面調度,根據某種算法淘汰、調入和調出頁面。

clip_image002

操作系統啓動一個.exe文件時,把.exe文件本身作爲一個頁文件處理(內存映射文件),這樣就大大減少了系統頁文件的大小。

把系統頁文件分散到不同的磁盤分區中,這樣可以提高讀寫效率。

注意軟盤上的應用程序是一次性映射到物理內存的,因爲安裝程序時經常需要更換軟盤。

2.5. 數據對齊

數據對齊主要和cpu和編譯器有關,跟操作系統關係不大。

當CPU訪問正確對齊的數據時,它的運行效率最高。當數據大小的數據模數的內存地址是0時,數據是對齊的。例如, W O R D值應該總是從被2除盡的地址開始,而D W O R D值應該總是從被4除盡的地址開始,如此等等。當C P U試圖讀取的數據值沒有正確對齊時, CPU可以執行兩種操作之一。即它可以產生一個異常條件,也可以執行多次對齊的內存訪問,以便讀取完整的未對齊數據值。

數據對齊更深入的說明,請查看另一篇文檔《深入研究字節對齊問題》。

2.6. 內存管理的幾種方法

windows提供了3種進行內存管理的方法,它們是:

• 虛擬內存,以頁面爲單位進行內存,最適合用來管理大型對象或結構數組。

• 內存映射文件,最適合用來管理大型數據流(通常來自文件)以及在單個計算機上運行的多個進程之間共享數據。

• 內存堆棧,最適合用來管理大量的小對象。

malloc、new、allocator等內存管理是在應用程序的標準庫中處理的,不屬於操作系統內存管理的範圍,故本文不做探討,在其他文檔中再做論述。

3. 進程堆棧

3.1. 簡介

堆棧可以用來分配許多較小的數據塊。

堆棧的優點是,可以不考慮分配粒度和頁面邊界之類的問題,集中精力處理手頭的任務。堆棧的缺點是,分配和釋放內存塊的速度比其他機制要慢,並且無法直接控制物理存儲器的提交和回收。

從內部來講,堆棧是保留的地址空間的一個區域。開始時,保留區域中的大多數頁面沒有被提交物理存儲器。當從堆棧中進行越來越多的內存分配時,堆棧管理器將把更多的物理存儲器提交給堆棧。物理存儲器總是從系統的頁文件中分配的,當釋放堆棧中的內存塊時,堆棧管理器將收回這些物理存儲器。

Microsoft並沒有以文檔的形式來規定堆棧釋放和收回存儲器時應該遵循的具體規則,Windows 98 與Windows 2000的規則是不同的。可以這樣說,Windows 98 更加註重內存的使用,因此只要可能,它就收回堆棧。Windows 2000更加註重速度,因此它往往較長時間佔用物理存儲器,只有在一段時間後頁面不再使用時,纔將它返回給頁文件。Microsoft常常進行適應性測試並運行各種不同的條件,以確定在大部分時間內最適合的規則。隨着使用這些規則的應用程序和硬件的變更,這些規則也會有所變化。如果瞭解這些規則對你的應用程序非常關鍵,那麼請不要使用堆棧。相反,可以使用虛擬內存函數(即VirtualAlloc和VirtualFree),這樣,就能夠控制這些規則。

3.2. 默認堆棧

當進程初始化時,系統在進程的地址空間中創建一個堆棧。該堆棧稱爲進程的默認堆棧。按照默認設置,該堆棧的地址空間區域的大小是1 MB。但是,系統可以擴大進程的默認堆棧,使它大於其默認值。當創建應用程序時,可以使用/ H E A P鏈接開關,改變堆棧的1 M B默認區域大小。/ H E A P鏈接開關的句法如下:/HEAP:reserve[,commit]

單個進程可以同時擁有若干個堆棧。這些堆棧可以在進程的壽命期中創建和撤消。但是,默認堆棧是在進程開始執行之前創建的,並且在進程終止運行時自動被撤消。不能撤消進程的默認堆棧。

可以通過調用GetProcessHeap函數獲取你的進程默認堆棧的句柄。

3.3. 輔助堆棧

由於某種原因需要創建輔助堆棧:

保護組件。

更加有效地進行內存管理。

更快的訪問效率。

減少線程同步的開銷。

迅速釋放。

3.3.1. 保護組件

把不同組件放到不同的堆棧中,可以防止當一個組件的堆棧出錯時影響另外一個組件。假設有兩個組件,一個處理鏈表數據,一個處理二叉樹數據,把它們放到不同的輔助堆棧中,當鏈表內的指針錯誤操作導致堆棧出錯不會影響到二叉樹的正確處理。

3.3.2. 更有效的內存管理

通過在堆棧中分配同樣大小的對象,就可以更加有效地管理堆棧,這樣可以避免內存碎片。

如果每個堆棧只包含大小相同的對象,那麼釋放一個對象後,另一個對象就可以恰好放入被釋放的對象空間中。

3.3.3. 更快的訪問效率

如果把相同類型的數據連續放在同一個堆中,這樣就可以大大減少cpu訪問不同頁面的次數,也可能大大減少訪問虛擬內存頁面的次數,因此會獲得更佳的內存訪問效率。

3.3.4. 減少線程的開銷

多個線程訪問進程的默認堆棧是串行操作的,要經常不停的同步互斥操作。如果某個線程的數據不需要與其他線程進行共享,則沒有必要和其他線程競爭默認堆棧的訪問權。此時創建線程自己的堆棧,可以減少不必要的加鎖、解鎖開銷。

3.3.5. 迅速釋放

將專用堆棧用於某些數據結構後,就可以釋放整個堆棧,而不必顯式釋放堆棧中的每個內存塊。比如把某個樹的數據結構放到一個獨立的堆棧中,釋放這個樹的數據結果就不用一個個節點的慢慢釋放,直接撤銷堆即可。如果這個樹的數據比較大的話,效果會比較明顯。

3.4. 堆棧函數

創建堆棧使用HeapCreate,從堆棧中分配內存HeapAlloc,改變堆棧內存大小HeapReAlloc,查詢堆棧內存塊大小HeapSize,釋放堆棧內存塊HeapFree,撤銷堆棧HeapDestroy。

HeapAlloc函數執行的操作:

1) 遍歷分配的和釋放的內存塊的鏈接表。

2) 尋找一個空閒內存塊的地址。

3) 通過將空閒內存塊標記爲“已分配”並分配內存塊。

4) 將新內存塊添加給內存塊鏈接表。

注意當你分配較大的內存塊(大約1 MB或者更大)時,最好使用VirtualAlloc函數,應該避免使用堆棧函數。

以上堆棧函數適用於win98和win2k。

C++中的new/delete要調用malloc/free,而malloc/free最終要調用上面的堆棧函數。

ToolHelp的各個函數可以用來枚舉進程的各個堆棧和這些堆棧中分配的內存塊。函數如下:Heap32First、Heap32Next、Heap32ListFirst和Heap32ListNext,適用於win98和win2k。

以下堆棧函數只適用於win2k:GetProcessHeaps(獲取進程多個堆棧的句柄)、HeapValidate(驗證堆棧完整性)、HeapCompact(合併空閒地址塊)、HeapLock/HeapUnlock(線程對堆棧加鎖/解鎖,如果在創建堆棧時未設置HEAP_NO_SERIALIZE,則在HeapAlloc和HeapFree時內部加鎖)、HeapWalk(遍歷堆棧,此時最好加鎖,防止有其他線程分配或釋放內存)。

4. 線程堆棧

4.1. windows 2000線程堆棧

每當創建一個線程時,系統就會爲線程的堆棧(每個線程有它自己的堆棧)保留一個堆棧空間區域,並將一些物理存儲器提交給這個已保留的區域。

按照默認設置,系統保留1 MB的地址空間並提交兩個頁面的內存。但是,這些默認值是可以修改的,方法是在你鏈接應用程序時設定Microsoft的鏈接程序的/STACK選項:/STACK:reserve[,commit]。

當創建一個線程的堆棧時,系統將會保留一個鏈接程序的/ STACK開關指明的地址空間區域。但是,當調用CreateThread或_beginthreadex函數時,可以重設原先提交的內存數量。這兩個函數都有一個參數,可以用來重載原先提交給堆棧的地址空間的內存數量。如果設定這個參數爲0,那麼系統將使用/ S TACK開關指明的已提交的堆棧大小值,即1 MB的保留區域,每次提交一個頁面的內存。

下圖顯示了在頁面大小爲4KB的計算機上的一個堆棧區域的樣子(保留的起始地址是0x08000000) 。該堆棧區域和提交給它的所有物理存儲器均擁有頁面保護屬性PAGE_READWRITE。

clip_image004

當保留了這個區域後,系統將物理存儲器提交給區域的頂部的兩個頁面。在允許線程啓動運行之前,系統將線程的堆棧指針寄存器設置爲指向堆棧區域的最高頁面的結尾處(一個非常接近0x08100000的地址)。這個頁面就是線程開始使用它的堆棧的位置。從頂部向下的第二個頁面稱爲保護頁面。當線程調用更多的函數來擴展它的調用樹狀結構時,線程將需要更多的堆棧空間。

可以看出棧是向下增長的。

每當線程試圖訪問保護頁面中的存儲器時,系統就會得到關於這個情況的通知。作爲響應,系統將提交緊靠保護頁面下面的另一個存儲器頁面。然後,系統從當前保護頁面中刪除保護頁面的保護標誌,並將它賦予新提交的存儲器頁面。這種方法使得堆棧存儲器只有在線程需要時纔會增加。最終,如果線程的調用樹繼續擴展,堆棧區域就會變成下圖所示的樣子。

clip_image006

假定線程的調用樹非常深,堆棧指針C P U寄存器指向堆棧內存地址0 x 0 8 0 0 3 0 0 4。這時,當線程調用另一個函數時,系統必須提交更多的物理存儲器。但是,當系統將物理存儲器提交給0 x 0 8 0 0 1 0 0 0地址上的頁面時,系統執行的操作與它給堆棧的其他內存區域提交物理存儲器時的操作並不完全一樣。

最底下的頁面總是被保留的,從來不會被提交。

完整的線程堆棧區域

clip_image008

當系統將物理存儲器提交給0x08001000地址上的頁面時,它必須再執行一個操作,即它要引發一個EXCEPTION_STACK_OVERFLOW 異常處理(在Wi nNT.h 文件中定義爲0 x C00000FD)。通過使用結構化異常處理(SEH),你的程序將能得到關於這個異常處理條件的通知,並且能夠實現適度恢復。

如果在出現堆棧溢出異常條件之後,線程繼續使用該堆棧,那麼在0 x080010 0 0地址上的頁面中的全部內存均將被使用,同時,該線程將試圖訪問從0 x 0 8 0 0 0 0 0 0開始的頁面中的內存。當該線程試圖訪問這個保留的(未提交的)內存時,系統就會引發一個訪問違規異常條件。如果在線程試圖訪問該堆棧時引發了這個訪問違規異常條件,線程就會陷入很大的麻煩之中。這時,系統就會接管控制權,並終止進程的運行—不僅終止線程的運行,而切終止整個進程的運行。

最後一個頁面始終被保留着。這樣做的目的是爲了防止不小心改寫進程使用的其他數據。

4.2. windows 98線程堆棧

在win98上,線程的堆棧前後都有一個64K的保護區塊,可以防止線程堆棧的上溢和下溢,這是win98的一個不錯的特色。

堆棧下溢的示例:

int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE,

PSTR pszCmdLine, int nCmdShow)

{

char szBuf[100];

szBuf[10000] = 0; // Stack underflow,注意棧是向下增長的(與地址相反)

return(0);

}

當該函數的賦值語句執行時,便嘗試訪問線程堆棧結尾處之外的內存。當然,編譯器和鏈接程序不會抓住上面代碼中的錯誤,但是,如果應用程序是在Windows 98下運行,那麼當該語句執行時,就會引發訪問違規。這是Windows 98的一個出色特性,而Windows 2000是沒有的。在Wi ndows2000中,可以在緊跟線程堆棧的後面建立另一個區域。如果出現這種情況,並且你試圖訪問你的堆棧外面的內存,那麼你將會破壞與進程的另一個部分相關的內存,而系統將不會發現這個情況。

4.3. c/c++運行庫線程堆棧檢查

C / C + +運行期庫包含一個堆棧檢查函數。當編譯源代碼時,編譯器將在必要時自動生成對該函數的調用。堆棧檢查函數的作用是確保頁面被適當地提交給線程的堆棧。

示例代碼:

void SomeFunction()

{

int nValues[4000];

// Do some processing with the array.

nValues[0] = 0; // Some assignment

}

該函數至少需要16 000個字節(4000 x sizeof(int),每個整數是4個字節)的堆棧空間,以便放置整數數組。通常情況下,編譯器生成的用於分配該堆棧空間的代碼只是將C P U的堆棧指針遞減16 000個字節。但是,在程序試圖訪問內存地址之前,系統並不將物理存儲器分配給堆棧區域的這個較低區域。

在使用4 KB或8 KB頁面的系統上,這個侷限性可能導致一個問題出現。如果初次訪問堆棧是在低於保護頁面的一個地址上進行的(如上面這個代碼中的賦值行所示),那麼線程將訪問已經保留的內存並且引發訪問違規。爲了確保能夠成功地編寫上面所示的函數,編譯器將插入對C運行期庫的堆棧檢查函數的調用。

當編譯程序時,編譯器知道你針對的C P U系統的頁面大小。x 8 6編譯器知道頁面大小是4K B,A l p h a編譯器知道頁面大小是8 KB。當編譯器遇到程序中的每個函數時,它能確定該函數需要的堆棧空間的數量。如果該函數需要的堆棧空間大於目標系統的頁面大小,編譯器將自動插入對堆棧檢查函數的調用。

下面這個僞代碼顯示了堆棧檢查函數執行什麼操作。之所以稱它是僞代碼,是因爲這個函數通常是由編譯器供應商用彙編語言來實現的:

// The C run-time library knows the page size for the target system.

#ifdef _M_ALPHA

#define PAGESIZE (8 * 1024) //8-KB page

#else

#define PAGESIZE (4 * 1024) //4-KB page

#endif

void StackCheck(int nBytesNeededFromStack)

{

//Get the stack pointer position.

//At this point, the stack pointer has NOT been decremented

//to account for the function's local variables.

PBYTE pbStackPtr = (CPU's stack pointer);

while(nBytesNeededFromStack >= PAGESIZE)

{

// Move down a page on the stack--should be a guard page.

pbStackPtr -= PAGESIZE;

// Access a byte on the guard page--forces new page to be

// committed and guard page to move down a page.

pbStackPtr[0] = 0;

// Reduce the number of bytes needed from the stack.

nBytesNeededFromStack -= PAGESIZE;

}

//Before returning, the StackCheck function sets the CPU's

//stack pointer to the address below the function's

//local variables.

}

5. 虛擬內存管理

注意:我們這裏說的虛擬內存指的是進程私有地址空間,而不是頁文件(也有把頁文件稱爲操作系統的虛擬內存)。

用於管理虛擬內存的函數可以用來直接保留一個地址空間區域,將物理存儲器(來自頁文件)提交給該區域,並且可以設置你自己的保護屬性。

5.1. 獲取系統內存信息

系統內存信息,比如頁面的大小,分配粒度大小、最小內存地址、最大內存地址等,都可以通過GetSystemInfo來獲取。

函數原型:VOID GetSystemInfo(LPSYSTEM_INFO psinf);

5.2. 獲取全局內存狀態

可以通過GlobalMemoryStatus來獲取全局內存狀態,比如整體物理內存大小、整體頁文件大小、進程虛擬內存大小、進程可用虛擬內存大小等。

函數原型:VOID GlobalMemoryStatus(LPMEMORYSTATUS pmst);

5.3. 查詢內存塊的有關信息

可以通過VirtualQuery/ VirtualQueryEx查詢內存塊的有關信息,如基地址、塊大小,存儲器類型和保護屬性等。

5.4. 保留和提交虛擬內存

通過VirtualAlloc可以保留或提交一塊虛擬內存空間。

保留的基地址被圓整爲64K的整數倍,保留的大小爲cpu頁面大小的整數倍。如果內存長期被保留不釋放,建議從最高地址往下分配,這樣可以把內存碎片放在用戶空間的末尾,此時需要在分配類型上設置或參數MEM_TOP_DOWN。

當保留一個區域後,必須將物理存儲器提交給該區域,然後才能訪問該區域中包含的內存地址。

系統從它的頁文件中將已提交的物理存儲器分配給一個區域。物理存儲器總是按頁面邊界和頁面大小的塊來提交的。

若要提交物理存儲器,必須再次調用VirtualAlloc函數。

提交物理存儲器時可以只提交部分區域,每次提交的頁面保護屬性頁可以不同。提交的大小(單位爲字節)會被操作系統圓整爲頁面大小的整數倍。

把分配類型設置爲MEM_RESERVE | MEM_COMMIT就可以保留並提交一塊虛擬內存空間。

5.5. 何時提交和回收虛擬內存

對於大塊不確定內存操作,可以先保留一個足夠大的內存區域,在需要時再提交物理內存,這樣可以節省大量的物理內存。

提交方式有以下4種:

1、總是提交。每次都調用VirtualAlloc提交物理內存,讓操作系統來判斷是否已經提交,這樣可能會導致大量的無效調用,因爲該頁面很可能已經提交過。

2、提交前查詢。先調用VirtualQuery查詢一下該內存塊是否被提交,然後決定是否調用VirtualAlloc。此方法只是減少了VirtualAlloc調用次數,效率可能比第一種還低。

3、跟蹤提交頁面。把已經提交的頁面都記錄起來,每次需要新內存時先看已經提交的頁面是否有足夠內存可用,否則調用VirtualAlloc提交物理內存。此方法效率較高,但是代碼可能比較複雜。

4、使用結構化異常處理(SEH)。但進程試圖寫一個未提交的保留頁面時,系統會觸發一個內存違規異常,在內存違規異常處理函數中提交物理內存,然後系統返回到異常觸發點處繼續執行指令,就好像什麼都沒有發生。此方法代碼清晰,效率很高,推薦使用。

使用VirtualFree可以回收全部的保留頁面(包括提交和未提交的),也可以只回收部分物理頁面。

物理頁面回收的3種方法:

1、 對象大小爲頁面的整數倍。刪除對象時直接回收相應的頁面。

2、 把每個頁面放置固定數目的對象。當頁面中所有的對象都刪除時,回收該頁面。

3、 低優先級定時回收。定時檢查每個頁面中的所有對象是否都釋放,如果是則回收該頁面,這種做法的好處是比較通用,而且可以在進程比較空閒時執行,缺點是代碼相對複雜。

5.6. 改變頁面保護屬性和復位內存頁面

可以通過VirtualProtect來改變內存保護屬性。例如,你編寫了一個用於管理鏈接表的代碼,將它的節點存放在一個保留區域中。可以設計一些函數,以便處理該鏈接表,這樣,它們就可以在每個函數開始運行時將已提交內存的保護屬性改爲PAGE_READWRITE ,然後在每個函數終止運行時將保護屬性重新改爲PAGE_NOACCESS。

通過這樣的設置,就能夠使鏈接表數據不受隱藏在程序中的其他錯誤的影響。如果進程中的任何其他代碼存在一個迷失指針,試圖訪問你的鏈接表數據,那麼就會引發訪問違規。當試圖尋找應用程序中難以發現的錯誤時,利用保護屬性是極其有用的。

可以通過在VirtualAlloc中設置MEM_RESET可以復位內存頁面,這些頁面會被操作系統設置爲未修改頁面,這樣在下次缺頁中斷時就可以直接把頁面文件的頁面加載到這些未修改頁面上,而不需要保存它們到頁文件中去。

5.7. 如何使用4G以上內存

1)在64位的cpu上安裝64位windows可以直接支持4G以上內存的訪問。

2)32位操作系統4G內存以上支持情況:

    WindowsNT4.0 Server與Enterprise版都屬於32位服務器操作系統,支持最大內存都只有4G。

Windows2000系列服務器版操作系統可支持容量最高的是數據中心版,可支持32G;高級服務器版只支持最高8G的內存容量;2000普通服務器版只支持最高4G的內存容量。

Windows2003 Enterprise支持最高32G的內存。

在32位cpu上訪問4G以上內存,這是通過X86的PAE(Intel Physical Address Extension)實現的。而windows實現起來的話相當與把內存分頁,頁表12位,物理地址24 位,組合在一起就是2的36次方,也就是64GB。

PAE需要處理器爲Intel Pentium Pro以上。

在cpu和操作系統支持的情況下,應用程序可以通過AWE來使用4G以上內存。

6. 內存映射文件

6.1. 基本概念

與虛擬內存一樣,內存映射文件可以用來保留一個地址空間的區域,並將物理存儲器提交給該區域。它們之間的差別是,物理存儲器來自一個已經位於磁盤上的文件,而不是系統的頁文件。一旦該文件被映射,就可以訪問它,就像整個文件已經加載內存一樣。

6.2. 用途

內存映射文件可以用於3個不同的目的:

• 系統使用內存映射文件,以便加載和執行.exe和DLL文件。這可以大大節省頁文件空間和應用程序啓動運行所需的時間。

• 可以使用內存映射文件來訪問磁盤上的數據文件。這使你可以不必對文件執行I/O操作,並且可以不必對文件內容進行緩存。

• 可以使用內存映射文件,使同一臺計算機上運行的多個進程能夠相互之間共享數據。Windows確實提供了其他一些方法,以便在進程之間進行數據通信,但是這些方法都是使用內存映射文件來實現的,這使得內存映射文件成爲單個計算機上的多個進程互相進行通信的最有效的方法。

6.3. 可執行程序和DLL的內存映射

可執行文件內存映射過程:

1) 系統找出在調用CreateProcess時設定的.exe文件。如果找不到這個.exe文件,進程將無法創建,CreateProcesss將返回FALSE。

2) 系統創建一個新進程內核對象。

3) 系統爲這個新進程創建一個私有地址空間。

4) 系統保留一個足夠大的地址空間區域,用於存放該.exe文件。該區域需要的位置在. e x e文件本身中設定。按照默認設置, .exe文件的基地址是0x00400000(這個地址可能不同於在6 4位Windows 2000上運行的6 4位應用程序的地址),但是,可以在創建應用程序的. exe文件時重載這個地址,方法是在鏈接應用程序時使用鏈接程序的/BASE選項。

5) 系統注意到支持已保留區域的物理存儲器是在磁盤上的.exe文件中,而不是在系統的頁文件中。

當.exe文件被映射到進程的地址空間中之後,系統將訪問.exe文件的一個部分,該部分列出了包含.exe文件中的代碼要調用的函數的DLL文件。然後,系統爲每個DLL文件調用LoadLibrary函數,如果任何一個DLL需要更多的DLL,那麼系統將調用LoadLibrary函數,以便加載這些DLL。

DLL內存映射過程:

1) 系統保留一個足夠大的地址空間區域,用於存放該D L L文件。該區域需要的位置在D L L文件本身中設定。按照默認設置, Microsoft的Visual C++ 建立的DLL文件基地址是0 x 10000000(這個地址可能不同於在64位Windows 2000上運行的64位DLL的地址)但是,你可以在創建DLL文件時重載這個地址,方法是使用鏈接程序的/BASE選項。Windows提供的所有標準系統DLL都擁有不同的基地址,這樣,如果加載到單個地址空間,它們就不會重疊。

2) 如果系統無法在該DLL的首選基地址上保留一個區域,其原因可能是該區域已經被另一個DLL或.exe佔用,也可能是因爲該區域不夠大,此時系統將設法尋找另一個地址空間的區域來保留該DLL。如果一個DLL無法加載到它的首選基地址,這將是非常不利的,原因有二。首先,如果系統沒有再定位信息,它就無法加載該DLL(可以在DLL創建時,使用鏈接程序的/FIXED開關,從DLL中刪除再定位信息,這能夠使DLL變得比較小,但是這也意味着該DLL必須加載到它的首選地址中,否則它就根本無法加載)。第二,系統必須在DLL中執行某些再定位操作。在Windows 98中,系統可以在頁面被轉入RAM時執行再定位操作。在Windows 2000中,這些再定位操作需要由系統的頁文件提供更多的存儲器,它們也增加了加載DLL所需要的時間量。

3) 系統會記錄當前DLL是映射到磁盤文件還是系統的頁文件中。

當所有的.exe和DLL文件都被映射到進程的地址空間之後,系統就可以開始執行.exe文件的啓動代碼。當.exe文件被映射後,系統將負責所有的分頁、緩衝和高速緩存的處理。例如,如果.exe文件中的代碼使它跳到一個尚未加載到內存的指令地址,那麼就會出現一個異常(缺頁中斷)。系統能夠捕捉這個異常,並且自動將這頁代碼從該文件的映像加載到一個RAM頁面。然後,系統將這個RAM頁面映射到進程的地址空間中的相應位置,並且讓線程繼續運行,就像這頁代碼已經加載了一樣。當然,這一切對應用程序透明。

所有的.exe和DLL映射文件的內容被分割爲不同的節。代碼放在一個節中,全局變量放在另一個節中。各個節按照頁面邊界來對齊。通過調用Get SystemInfo函數,應用程序可以確定正在使用的頁面的大小。在. e x e或D L L文件中,代碼節通常位於數據數據節的前面。

多個進程的.exe或DLL映射文件採用寫時拷貝的方法共享RAM和頁文件,這樣可以避免修改全局變量時對不同進程的影響。

6.4. 可執行程序或DLL的不同示例共享靜態數據

每個.exe或DLL文件的映像都由許多節組成。按照規定,每個標準節的名字均以圓點開頭。例如,當編譯你的程序時,編譯器會將所有代碼放入一個名叫.text的節中。該編譯器還將所有未經初始化的數據放入一個.bss節,而已經初始化的所有數據則放入.data節中。

.exe或D L L文件分節的屬性

屬性

含義

READ

該節中的字節可以讀取

WRITE

該節中的字節可以寫入

EXECUTE

該節中的字節可以執行

SHARED

該節中的字節可以被多個實例共享(本屬性能夠有效地關閉copy -on-write機制)

編譯器產生的標準節

節名

作用

.bss

未經初始化的數據

.CRT

C運行期只讀數據

.data

已經初始化的數據

.debug

調試信息

.didata

延遲輸入文件名錶

.edata

輸出文件名錶

.idata

輸入文件名錶

.rdata

運行期只讀數據

.reloc

重定位表信息

.rsrc

資源

.text

.exe或DLL文件的代碼

.tls

線程的本地存儲器

.xdata

異常處理表

要想在.exe或dll不同的實例間共享變量,必須滿足以下3個條件:

1、 創建分節。如:

#pragma data_seg("Shared")

LONG g_lInstanceCount = 0;

#pragma data_seg()

2、 變量必須初始化,否則該變量就被放到其他分節中,達不到共享的目的。

如:#pragma data_seg("Shared")

LONG g_lInstanceCount;

#pragma data_seg()

3、 必須把該分節設置爲共享屬性RWS。

可以在連接開關中設置CTION:Shared,RWS。

還可以在代碼中設置:#pragma comment(linker, "/SECTION:Shared,RWS")。

如:

#pragma data_seg("Shared")

volatile LONG g_lApplicationInstances = 0;

#pragma data_seg()

#pragma comment(linker, "/Section:Shared,RWS")

雖然可以創建共享節,但是,由於兩個原因, Microsoft並不鼓勵你使用共享節。第一,用這種方法共享內存有可能破壞系統的安全。第二,共享變量意味着一個應用程序中的錯誤可能影響另一個應用程序的運行,因爲它沒有辦法防止某個應用程序將數據隨機寫入一個數據塊。

6.5. 內存映射數據文件

操作系統可以將一個數據文件映射到進程的地址空間中。這樣,對大量的數據進行操作是非常方便的。

這種方法的最大優點是,系統能夠爲你管理所有的文件緩存操作。不必分配任何內存,或者將文件數據加載到內存,也不必將數據重新寫入該文件,或者釋放任何內存塊。但是,內存映射文件仍然可能出現因爲電源故障之類的進程中斷而造成數據被破壞的問題。

若要使用內存映射文件,必須執行下列操作步驟:

1)使用CreateFile創建或打開一個文件內核對象,該對象用於標識磁盤上你想用作內存映射文件的文件。

2) 使用CreateFileMapping創建一個文件映射內核對象,告訴系統該文件的大小和你打算如何訪問該文件。

3) 通過MapViewOfFile讓系統將文件映射對象的全部或一部分映射到你的進程地址空間中。

當完成對內存映射文件的使用時,必須執行下面這些步驟將它清除:

1) 使用UnmapViewOfFile告訴系統從你的進程的地址空間中撤消文件映射內核對象的映像。

2) 使用CloseHandle關閉文件映射內核對象。

3) 使用CloseHandle關閉文件內核對象。

注意讀寫權限分配規則:CreateFile≥CreateFileMapping≥MapViewOfFile。這個可以理解,後者都是基於前者進行操作的,不能超越基礎權限,另外給程序員帶來了一定的靈活性。

另外,如果CreateFileMapping或MapViewOfFile設置了寫時拷貝屬性,則往映射的頁面中寫數據時,內核會在系統的頁文件中創建新頁面並把原始頁面數據拷貝過來,然後把新創建的頁面地址映射到進程的虛擬空間,並把新頁面的屬性被設置爲讀寫屬性,之後對數據的任何修改都是在私有頁面上進行,對映射的數據文件沒有任何影響。

注意:設置了寫時拷貝屬性的頁面,在撤銷內存文件映射時系統會回收物理頁面,所有的修改都會丟失。

可以使用FlushViewOfFile強制系統把修改過的部分或全部頁面數據寫入數據文件,因爲系統有自己的頁面管理策略,可能不會馬上把緩存數據寫入數據文件。

Windows 98不支持寫時拷貝屬性。

6.6. 內存映射處理大文件

首先映射一個文件的開頭的視圖。當完成對文件的第一個視圖的訪問時,可以取消它的映像,然後映射一個從文件中的一個更深的位移開始的新視圖。必須重複這一操作,直到訪問了整個文件。這使得大型內存映射文件的處理不太方便,但是,幸好大多數文件都比較小,因此不會出現這個問題。

6.7. 內存映射與數據視圖的相關性

相同內存映射對象的不同視圖,如果它們有部分重疊,則重疊部分在進程的虛擬地址空間上有多個拷貝,但都對應於相同的物理頁面,不浪費物理內存。這種情況對於同一進程或不同進程之間都是如此。

但是如果是相同文件的不同內存映射對象,則重疊部分的物理頁面可能會重複加載,可能造成物理內存浪費。爲什麼說是可能,因爲你雖然有重疊,但是如果你不訪問重疊部分的文件頁面,就不會加載到RAM。

如果CreateFile時沒有阻止其他進程對這個文件的寫訪問,則有可能導致內存映射中RAM頁面內容和原始文件不一致。

可以使用MapViewOfFileEx把文件內容映射到特定地址,只要地址是64k的整數倍。

關閉視圖只是釋放虛擬內存地址,並不釋放物理頁面,只有內存映射文件對象的引用計數爲0時才釋放物理頁面。

6.8. 使用內存映射文件在多個進程間共享數據

儘管windows有多種進程間通信機制,如:RPC、COM、OLE、DDE、窗口消息(尤其是WM_COPYDATA)、剪貼板、郵箱、管道和套接字等。但是,在同一個主機上還是內存映射文件的效率最高。

內存映射可以使用普通磁盤文件,也可以使用系統頁文件。如果只是共享和交換數據,使用磁盤文件很不方便,此時推薦使用系統頁文件。

使用系統頁文件就不用創建文件了,只需要像通常那樣調用CreateFileMapping函數,並且傳遞INVALID_HANDLE_VALUE作爲hFile參數即可,其他用法與磁盤文件相同。

與所有內核對象一樣,可以使用3種方法與多個進程共享內存映射文件對象,這3種方法是句柄繼承性、句柄命名和句柄複製。

採用句柄對象命名的方法可讀性較好,推薦使用。在一個進程中調用CreateFileMapping創建內存映射文件對象,在另一個進程中使用OpenFileMapping打開內存映射文件對象,然後建立視圖,分別映射文件的相同區塊到自己的進程空間中就可以實現數據共享。

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