程式的虛擬地址空間——NULL指標分割槽

查志強發表於2015-01-13

【原文:http://blog.chinaunix.net/uid-14735472-id-3987296.html

程式的虛擬地址空間

昨晚看到了深夜,終於對程式的虛擬地址空間有了個大致的瞭解,很激動,也很欣慰。回頭想來,一個程式設計師,真的應該知道這些知識,否則還真不太稱職。
首先告訴大家,我後面提到的這些知識在《windows核心程式設計》中都有,強烈建議大家把這本書翻翻,我相信會對你的程式設計境界拔高好幾個層次的。可是我最近沒那麼多時間,因此就只能瞭解個大概,然後等今後閒暇時再看這本書吧。
昨天我媳婦還反覆和我說:學東西必須要有選擇,不能對IT行業的所有知識亂學習,而且不要學那種實際意義不大的知識或是容易被淘汰的知識。其實她說的蠻對 的,但是我要說,有關《windows核心程式設計》裡的知識永遠都不會過時,因為它侵入到底層和內部了,就像C++,你覺得會過時嗎?就像windows永 遠不會被淘汰一樣,呵呵。

下面我就來粗略的說說我瞭解的一些基本知識:
32位機器,每個程式有4G的虛擬地址空間。大致分為4塊,從低地址到高地址依次是:NULL區,使用者區,隔離區,核心區。使用者私有的資料都在使用者區(當 然這個區裡又可以細分,其中也包括一部分可以共享的內容),系統核心等東西都在核心區。總體來說,A程式的虛擬地址空間中的內容和B程式相比,只有各自的 使用者區不一致。通常使用者區中,程式又會將exe檔案(由頭資料和段資料組成)中定義的程式碼段、堆疊段、資料段等各個段對映到使用者區的特定不同部位。對於這 部分割槽域,使用者需要用VirtualAlloc先為自己預留後再提交,最後在自己的頁面被cpu訪問時再從exe映像中將資料載入到主存,然後將虛擬地址 對映為主存的實體地址。基本上這樣就可以了,至於系統如何進行頁面的管理以及地址對映如何實現等細節請大家再參考別的文獻。

我本以為很複雜呢,結果寫出來,就這麼一小段,呵呵,看來是高估了自己理解的東西了,呵呵。

下面貼出我看的一些資料:

虛擬儲存器是一個抽象概念,它為每一個程式提供了一個假象,好像每個程式都在獨佔的使用主存。每個程式看到的儲存器都是一致的,稱之為虛擬地址空間。

     每個程式看到得虛擬地址空間有大量準確定義的區(area)構成,每個區都有專門的功能。從最低的地址看起

  • 程式程式碼和資料:程式碼是從同一固定地址開始,緊接著的是和C全域性變數相對應的資料區。 (應該就是所謂的靜態儲存空間)
  • 堆:程式碼和資料區後緊隨著的是執行時堆。作為呼叫mallocfree這樣的C標準庫函式,堆可以在執行時動態的擴充套件和收縮。(應該就是所謂的動態儲存區)
  • 共享庫:在地址空間的中間附近是一塊用來存放像C標準庫和數學庫這樣共享庫的程式碼和資料的區域。(C標準庫函式的指令,連線階段把他們加入到編譯後的程式)
  • :位於使用者虛擬地址空間頂部的是使用者,編譯器用它來實現函式呼叫。和堆一樣每次我們從函式返回時,就會收縮。
  • 核心虛擬儲存器:核心是作業系統總是駐留在儲存器中的部分。地址空間頂部的四分之一部分是為核心預留的。(系統函式?這裡說的UNIX系統,不知道windows下是不是這樣的?)

     今天大多數計算機的字長都是32位元組,這就限制了虛擬地址空間為4千兆位元組(4GB

引言

  Windows的記憶體結構是深入理解Windows作業系統如何運作的關鍵之所在,通過對記憶體結構的認識可清楚地瞭解諸如程式間資料的共享、對記憶體進行有效的管理等問題,從而能夠在程式設計時使程式以更加有效的方式執行。Windows作業系統對記憶體的管理可採取多種不同的方式,其中虛擬記憶體的管理方式可用來管理大型的物件和結構陣列。

  在Windows系統中,任何一個程式都被賦予其自己的虛擬地址空間,該虛擬地址空間覆蓋了一個相當大的範圍,對於32位程式,其地址空間為232=4,294,967,296 Byte,這使得一個指標可以使用從0x000000000xFFFFFFFF4GB範圍之內的任何一個值。雖然每一個32位程式可使用4GB的地址空間,但並不意味著每一個程式實際擁有4GB的 實體地址空間,該地址空間僅僅是一個虛擬地址空間,此虛擬地址空間只是記憶體地址的一個範圍。程式實際可以得到的實體記憶體要遠小於其虛擬地址空間。程式的虛 擬地址空間是為每個程式所私有的,在程式內執行的執行緒對記憶體空間的訪問都被限制在呼叫程式之內,而不能訪問屬於其他程式的記憶體空間。這樣,在不同的程式中 可以使用相同地址的指標來指向屬於各自呼叫程式的內容而不會由此引起混亂。下面分別對虛擬記憶體的各具體技術進行介紹。
地址空間中區域的保留與釋放

在程式建立之初並被賦予地址空間時,其虛擬地址空間尚未分配,處於空閒狀態。這時地址空間內的記憶體是不能使用的,必須首先通過VirtualAlloc()函式來分配其內的各個區域,對其進行保留

LPVOID VirtualAlloc(
 LPVOID lpAddress
 DWORD dwSize
 DWORD flAllocationType,
 DWORD flProtect
);

其引數lpAddress包含一個記憶體地址,用於定義待分配區域的首地址。通常可將此引數設定為NULL,由系統通過搜尋地址空間來決定滿足條件的未保留地址空間。這時系統可從地址空間的任意位置處開始保留一個區域,而且還可以通過向引數flAllocationType設定MEM_TOP_DOWN標誌來指明在儘可能高的地址上分配記憶體。如果不希望由系統自動完成對記憶體區域的分配而為lpAddress設定了記憶體地址(必須確保其始終位於程式的使用者模式分割槽中,否則將會導致分配的失敗),那麼系統將在進行分配之前首先檢查在該記憶體地址上是否存在足夠大的未保留空間,如果存在一個足夠大的空閒區域,那麼系統將會保留此區域並返回此保留區域的虛擬地址,否則將導致分配的失敗而返回NULL。這裡需要特別指出的是,在指定lpAddress的記憶體地址時,必須確保是從一個分配粒度的邊界處開始。
一般來說,在不同的CPU平臺下分配粒度各不相同,但目前所有Windows環境下的CPUx8632Alpha64Alpha以及IA-64等均是採用64KB的分配粒度。如果保留區域的起始地址沒有遵循從64KB分配粒度的邊界開始之一原則,系統將自動調整該地址到最接近的64K的倍數。例如,如果指定的lpAddress0x00781022,那麼此保留區域實際是從0x00780000開始分配的。引數dwSize指定了保留區域的大小。但是系統實際保留的區域大小必須是CPU頁面大小的整數,如果指定的dwSize並非CPU頁面的整數系統將自動對其進行調整,使其達到與之最接近的頁面大小整數與分配粒度一樣,對於不同的CPU平臺其頁面大小也是不一樣的。在x86平臺下,頁面大小為4KB,在32Alpah平臺下,頁面大小為8KB。在使用時可以通過GetSystemInfo()來決定當前主機的頁面大小。引數flAllocationTypeflProtect分別定義了分配型別和訪問保護屬性。由於VirtualAlloc()可用來保留一個區域也可以用來佔用物理儲存器,因此通過flAllocationType來指定當前是要保留一個區域還是要佔用物理儲存器。其可能使用的記憶體分配型別有:

分配型別

型別說明

MEM_COMMIT

為特定的頁面區域分配記憶體中或磁碟的頁面檔案中的物理儲存

MEM_PHYSICAL

分配實體記憶體(僅用於地址視窗擴充套件記憶體)

MEM_RESERVE

保留程式的虛擬地址空間,而不分配任何物理儲存。保留頁面可通過繼續呼叫VirtualAlloc()而被佔用

MEM_RESET

指明在記憶體中由引數lpAddressdwSize指定的資料無效

MEM_TOP_DOWN

在儘可能高的地址上分配記憶體(Windows 98忽略此標誌)

MEM_WRITE_WATCH

必須與MEM_RESERVE一起指定,使系統跟蹤那些被寫入分配區域的頁面(僅針對Windows 98


  分配成功完成後,即在程式的虛擬地址空間中保留了一個區域,可以對此區域中的記憶體進行保護許可權許可範圍內的訪問。當不再需要訪問此地址空間區域時,應釋放此區域。由VirtualFree()負責完成。其函式原型為:

BOOL VirtualFree(
 LPVOID lpAddress,
 DWORD dwSize,
 DWORD dwFreeType
);

其中,引數lpAddress為指向待釋放頁面區域的指標。如果引數dwFreeType指定了MEM_RELEASE,則lpAddress必須為頁面區域被保留時由VirtualAlloc()所返回的基地址。引數dwSize指定了要釋放的地址空間區域的大小,如果引數dwFreeType指定了MEM_RELEASE標誌,則將dwSize設定為0,由系統計算在特定記憶體地址上的待釋放區域的大小。引數dwFreeType為所執行的釋放操作的型別,其可能的取值為MEM_RELEASEMEM_DECOMMIT,其中MEM_RELEASE標誌指明要釋放指定的保留頁面區域,MEM_DECOMMIT標誌則對指定的佔用頁面區域進行佔用的解除。如果VirtualFree()成功執行完成,將回收全部範圍的已分配頁面,此後如再對這些已釋放頁面區域記憶體的訪問將引發記憶體訪問異常。釋放後的頁面區域可供系統繼續分配使用。

  下面這段程式碼演示了由系統在程式的使用者模式分割槽內保留一個64KB大小的區域,並將其釋放的過程:

// 在地址空間中保留一個區域

LPBYTE bBuffer = (LPBYTE)VirtualAlloc(NULL, 65536, MEM_RESERVE, PAGE_READWRITE);

……

// 釋放已保留的區域

VirtualFree(bBuffer, 0, MEM_RELEASE);

flProtect頁面保護屬性

我們可以給每個已分配的物理儲存頁指定不同的頁面保護屬性。表13-3列出了所有的頁面保護屬性。

13-3  記憶體頁面保護屬性

保護屬性

  

PAGE_NOACCESS

試圖讀取頁面、寫入頁面或執行頁面中的程式碼將引發訪問違規

PAGE_READONLY

試圖寫入頁面或執行頁面中的程式碼將引發訪問違規

PAGE_READWRITE

試圖執行頁面中的程式碼將引發訪問違規

PAGE_EXECUTE

試圖讀取頁面或寫入頁面將引發訪問違規

PAGE_EXECUTE_READ

試圖寫入頁面將引發訪問違規

PAGE_EXECUTE_READWRITE

對頁面執行任何操作都不會引發訪問違規

PAGE_WRITECOPY

試圖執行頁面中的程式碼將引發訪問違規。試圖寫入頁面將使系統為程式單獨建立一份該頁面的私有副本(以頁交換檔案為後備儲存器)

PAGE_EXECUTE_WRITECOPY

對頁面執行任何操作都不會引發訪問違規。試圖寫入頁面將使系統為程式單獨建立一份該頁面的私有副本(以頁交換檔案為後備儲存器)

一些惡意軟體將程式碼寫入到用於資料的記憶體區域(比如執行緒),通過這種方式讓應用程式執行惡意程式碼。Windows資料執行保護(Data Execution Protection,後面簡稱為DEP)特性提供了對此類惡意攻擊的防護。如果啟用了DEP,那麼只有對那些真正需要執行程式碼的記憶體區域,作業系統才會使用PAGE_EXECUTE_*保護屬性。其他保護屬性(最常見的就是PAGE_READWRITE)用於只應該存放資料的記憶體區域(比如執行緒和應用程式的堆)

如果CPU試圖執行某個頁面中的程式碼,而該頁又沒有PAGE_EXECUTE_*保護屬性,那麼CPU會丟擲訪問違規異常。

系統還對Windows支援的結構化異常處理機制(structured exception handling mechanism)做了更進一步的保護,結構化異常處理機制會在第2325章詳細介紹。如果應用程式在連結時使用了/SAFESEH開關,那麼異常處理器會被註冊到映像檔案中一個特殊的表中。這樣,當將要執行一個異常處理器時,作業系統會先檢查該處理器有沒有在表中註冊過,然後決定是否允許它執行。

有關DEP的更多資訊,請訪問http://go.microsoft.com/fwlink/?LinkId=28022,可以在此找到Microsoft白皮書“03_CIF_Memory_Protection.DOC”。

                                                                       

13.6.1  寫時複製

在表13.3中列出的保護屬性中,除最後兩個屬性PAGE_WRITECOPYPAGE_EXECUTE_WRITECOPY之外,其餘的都不言自明。這兩個保護屬性存在的目的是為了節省記憶體和頁交換檔案的使用。Windows支援一種機制,允許兩個或兩個以上的程式共享同一塊儲存器。因此,如果有10個記事本程式正在執行,所有的程式會共享應用程式的內碼表和資料頁。讓所有的應用程式例項共享相同的儲存頁極大地提升了系統的效能,但另一方面,這也要求所有的應用程式例項只能讀取其中的資料或是執行其中的程式碼。如果某個應用程式例項修改並寫入一個儲存頁,那麼這等於是修改了其他例項正在使用的儲存頁,最終將導致混亂。

為了避免此類混亂的發生,作業系統會給共享的儲存頁指定寫時複製屬性。當系統把一個.exe.dll對映到一個地址空間的時候,系統會計算有多少頁面是可寫的。(通常,包含程式碼的頁面被標記為PAGE_EXECUTE_READ,而包含資料的頁面被標記為PAGE_READWRITE)然後系統會從頁交換檔案中分配儲存空間來容納這些可寫頁面。除非應用程式真的寫入可寫頁面,否則不會用到頁交換檔案中的儲存器。

當執行緒試圖寫入一個共享頁面時,系統會介入並執行下面的操作。

(1)   系統在記憶體中找到一個閒置頁面。注意,該閒置頁面的後備頁面來自頁交換檔案,它是系統最初將模組對映到程式的地址空間時分配的。由於系統在第一次進行對映的時候分配了所有可能需要的頁交換檔案空間,這一步不可能失敗。

(2)   系統把執行緒想要修改的頁面內容複製到在第1步中找到的閒置頁面。系統會給該閒置頁面指定PAGE_READWRITEPAGE_EXECUTE_READWRITE保護屬性,系統不會對原始頁面的保護屬性和資料做任何修改。

(3)   然後,系統更新程式的頁面表,這樣一來,原來的虛擬地址現在就對應到記憶體中一個新的頁面了。

系統在執行這些步驟之後,程式就可以訪問它自己的副本了。第17章將進一步介紹儲存器共享和寫時複製。

此外,在預訂地址空間或調撥物理儲存器時,不能使用PAGE_WRITECOPYPAGE_EXECUTE_WRITECOPY保護屬性。這樣做會導致呼叫VirtualAlloc失敗,此時呼叫GetLastError會返回錯誤碼ERROR_INVALID_PARAMETER。這兩個屬性是作業系統在對映.exeDLL映像檔案時用的。

13.6.2  一些特殊的訪問保護屬性標誌

除了已經介紹過的保護屬性之外,另外還有3個保護屬性標誌:PAGE_NOCACHEPAGE_WRITECOMBINEPAGE_GUARD。使用這些標誌時,只需將它們與除了PAGE_NOACCESS之外的任何其他保護屬性進行按位或操作即可。

第一個保護屬性標誌PAGE_NOCACHE,用來禁止對已調撥的頁面進行快取。該標誌存在的主要目的是為了讓需要操控記憶體緩衝區的驅動程式開發人員使用,不建議將該標誌用於除此以外的其他用途。

                                                                       

第二個保護屬性標誌PAGE_WRITECOMBINE也是給驅動程式開發人員用的。它允許把對單個裝置的多次寫操作組合在一起,以提高效能。

最後一個保護屬性標誌PAGE_GUARD,使應用程式能夠在頁面中的任何一個位元組被寫入時得到通知。這個標誌有一些巧妙的用法。Windows在建立執行緒時會用到它。有關該標誌的更多資訊,請參閱第16章。


物理儲存器的提交與回收

  在地址空間中保留一個區域後,並不能直接對其進行使用,必須在把物理儲存器提交給該區域後,才可以訪問區域中的記憶體地址。在提交過程中,物理儲存器是按頁面邊界和頁面大小的塊來進行提交的。若要為一個已保留的地址空間區域提交物理儲存器,需要再次呼叫VirtualAlloc()函式,所不同的是在執行物理儲存器的提交過程中需要指定flAllocationType引數為MEM_COMMIT標誌,使用的保護屬性與保留區域時所用保護屬性一致。在提交時,可以將物理儲存器提交給整個保留區域,也可以進行部分提交,由VirtualAlloc()函式的lpAddress引數和dwSize引數指明要將物理儲存器提交到何處以及要提交多少物理儲存器。
與保留區域的釋放類似,當不再需要訪問保留區域中被提交的物理儲存器時,提交的物理儲存器應得到及時的釋放。該回收過程與保留區域的釋放一樣也是通過VirtualFree()函式來完成的。在呼叫時為VirtualFree()的dwFreeType引數指定MEM_DECOMMIT標誌,並在引數lpAddressdwSize中傳遞用來標識要解除的第一個頁面的記憶體地址和釋放的位元組數。此回收過程同樣也是以頁面為單位來進行的,將回收設定範圍所涉及到的所有頁面。下面這段程式碼演示了對先前保留區域的提交過程,並在使用完畢後將其回收:

// 
在地址空間中保留一個區域

LPBYTE bBuffer = (LPBYTE)VirtualAlloc(NULL, 65536, MEM_RESERVE, PAGE_READWRITE);

// 提交物理儲存器

VirtualAlloc(bBuffer, 65536, MEM_COMMIT, PAGE_READWRITE);

……

// 回收提交的物理儲存器

VirtualFree(bBuffer, 65536, MEM_DECOMMIT);

// 釋放已保留的區域

VirtualFree(bBuffer, 0, MEM_RELEASE);

 

  由於未經提交的保留區域實際是無法使用的,因此在程式設計過程中允許通過一次VirtualAlloc()呼叫而完成對地址空間的區域保留及對保留區域的物理儲存器的提交。相應的,回收、釋放過程也可由一次VirtualFree()呼叫來實現。上述程式碼可按此方法改寫為:

// 在地址空間中保留一個區域並提交物理儲存器

LPBYTE bBuffer = (LPBYTE)VirtualAlloc(NULL, 65536, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

……

// 釋放已保留的區域並回收提交的物理儲存器

VirtualFree(bBuffer, 0, MEM_RELEASE | MEM_DECOMMIT); 

頁檔案的使用

  在前面曾多次提到物理儲存器,這裡所說的物理儲存器並不侷限於計算機記憶體,還包括在磁碟空間上建立的頁檔案,其儲存空間大小為計算機記憶體和頁檔案儲存容量之。由於通常情況下磁碟儲存空間要遠大於記憶體的儲存空間,因此頁檔案的使用對於應用程式而言相當於透明的增加了其所能使用的記憶體容量。在使用時,由作業系統和CPU負責對頁檔案進行維護和協調。只有在應用程式需要時才臨時將頁檔案中的資料載入到記憶體供應用程式訪問之用,在使用完畢後再從記憶體交換回頁檔案

程式中的執行緒在訪問位於已提交物理儲存器的保留區域的記憶體地址時,如果此地址指向的資料當前已存在於記憶體,CPU將直接將程式的 虛擬地址對映為實體地址,並完成對資料的訪問;如果此資料是存在於頁檔案中的,就要試圖將此資料從頁檔案載入到記憶體。在進行此處理時,首先要檢查記憶體中是 否有可供使用的空閒頁面,如果有就可以直接將資料載入到記憶體中的空閒頁面,否則就要從記憶體中尋找一個暫不使用的可釋放的頁面並將資料載入到此頁面。如果被 釋放頁面中的資料仍為有效資料(即以後還會用到),就要先將此頁面從記憶體寫入到頁檔案。在資料載入到記憶體後,仍要在CPU將 虛擬地址對映為實體地址後方可實現對資料的訪問。與對物理儲存器中資料的訪問有所不同,在執行可執行程式時並不進行程式程式碼和資料的從磁碟檔案到頁檔案的 複製過程,而是在確定了程式的程式碼及其資料的大小後,由系統直接將可執行程式的映像用作程式的保留地址空間區域。這樣的處理方式大大縮短了程式的啟動時 間,並可減小頁檔案的尺寸。

 

 

上面提到的“資料是否在記憶體中”,我認為應該是判斷系統快取中是否有需要的頁面。

==========================================================================================

對記憶體的管理

  使用虛擬記憶體技術將能夠對記憶體進行管理。對當前記憶體狀態的動態資訊可通過GlobalMemoryStatus()函式來獲取。GlobalMemoryStatus()的函式原型為:

 

VOID GlobalMemoryStatus(LPMEMORYSTATUS lpBuffer);

 

  其引數lpBuffer為一個指向記憶體狀態結構MEMORYSTATUS的指標,而且要預先對該結構物件的資料成員進行初始化。MEMORYSTATUS結構定義如下:

 

typedef struct _MEMORYSTATUS {

 DWORD dwLength; // MEMORYSTATUS結構大小

 DWORD dwMemoryLoad; // 已使用記憶體所佔的百分比

 DWORD dwTotalPhys; // 物理儲存器的總位元組數

 DWORD dwAvailPhys; // 空閒物理儲存器的位元組數

 DWORD dwTotalPageFile; // 頁檔案包含的最大位元組數

 DWORD dwAvailPageFile; // 頁檔案可用位元組數

 DWORD dwTotalVirtual; // 使用者模式分割槽大小

 DWORD dwAvailVirtual; // 使用者模式分割槽中空閒記憶體大小

} MEMORYSTATUS, *LPMEMORYSTATUS;

下面這段程式碼通過設定一個定時器而每隔5秒更新一次當前系統對記憶體的使用情況:

// 設定定時器

SetTimer(0, 5000, NULL);

……

void CSample22Dlg::OnTimer(UINT nIDEvent)

{

 // 獲取當前記憶體使用狀態

 MEMORYSTATUS mst;

 GlobalMemoryStatus(&mst);

 // 已使用記憶體所佔的百分比

 m_dwMemoryLoad = mst.dwMemoryLoad;

 // 物理儲存器的總位元組數

 m_dwAvailPhys = mst.dwAvailPhys / 1024;

 // 空閒物理儲存器的位元組數

 m_dwAvailPageFile = mst.dwAvailPageFile / 1024;

 // 頁檔案包含的最大位元組數

 m_dwAvailVirtual = mst.dwAvailVirtual / 1024;

 // 頁檔案可用位元組數

 m_dwTotalPageFile = mst.dwTotalPageFile / 1024;

 // 使用者模式分割槽大小

 m_dwTotalPhys = mst.dwTotalPhys / 1024;

 // 使用者模式分割槽中空閒記憶體大小

 m_dwTotalVirtual = mst.dwTotalVirtual / 1024;

 // 更新顯示

 UpdateData(FALSE);

 CDialog::OnTimer(nIDEvent);

}

 

  對記憶體的管理除了對當前記憶體的使用狀態資訊進行獲取外,還經常需要獲取有關程式的虛擬地址空間的狀態資訊。可由VirtualQuery()函式來進行查詢,其原型宣告如下:

 

DWORD VirtualQuery(

 LPCVOID lpAddress, // 記憶體地址

 PMEMORY_BASIC_INFORMATION lpBuffer, // 指向記憶體資訊結構的指標

 DWORD dwLength // 記憶體的大小

);

 

  其中lpAddress引數為要查詢的虛擬記憶體地址,該值將被調整到最近的頁邊界處。當前計算機的頁面大小可通過GetSystemInfo()函式獲取,該函式需要一個指向SYSTEM_INFO結構的指標作為引數,獲取到的系統資訊將填充在該資料結構物件中。下面這段程式碼通過對GetSystemInfo()的呼叫而獲取了當前的系統資訊:

 

// 得到當前系統資訊

GetSystemInfo(&m_sin);

// 位遮蔽,指明哪個CPU是活動的

m_dwActiveProcessorMask = m_sin.dwActiveProcessorMask;

// 保留的地址空間區域的分配粒度

m_dwAllocationGranularity = m_sin.dwAllocationGranularity;

// 程式的可用地址空間的最小記憶體地址

m_dwMaxApplicationAddress = (DWORD)m_sin.lpMaximumApplicationAddress;

// 程式的可用地址空間的最大記憶體地址

m_dwMinApplicationAddress = (DWORD)m_sin.lpMinimumApplicationAddress;

// 計算機中CPU的數目

m_dwNumberOfProcessors = m_sin.dwNumberOfProcessors;

// 頁面大小

m_dwPageSize = m_sin.dwPageSize;

// 處理器型別

m_dwProcessorType = m_sin.dwProcessorType;

//進一步細分處理器級別

m_wProcessorLevel = m_sin.wProcessorLevel;

// 系統處理器的結構

m_wProcessorArchitecture = m_sin.wProcessorArchitecture;

// 更新顯示

UpdateData(FALSE);

VirtualQuery()的第二個引數lpBuffer為一個指向MEMORY_BASIC_INFORMATION結構的指標。VirtualQuery()如成功執行,該結構物件中將儲存查詢到的虛擬地址空間狀態資訊。MEMORY_BASIC_INFORMATION結構的定義為:

typedef struct _MEMORY_BASIC_INFORMATION {

 PVOID BaseAddress; // 保留區域的基地址

 PVOID AllocationBase; // 分配的基地址

 DWORD AllocationProtect; // 初次保留時所設定的保護屬性

 DWORD RegionSize; // 區域大小

 DWORD State; // 狀態(提交、保留或空閒)

 DWORD Protect; // 當前訪問保護屬性

 DWORD Type; // 頁面型別

} MEMORY_BASIC_INFORMATION; 

 

  通過VirtualQuery()函式對由lpAddressdwLength引數指定的虛擬地址空間區域的查詢而獲取得到的相關狀態資訊:

 

// 更新顯示

UpdateData(TRUE);

// 虛擬地址空間狀態結構

MEMORY_BASIC_INFORMATION mbi;

// 查詢指定虛擬地址空間的狀態資訊

VirtualQuery((LPCVOID)m_dwAddress, &mbi, 1024);

// 保留區域的基地址

m_dwBaseAddress = (DWORD)mbi.BaseAddress;

// 分配的基地址

m_dwAllocateBase = (DWORD)mbi.AllocationBase;

// 初次保留時所設定的保護屬性

m_dwAllocateProtect = mbi.AllocationProtect;

// 區域大小

m_dwRegionSize = mbi.RegionSize;

// 狀態(提交、保留或空閒)

m_dwState = mbi.State;

// 當前訪問保護屬性

m_dwProtect = mbi.Protect;

// 頁面型別

m_dwType = mbi.Type;

// 更新顯示

UpdateData(FALSE);

 

  小結

 

  本文主要對記憶體管理中的虛擬記憶體技術的基本原理、使用方法和對記憶體的管理等進行了介紹。通過本文將能夠掌握虛擬記憶體的一般使用方法,與之相關的記憶體管理技術還包括記憶體檔案對映和堆管理等技術,讀者可參閱相關文章。這幾種記憶體管理技術同屬Windows程式設計中的高階技術,在應用程式中適當使用將有助於程式效能的提高。本文所述程式在Windows 2000 Professional下由Microsoft Viusual C++ 6.0編譯通過。

程式的虛擬地址空間

每個程式都被賦予它自己的虛擬地址空間。對於3 2位程式來說,這個地址空間是4 G B,因為3 2位指標可以擁有從0 x 0 0 0 0 0 0 0 00 x F F F F F F F F之間的任何一個值。這使得一個指標能夠擁有4 294 967 296個值中的一個值,它覆蓋了一個程式的4 G B虛擬空間的範圍。對於6 4位程式來說,這個地址空間是1 6 E B1 01 8位元組),因為6 4位指標可以擁有從0 x 0 0 0 00 0 0 0 0 0 0 0 0 0 0 00 x F F F F F F F F F F F F F F F F之間的任何值。這使得一個指標可以擁有18 446 744 073 709 551 616個值中的一個值,它覆蓋了一個程式的1 6 E B虛擬空間的範圍。這是相當大的一個範圍。

由於每個程式可以接收它自己的私有的地址空間,因此當程式中的一個執行緒正在執行時,該執行緒可以訪問只屬於它的程式的記憶體。屬於所有其他程式的記憶體則隱藏著,並且不能被正在執行的執行緒訪問。

注意在Windows 2000中,屬於作業系統本身的記憶體也是隱藏的,正在執行的執行緒無法訪問。這意味著執行緒常常不能訪問作業系統的資料。Windows 98中,屬於作業系統的記憶體是不隱藏的,正在執行的執行緒可以訪問。因此,正在執行的執行緒常常可以訪問作業系統的資料,也可以破壞作業系統(從而有可能導致作業系統崩潰)。在Windows 98中,一個程式的執行緒不可能訪問屬於另一個程式的記憶體。

前面說過,每個程式有它自己的私有地址空間。程式A可能有一個存放在它的地址空間中的資料結構,地址是0 x 1 2 3 4 5 6 7 8而程式B則有一個完全不同的資料結構存放在它的地址空間中,地址是0 x 1 2 3 4 5 6 7 8。當程式A中執行的執行緒訪問地址為0 x 1 2 3 4 5 6 7 8的記憶體時,這些執行緒訪問的是程式A的資料結構。當程式B中執行的執行緒訪問地址為0 x 1 2 3 4 5 6 7 8的記憶體時,這些執行緒訪問的是程式B的資料結構。程式A中執行的執行緒不能訪問程式B的地址空間中的資料結構。反之亦然。

當你因為擁有如此大的地址空間可以用於應用程式而興高采烈之前,記住,這是個虛擬地址空間,不是實體地址空間。該地址空間只是記憶體地址的一個範圍。在你能夠成功地訪問資料而不會出現違規訪問之前,必須賦予物理儲存器或者將物理儲存器對映到各個部分的地址空間。本章後面將要具體介紹這是如何操作的。

虛擬地址空間如何分割槽

每個程式的虛擬地址空間都要劃分成各個分割槽。地址空間的分割槽是根據作業系統的基本實現方法來進行的。不同的Wi n d o w s核心,其分割槽也略有不同。表 1顯示了每種平臺是如何對程式的地址空間進行分割槽的。

程式的地址空間如何分割槽

分割槽

32Windows 2000(x86Alpha處理器)

32Windows 2000(x86w/3GB使用者方式)

64Windows 2000(AlphaIA-64處理器)

Windows 98

N U L L指標分配的分割槽

0 x 0 0 0 0 0 0 0 0  ——0x 0 0 00 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 00 0 0 F F F

DOS/16Windows應用程式相容分割槽

0 x 0 0 0 0 0 1 0 0 0 0 x 00 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<將近2G>

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 FF F F F F F

64-KB禁止進入分割槽

0 x 7 F F F 0 0 0 0——0x7FFF FFFF

0 x B F F F 0 0 0 0——0 x B F F F F F F F

0 x 0 0 0 0 0 3 F F F F F F0 0 0——0 x 0 0 0 0 0 3 F F F F F F F F F F

共享記憶體對映

0 x 8 0 0 0 0 0 0 0

檔案(MMF)核心方式

0 x 8 0 0 0 0 0 0 0 —— 0 x F FF F F F F F<2G>

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 00 0 0 0 0 0 0 x F F F F F FF F

1. NULL指標分割槽是NULL指標的地址範圍。
    
對這個區域的讀寫企圖都將引發訪問違規。 
2. DOS/WIN16
分割槽是98中專門用於16位的
    DOS
windows程式執行的空間,所有的16
    
位程式將共享這個4M的空間。Win2000中不
    
存在這個分割槽,16位程式也會擁有自己獨立的虛擬地址空間。有的文章中稱win2000中不能執行16位程式,是不確切的。 
3.
使用者分割槽是程式的私有領域,Win2000中,程式的可執行程式碼和其它使用者模組均載入在這裡,記憶體對映檔案也會載入在這裡。Win98中的系統共享DLL和記憶體對映檔案則載入在共享分割槽中。 
4.
禁止訪問分割槽只有在win2000中有。這個分割槽是使用者分割槽和核心分割槽之間的一個隔離帶,目的是為了防止使用者程式違規訪問核心分割槽。 
5. MMF
分割槽只有win98中有,所有的記憶體對映檔案和系統共享DLL將載入在這個地址。而2000中則將其載入到使用者分割槽。 
6. 
核心方式分割槽對使用者的程式來說是禁止訪問的,作業系統的程式碼在此。核心物件也駐留在此。
另外要說明的是,win98中對於核心分割槽本也應該提供保護的,但遺憾的是並沒有做到,因而98中程式可以訪問核心分割槽的地址空間。
對於使用者分割槽,又可以細分成若干區域。(這些區域具體會在第四階段詳細剖析。因為這部分內容牽扯到PE檔案結構,只有學習並理解了PE檔案結構後,才能理解這部分內容,為了便於後面的講解,在此講這部分割槽域先大致分為4塊:)

3 2Windows 2000的核心與6 4Windows 2000的核心擁有大體相同的分割槽,差別在於分割槽的大小和位置有所不同。另一方面,可以看到Windows 98下的分割槽有著很大的不同。下面讓我們看一下系統是如何使用每一個分割槽的。

NULL指標分配的分割槽適用於Windows 2000Windows 98

程式地址空間的這個分割槽的設定是為了幫助程式設計師掌握N U L L指標的分配情況。如果你的程式中的執行緒試圖讀取該分割槽的地址空間的資料,或者將資料寫入該分割槽的地址空間,那麼C P U就會引發一個訪問違規。保護這個分割槽是極其有用的,它可以幫助你發現N U L L指標的分配情況。

C / C + +程式中常常不進行嚴格的錯誤檢查。例如,下面這個程式碼就沒有進行任何錯誤檢查:

int* pnSomeInteger = (int*) malloc(sizeof(int));
*pnSomeInteger = 5;

如果m a l l o c不能找到足夠的記憶體來滿足需要,它就返回N U L L。但是,該程式碼並不檢查這種可能性,它認為地址的分配已經取得成功,並且開始訪問0 x 0 0 0 0 0 0 0 0地址的記憶體。由於這個分割槽的地址空間是禁止進入的,因此就會發生記憶體訪問違規現象,同時該程式將終止執行。這個特性有助於程式設計員發現應用程式中的錯誤。

使用者方式分割槽適用於Windows 2000Windows 98

這個分割槽是程式的私有(非共享)地址空間所在的地方。一個程式不能讀取、寫入、或者以任何方式訪問駐留在該分割槽中的另一個程式的資料。對於所有應用程式來說,該分割槽是維護程式的大部分資料的地方。由於每個程式可以得到它自己的私有的、非共享分割槽,以便存放它的資料,因此,應用程式不太可能被其他應用程式所破壞,這使得整個系統更加健壯。

Windows 2000中,所有的. e x eD L L模組均載入這個分割槽。每個程式可以將這些D L L載入到該分割槽的不同地址中(不過這種可能性很小)。系統還可以在這個分割槽中對映該程式可以訪問的所有記憶體對映檔案

共享的MMF分割槽僅適用於Windows 98

這個1 G B分割槽是系統用來存放所有3 2位程式共享資料的地方。例如,系統的動態連結庫K e r n e l 3 2 . d l lA d v A P I 3 2 . d l lU s e r 3 2 . d l lG D I 3 2 . d l l等,全部存放在這個地址空間分割槽中,因此,所有3 2位程式都能很容易同時訪問它們。系統還為每個程式將D L L載入相同的記憶體地址。此外,系統將所有記憶體對映檔案對映到這個分割槽中。

物理儲存器與頁檔案

在較老的作業系統中,物理儲存器被視為計算機擁有的R A M的容量。換句話說,如果計算機擁有1 6 M BR A M,那麼載入和執行的應用程式最多可以使用1 6 M BR A M。今天的作業系統能夠使得磁碟空間看上去就像記憶體一樣。磁碟上的檔案通常稱為頁檔案,它包含了可供所有程式使用的虛擬記憶體

當然,若要使虛擬記憶體能夠執行,需要得到C P U本身的大量幫助。當一個執行緒試圖訪問一個位元組的記憶體時, C P U必須知道這個位元組是在R A M中還是在磁碟上。

從應用程式的角度來看,頁檔案透明地增加了應用程式能夠使用的R A M(即記憶體)的數量。如果計算機擁有6 4 M BR A M,同時在硬碟上有一個100 MB的頁檔案,那麼執行的應用程式就認為計算機總共擁有1 6 4 M BR A M

實際上並不擁有1 6 4 M BR A M。相反,作業系統與C P U相協調,共同將R A M的各個部分儲存到頁檔案中,當執行的應用程式需要時,再將頁檔案的各個部分重新載入到R A M。由於頁檔案增加了應用程式可以使用的R A M的容量,因此頁檔案的使用是視情況而定的。如果沒有頁檔案,那麼系統就認為只有較少的R A M可供應用程式使用。但是,我們鼓勵使用者使用頁檔案,這樣他們就能夠執行更多的應用程式,並且這些應用程式能夠對更大的資料集進行操作。最好將物理儲存器視為儲存在磁碟驅動器(通常是硬碟驅動器)上的頁檔案中的資料。這樣,當一個應用程式通過呼叫Vi r t u a l A l l o c函式,將物理儲存器提交給地址空間的一個區域時,地址空間實際上是從硬碟上的一個檔案中進行分配的。系統的頁檔案的大小是確定有多少物理儲存器可供應用程式使用時應該考慮的最重要的因素, R A M的容量則影響非常小。

第一種情況中,執行緒試圖訪問的資料是在R A M中。在這種情況下, C P U將資料的虛擬記憶體地址對映到記憶體的實體地址中,然後執行需要的訪問。執行緒試圖訪問的資料不在R A M中,而是存放在頁檔案中的某個地方。這時,試圖訪問就稱為頁面失效, C P U將把試圖進行的訪問通知作業系統。這時作業系統就尋找R A M中的一個記憶體空頁。如果找不到空頁,系統必須釋放一個空頁。如果一個頁面尚未被修改,系統就可以釋放該頁面。但是,如果系統需要釋放一個已經修改的頁面,那麼它必須首先將該頁面從R A M拷貝到頁交換檔案中,然後系統進入該頁檔案,找出需要訪問的資料塊,並將資料載入到空閒的記憶體頁面。然後,作業系統更新它的用於指明資料的虛擬記憶體地址現在已經對映到R A M中的相應的物理儲存器地址中的表。這時C P U重新執行生成初始頁面失效的指令,但是這次C P U能夠將虛擬記憶體地址對映到一個物理R A M地址,並訪問該資料塊。

當閱讀了上一節後,你必定會認為,如果同時執行許多檔案的話,頁檔案就可能變得非常大,而且你會認為,每當你執行一個程式時,系統必須為程式的程式碼和資料保留地址空間的一些區域,將物理儲存器提交給這些區域,然後將程式碼和資料從硬碟上的程式檔案拷貝到頁檔案中已提交的物理儲存器中。

實際上系統並不進行上面所說的這些操作。如果它進行這些操作的話,就要花費很長的時間來載入程式並啟動它執行。相反,當啟動一個應用程式的時候,系統將開啟該應用程式的. e x e檔案,確定該應用程式的程式碼和資料的大小。然後系統要保留一個地址空間的區域,並指明與該區域相關聯的物理儲存器是在. e x e檔案本身中即系統並不是從頁檔案中分配地址空間,而是將. e x e檔案的實際內容即映像用作程式的保留地址空間區域。當然,這使應用程式的載入非常迅速,並使頁檔案能夠保持得非常小

一、開始之前,讓我們來了解一下Windows中記憶體管理的一些知識:

 

1. 機器的實體記憶體由兩部分組成。一部分為機器的主存RAM,也就是我們記憶體條的大小;另一部分為虛擬記憶體,它就在機器的硬碟上,以頁檔案的形式存在。

 

2. 每個程式都有自己的虛擬地址空間,對於具有32位定址能力的機器來說,這個虛擬空間的大小為4GB。現在我們使用的機器就是4GB

 

3. 程式的4GB虛擬地址空間又可以分成幾個部分,其中程式真正私有的空間少於2GB(這段地址空間被稱作“使用者方式分割槽”),其餘的2GB多空間都是給作業系統的,且這部分空間被所有的程式共享。(參考Windows核心程式設計Chapter 13

 

4. 為程式“分配記憶體”,這個概念可以細化:“保留段地址空間”,“提交一段記憶體空間”,“將記憶體空間對映到主存”。在程式中我們通常所訪問的地址都必須是程式地址空間中被保留和提交的那段地址空間。

 

4.1 “保留段地址空間”:即從程式4GB地址空間中保留段地址空間,這個過程通過VirtualAlloc函式完成,並把分配型別引數設定為MEM_RESERVE。這段空間的起始地址必須是系統分配粒度的整數,大小必須是系統頁面大小的整數

 

4.2 “提交一段記憶體空間”:即為程式已保留的地址空間對映機器的實體記憶體,這裡要特別注意,所謂實體記憶體一般並不是機器的主存,而只是機器的虛擬記憶體。這個過程同樣又VirtualAlloc完成,只是把分配型別引數設定為MEM_COMMIT。這段空間的起始地址和大小都必須是頁面大小的整數。這樣程式的對應被提交的區域就被對映到機器的虛擬記憶體上。

 

4.3 “將記憶體空間對映到主存”:這點很重要,作業系統總是隻有在程式提交的頁面被訪問時才將相應的頁面載入到主存中,同時修改程式對應頁面的地址空間對映。這時,程式的地址空間中的對應區域才和機器上的主存對應起來。

 

Virtual Size

 

      該指標記錄了當前程式申請成功的其虛擬地址空間的總的空間大小,包括DLL/EXE佔用的地址和通過VirtualAlloc API ReserveMemory Space數量。請注意,該指標包括保留的地址空間。

 

Private Bytes

 

       該指標記錄了程式使用者方式分割槽地址空間中已提交的總的空間大小。無論是直接呼叫API申請的記憶體,被Heap Manager申請的記憶體,或者是CLR managed heap,都算在裡面。

 

Working Set

 

       該指標記錄了所有對映到程式虛擬地址空間的機器主存的大小,它不僅僅是使用者方式分割槽部分的對映,而是整個程式地址空間的對映。即它同時包括核心方式分割槽中對映到機器主存的部分。由4.3可知,在使用者方式分割槽部分只有在程式提交的頁面被訪問時才將相應的頁面載入到主存中。而對於該部分的大小總是系統頁面大小的整數

 

       這裡有一個問題,隨著程式的不斷執行,程式被訪問的頁面將可能不斷增加,這是否意味著“Working Set”的大小會不斷的累加呢?顯然不是。在程式執行過程中影響“Working Set”的因素包括:(1) 機器可用主存的大小 (2) 程式本身“Working Set”的大小範圍。當機器的可用主存小於一定值時,系統會釋放一些老的最近沒有被訪問的頁面,把這些頁面通過交換檔案交換到機器的虛擬記憶體中;當Working Set的大小大於該程式所設定的最大值時,同樣會把一些老的頁面交換到機器的虛擬記憶體中。當這些頁面下次再被訪問時,它們才載入到主存。

 

       由上可知,Working Set“一定比”Private Bytes“小,因為它只是”Private Bytes“對應的地址空間中被載入到主存的那部分

 

Page Faults”

 

       該指標和Working Set密切相關,當程式訪問某個頁面,而這個頁面卻不在主存中時,就要發生一次Page Fault“,即程式訪問非”Working Set“中的頁面時,發生一次”Page Fault“,同時系統將對應頁面載入到主存中。

 

       接下來的三個指標是對Working Set“的細化:

 

WS Private“

 

       該指標記錄了程式Working Set“中被該程式所獨享的空間大小。

 

"WS Shareable"

 

       該指標記錄了程式Working Set“中能與別的程式共享的空間大小

 

WS Shared“

 

       該指標記錄了程式Working Set“中已經與別的程式共享的空間大小

 

WS Shareable“和”WS Shared“兩個指標一看令人感到疑惑,因為既然”Working Set“屬於”Private Bytes“中的一部分,而”Private Bytes“是程式私有的,為什麼會有”WS Shareable“和”WS Shared“這兩項呢?

 

       認真一想,其實很容易理解,比如兩個程式都需要同一個DLL的支援,所以在程式執行過程中,這個DLL被對映到了兩個程式的地址空間中,如果這個DLL的大小為4K,在兩個程式中都要提交4K的虛擬地址空間來對映這個DLL。當第一個程式訪問了這個DLL時,這個DLL被載入到機器主存中,這時,第二個程式也要訪問該DLL,這時,系統就不會再載入一遍該DLL了,因為這個DLL已經在主存中了。當然上面所說的訪問僅僅是讀取的操作,如果這時候某個程式要修改DLL對應這段地址中的某個單元時,這時,系統必須為第二個程式分配另外的新頁面,並把要修改位置對應的頁面拷貝的這個新頁面,同時,第二個程式中的這個DLL被對映到這個新頁面上。

 

       上面的分析中,DLL對應的4K的記憶體在第一個程式中便是WS Shareable“。另外,核心方式分割槽中的所有程式碼都是被所有程式共享的,只要一個程式訪問了這些頁面,則在所有的程式的Working Set“中都能體現。

 

三、下面我們來討論一下這些記憶體指標與程式記憶體消耗之間的關係

 

       在計算機更新換代不斷加速的今天,我們往往很少關注程式對記憶體的消耗,除非程式的記憶體消耗超出了我們的忍受範圍——大量的洩漏、執行速度下降等。

 

       那麼,當我們在測程式的記憶體使用量時,到底應該使用哪個指標能更好的反應程式的記憶體消耗呢?由於Windows自帶的Task Manager中的Memory Usage“所對應的指標就是”Working Set,所以大部分人認為該指標能夠很好的反應程式的記憶體使用量。

 

在得出結論之前,讓我們來分析一下以上的這些指標:

 

就從Working Set“開始吧。

 

Working Set“:

 

       程式中被載入到機器主存的所有頁面大小的。它可細分為WS Shareable“和”WS Shared“。程式訪問頁面不再Working Set“中時,會發生一次”Page Fault“且同時發生一次主存與虛擬記憶體之間的資料交換。綜上所述,我們可以得出結論:

 

(a)Working Set“不是程式記憶體消耗的全部;

 

(b)所有程式Working Set“的和也不等有機器主存總的消耗量,因為存在”Working Shareable“與別的程式共享;

 

(c)Working Set“太大會影響機器的執行速度,因為”Working Set“太大會導致機器的可用主存太少,從而導致將程式的老頁面釋放到虛擬記憶體,同時,程式”Working Set“中的頁面減少後,使程式發生”Page Fault“的頻率更高。因為在主存與虛擬記憶體之間交換資料需要時間,所以機器的執行速度要減慢。

 

(d)Working Set“由於資料交換的存在,該指標是動態的,在測量的過程中會不斷變化。(變化的最小單位為4K

 

       所以Working Set“指標強調的是程式對機器主存的消耗,不是程式記憶體的全部資訊。

 

"Private Bytes"

 

       該指標包含所有為程式提交的記憶體,包括機器主存和虛擬記憶體,可以認為它是程式對實體記憶體消耗且該指標相對來說更加穩定。在程式產生記憶體洩漏時,該值一定是不斷上漲的。

 

       綜上所述,個人更傾向於使用Private Bytes“來定量程式的記憶體消耗和分析程式的記憶體洩漏。


相關文章