windows記憶體管理(2)

來源:互聯網
上載者:User

因為工作集的頁駐留在實體記憶體中,因此對這些頁的訪問不涉及磁碟I/O,相對而言非常快;反之,如果執行的代碼或者訪問的資料不在工作集中,則會引發額外的磁碟I/O,從而降低程式的運行效率。一個極端的情況就是所謂的顛簸或抖動(thrashing),即程式的大部分的執行時間都花在了調頁操作上,而不是代碼執行上。

如前所述,虛擬記憶體管理器在調頁時,不僅僅只是調入需要的頁,同時還將其附近的頁也一起調入記憶體中。綜合這些知識,對開發人員來說,如果想提高程式的運行效率,應該考慮以下兩個因素。

(1)對代碼來說,盡量編寫緊湊代碼,這樣最理想的情形就是工作集從不會到達最大閥值。在每次調入新頁時,也就不需要置換已經載入記憶體的頁。因為根據locality特性,以前執行的代碼和訪問的資料在後面有很大可能會被再次執行或訪問。這樣程式執行時,發生的缺頁錯誤數就會大大降低,即減少了磁碟I/O,在圖4-6中也可以看到一個程式執行時截至當時共發生的缺頁錯誤次數。即使不能達到這種理想情形,緊湊的代碼也往往意味著接下來執行的代碼更大可能就在相同的頁或相鄰頁。根據時間locality特性,程式80%的時間花在了20%的代碼上。如果能將這20%的代碼盡量緊湊且排在一起,無疑會大大提高程式的整體運行效能。

(2)對資料來說,盡量將那些會一起訪問的資料(比如鏈表)放在一起。這樣當訪問這些資料時,因為它們在同一頁或相鄰頁,只需要一次調頁操作即可完成;反之,如果這些資料分散在多個頁(更糟的情況是這些頁還不相鄰),那麼每次對這些資料的整體訪問都會引發大量的缺頁錯誤,從而降低效能。利用Win32提供的預留和提交兩步機制,可以為這些會一同訪問的資料預留出一大塊空間。此時並沒有分配實際儲存空間,然後在後續執行過程中產生這些資料時隨需為它們提交記憶體。這樣既不浪費真正的實體儲存體(包括調頁檔案的磁碟空間和實體記憶體空間),又能利用locality特性。另外記憶體池機制也是基於類似的考慮。

4.1.6 Win32記憶體相關API

在Win32平台下,開發人員可以通過如下5組函數來使用記憶體(完成申請和釋放等操作)。

(1)傳統的CRT函數(malloc/free系列):因為這組函數的平台無關性,如果程式會被移植到其他非Windows平台,則這組函數是首選。也正因為這組函數非Win32專有,而且介紹這組函數的資料俯拾皆是,這裡不作詳細介紹。

(2)global heap/local heap函數(GlobalAlloc/LocalAlloc系列):這組函數是為了向後相容而保留的。在Windows 3.1平台下,global heap為系統中所有進程共有的堆,這些進程包括系統進程和使用者進程。它們對此global heap記憶體的申請會交錯在一起,從而使得一個使用者進程的不小心的記憶體使用量錯誤會導致整個作業系統的崩潰。local heap又被稱為“******* heap”,與global heap相對應,local heap為每個進程私人。進程通過LocalAlloc從自己的local heap裡申請記憶體,而不會相互幹擾。除此之外,進程不能通過另外的使用者自訂堆或者其他方式動態地申請記憶體。到了Win32平台,由於考慮到安全因素,global heap已經廢棄,local heap也改名為“process heap”。為了使得以前針對Windows 3.1平台寫的應用程式能夠運行在新的Win32平台上,GlobalAlloc/ LocalAlloc系列函數仍然得到沿用,但是這一系列函數最後都是從process heap中分配記憶體。不僅如此,Win32平台還允許進程除process heap之外產生和使用新的使用者自訂堆,因此在Win32平台下建議不使用GlobalAlloc/LocalAlloc系列函數進行記憶體操作,因此這裡不詳細介紹這組函數。

(3)虛擬記憶體函數(VirtualAlloc/VirtualFree系列):這組函數直接通過保留(reserve)和提交(commit)虛擬記憶體地址空間來操作記憶體,因此它們為開發人員提供最大的自由度,但相應地也為開發人員記憶體管理工作增加了更多的負擔。這組函數適合於為大型連續的資料結構數組開闢空間。

(4)記憶體對應檔函數(CreateFileMapping/MapViewOfFile系列):系統使用記憶體對應檔函數系列來載入.exe或者.dll檔案。而對開發人員而言,一方面通過這組函數可以方便地操作硬碟檔案,而不用考慮那些繁瑣的檔案I/O操作;另一方面,運行在同一台機器上的多個進程可以通過記憶體對應檔函數來共用資料(這也是同一台機器上進程間進行資料共用和通訊的最有效率和最方便的方法)。

(5)堆記憶體函數(HeapCreate/HeapAlloc系列):Win32平台中的每個堆都是各進程私人的,每個進程除了預設的進程堆,還可以另外建立使用者自訂堆。當程式需要動態建立多個小資料結構時,堆函數系列最為適合。一般來說CRT函數(malloc/free)就是基於堆記憶體函數實現的。

1.虛擬記憶體

虛擬記憶體相關函數共有4對,即VirtualAlloc/VirtualFree、VirtualLock/VirtualUnlock、VirtualQuery/VirtualQueryEx及VirtualProtect/VirtualProtectEx。其中最重要的是第一對,本節主要介紹這一對。

LPVOID VirtualAlloc(

   LPVOID lpAddress,

   DWORD dwSize,

   DWORD flAllocationType,

   DWORD flProtect

);

VirtualAlloc根據flAllocationType的不同,可以保留一段虛擬記憶體地區(MEM_ RESERVE)或者提交一段虛擬記憶體地區(MEM_COMMIT)。當保留時,除了修改進程的VAD之外(準確地說是增加了一項),並沒有分配其他資源,如調頁檔案空間或者實際實體記憶體,甚至沒有建立頁表項。因此非常快捷,而且執行速度與保留空間的大小沒有關係。因為保留僅僅只是讓記憶體管理器預留一段虛擬位址空間,並沒有實在的儲存(硬碟上的調頁檔案空間或者實體記憶體),因此訪問保留地址會引起訪問違例,這是一種嚴重錯誤,會直接導致進程退出;相反,提交虛擬記憶體時,記憶體管理器必須從系統調頁檔案中開闢實際的儲存空間,因此速度會比保留操作慢。但是需要注意的是,此時在實體記憶體中並沒有立刻分配空間用來與這段虛擬記憶體空間相對應,甚至也沒有相應的頁表項被建立,但是提交操作會相應修改VAD項。只有首次訪問這段虛擬位址空間中的某個地址時,由於缺頁中斷,虛擬記憶體管理器尋找VAD,接著根據VAD的內容,動態建立PTE,然後根據PTE資訊,分配實體記憶體頁,並實際訪問該記憶體。由此可見,真正花費時間的操作不是提交記憶體,而是對提交記憶體的第一次訪問!這種lazy-evaluation機制對程式運行效能是十分有益的,因為如果某個程式提交了大段記憶體,但只是零星地對其中的某些頁進行訪問,如果沒有這種lazy-evaluation機制,提交大段記憶體會極大地降低系統的效能。

與之相對,VirtualFree釋放記憶體,它提供兩種選擇:可以將提交的記憶體釋放給系統,但是不釋放保留的虛擬記憶體地址空間;也可以在釋放記憶體的同時將虛擬記憶體地址空間一併釋放,這樣這塊虛擬記憶體地址空間的狀態變回初始的自由狀態。如果記憶體是提交狀態,VirtualFree因為會釋放真正的儲存空間而比較慢;如果只是釋放保留的虛擬記憶體地址空間,那麼因為只需要修改VAD,該操作會很快。

除此之外,VirtualLock保證某塊記憶體在lock期間一直在實體記憶體中,因此對該記憶體的訪問不會引起缺頁中斷。lock的記憶體用VirtualUnlock解鎖。因為VirtualLock會把記憶體鎖定在實體記憶體中,如果這些記憶體實際中訪問的並不頻繁,那麼會使得其他經常使用到的記憶體反而增大了被調頁出去的機率,從而降低了系統的整體效能,因此在實際使用中,並不推薦使用VirtualLock/VirtualUnlock函數。VirtualQuery可以獲得傳入指標所在的虛擬記憶體塊的狀態,如包含該指標所在頁的虛擬記憶體地區的基址,以及該地區的狀態等。VirtualProtect可用來修改某段地區的提交記憶體頁的存取保護標誌。

2.記憶體對應檔

記憶體對應檔主要有三個用途,Windows利用它來有效使用exe和dll檔案,開發人員利用它來方便地訪問硬碟檔案,或者實現不同進程間的記憶體共用。第一種這裡不詳細介紹,只介紹後兩種用途。首先討論它提供的方便訪問硬碟檔案的機制,一旦通過這種機制將一個硬碟檔案(部分或者全部)映射到進程的一段虛擬位址空間中,讀寫該檔案的內容就像通過指標訪問變數一樣。假設pViewMem為檔案對應到記憶體的首址,那麼:

*pViewMem = 100;                     //寫檔案的第1個位元組

char ch = *(pViewMem + 50);     //讀檔案的第50個位元組內容

下面介紹這種機制的使用步驟。

(1)建立或者開啟一個硬碟檔案。

此步驟用來獲得一個檔案對象的控制代碼,用CreateFile函數來建立或者開啟一個檔案:

HANDLE CreateFile(

PCSTR pszFileName,

DWORD dwDesiredAccess,

DWORD dwShareMode,

PSECURITY_ATTRIBUTES psa,

DWORD dwCreationDisposition,

DWORD dwFlagsAndAttributes,

HANDLE hTemplateFile);

其中pszFileName參數指示該檔案的路徑名,dwDesiredAccess參數表示該檔案內容將會被如何訪問,此參數包括0、GENERIC_READ、GENERIC_WRITE,以及GENERIC_ READ | GENERIC_WRITE共4種可能,分別表示“不能讀也不能寫”(在只為了讀取該檔案屬性時使用)、“唯讀”、“唯寫”,以及“既可讀也可寫”;dwShareMode參數用來限定對該檔案的任何其他訪問的許可權,也包括上述4種類型。剩餘的幾個參數因為與要討論的問題關係不大,所以不贅述。

此函數成功時,會返回一個檔案物件控點;否則會返回INVALID_HANDLE_ VALUE。

(2)建立或者開啟一個檔案對應核心對象。

還需要有一個檔案對應核心對象,正是它真正將檔案內容映射到記憶體中。如果已經存在此核心對象,只需通過OpenFileMapping函數將其開啟即可,這個函數返回該命名物件的控制代碼。大多數情況下,需要建立一個檔案對應核心對象,此時調用CreateFileMapping函數:

HANDLE CreateFileMapping(

HANDLE hFile,

PSECURITY_ATTRIBUTES psa,

DWORD fdwProtect,

DWORD dwMaximumSizeHigh,

DWORD dwMaximumSizeLow,

PCTSTR pszName);

hFile參數是第一個步驟中返回的檔案核心物件控點;psa參數是指明核心對象安全特性的,不詳述;fdwProtect參數指明了對映射到記憶體頁中的檔案內容的存取許可權,這個許可權必須與第一個步驟中的檔案存取權限對應;dwMaximumSizeHigh和dwMaximumSizeLow參數指明映射的最大的空間大小,因為Windows支援大小達到64位的檔案,因此需要兩個32位的參數;pszName為核心對象名稱。

此步只是建立了一個檔案對應核心對象,並沒有預留或者提交虛擬位址空間,更沒有實體記憶體頁被分配出來存放檔案內容。

(3)對應檔的內容到進程虛擬位址空間。

訪問檔案內容之前,必須將要訪問的檔案內容映射到記憶體中,通過MapViewOfFile函數完成:

PVOID MapViewOfFile(

HANDLE hFileMappingObject,

DWORD dwDesiredAccess,

DWORD dwFileOffsetHigh,

DWORD dwFileOffsetLow,

SIZE_T dwNumberOfBytesToMap);

其中參數分別為:用來映射記憶體映射核心對象的控制代碼,映射的檔案內容到記憶體記憶體頁的存取許可權,需要映射的檔案內容的起始部分在檔案中的位移及大小。映射時並不需要一次將整個檔案的內容全部映射到記憶體中。

這個函數的操作包括從進程虛擬位址空間中預留出所需映射大小的一段地區,然後提交。提交時並不是從系統的調頁檔案中開闢空間用來作為該段地區的備份儲存,而是記憶體映射核心對象所對應的檔案的指明地區。與虛擬記憶體使用的惟一不同就是該段虛擬位址空間地區的備份儲存不同,其他都是一樣的。同樣,此時並沒有真正的實體記憶體開闢出來,直到通過返回的指標訪問已經映射到記憶體中的檔案內容時,因為發生缺頁錯誤,系統才會分配實體記憶體頁,並將對應的檔案儲存體中的內容調頁到該實體記憶體頁

4)訪問檔案內容。

現在可以通過MapViewOfFile函數返回的指標來訪問該段映射到記憶體中檔案內容,就像本小節示範的那樣,通過指標訪問硬碟檔案內容。

這裡需要提醒的是,通過該指標修改檔案內容時,修改的結果常常不會立刻反映到檔案中,因為實際上是在對調入實體記憶體頁中的資料進行修改。考慮到效能因素,該頁並不會每做一次修改就立刻將該修改同步到硬碟檔案中。如果需要在某個時候強制將之前所做的修改一次性同步到與之對應的硬碟檔案中時,可以通過FlushViewOfFile函數達到這個目的:

BOOL FlushViewOfFile(PVOID pvAddress, SIZE_T dwNumberOfBytesToFlush);

這個函數傳入需要將修改同步到硬碟檔案中的記憶體塊的起始地址和大小。

(5)取消檔案內容到進程虛擬位址空間的映射。

當該段映射到記憶體中的檔案內容訪問完畢,不再需要訪問時,為了有效地利用系統的資源,應該及時回收該段記憶體,這時調用UnmapViewOfFile函數:

BOOL UnmapViewOfFile(PVOID pvBaseAddress);

此函數傳入MapViewOfFile函數返回的指標,系統回收對應的MapViewOfFile調用時預留並提交的虛擬記憶體地址空間地區,這樣該段地區可被其他申請使用。另外因為對應的備份儲存不是系統的調頁檔案,所以不存在備份儲存回收的問題。

(6)關閉檔案對應核心對象和檔案核心對象。

最後,在完成任務不再使用該檔案時,通過CloseHandle(hFile)和CloseHandle (hMapping)來關閉檔案並釋放記憶體對應檔的核心物件控點。

下面接著討論如何利用記憶體對應檔核心對象來進行進程間的記憶體共用。

進程間通過記憶體對應檔進行記憶體共用時,該記憶體對應檔核心對象常常不是基於某一個硬碟檔案,而是從系統的調頁檔案中開闢空間作為臨時用做共用的儲存空間。因此與單純地利用記憶體對應檔來訪問硬碟檔案內容稍有不同,下面是通過記憶體對應檔來進行進程間記憶體共用的步驟。假設有進程A和進程B,進程A通過CreateFileMapping建立一個基於系統調頁檔案的名為“SharedMem”的記憶體對應檔核心對象:

HANDLE m_hFileMapA = CreateFileMapping

(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0,

10 * 1024, TEXT("SharedMem"));

需要注意的是,因為現在不再基於普通的硬碟檔案,所以不需要調用CreateFile來建立或者開啟檔案這個步驟,注意此時傳入的檔案控制代碼參數為INVALID_HANDLE_VALUE,此參數代表從調頁檔案中開闢空間作為共用記憶體。

進程B通過OpenFileMapping開啟剛才進程A建立的名為“SharedMem”的記憶體對應檔核心對象:

HANDLE m_hFileMapB = OpenFileMapping(..., TEXT("SharedMem"));

進程A和進程B都可以用此記憶體對應檔核心對象將從系統調頁檔案中開闢的那Block Storage空間的全部或者部分映射到記憶體中,然後即可使用。

進程A:

...

PVOID pViewA = MapViewOfFile(m_hFileMapA, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);

...

進程B:

...

PVOID pViewB = MapViewOfFile(m_hFileMapB, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);

...

它們各自對該共用記憶體的修改都能夠及時地被對方看到。另外需要注意的是,它們映射到的虛擬記憶體空間地區並不一定有相同的起始地址,這是因為它們擁有自己的虛擬位址空間。

還有一個需要引起注意,但很難發現的問題是因為建立基於系統調頁檔案的記憶體對應檔核心對象是通過傳入hFile為INVALID_HANDLE_VALUE的參數來標記的,而建立或者開啟普通硬碟檔案失敗時的傳回值也是INVALID_HANDLE_VALUE,因此諸如下面這段代碼存在的bug是很難發現的:

...

HANDLE hFile = CreateFile(...);

HANDLE hMap = CreateFileMapping(hFile, ...);

if (hMap == NULL)

return(GetLastError());

...

這段代碼的本意是首先建立或者開啟一個普通的硬碟檔案,然後建立一個基於此檔案的記憶體對應檔核心對象,而並不是想建立一個基於系統調頁檔案的該對象。但是可以看到,當第1句CreateFile執行失敗時,返回INVALID_HANDLE_VALUE。這個傳回值立刻被傳入到CreateFileMapping函數,結果建立了一個基於系統調頁檔案的記憶體對應檔核心對象。這並不是這段代碼的原意,而且也會造成問題。因為基於普通硬碟檔案的記憶體對應檔核心對象的操作往往希望將最後的結果儲存在該檔案中,而基於系統調頁檔案的記憶體對應檔核心對象的操作往往只是關注該資料在執行期的結果,操作完畢後並不儲存該結果。當CreateFile失敗且程式運行後,程式運行無誤。但是當檢查結果檔案時,會發現該檔案要麼沒有被建立,要麼資料沒有改動,因為隨後的操作都是基於系統調頁檔案的!

因此當使用基於普通硬碟檔案的記憶體對應檔核心對象時,一定要在CreateFile調用完後檢查傳回值。

3.堆

分配多個小塊記憶體一般都會選擇使用堆函數,比如鏈表節點和樹節點等,堆函數的最大優點就是開發人員不用考慮頁邊界之類的瑣碎事情;劣勢就是堆函數的操作相對虛擬記憶體和記憶體對應檔來說速度要慢些,而且無法像虛擬記憶體或者記憶體對應檔那樣直接提交或者回收實體儲存體。

進程都有一個預設的堆,其初始地區大小預設是1 MB,連結時可以通過/HEAP參數修改此預設值。很多操作的臨時儲存都使用進程的預設堆,比如絕大多數的Win32函數,進程預設堆的控制代碼可以通過GetProcessHeap函數獲得。

因為程式大部分的記憶體需求都是從進程預設堆中分配的,而且在多線程情況下還需要考慮安全執行緒問題。因此對特定的應用,這種情況會造成程式的效能下降。針對這種需求,Win32提供了自訂堆機制。

自訂堆的步驟如下。

(1)建立自訂堆。

與進程預設堆(進程建立時系統自動建立)不同,自訂堆需要開發人員首先通過HeapCreate函數建立:

HANDLE HeapCreate(

DWORD fdwOptions,

SIZE_T dwInitialSize,

SIZE_T dwMaximumSize);

fdwOptions參數可以指明是否需要序列化訪問支援(HEAP_NO_SERIALIZE),以及分配和回收記憶體出錯時是否拋出異常(HEAP_GENERATE_EXCEPTIONS)。當該自訂堆會被多個線程同時訪問時,需要加上序列化訪問支援,但相應的效能會有所下降。

dwInitialSize參數指明該自訂堆建立時提交的儲存大小(頁大小的倍數),dwMaximumSize參數則指明該自訂堆從進程虛擬位址空間中預留出的地區大小。隨著對此自訂堆記憶體的分配,提交的儲存大小隨之變大,但此參數限制了增大的極限。另一種情況時是dwMaximumSize為0,此時該自訂堆可以一直增長,直到進程虛擬位址空間用完。

(2)從自訂堆中分配記憶體。

從自訂堆中分配記憶體調用函數HeapAlloc(從進程預設堆中分配記憶體也調用此函數):

PVOID HeapAlloc(

HANDLE hHeap,

DWORD fdwFlags,

SIZE_T dwBytes);

hHeap參數即上一步驟中返回的堆核心物件控點,fdwFlags可以取HEAP_ ZERO_MEMORY、HEAP_GENERATE_EXCEPTIONS和HEAP_NO_SERIALIZE共3個值,HEAP_ZERO_MEMORY指明返回的記憶體必須全部清0。HEAP_GENERATE_ EXCEPTIONS指明此次分配記憶體如果失敗,需要拋出異常。如果該自訂堆建立時指明過此參數,則其上的記憶體配置不必再指明此參數;如果堆建立時沒有指明,則可以在每次申請時指明。HEAP_NO_SERIALIZE參數指明此次分配不必序列化訪問支援。最後的dwBytes參數指明此次分配的記憶體大小,傳回值為分配記憶體的起始位置。

(3)釋放記憶體。

從堆中釋放記憶體調用HeapFree函數:

BOOL HeapFree(

HANDLE hHeap,

DWORD fdwFlags,

PVOID pvMem);

這個函數的參數意義很明顯,無須贅述。需要指出的是,這樣釋放記憶體並不能保證所有實體儲存體被回收,一是因為實體儲存體以頁大小為單位判斷是否可以回收;二是Windows設計堆機制時對效率的考慮。

(4)銷毀自訂堆。

當程式不再需要使用某個自訂堆時,調用HeapDestroy函數:

BOOL HeapDestroy(HANDLE hHeap);

對堆的銷毀有幾點需要說明,一是當堆銷毀時,所有從該堆分配的記憶體全部被回收,而不必對那些記憶體一一進行釋放,同時該堆佔用實體儲存體以及虛擬位址空間地區也會被系統回收;二是如果沒有顯式銷毀自訂堆,這些堆會在程式退出時被系統銷毀。需要注意的是,線程建立的自訂堆並不會線上程退出時被銷毀,而是當整個進程退出時才會被銷毀,從資源利用效率角度出發,應該在自訂堆不再被使用時立即銷毀;三是進程預設堆不能通過此函數銷毀,更嚴格地說,進程預設堆在進程退出前是不能被銷毀的。

自訂堆的其他函數如下。

(1)獲得進程所有堆:

DWORD GetProcessHeaps(

DWORD dwNumHeaps,

PHANDLE pHeaps);

此函數返回進程目前所有的堆(包括進程預設堆),傳入存放所有堆核心物件控點的數組,以及數組的大小,傳回值為堆數目。

(2)修改分配記憶體的大小:

PVOID HeapReAlloc(

HANDLE hHeap,

DWORD fdwFlags,

PVOID pvMem,

SIZE_T dwBytes);

這個函數可以修改原來分配的記憶體塊(pvMem)的大小,新的大小由參數dwBytes指明。

(3)查詢某塊分配記憶體的大小:

SIZE_T HeapSize(

HANDLE hHeap,

DWORD fdwFlags,

LPCVOID pvMem);

這個函數可以查詢到原來分配的一個記憶體塊的大小。當該記憶體塊指標是外部模組傳入時,如果需要知道該塊確切大小時,這個函數就可以發揮作用。

(4)堆壓縮:

UINT HeapCompact(

HANDLE hHeap,

DWORD fdwFlags);

此函數將相鄰的回收回來的自由塊合并在一起,需要注意的是,這個函數並不能移動已經分配的記憶體塊,即它並不能消除記憶體片段

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.