windows記憶體管理(1)

來源:互聯網
上載者:User

記憶體管理(一)

2009-10-27 15:31

本質上虛擬記憶體就是要讓一個程式的代碼和資料在沒有全部載入記憶體時即可運行。運行過程中,當執行到尚未載入記憶體的代碼,或者要訪問還沒有載入到記憶體的資料時,虛擬記憶體管理器動態地將這部分代碼或資料從硬碟載入到記憶體中。而且在通常情況下,虛擬記憶體管理器也會相應地先將記憶體中某些代碼或者資料置換到硬碟中,為即將載入的代碼或資料騰出空間。

因為記憶體和硬碟之間的資料轉送相對代碼執行來說,是非常慢的操作,因此虛擬記憶體管理器在保證工作正確的前提下,還必須考慮效率因素。比如,它需要最佳化置換演算法,盡量避免就要執行的代碼或訪問的資料剛被置換出記憶體,而很久沒有訪問的代碼或資料卻一直駐留在記憶體中。另外它還需要將駐留在記憶體的各個進程的代碼或資料維持在一個合理的數量上,並且根據該進程的效能表現動態調整此數量,等等,使得程式運行時將其涉及的磁碟I/O次數降到儘可能低,以提高程式的運行效能。

本章前一部分著重介紹Windows的虛擬記憶體管理機制,後一部分則簡要介紹Linux的虛擬記憶體管理機制。

4.1 Windows記憶體管理

如果從應用程式的角度來看Windows虛擬記憶體管理系統,可以扼要地歸結為一句話。即Win32虛擬記憶體管理器為每一個Win32進程提供了進程私人且基於頁的4 GB(32位)大小的線性虛擬位址空間,這句話可以分解如下:

(1)“進程私人”意味著每個進程都只能訪問屬於自己的地址空間,而無法訪問其他進程的地址空間,也不用擔心自己的地址空間會被其他進程看到(父子進程例外,比如調試器利用父子進程關係來訪問被調試進程的地址空間,這裡不詳述)。需要注意的是,進程運行時用到的dll並沒有屬於自己的虛擬位址空間。而是其所屬進程的虛擬位址空間,dll的全域資料,以及通過dll函數申請的記憶體都是從調用其進程的虛擬位址空間中開闢。

(2)“基於頁”是指虛擬位址空間被劃分為多個稱為“頁”的單元,頁的大小由底層處理器決定,x86中頁的大小為4 KB。頁是Win32虛擬記憶體管理器處理的最小單元,相應的實體記憶體也被劃分為多個頁。虛擬記憶體地址空間的申請和釋放,以及記憶體和磁碟的資料轉送或置換都是以頁為最小單位進行的。

(3)“4 GB大小”意味著進程中的地址取值範圍可以從0x00000000到0xFFFFFFFF。Win32將低區的2 GB留給進程使用,高區的2 GB則留給系統使用。

Win32中用來輔助實現虛擬記憶體的硬碟檔案稱為“調頁檔案”,可以有16個,調頁檔案用來存放被虛擬記憶體管理器置換出記憶體的資料。當這些資料再次被進程訪問時,虛擬記憶體管理器會先將它們從調頁檔案中置換進記憶體,這樣進程可以正確訪問這些資料。使用者可以自己配置調頁檔案。出於空間利用效率和效能的考慮,程式碼(包括exe和dll檔案)不會被修改,所以當它們所在的頁被置換出記憶體時,並不會被寫進調頁檔案中,而是直接拋棄。當再次被需要時,虛擬記憶體管理器直接從存放它們的exe或dll檔案中找到它們並調入記憶體。另外對exe和dll檔案中包含的唯讀資料的處理與此類似,也不會為它們在調頁檔案中開闢空間。

當進程執行某段代碼或者訪問某些資料,而這些代碼或者資料還沒有在記憶體時,這種情形稱為“缺頁錯誤”。缺頁錯誤的原因有很多種,最常見的一種就是已經提到的,即這些代碼和資料被虛擬記憶體管理器置換出了記憶體,這時虛擬記憶體管理器在這段代碼執行或者這些資料被訪問前將它們調入記憶體。這個操作對開發人員來說是透明的,因此大大簡化了開發人員的負擔。但是調頁錯誤涉及磁碟I/O,大量的調頁錯誤會大大降低程式的總體效能。因此需要瞭解缺頁錯誤的主要原因,以及規避它們的方法。

4.1.1 使用虛擬記憶體

Win32中分配記憶體分為兩個步驟:“預留”和“提交”。因此在進程虛擬位址空間中的頁有3種狀態:自由(free)、預留(reserved)和提交(committed)。

(1)自由表示此頁尚未被分配,可以用來滿足新的記憶體配置請求。

(2)預留指從虛擬位址空間中划出一塊地區(region,頁的整數倍數大小),划出之後這個地區中的頁不能用來滿足新的記憶體配置請求,而是用來供要求“預留”此段地區的代碼以後使用。預留時並沒有分配實體儲存體,只是增加了一個描述進程虛擬位址空間使用狀態的資料結構(VAD,虛擬位址描述符),用來記錄這段地區已被預留。“預留”操作相對較快,因為沒有真正分配實體儲存體。也正因為沒有分配真正的實體儲存體,所以預留的空間並不能夠直接存取,對預留頁的訪問會引起“記憶體訪問違例”(記憶體訪問違例會導致整個進程立刻退出,而不僅僅是中止引起該違例的線程)。

(3)提交,若想得到真正的實體儲存體,必須對預留的記憶體進行提交。提交會從調頁檔案中開闢空間,並修改VAD中的相應項。注意,提交時也並沒有立刻從實體記憶體中分配空間,而只是從磁碟的調頁檔案中開闢空間。這個空間用做以後置換的備份空間,直到有代碼第一次訪問這段提交記憶體中的某些資料時,系統發現並沒有真正的實體記憶體,拋出缺頁錯誤。虛擬記憶體管理器處理此缺頁錯誤,直到這時才會真正分配實體記憶體,提交也可以在預留的同時一起進行。需要注意的是,提交操作會從調頁檔案中開闢磁碟空間,所以比預留操作的時間長。

這也是Win32虛擬記憶體管理中的demand-paging策略的一個體現,即不到真正訪問時,不會為某虛擬位址分配真正的實體記憶體。這種策略一是出於效能考慮,將工作分段完成,提高總體效能;二是出於空間效率考慮,不到真正訪問時,Win32總是假定進程不會訪問大多數的資料,因而也不必為它們開闢儲存空間或將其置換進實體記憶體,這樣可以提高儲存空間(磁碟和實體記憶體)的使用效率。

設想某些程式對記憶體有很大的需求,但又不是立即需要所有這些記憶體,那麼一次就從實體儲存體中開闢空間滿足這些還只是“潛在”的需求,從執行效能和儲存空間效率來說,都是一種浪費。因為只是“潛在”需求,極有可能這些分配的記憶體中很大一部分最後都沒有真正被用到。如果在申請的時候就一次性為它們分配全部實體儲存體,無疑會極大地降低空間的利用效率。

另一方面,如果完全不用預留及提交機制,只是隨需分配記憶體來滿足每次的請求,那麼對一個會在不同時間點頻繁請求記憶體的代碼來說,因為在它請求記憶體的不同時間點的間隙極有可能會有其他代碼請求記憶體。這樣這段在不同時間點頻繁請求記憶體的代碼請求得到的記憶體因為虛擬位址不連續,無法很好地利用空間locality特性,對其整體進行訪問(比如遍曆操作)時就會增加缺頁錯誤的數量,從而降低程式的效能。

預留和提交在Win32中都使用VirtualAlloc函數完成,預留傳入MEM_RESERVE參數,提交傳入MEM_COMMIT參數。釋放虛擬記憶體使用VirtualFree函數,此函數根據不同的傳入參數,與VirtualAlloc相對應,可以釋放與虛擬位址地區相對應的實體儲存體,但該虛擬位址地區還可處於預留狀態,也可以連同虛擬位址地區一起釋放,該段地區恢複為自由狀態。

線程棧和進程堆的實現都利用了這種預留和提交兩步機制,下面僅以線程棧為例來說明Win32系統是如何使用這種預留和提交兩步機制的。

建立線程棧時,只是一個預留的虛擬位址地區,預設是1 MB(此大小可在CreateThread或在連結時通過連結選項修改),初始時只有前兩頁是提交的。當線程棧因為函數的嵌套調用需要更多的提交頁時,虛擬記憶體管理器會動態地提交該虛擬位址地區中的後續頁以滿足其需求,直到到達1 MB的上限。當到達此預留地區大小的上限(預設1 MB)時,虛擬記憶體管理器不會增加預留地區大小,而是在提交最後一頁時拋出一個棧溢出異常,拋出棧溢出異常時該棧還有一頁空間可用,程式仍可正常運行。而當程式繼續使用棧空間,用完最後一頁後,還繼續需要儲存空間,這時就超過了上限,會直接導致進程退出

所以為防止線程棧溢出導致整個程式退出,應該注意盡量控制棧的使用大小。比如減少函數的嵌套層數,減少遞迴函式的使用,盡量不要在函數中使用太大的局部變數(大的對象可以從堆中開闢空間存放,因為堆會動態擴大,而線程棧的可用記憶體地區線上程建立時就已固定,之後在整個線程生命期間無法擴充)。

另外為了防止因為一個線程棧的溢出導致整個進程退出,可以對可能會產生線程棧溢出的線程體函數加異常處理,捕獲在提交最後一頁時拋出的溢出異常,並做出相應處理。

4.1.2 訪問虛擬記憶體時的處理流程

對某虛擬記憶體地區進行了預留並提交之後,就可以對該地區中的資料進行訪問了,描述了當程式對某段記憶體訪問時的處理流程:

4-1所示,當該資料已在實體記憶體中時,虛擬記憶體管理器只需將指向該資料的虛擬位址映射為物理指標,即可訪問到實體記憶體中的真正資料。這一步不會涉及磁碟I/O,速度相對較快。

當第一次訪問一段剛剛提交的記憶體中的資料時,因為並沒有真正的實體記憶體分配給它。或者該資料以前已被訪問過,但是被虛擬記憶體管理器置換出了記憶體。這兩種情形都會引發缺頁錯誤,虛擬記憶體管理器此時會處理這一缺頁錯誤,它先檢測此資料是否在調頁檔案中已有備份空間(exe和dll的字碼頁和唯讀資料頁情形與此類似,但是其備份空間不在調頁檔案,而是包含它們的exe或dll檔案)。如果是這兩種情況,表明訪問的資料在磁碟中有備份,接下來虛擬記憶體管理器就需要在實體記憶體中找到合適的頁,並將存放在磁碟的備份資料置換進實體記憶體。

圖4-1 訪問虛擬記憶體的處理流程

虛擬記憶體管理器首先查詢當前實體記憶體中是否有空閑頁,虛擬記憶體管理器維護一個稱為“頁幀資料庫”(page-frame database)的資料結構,此資料結構是作業系統全域的,當Windows啟動時被初始化,用來跟蹤和記錄實體記憶體中每一個頁的狀態,它會用一個鏈表將所有空閑頁串連起來,當需要空閑頁時,直接尋找此空閑頁鏈表,如果有,直接使用某個空閑頁;否則根據調頁演算法首先選出某個頁。需要指出的是,虛擬記憶體管理器調頁時並不是只調入一個頁,為了利用局部特性,它在調入包含所需資料的頁的同時,會將其附近的幾個頁一起調入記憶體。這裡為了簡單和清楚起見,假定只調入目標頁。但應該意識到Win32調頁時的這個特性,因為可以利用它來提高程式效率。這個頁將會用來存放即將從磁碟置換進來的頁的內容。選出某個記憶體頁後,接著檢查此頁狀態,如果此頁自上次調進記憶體以來尚未被修改過,則直接使用此頁(字碼頁和唯讀頁也可以直接使用);反之,如果此頁已被修改過(“髒”),則需要先將此頁的內容“寫”到調頁檔案中與此頁相對應的備份頁中,並隨即將此頁標為空白閑頁。

現在,有了一個空閑頁用來存放即將要訪問的資料。此時,虛擬記憶體管理器會再次檢測,此資料是否是剛被申請的記憶體且是第一次被訪問。如果是,則直接將此空閑頁清0使用即可(不必從磁碟中將其備份頁的內容讀進,因為該備份頁中的內容無意義);如果不是,則需要將調頁檔案中該頁的備份頁讀到此空閑頁中,並隨即將此頁的狀態從空閑頁改為活動頁。

此時,此資料已在實體記憶體頁中,通過虛擬位址映射到物理地址,即就可訪問此資料了。

上述為訪問成功時的情形,但情形並非總是如此。比如當使用者定義了一個數組,而此數組剛好在其所在頁的下邊界,且此頁的下一頁剛好是自由或者預留的(不是提交的,即沒有真正的實體儲存體)。當程式不小心向下越界訪問此數組,則首先引發缺頁錯誤。隨即虛擬記憶體管理器在處理缺頁錯誤時檢測到它也不在調頁檔案中,這就是所謂的“訪問違例”(access violation)。訪問違例意味著要訪問的地址所在的虛擬記憶體頁還沒有被提交,即沒有實際的實體儲存體與之對應,訪問違例會直接導致整個進程退出(即crash)。

可以看到,指標越界訪問的後果根據運行時實際情況而有所不同。如上所述,當數組並非處於其所在頁的邊界,越界後還在同一頁中,這時只會“誤訪問”(誤讀或誤寫,其中誤讀只會影響到正在執行的代碼;誤寫則會影響到其他處代碼的執行)該頁中其他資料,而不會導致整個進程的crash。即使在該數組真的處於其所在頁的邊界,且越界後指標值落在了其相鄰頁。但如果此相鄰頁碰巧也為一個提交頁,此時仍然只是“誤訪問”,也不會導致進程的crash。這也意味著,同一個應用程式的代碼中存在著指標越界訪問錯誤,運行時有時crash,但有時則不會。

Microsoft提供了一個監測指標越界訪問的工具pageheap,它的原理就是強制使每次分配的記憶體都位於頁的邊界,同時強制該頁的相鄰頁為自由頁(即不分配其相鄰頁給程式使用)。這樣每次越界訪問都會立即引起access violation,導致程式crash。從而使得指標越界訪問錯誤在開發期間一定會被暴露出來,而不會發生某個指標越界訪問錯誤一直隱藏到Release版本,直到終端使用者使用時才被發現的情形。

4.1.3 虛擬位址到物理地址的映射

如上所述,在確保訪問的資料已在實體記憶體中後,還需要先將虛擬位址轉換為物理地址,即“地址映射”,才能夠真正訪問此資料。本節講述Win32中虛擬記憶體管理器如何將虛擬位址映射為物理地址。

Win32通過一個兩層表結構來實現地址映射,因為4 GB虛擬位址空間為每個進程私人,相應地,每個進程都維護一套自己的層次表結構用來實現其地址映射。第一層表稱為“頁目錄”(page directory),實際上就是一個記憶體頁(4 KB = 4 096 byte)。這一頁以四個位元組為單元分為1 024項,每一項稱為一個“頁目錄項”(Page Directory Entry,PDE);第二層表稱為“頁表”(page table),共有1 024個頁表。頁目錄中每一個頁目錄項PDE對應這一層中的某一個頁表,每一個頁表也佔了一個記憶體頁。這一頁中的4 KB,即4 096個位元組也像頁目錄那樣被分成1 024項,每項4個位元組,頁表的每一項則稱為“頁表項”(Page Table Entry,PTE)。每一個頁表項PTE都指向實體記憶體中的某一個頁幀,4-2所示。

圖4-2 頁表

已經知道,Win32提供了4 GB(32位)大小的虛擬位址空間。因此每個虛擬位址都是一個32位的整數值,這32位由3個部分組成,4-3所示。

圖4-3 虛擬位址空間

這三個部分中的第一部分,即前10位為頁目錄下標,用其可以定位在頁目錄的1 024項中的某一項。根據定位到的那一項的項值,可以找到第2層頁表中的某一個頁表。虛擬位址的第二部分,即中間的10位為頁表下標,可用來定位剛剛找到的頁表的1 024項中的某一項。此項值可以找到實體記憶體中的某一個頁,此頁包含此虛擬位址所代表的資料。最後用虛擬位址的第三部分,即最後12位可用來定位此物理頁中的特定的位元組位置,12位剛好可以定位一個頁中的任意位置的位元組。

舉一個具體的例子,假設在程式中訪問一個指標(Win32中的“指標”意味虛擬位址),此指標值為0x2A8E317F,圖4-4所示為虛擬位址到物理地址的映射過程。

0x2A8E317F的二進位寫法為0010101010,0011100011,000101111111,為了方便起見,將這32位分成10位、10位和12位。第一個10位00101010用來定位頁目錄中的頁目錄項,因為頁目錄項為四個位元組,定位前將此10位左移兩位,即0010101000(0x2A8)。再用此值作為下標找到對應的頁目錄項,此頁目錄項指向一個頁表。同樣方法再用第二個10位0011100011定位此頁表中的頁表項。此頁表項指向真正的實體記憶體,然後用最後12位000101111111定位頁內的資料(此時這12位不用再左移,因為物理頁內定位時,需要能定位到每一個位元組。而不像頁目錄和頁表中,只需要定位每4個位元組的第1個位元組),即為此指標指向的資料。

上面假設的是此資料已在實體記憶體中,其實,“判斷訪問的資料是否在記憶體中”這一步驟,也是在這個地址映射過程中完成的,Win32總是假使資料已在實體記憶體中,並進行地址映射。頁表項中有一位用來標識包含此資料的頁是否在實體記憶體頁中,當取得頁表項時,檢測此位,如果在,就是本節描述的過程,如果不在,則拋出缺頁錯誤,此時此頁表項中包含了此資料是否在調頁檔案中,如果不在,則為訪問違例,如果在,此頁表項可查出了此資料頁在哪個調頁檔案中,以及此資料頁在該調頁檔案中的起始位置,然後根據這些資訊將此資料頁從磁碟中調入實體記憶體中,再繼續進行地址映射過程。

已經說過,為了實現虛擬位址空間各進程私人,每個進程都擁有自己的頁目錄和頁表結構,對不同進程而言,頁目錄中的頁目錄項值(PDE),以及頁表中的頁表項值(PTE)都是不同的,因此相同的指標(虛擬位址)被不同的進程映射到的物理地址也是不同的。這也意味著,在不同進程間傳遞指標是沒有意義的。

4.1.4 虛擬記憶體空間使用狀態記錄

當通過VirtualAlloc申請一塊虛擬記憶體時,虛擬記憶體管理器是如何知道哪些記憶體塊是自由的,可以用來滿足此次記憶體請求呢?即Win32虛擬記憶體如何維護和記錄每一個進程的4 GB虛擬記憶體地址空間的使用狀態,如各個地區的狀態、大小及起始地址呢?

上一節中,讀者也許會認為可以通過遍曆頁目錄和頁表中的項值來收集虛擬記憶體空間的使用狀態,但這樣做首先有效率問題,因為每次申請記憶體都需要做一次搜尋。但這個方法不僅僅是因為效率有問題,而且還是行不通的,對預留的頁來說,虛擬記憶體管理器並沒有為之分配實體儲存體。所以也就不會為其填寫頁表項,這時遍曆頁表無法分辨某塊虛擬記憶體是自由還是預留的。另外即使對提交頁來說,遍曆頁表也無法得到完整的資訊,正如4.1.1節中提到的Win32在虛擬記憶體管理時用到的主要策略demand-paging,即Win32虛擬記憶體管理器在程式沒有實際訪問某塊記憶體前,總是假定這塊記憶體不會被訪問到,因此不會為這塊記憶體做過多處理,包括不會為其分配真正的實體記憶體空間,甚至頁表,即進程中用來完成虛擬位址到物理地址映射的頁表的儲存空間也是隨需分配的。

Win32虛擬記憶體管理器使用另外一個資料結構來記錄和維護每個進程的4 GB虛擬位址空間的使用及狀態資訊,這就是虛擬位址描述符樹(Virtual Address Descriptor,VAD)。每一個進程都有一個自己的VAD集合,這個集合中的VAD被組織成一個自平衡二叉樹,以提高尋找的效率。另外只有預留或者提交的記憶體塊才會有VAD,自由的記憶體塊沒有VAD(因此不在VAD樹結構中的虛擬位址塊就是自由的)。VAD的組織4-5所示。

圖4-5 VAD的組織圖

(1)當程式申請一塊新記憶體時,虛擬記憶體管理器只需訪問VAD樹。找到兩個相鄰VAD,只要小的VAD的上限與大的VAD的下限之間的差值滿足所申請的記憶體塊的大小需求,即可使用二者之間的虛擬記憶體。

(2)當第一次訪問提交的記憶體時,虛擬記憶體管理器根據上一節描述的流程。即總是假定該資料頁已在實體記憶體中,並進行虛擬位址到物理地址的轉換。當找到相應的頁目錄項後發現該頁目錄項並沒有指向一個合法的頁表,它就會尋找該進程的VAD樹。找到包含該地址的VAD,並根據VAD中的資訊,比如該記憶體塊的大小、範圍,以及在調頁檔案中的起始位置等,隨需產生相應的頁表項,然後從剛才發生缺頁錯誤的地方繼續進行地址映射。由此可以看出,一個虛擬記憶體頁被提交時,除了在調頁檔案中開闢一個備份頁之外,不會產生包含指向它的頁表項的頁表,也不會填充指向它的頁表項,更不會為之開闢真正的實體記憶體頁,而是直到第一次訪問這個提交頁時,才會“隨需地”從VAD中取得包含該頁的整個地區的資訊,產生相應頁表,並填充相應頁的表項。

(3)當訪問預留的記憶體時,虛擬記憶體管理器也是根據上一節描述的流程進行虛擬位址到物理地址的映射,找到相應的頁目錄項後發現該頁目錄項並沒有指向一個合法的頁表,它就會尋找該進程的VAD樹,找到包含該地址的VAD。這時它會發現此段記憶體塊只是預留的,而沒有提交,即並沒有對應的真正的實體儲存體,這時直接拋出訪問違例,進程退出。

(4)當訪問自由的記憶體時,虛擬記憶體管理器還是根據上一節描述的流程進行虛擬位址到物理地址的映射。找到相應的頁目錄項後發現該頁目錄項並沒有指向一個合法的頁表,它就會尋找該進程的VAD樹,發現並沒有VAD包含此虛擬位址,此時可以知道該地址所在的虛擬位址頁是自由狀態,直接拋出訪問違例,進程退出。

4.1.5 進程工作集

因為頻繁的調頁操作引起的磁碟I/O會大大降低程式的運行效率,因此對每一個進程,虛擬記憶體管理器都會將其一定量的記憶體頁駐留在實體記憶體中。並跟蹤其執行的效能指標,動態調整這個數量。Win32中駐留在實體記憶體中的記憶體頁稱為進程的“工作集”(working set),進程的工作集可以通過“工作管理員”查看,其中“記憶體使用量”列即為工作集大小。圖4-6中綠色方框的數字是筆者寫作本書時所用Word編輯器的工作集大小,即38740 KB。

工作集是會動態變化的,進程初始時只有很少的字碼頁和資料頁被調入記憶體。當執行到未被調入記憶體的代碼或者訪問到尚未調入記憶體的資料時,這些字碼頁或者資料頁會被調入實體記憶體,工作集也隨之增長。但工作集不能無限增長,系統為每個進程都定義了一個預設的最小工作集(根據系統實體記憶體大小,此值可能為20~50 MB)和最大工作集(根據系統實體記憶體大小,此值可能為45~345 MB)。當工作集到達最大工作集,即進程需要再次調入新頁到實體記憶體中時,虛擬記憶體管理器會將其原來的工作集中的某些頁先置換出記憶體,然後將需要調入的新頁調入記憶體。

圖4-6 工作集

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.