長期以來,在計算機系統中,內存都是一種緊缺和寶貴的資源,應用程序必須在載入內存後才能執行。早期,在內存空間不夠大時,同時運行的應用程序的數量會受到很大的限制,甚至當某個應用程序在某個運行時所需內存超過物理內存時,應用程序就會沒法運行。現代操做系統(Windows、Linux)經過引入虛擬內存進行內存管理,解決了應用程序在內存不足時不能運行的問題。
本質上,虛擬內存就是要讓一個程序的代碼和數據在沒有所有載入內存時便可運行。運行過程當中,當執行到還沒有載入內存的代碼,或者要訪問尚未載入到內存的數據時,虛擬內存管理器動態地將相應的代碼或數據從硬盤載入到內存中。並且在一般狀況下,虛擬內存管理器也會相應地先將內存中某些代碼或數據置換到硬盤中,爲即將載入的代碼或數據騰出空間。
由於內存和硬盤間的數據傳輸相對於代碼執行很是慢,所以虛擬內存管理器在保證工做正確的前提下還必須考慮效率因素,如須要優化置換算法,儘可能避免將要被執行的代碼或訪問的數據剛被置換出內存,而好久沒有訪問的代碼或數據卻一直駐留在內存中。虛擬內存管理器還須要將駐留在內存中的各個進程的代碼數據維持在一個合理的數量上,而且根據進程性能的表現動態調整,使得程序運行時將涉及的磁盤IO次數降到儘量低,以提升程序的運行性能。算法
Win32虛擬內存管理器爲每個Win32進程提供了進程私有而且基於頁的4GB(32bit)大小的線性虛擬地址空間。
進程私有即每一個進程只能訪問屬於本身的內存空間,而沒法訪問屬於其它進程的地址空間,也不用擔憂本身的地址空間被其它進程看到(父子進程例外,好比調試器利用父子進程關係來訪問被被調試進程的地址空間)。進程運行時用到的dll並無屬於本身的地址空間,而是其所屬進程的虛擬地址空間,dll的全局數據,以及經過dll函數申請的內存都是從調用其進程的虛擬地址空間開闢的。
基於頁是指虛擬地址空間被劃分爲多個稱爲頁的單元,頁的大小由底層處理器決定,x86架構處理器中頁的大小爲4KB。頁是Win32虛擬內存管理器處理的最小單元,相應的物理內存也被劃分爲多個頁。虛擬內存地址空間的申請和釋放,以及內存和磁盤的數據傳輸或置換都是以頁爲最小單位進行的。
4GB大小意味着進程中的地址取值範圍能夠從0x00000000到0xFFFFFFFF,Win32將低區的2GB留給進程使用,高區的2GB留給系統使用。
Win32中用來輔助實現虛擬內存的硬盤文件稱爲調頁文件,能夠有16個,調頁文件用來存放被虛擬內存管理器置換出內存的數據。當調頁文件的數據再次被進程訪問時,虛擬內存管理器會將其從調頁文件中置換進內存,進程能夠正確對其訪問。用戶能夠本身配置調頁文件,出於空間利用效率和性能的考慮,程序代碼不會被修改(包括exe和dll),因此當其所在頁被置換出內存時,並不會被寫進調頁文件中,而是直接拋棄。當再次被須要時,虛擬內存管理器直接從存放程序代碼的exe或dll文件中找到並調入內存。另外,對exe和dll文件中包含的只讀數據的處理與程序代碼處理相同,不會在調頁文件中開闢空間存儲。
當進程執行某段代碼或訪問某些數據,而代碼或數據還不在內存中時,稱爲缺頁錯誤。缺頁錯誤的緣由不少,最多見的是代碼和數據被虛擬內存管理器置換出內存,虛擬內存管理器會在代碼被執行或數據被訪問前將其調入內存。內存置換對開發人員來講是透明的,大大簡化了開發人員的工做。但調頁錯誤涉及磁盤IO,大量的調頁錯誤會大大下降程序的整體性能,所以須要瞭解缺頁錯誤的主要緣由和規避方法。數據庫
Win32中分配內存分爲兩個步驟,預留和提交。所以在進程虛擬地址空間中的頁有三種狀態:自由free、預留reserved和提交committed。
自由表示此頁還沒有被分配,能夠用來知足新的內存分配請求。
預留是指從虛擬地址空間劃出一塊區域(region,頁的整數倍),劃出後的內存空間不能用來知足新的內存分配請求,而是用來供要求預留此段內存的代碼之後使用。預留時並無分配物理內存,只是增長了一個描述進程虛擬地址空間使用狀態的數據結構(VAD,虛擬地址描述符),用來記錄此段內存空間已經被預留。預留操做相對較快,由於沒有真正分配物理內存,所以預留的空間不可以直接訪問,對預留頁的訪問會引發內存訪問違例。
提交,若是想要獲得真正的物理內存,必須對預留的內存進行提交。提交會從調頁文件中開闢空間,並修改VAD中的相應項。提交時也並無馬上從物理內存中分配空間,而是從磁盤的調頁文件中開闢空間,做爲置換的備份空間。當代碼第一次訪問提交內存中的數據時,系統發現並沒由真正的物理內存,拋出缺頁操做。虛擬內存管理器會處理缺頁錯誤,直到此時纔會真正分配物理內存,提交也能夠在預留的同時進行。提交操做會從磁盤的調頁文件中開闢空間,因此比預留操做耗時。
Win32虛擬內存管理中demand-paging策略要求不到真正訪問時不會爲某虛擬地址分配真正的物理內存。demand-paging策略一是處於性能考慮,將工做分段完成,提升整體性能;二是出於空間效率考慮,不到真正訪問時,Win32老是假定認爲進程不會訪問大多數數據,於是沒必要要爲其開闢存儲空間或將其置換進物理內存,以提升存儲空間的利用率。
若是某些程序對內存有很大的需求,但並非馬上須要全部內存,則一次性從物理存儲中開闢空間知足潛在的需求,從執行性能和存儲空間效率上是一種浪費。因爲需求只是潛在的,極有可能分配的內存中很大一部分最後都沒有被真正利用。若是在申請時一次性爲其分配全部物理存儲,會極大下降空間的利用率。
但若是徹底不用預留和提交機制,只是隨需分配內存來知足每次的請求,則對一個會在不一樣時間點頻繁請求內存的代碼來講,由於在其請求內存的不一樣時間點的間隙極有可能會由其它代碼請求內存,會致使在不一樣時間點頻繁請求內存的代碼獲得的內存由於虛擬地址不連續,沒法很好利用空間的locality特性,對其總體進行訪問(如遍歷)時就會增長缺頁錯誤的數量,從而下降程序性能。
預留和提交在Win32程序中都使用VirtualAlloc函數完成,預留傳入MEM_RESERVE參數,提交傳入MEM_COMMIT參數。釋放虛擬內存時使用VirtualFree函數,根據不一樣的傳入參數,與VirtualAlloc函數對應,能夠釋放與虛擬地址區域相對應的物理內存,但虛擬地址區域還能夠處於預留狀態,也能夠連同虛擬地址區域一同釋放,則虛擬地址區域恢復爲自由狀態。
線程棧和進程堆的實現利用了預留和提交兩步機制,Win32系統中,線程棧使用預留和提交兩步機制以下:
建立線程棧時,只是預留一個虛擬的地址區域,默認爲1M(能夠在CreateThread或連接時經過連接選項修改),初始時只有前兩頁是提交的。當線程棧由於函數的嵌套調用須要更多的提交頁時,虛擬內存管理器會動態地提交線程的虛擬地址區域中的後續頁以知足其需求,直到到達1M的上限。當到達預留區域大小的上限(默認1M)時,虛擬內存管理器不會增長預留區域的大小,而是在提交最後一頁時拋出一個棧溢出異常,拋出棧溢出異常時線程棧還有一頁空間能夠利用,程序仍可正常運行。當程序繼續使用棧空間,用完最後一頁時,還繼續須要存儲空間,此時超過上限,會直接致使進程退出。
爲了防止線程棧溢出致使整個程序退出,應該儘可能控制棧的使用大小。好比減小函數的嵌套層數,減小遞歸函數的使用,儘可能不要在函數中使用較大的局部變量(大的對象能夠從堆中開闢空間存放,由於堆會動態擴大,而線程棧的可用內存區域在線程建立時已經固定,在線程的整個生命期都沒法擴展)。
爲了防止一個線程棧的溢出致使整個程序退出,能夠對可能產生線程棧溢出的線程體函數加異常處理,捕獲在提交最後一頁時拋出的溢出異常,並作相應處理。數組
對某虛擬內存區域進行了預留並提交後,就能夠對虛擬內存區域中數據進行訪問。當程序對某段內存訪問時處理流程以下:
若是數據已經在物理內存中,虛擬地址管理器只須要將指向數據的虛擬內存地址映射爲物理地址,便可訪問到物理內存中的數據。此時不會涉及磁盤IO,速度較快。
當第一次訪問一段剛剛提交的內存中的數據時,由於並無真正的物理內存分配,或者被訪問數據之前已經被訪問過,但已經被虛擬內存管理器置換出物理內存,此時會觸發缺頁錯誤。虛擬內存管理器會處理缺頁錯誤,虛擬內存管理器會先檢測數據是否在調頁文件中已有備份空間(exe、dll的代碼頁和只讀數據的備份空間不在調頁文件,而是exe、dll文件),若是訪問的數據在磁盤有備份空間,虛擬內存管理器須要在物理內存中找到合適的頁,並將存放在磁盤的備份數據置換進物理內存。
虛擬內存管理器首先查詢當前物理內存中是否有空閒頁,虛擬內存管理器維護一個名稱爲頁幀數據庫(page-frame database)的數據結構,此數據結構是操做系統全局的,當Windows系統啓動時被初始化,用來跟蹤和記錄物理內存中每個頁的狀態,並用一個鏈表將全部空閒頁鏈接起來,當須要空閒頁時,直接查找此空閒頁鏈表,若是有,直接使用某個空閒頁;不然,根據調頁算法首先選出某個頁。虛擬內存管理器調頁時並非只調入一個頁,爲了利用局部特性,在調入包含所需數據頁的同時,會將相鄰的幾個頁一塊兒調入內存,以提升程序效率。在選出某個內存頁後,接着檢查此頁狀態,若是此頁自上次調入內存以來還沒有被修改,則直接使用此頁(代碼頁和只讀頁也能夠直接使用)。若是此頁已經被修改,則須要先將此頁的內容寫到磁盤的調頁文件中相應的備份頁,並隨即將此頁標記爲空閒頁。此時已經有一個空閒頁用來存放即將要訪問的數據。虛擬內存管理器會再次檢測,此數據是否剛被申請的內存而且第一次被訪問,若是是直接將此空閒頁清0使用便可,沒必要從磁盤的調頁文件中讀取相應備份頁;若是不是,則須要將磁盤調頁文件中相應的備份頁讀到此空閒空間中,並隨即將此頁狀態從空閒頁改成活動頁。
此時,數據已經在物理內存頁中,經過虛擬地址映射到物理地址便可訪問數據。
實際的數據訪問中,情形會比較複雜,好比當用戶定義了一個數組,而此數組恰好在其所在頁的下邊界,且此頁的下一頁是自由或者預留狀態(非提交,沒有真正的物理內存)。當程序不當心向下越界訪問此數組,則首先引起缺頁錯誤。隨即虛擬內存管理器在處理缺頁錯誤時檢測到其不在調頁文件中,即所謂的訪問違例(access violation)。訪問違例意味着要訪問的地址所在的虛擬內存地址尚未被提交,即沒有實際的物理存儲地址與虛擬內存地址對應,訪問違例會直接致使整個進程退出(crash)。
指針越界訪問的後果根據運行時實際狀況而有所不一樣,當數組並不是處於其所在頁的邊界時,越界後還在同一頁中,此時只會引發誤訪問(誤讀或誤寫,誤讀只會影響到正在執行的代碼,誤寫則會影響其它處代碼的執行),本頁中的其它數據,而不會致使整個進程crash。即便數組真的存在於所在頁的邊界,且越界後指針值落在其相鄰頁,但若是此相鄰頁也爲提交狀態,此時仍然爲誤訪問,也不會致使進程的crash。所以,同一程序的代碼中存在數組指針訪問越界錯誤,運行時有時會crash,有時不會。
MicroSoft提供了一個檢測指針越界訪問的工具pageheap,原理是強制使每次分配的內存都位於頁的邊界,同時強制頁的相鄰頁爲自由頁,此時每次越界訪問都會引發訪問違例,致使程序crash,從而使得指針越界訪問錯誤在開發階段必定會暴露出來,而不會發生某個指針越界訪問錯誤一直隱藏到發佈版本,直到最終用戶訪問時纔會被發現。性能優化
在確保訪問的數據已經在物理內存中後,還須要先將虛擬地址轉換爲物理地址,即地址映射,才能訪問數據。
Win32經過一個兩層表結構來實現地址映射,由於4GB虛擬地址空間爲每一個進程私有,每一個進程都維護一套本身的層次結構用來實現其地址映射。第一層表爲頁目錄(page directory),實際就是一個內存頁(4KB=4096Byte),以4個字節爲單元分爲1024項,每一項稱爲頁目錄項(PDE,page directory entry);第二層表爲頁表(page table),共有1024個頁表。頁目錄中每個頁目錄項對應一個頁表,每個頁表也佔一個內存頁。頁表的4KB也被分爲1024項,每項4個字節,稱爲頁表項(PTE,page table entry)。每個頁表項都指向物理內存中的某個頁幀。
Win32提供了4GB(32bit)大小的虛擬地址空間,所以每一個虛擬地址都是一個32位的整數值,由三部分組成,前10bit爲頁目錄下標,用於定位在頁目錄的1024項的某一項,根據定位到的某一項的值能夠找到第二層頁表中的某一個頁表;後續10bit爲頁表下標,用於定位頁表的1024項中的某一項,其值能夠找到物理內存中的某一個頁,此頁包含此虛擬地址所表明的數據;後12bit爲字節下標,用於定位物理頁中特定的字節位置,12位恰好能夠定位一個頁中的任意位置的字節。
假設在程序中訪問一個指針(虛擬地址),指針值爲0X2A8E317F,虛擬地址到物理地址的映射過程以下:
0X2A8E317F的二進制爲0010 1010 1000 1110 0011 0001 0111 1111,將其分爲三部分,前10bit爲00 1010 1010,用於定位頁目錄中的頁目錄項,由於頁目錄項爲4個字節,定位前將00 1010 1010左移2bit,獲得10 1010 1000(0X2A8),使用0X2A8做爲下標找到對應的頁目錄項,此頁目錄項指向一個頁表。使用後續10bit即00 1110 0011定位此頁表中的頁表項,00 1110 0011左移2bit後爲11 1000 1100(0X38B),使用0X38B做爲下標找到此頁表中對應的頁表項。找到的頁表項指向真正的內存。最後使用最後12bit即0001 0111 1111(0X17F),定位頁內的數據,即爲此指針指向的數據。
Win32老是假定數據已經在物理內存中,並進行地址映射。頁表項中有一位用於標記包含此數據的頁是否在物理內存頁中,當取得頁表項時,檢測此位,若是在,進行地址映射;若是不在,拋出缺頁錯誤,此時此頁表項中包含了此數據是否在調頁文件中,若是不在,則訪問違例;若是在,此頁表項能夠查出此數據頁是否在調頁文件中,以及此數據頁在調頁文件中的起始位置,而後將此數據頁從磁盤中調入物理內存中,再繼續進行地址映射過程。爲了實現虛擬地址空間各進程私有,每一個進程都有本身的頁目錄項和頁表結構,對不一樣進程而言,頁目錄中的頁目錄項,以及頁表中的頁表項都是不一樣的,所以相同的指針(虛擬地址)被不一樣的進程映射到的物理地址也是不一樣的,即不一樣進程間傳遞指針是沒有意義的。數據結構
Win32虛擬內存管理器使用另外一個數據結構來記錄和維護每一個進程的4GB虛擬地址空間的使用及狀態信息,即虛擬地址描述符樹(VAD,Virtua Address Discriptor)。每個進程都有本身的VAD集合,VAD集合被組織成一個自平衡二叉樹,以提升查找的效率。另外因爲只有預留或提交的內存塊纔會有VAD,自由的內存塊沒有VAD(即不在VAD樹結構中的虛擬地址塊就是自由的)。
(1)當程序申請一塊新內存時,虛擬內存管理器執行訪問VAD,找到兩個相鄰VAD,只要小的VAD的上限與大的VAD的下限之間的差值知足所申請的內存塊的大小需求,便可使用兩者之間的虛擬內存。
(2)當第一訪問提交的內存時,虛擬內存管理器老是假定要訪問的數據所在數據頁已經在物理內存中,並進行虛擬地址到物理地址映射。當找到相應的頁目錄項後發現頁目錄項並無指向一個合法的頁表,虛擬內存管理器就會查找進程的VAD樹,找到包含該地址的VAD,並根據VAD中的信息,好比內存塊大小、範圍,以及在調頁文件中的起始位置,隨需生成相應的頁表項。而後從剛纔發生缺頁錯誤的位置繼續進行地址映射。所以,一個虛擬內存頁被提交時,除了在調頁文件中開闢一個備份頁外,不會生成指向它的頁表項的頁表,也不會填充指向它的頁表項,更不會開闢真正的物理內存頁,而是直到第一次訪問提交頁時纔會隨需地從VAD中取得包含該頁的整個區域的信息,生成相應頁表,並填充相應頁的頁表項。
(3)當可以訪問預留的內存時,虛擬地址管理器進行虛擬地址到物理地址的映射,找到相應的頁目錄項後發現頁目錄項並無指向一個合法的頁表,虛擬地址管理器就會查找進程的VAD樹,找到包含該地址的VAD,此時發現此段內存塊只是預留的,而沒有提交,即沒有對應物理內存,直接拋出訪問違例,進程退出。
(4)當訪問自由的內存時,虛擬地址內存管理器進行虛擬地址到物理地址的映射,找到相應的頁目錄項後發現頁目錄項並無指向一個合法的頁表,虛擬地址管理器就會查找進程的VAD樹,發現並無VAD包含此虛擬地址,發現此虛擬地址所在的虛擬內存頁是自由狀態,直接拋出訪問違例,進程退出。架構
由於頻繁的調頁操做引發的磁盤IO會大大下降程序的運行效率,所以對每個進程,虛擬內存管理器都會將必定量的內存頁駐留在物理內存中,並跟蹤其執行的性能指標,並動態調整駐留的內存頁數量。Win32中駐留在物理內存中的內存頁稱爲進程的工做集(working set),進程的工做集能夠經過任務管理器查看,內存使用列即爲工做集大小。
工做集是會動態變化的,進程初始時只有不多的代碼頁和數據頁被調入物理內存。當執行到未被調入內存的代碼或訪問到還沒有調入內存的數據時,相應代碼頁或數據頁會被調入物理內存,工做集也會隨之增長。但工做集不能無限增長,系統爲每一個進程設定了一個最小工做集和最大工做集,當工做集達到最大工做集大小,進程須要再次調入新頁到物理內存時,虛擬內存管理器會架構原來工做集中某些內存頁先置換出物理內存,而後再將須要調入的新頁調入內存。
由於工做集的頁駐留在物理內存中,對工做集頁的訪問不會涉及磁盤IO,所以速度很是快。若是訪問的代碼或數據不在工做集中,會引起額外的磁盤IO,從而下降程序的執行效率。極端狀況下會出現所謂的顛簸或抖動(thrashing),即程序的大部分執行時間都花在調頁操做上,而不是執行代碼上。
虛擬內存管理器在調頁時,不只僅只是調入須要的頁,同時還將其附近的頁一塊兒調入內存中,對於開發人員,若是要提升程序的運行效率須要考慮以下:
(1)對代碼李碩,儘可能編寫緊湊代碼,最理想情形是工做集不會達到最大閾值,在每次調入新頁時,就不須要置換已經載入的內存頁,由於根據locality特性,之前執行的代碼和訪問的數據在後面有很大可能會被再次執行好訪問,所以程序執行時,缺頁錯誤會大大下降,即減小磁盤IO。從進程任務管理器也能夠查看一個進程從開始時到當前時刻共發生的缺頁錯誤次數。即便不能達到理想情形,緊湊的代碼每每意味着接下來執行的代碼更大可能就在當前頁或相鄰頁。根據時間locality特性,程序80%的時間花費在20%代碼上,若是能將耗時的20%代碼儘可能緊湊且排在一塊兒,會大大提升程序的總體性能。
(2)對數據來講,儘可能將那些會一塊兒訪問的數據(如鏈表)放在一塊兒,當訪問數據時,數據在同一頁或相鄰頁,只須要一次調頁操做就能夠完成。若是數據分散在分散在多個頁(多個頁不相鄰),每次對數據的總體訪問都會引起大量的缺頁錯誤,從而下降性能。利用Win32提供的預留和提交兩步機制,能夠爲一同訪問的數據預留一大塊空間,此時並無分配實際存儲空間,而是在後續執行過程當中生成數據時格局須要提交內存,既不浪費存儲空間(物理內存和磁盤的調頁文件存儲空間),又能利用locality特性。ide
Linux的內存管理主要分爲兩部分,一部分負責物理內存的申請與釋放,物理內存的申請與釋放的最小單位爲頁,在IA32中,頁的大小爲4KB;另外一部分負責處理虛擬內存,虛擬內存的主要操做包括虛擬地址空間與物理地址空間的映射,物理內存頁與磁盤頁之間的置換等。函數
一個32位Linux進程的地址空間爲4GB,其中高位1GB,即0XC0000000--0XFFFFFFFF,爲內核空間,低位3GB,即0X00000000--0XBFFFFFFF爲用戶地址空間。用戶地址空間進一步被分爲程序代碼區、數據區(包括初始化數據區DATA和未初始化數據區BSS)、堆和棧。程序代碼區佔據最低端,往上是初始化數據區DATA和未初始化數據區BSS。代碼區存放應用程序的機器代碼,運行過程當中代碼不能修改,所以代碼區內存爲只讀,且大小固定。數據區中存放應用程序的全局數據,靜態數據和常量字符串,數據區大小也是固定的。
堆從未初始化數據區開始,向上端動態增加,增加過程當中虛擬地址值變大;棧從高位地址開始,向下動態增加,虛擬地址值變小。
堆是應用程序在運行過程當中動態申請的內存空間,如經過malloc/new動態生成對象或開闢內存空間時,最終會調用系統調用brk來動態調整數據區的大小。當申請的動態內存區域使用完畢,須要開發者明確使用相應的free/delete對申請的動態內存空間進行釋放,free/delete最終也會使用brk系統調用調整數據區的大小。
棧是用來存放函數的傳入參數、臨時變量以及返回地址等數據,不須要經過malloc/new開闢空間,棧的增加與縮減是由於函數的調用與返回,不須要開發人員操做,沒有內存泄漏的危險。
初始化數據區存放的是編譯期就可以知道由程序設定初始值的全局變量及靜態變量等,其初始值必須保存在最終生成的二進制文件中,而且在程序運行時會原封不動地將此區域映射到進程的初始化數據區。若是一個全局變量或靜態變量在源代碼中沒有被賦初始值,在程序啓動後,在第一次被賦值前,其初始值爲0,本質上是有初始值的,其初始值爲0。但當最終生成二進制文件時,未初始化數據區不會佔據對應變量總大小的區域,而是隻用一個值進行標識其未初始化數據區的總大小。如一個程序的代碼指令有100KB,全部初始化數據總大小爲100KB,全部未初始化數據總大小爲150KB,則在最終生成的二進制文件中代碼區有100KB,接着是100KB的初始化數據區,而後是4字節的大小空間,用於標記未初始化數據區大小,其值爲150X1024,用於節省磁盤空間。但在進程虛擬地址空間中,對應未初始化數據區的大小必須是150KB,由於在程序運行時,程序必須真正可以訪問到變量中的每個,即當程序啓動時,當檢測到二進制文件中未初始化數據區的值爲150X1024,則系統會開闢出150KB大小的區域做爲進程的未初始化數據區並同時使用0對其進行初始化。工具
物理內存是用來存放代碼指令與供代碼指令操做的數據的最終場所,所以物理內存的管理是內存管理系統極其重要的任務。Linux使用頁分配器(page allocator)來管理物理內存,頁分配器負責分配和回收全部的物理內存頁(物理內存的分配與回收的最小單位爲4KB大小的頁)。
頁分配器的核心算法稱爲兄弟堆算法(buddy-heap algorithm),算法思想是每一個物理內存區域都會有一個與之相鄰的所謂兄弟區域,當兩個區域被回收後,會被合併成爲一個區域。若是被合併區域的相鄰區域也被回收後,會被進一步合併爲更大的區域。當有物理內存請求到來時,頁分配器會首先檢測是否有大小與之一致的區域。若是有,直接使用找到的匹配區域知足請求;若是沒有,則找到更大的一個區域,並繼續劃分,直到分出的區域可以知足請求。爲了配合兄弟堆算法,必須有鏈表來記錄自由的物理內存區域,對於每一個相同大小的自由區域,會有一個鏈表將其鏈接,每種大小的區域都會有一個鏈表對其進行管理。自由區域的大小都是2的冪。
當有一個8KB大小的內存請求到來,當前最小可供分配的區域爲64KB,此時64KB會被劃分爲兩個32KB,繼而將低位的32KB繼續劃分爲兩個16KB大小的區域,再將最低位的16KB大小區域劃分爲兩個8KB大小的區域,而後分配高位的8KB區域知足請求。佈局
虛擬內存管理器的主要任務是維護應用程序的虛擬地址空間使用信息,如哪些區域已經被使用(映射),是否有磁盤文件做爲備份存儲。若是有,每一個區域對應在磁盤的哪一個區域,另一個重要功能就是調頁,如程序訪問某些還沒有調至物理內存的數據時,虛擬內存管理器負責定位數據,並將其置換進物理內存。若是物理內存此時沒有自由頁,還須要將物理內存中的某些頁先置換出去。
用來維護應用程序的虛擬地址空間使用信息的數據結構是vm_area_struct。每一個vm_area_struct結構體都描述了一個進程虛擬地址空間中被分配的區域,當vm_area_struct個數不超過32個時,被鏈接成爲一個鏈表;當超過32個時,全部的vm_area_struct會被組織爲一棵自平衡二叉樹,利於提升查詢速度。當程序經過某個指針訪問某個數據時,系統會查詢vm_area_struct樹,若是發現指針沒有落在任何一個vm_area_struct所表示的區域內,則斷定指針所表明的地址沒有被分配,即非法的指針訪問。
當經過程序的指針訪問某個數據時,由於指針本質是一個虛擬地址值,所以虛擬地址值必須被轉化爲物理地址值,才能真正訪問其所指代的數據。Linux使用三層映射策略將一個虛擬地址映射爲一個物理地址。與Windows相比,多了Middle層,當對於IA32體系,Middle層沒有用,所以Linux與Windows相同。