標籤:虛擬記憶體
這個系列會總結電腦,網路相關的一些重要的底層原理。很多底層原理大家上學的時候都學過,但是在學校的時候大部分的同學都是為了應付考試而學習,過幾天全忘了。隨著工作的時間越久,越體會到這些基礎知識的重要性。做技術和練武功一樣,當你到了一定的階段,也會遇到一個瓶頸,突破了你的眼界就會大不同,突破不了,只能困在原地無法成長。我自己深有體會,這些基礎知識,底層原理是助你打破瓶頸的靈丹妙藥。當理解了一些底層原理之後,會發現現在很多熱門技術,原理,常見的設計都是在底層基礎上發展而來的。
這篇總結一下單機系統的虛擬記憶體原理。在聊聊高並發(三十四)Java記憶體模型那些事(二)理解CPU快取的工作原理 這篇中說了CPU快取的一些原理,虛擬記憶體也是緩衝原理的應用,它是單機系統管理記憶體的手段,很多設計非常經典,在分布式儲存系統中可以看到這些原理和設計的應用。
虛擬記憶體是單機系統最重要的幾個底層原理之一,它由底層硬體和作業系統兩者軟硬體結合來實現,是硬體異常,硬體地址翻譯,主存,磁碟檔案和核心的完美互動。它主要提供了3個能力:
1. 給所有進程提供一致的地址空間,每個進程都認為自己是在獨佔使用單機系統的儲存資源
2. 保護每個進程的地址空間不被其他進程破壞,隔離了進程的地址訪問
3. 根據緩衝原理,上層儲存是下層儲存的緩衝,虛擬記憶體把主存作為磁碟的快取,在主存和磁碟之間根據需要來回傳送資料,高效地使用了主存
包括幾塊內容
1. 虛擬位址和物理地址
2. 頁表
3. 地址翻譯
4. 虛擬記憶體相關的資料結構
5. 記憶體映射
虛擬位址和物理地址
對於每個進程來說,它使用到的都是虛擬位址,每個進程都看到一樣的虛擬位址空間,對於32位電腦系統來說,它的虛擬位址空間是 0 - 2^32,也就是0 - 4G。對於64位的電腦系統來說,理論的虛擬位址空間是 0 - 2^64,遠高於目前常見的實體記憶體空間。虛擬位址空間不需要和物理地址空間一樣大小。
Linux核心把虛擬位址空間分為兩部分: 使用者進程空間和核心進程空間,兩者的比例一般是3:1,比如4G的虛擬位址空間,3G使用者使用者進程,1G用於核心進程。
是一個典型的Linux進程的虛擬位址空間分布
在說CPU快取的時候說過CPU只直接和寄存器和快取打交道,CPU在執行進程的指令時要取一個實際的物理地址的值的時候主要有幾步:
1. 把進程指令使用的虛擬位址通過MMU轉換成物理地址
2. 把物理地址映射到快取的緩衝行
3. 如果快取命中就返回
4. 如果不命中,就產生一個緩衝缺失中斷,從主存相應的物理地址取值,並載入到快取中。CPU從中斷中恢複,繼續執行中斷前的指令
所以快取是和物理地址相映射的,進程指令中使用到的是虛擬位址。
作業系統記憶體管理
在緩衝原理中,資料都是按塊來進行邏輯劃分的,一次換入/換出的資料都是以塊為最小單位,這樣提高了資料處理的效能。同樣的原理應用到具體的記憶體管理時,使用了頁(page)來表示塊,虛擬位址空間劃分為多個固定大小的虛擬頁(Virtual Page, VP),物理地址空間劃分為多個固定大小的物理頁(Physical Page, PP), 通常虛擬頁大小等於物理頁大小,這樣簡化了虛擬頁和物理頁的映射。虛擬頁的大小通常在4KB - 2MB之間。在JVM調優的時候有時候會使用2MB的大記憶體頁來提高GC的效能。
要明白一個重要的概念:
1. 對於CPU來說,它的目標儲存空間是實體記憶體,使用快取做實體記憶體的緩衝
2. 同樣,對於虛擬記憶體來說,它的目標儲存空間是磁碟空間,使用實體記憶體做磁碟的緩衝
所以,從緩衝原理的角度來理解,在任何時刻,虛擬頁的集合都分為3個不相交的子集:
1. 未分配的頁,即沒有任何資料和這些虛擬頁關聯,不佔用任何磁碟空間
2. 緩衝的頁,即已經分配了的虛擬頁,並且已經緩衝在具體的物理頁中
3. 未緩衝的頁,即已經為磁碟檔案分配了虛擬頁,但是還沒有緩衝到具體的物理頁中
虛擬記憶體系統和快取系統一樣,需要判斷一個虛擬頁面是否緩衝在DRAM(主存)中,如果命中,就直接找到對應的物理頁。如果不命中,作業系統需要知道這個虛擬頁對應磁碟的哪個位置,然後根據相應的替換策略從DRAM中選擇一個犧牲的物理頁,把虛擬頁從磁碟中載入到DRAM物理主存中
虛擬記憶體的這種緩衝管理機制是通過作業系統核心,MMU(記憶體管理單元)中的地址翻譯硬體和每個進程存放在主存中的頁表(page table)資料結構來實現的。
頁表
頁表(page table)是存放在主存中的,每個進程維護一個單獨的頁表。它是一種管理虛擬記憶體頁和實體記憶體頁映射和緩衝狀態的資料結構。它邏輯上是由頁表條目(Page Table Entry, PTE)為基本元素構成的數組。
1. 數組的索引號對應著虛擬頁號
2. 數組的值對應著物理頁號
3. 數組的值可以留出幾位來表示有效位,許可權控制位。有效位為1的時候表示虛擬頁已經緩衝。有效位為0,數組值為null時,表示未分配。有效位為0,數組值不為null,表示已經分配了虛擬頁,但是還未緩衝到具體的物理頁中。
許可權控制位有可讀,可寫,是否需要root許可權
聊聊高並發(三十四)Java記憶體模型那些事(二)理解CPU快取的工作原理 這篇中解釋了緩衝相聯度的概念,DRAM緩衝是全相聯的,即只有一組,任意的緩衝行可以緩衝任意的內容,有一個比較判斷的過程,即任意的虛擬頁可以對應任意的物理頁。
DARM緩衝的命中稱為頁命中,不命中稱為缺頁。舉個例子來說,
1. CPU要訪問的一個虛擬位址在虛擬頁3上(VP3),通過地址翻譯硬體從頁表的3號頁表條目中取出內容,發現有效位0,即沒有緩衝,就產生一個缺頁異常
2. 缺頁異常調用核心的缺頁例外處理常式,它會根據替換演算法選擇一個DRAM中的犧牲頁,比如PP3。PP3中已經緩衝了VP4對應的磁碟檔案的內容,如果VP4的內容有改動,就重新整理到磁碟中去。然後把VP3對應的磁碟檔案內容載入到PP3中。然後更新頁表條目,把PTE3指向PP3,並修改PTE4,不再指向PP3.
3. 缺頁例外處理常式返回後重新啟動缺頁異常前的指令,這時候虛擬位址對應的內容已經緩衝在主存中了,頁命中也可以讓地址翻譯硬體正常處理了
磁碟和主存之間傳送頁的活動叫做交換(swapping)或者頁面調度(頁面調入,頁面調出)。現代作業系統都採用按需調度的策略,即不命中發生時才調入頁面。作業系統都會在主存中分配一塊交換區(swap)來作緩衝區,加速頁面調度。
由於頁的交換會引起磁碟流量,所以具有好的局部性的程式可以大大減少磁碟流量,提高效能。而如果局部性不好產生大量缺頁,從而導致不斷地在磁碟和主存交換頁,這種現象叫緩衝顛簸。可以用Unix的函數getrusage來統計缺頁的次數
現代作業系統都採用多級頁表的方式來壓縮頁表的大小。舉個例子,
1. 對於32位的機器來說,支援4G的虛擬記憶體大小,如果每個頁表是4KB大小,那麼採用一級頁表的話,需要10^6(1MB)個頁表條目PTE。32位機器的頁表條目是4個位元組,那麼頁表需要4MB大小的空間。
2. 假設使用4MB大小的頁,那麼只需要1KB的頁表項。假設每個4MB大小的頁又分為4KB大小的子頁,那麼每個4MB大小的頁需要1KB的頁表項來指向子頁。也就是說可以分為兩級頁表,第一級頁表項只需要1KB的頁表項,每個一級頁表項又指向一個1KB的二級頁表項,二級頁表項則指向實際的物理頁。
頁表項載入是按需載入的,沒有分配的虛擬頁不需要建立頁表項, 所以可以一開始只建立一級頁表項,而二級頁表項按需建立,這樣大大壓縮了頁表的空間。
使用k級頁表項的地址翻譯如下:
Core i7採用4級頁表的結構
地址翻譯
地址翻譯就是把N個元素的虛擬位址空間(VAS)映射到M個元素的物理地址空間(PAS)的過程。下表是地址翻譯時用到的符號
下面看一下CPU如何把一個虛擬位址翻譯到對應的物理地址。
1. CPU有一個專門的頁表基地址寄存器(page table base register, PTBR)指向當前頁表的基地址,從而可以快速定位到該進程的頁表
2. n位的虛擬位址劃分為p位的虛擬位址位移量VPO和(n - p)位的虛擬頁號VPN
3. 物理地址同樣劃分為p位的物理地址位移量PPO和(m - p)位的物理頁號PPN
4. 由於虛擬頁大小和物理頁大小相同,所以VPO = PPO
頁面的命中完全由硬體完成,缺頁則由硬體和核心共同完成,已經在上面舉例說明了。
為了提高地址翻譯的效率,地址翻譯硬體還引入了一個硬體裝置來快取頁面表條目PTE,叫做翻譯後備緩衝區TLB(translation lookaside buffer)。它是一個小的,虛擬定址的緩衝,每一行都儲存一個由單個PTE組成的塊。TLB也遵循緩衝的設計原理,分為組,行,塊的結構。一個虛擬位址映射到TLB的緩衝結構如下:
而TLB的命中和不命中的流程如下:
Core i7處理器的地址翻譯硬體結構如下
總結一下地址翻譯的過程:
1. CPU拿到一個虛擬位址
2. 地址翻譯硬體要把這個虛擬位址翻譯成一個物理地址,從而可以再根據快取的映射關係,把這個物理地址對應的值找到
3. 地址翻譯硬體利用頁表資料結構,TLB硬體緩衝等技術,目的只是把一個虛擬位址映射到一個物理地址。要記住DRAM緩衝是全相聯的,所以一個虛擬位址和一個物理地址是動態關聯的,不能直接根據虛擬位址推匯出物理地址,必鬚根據DRAM從磁碟把資料緩衝到DRAM時存到頁表時存的實際物理頁才能得到實際的物理地址,用物理頁PPN + VPO就能算出實際的物理地址 (VPO = PPO,所以直接用VPO即可)。 PPN的值是存在頁表條目PTE中的。地址翻譯做了一堆工作,就是為了找到物理頁PPN,然後根據VPO頁面位移量,就能定位到實際的物理地址。
4. 得到實際物理地址後,根據快取的原理,把一個物理地址映射到快取具體的組,行,塊中,找到實際儲存的資料。
Linux虛擬記憶體機制
Linux把虛擬記憶體劃分成地區area的集合,每個存在的虛擬頁面都屬於一個area。一個area包含了連續的多個頁。Linux通過area相關的資料結構來靈活地管理虛擬記憶體。
1. 核心為每個進程維護了一個單獨的任務結構 task_struct
2. task_struct的mm指標指向了mm_struct,該結構描述了虛擬記憶體的運行狀態
3. mm_struct的pgd指標指向該進程的一級頁表的基地址。mmap指標指向了vm_area_struct鏈表
4. vm_area_struct是描述area結構的一個鏈表,鏈表節點的幾個重要屬性如下:vm_start表示area的開始位置,vm_end表示area的結束位置,vm_prot描述了area內的頁的讀寫權限,vm_flags描述該area內的頁面是與其他進程共用還是進程私人, vm_next指向下一個area節點
在Linux系統中,當MMU翻譯一個虛擬位址發生缺頁異常時,跳轉到核心的缺頁例外處理常式。
1. Linux的缺頁例外處理常式會先檢查一個虛擬位址是哪個area內的地址。只需要比較所有area結構的vm_start和vm_end就可以知道。area都是一個連續的塊。如果這個虛擬位址不屬於任何一個area,將發生一個段錯誤,終止進程
2. 要訪問的目標地址是否有相應的讀寫權限,如果沒有,將觸發一個保護異常,終止進程
3. 選擇一個犧牲頁,如果犧牲頁被修改過,那麼把它交換出去。從磁碟載入虛擬頁內容到物理頁,更新頁表
記憶體映射機制
虛擬記憶體的目標儲存空間是磁碟,所以虛擬記憶體地區是和磁碟中的檔案對應的。初始化虛擬記憶體地區的內容時,會把虛擬記憶體地區和一個磁碟檔案對象對應起來,這個過程叫記憶體映射(memory mapping)。虛擬記憶體可以映射的磁碟檔案對象包括兩種:
1. 一個普通的磁碟檔案,檔案中的內容被分成頁大小的塊。因為按需進行頁面調度,只有真正需要讀取這些虛擬頁時,才會交換到主存
2. 一個匿名檔案,匿名檔案是核心建立的,內容全是二進位0,它相當於一個預留位置,不會產生實際的磁碟流量。映射到匿名檔案中的頁叫做請求二進位零的頁(demand zero page)
一旦一個虛擬頁面被初始化了,它就在一個由核心維護的專門的交換區(swap area)之間換來換去。
由於記憶體映射機制,所以一個磁碟檔案對象可以被多個進程共用訪問,也可以被多個進程對象私人訪問。如果是共用訪問,那麼一個進程對這個對象的修改會顯示到其他進程。如果是私人訪問,核心會採用寫時拷貝copy on write的方式,如果一個進程要修改一個私人的寫時拷貝的對象,會產生一個保護故障,核心會拷貝這個私人對象,寫進程會在新的私人對象上修改,其他進程仍指向原來的私人對象。
理解了記憶體映射機制就可以理解幾個重要的函數:
1. fork函數會建立帶有獨立虛擬位址空間的新進程,核心會為新進程建立各種資料結構,分配一個唯一的PID,把當前進程的mm_struct, area結構和頁表都複製給新進程。兩個進程的共用同樣的地區,但是這些地區都被標記為私人的寫時拷貝。如果建立的進程對虛擬頁做修改,那麼會觸發寫時拷貝,為新的進程維護私人的虛擬位址空間。
2. mmap函數可以建立新的虛擬記憶體area,並把磁碟對象映射到建立的area。
mmap是一種高效的操作檔案的方式,直接把一個檔案對應到記憶體,通過修改記憶體就相當於修改了磁碟檔案,減少了普通檔案操作的一次拷貝操作。普通檔案操作時會先把檔案內容從磁碟複製到核心空間管理的一塊虛擬記憶體地區area,然後核心再把內容複寫到使用者空間管理的虛擬記憶體area。 mmap相當於建立了一個核心空間和使用者空間共用的area,檔案的內容只需要在這個area對應的實體記憶體和磁碟檔案之間交換即可。mmap一般是在非堆空間來建立area
參考資料:
《深入理解電腦系統》
電腦底層知識拾遺(一)理解虛擬記憶體機制