內存虛擬化(內存地址轉換)
前言
操作系統中的內存管理很複雜,涉及到了很多知識,最重要的就是虛擬內存。虛擬內存一方面是用來擴充空間,使進程擁有” 更多的內存 “,另一方面,他爲每個進程提供了一個一致、私有的地址空間,讓進程似乎在 “獨享主存”。在虛擬機中運行的操作系統的虛擬內存似乎和操作系統的虛擬內存不同,一個需要通過虛擬化技術來對 virtual memory(虛擬內存)進行虛擬化,一個則是 virtual memory(虛擬內存)。
地址轉換
在 Linux 中的地址轉換通常是 Virtual Address(虛擬地址)通過 MMU 和頁錶轉換得到 Physical Address(物理地址)。
之所以不直接用物理地址是因爲:
-
多個進程同時運行時,他們的映像文件地址可能會一致,發生衝突。
-
直接使用物理地址,不便於對進程地址空間進行隔離
-
物理內存有限,使用虛擬地址,在內存緊張時可以通過分時方式讓多個進程共享頁面
MMU
MMU 是處理器 / 核(processer)中的一個硬件單元,通常每個核有一個 MMU。MMU 由兩部分組成:TLB(Translation Lookaside Buffer) 和 table walk unit。
TLB
快表,直譯爲旁路快表緩衝,也可以理解爲頁表緩衝,地址變換高速緩存。
由於頁表存放在主存中,因此程序每次訪存至少需要兩次:一次訪存獲取物理地址,第二次訪存才獲得數據。提高訪存性能的關鍵在於依靠頁表的訪問局部性。當一個轉換的虛擬頁號被使用時,它可能在不久的將來再次被使用到,。
TLB 是一種高速緩存,內存管理硬件使用它來改善虛擬地址到物理地址的轉換速度。當前所有的個人桌面,筆記本和服務器處理器都使用 TLB 來進行虛擬地址到物理地址的映射。使用 TLB 內核可以快速的找到虛擬地址指向物理地址,而不需要請求 RAM 內存獲取虛擬地址到物理地址的映射關係。
table walk
-
從協處理器 CP15 的寄存器 2(TTB 寄存器,translation table base register ARM 架構,X86 中是 CR3)中取出保存在其中的第一級頁表 (translation table) 的基地址。這個基地址指的是 PA,也就是說頁表是直接按照這個地址保存在物理內存中的。
-
以 TTB 中的內容爲基地址,以 VA[31:20]爲索引值在一級頁表中查找對應表項。這個頁表項保存着第二級頁表 (coarse page table) 的基地址,這同樣是物理地址,也就是說第二級頁表也是直接按這個地址存儲在物理內存中的。
-
以 VA[19:12] 爲索引值在第二級頁表中查出表項,這個表項中就保存着物理頁面的基地址,我們知道虛擬內存管理是以頁爲單位的,一個虛擬內存的頁映射到一個物理內存的頁框,從這裏就可以得到印證,因爲查表是以頁爲單位來查的。
-
有了物理頁面的基地址之後,加上 VA[11:0] 這個偏移量就可以取出相應地址上的數據了。
這個過程稱爲 Translation Table Walk,Walk 這個詞用得非常形象。從 TTB 走到一級頁表,又走到二級頁表,又走到物理頁面,一次尋址其實是三次訪問物理內存。注意這個 “走” 的過程完全是硬件做的,每次 CPU 尋址時 MMU 就自動完成以上四步,不需要編寫指令指示 MMU 去做,前提是操作系統要維護頁表項的正確性,每次分配內存時填寫相應的頁表項,每次釋放內存時清除相應的頁表項,在必要的時候分配或釋放整個頁表。
頁表
page table 是每個進程獨有的,是軟件實現的,是存儲在 main memory(比如 DDR)中的。
因爲訪問內存中的頁表相對耗時,尤其是在現在普遍使用多級頁表的情況下,需要多次的內存訪問,爲了加快訪問速度,系統設計人員爲 page table 設計了一個硬件緩存 - TLB,CPU 會首先在 TLB 中查找,因爲在 TLB 中找起來很快。TLB 之所以快,一是因爲它含有的 entries 的數目較少,二是 TLB 是集成進 CPU 的,它幾乎可以按照 CPU 的速度運行。
如果在 TLB 中找到了含有該虛擬地址的 entry(TLB hit),則可從該 entry 中直接獲取對應的物理地址,否則就不幸地 TLB miss 了,就得去查找當前進程的 page table。這個時候,組成 MMU 的另一個部分 table walk unit 就被召喚出來了,這裏面的 table 就是 page table。
使用 table walk unit 硬件單元來查找 page table 的方式被稱爲 hardware TLB miss handling,通常被 CISC 架構的處理器(比如 IA-32)所採用。它要在 page table 中查找不到,出現 page fault 的時候纔會交由軟件(操作系統)處理。
與之相對的通常被 RISC 架構的處理器(比如 Alpha)採用的 software TLB miss handling,TLB miss 後 CPU 就不再參與了,由操作系統通過軟件的方式來查找 page table。使用硬件的方式更快,而使用軟件的方式靈活性更強。IA-64 提供了一種混合模式,可以兼顧兩者的優點。
虛擬機地址轉換
如果這個操作系統是運行在虛擬機上的,那麼這只是一箇中間的物理地址(Intermediate Phyical Address - IPA),需要經過 VMM/hypervisor 的轉換,才能得到最終的物理地址(Host Phyical Address - HPA)。從 VMM 的角度,guest VM 中的虛擬地址就成了 GVA(Guest Virtual Address),IPA 就成了 GPA(Guest Phyical Address)。
可見,如果使用 VMM,並且 guest VM 中的程序使用虛擬地址(如果 guest VM 中運行的是不支持虛擬地址的 RTOS,則在虛擬機層面不需要地址轉換),那麼就需要兩次地址轉換。
但是傳統的 IA32(x86_32)架構從硬件上只支持一次地址轉換,即由 CR3 寄存器指向進程第一級頁表的首地址,通過 MMU 查詢進程的各級頁表,獲得物理地址。
軟件實現 - 影子頁表
爲了支持 GVA->GPA->HPA 的兩次轉換,可以計算出 GVA->HPA 的映射關係,將其寫入一個單獨的影子頁表(sPT - shadow Page Table)。在一個運行 Linux 的 guest VM 中,每個進程有一個由內核維護的頁表,用於 GVA->GPA 的轉換,這裏我們把它稱作 gPT(guest Page Table)。
VMM 層的軟件會將 gPT 本身使用的物理頁面設爲 write protected 的,那麼每當 gPT 有變動的時候(比如添加或刪除了一個頁表項),就會產生被 VMM 截獲的 page fault 異常,之後 VMM 需要重新計算 GVA->HPA 的映射,更改 sPT 中對應的頁表項。可見,這種純軟件的方法雖然能夠解決問題,但是其存在兩個缺點:
-
實現較爲複雜,需要爲每個 guest VM 中的每個進程的 gPT 都維護一個對應的 sPT,增加了內存的開銷。
-
VMM 使用的截獲方法增多了 page fault 和 trap/vm-exit 的數量,加重了 CPU 的負擔。
在一些場景下,這種影子頁表機制造成的開銷可以佔到整個 VMM 軟件負載的 75%。
硬件實現 - EPT/NPT
爲此,各大 CPU 廠商相繼推出了硬件輔助的內存虛擬化技術,比如 Intel 的 EPT(Extended Page Table) 和 AMD 的 NPT(Nested Page Table),它們都能夠從硬件上同時支持 GVA->GPA 和 GPA->HPA 的地址轉換的技術。
GVA->GPA 的轉換依然是通過查找 gPT 頁表完成的,而 GPA->HPA 的轉換則通過查找 nPT 頁表來實現,每個 guest VM 有一個由 VMM 維護的 nPT。其實,EPT/NPT 就是一種擴展的 MMU(以下稱 EPT/NPT MMU),它可以交叉地查找 gPT 和 nPT 兩個頁表:
假設 gPT 和 nPT 都是 4 級頁表,那麼 EPT/NPT MMU 完成一次地址轉換的過程是這樣的(不考慮 TLB):
首先說明 gCR3 和 nCR3,他們分別是客戶機和主機 CR3 的拷貝。
首先它會查找 guest VM 中 CR3 寄存器(gCR3)指向的 PML4 頁表,由於 gCR3 中存儲的地址是 GPA,因此 CPU 需要查找 nPT 來獲取 gCR3 的 GPA 對應的 HPA。nPT 的查找和前面的頁表查找方法是一樣的,這裏我們稱一次 nPT 的查找過程爲一次 nested walk。
如果在 nPT 中沒有找到,則產生 EPT violation 異常(可理解爲 VMM 層的 page fault)。如果找到了,也就是獲得了 PML4 頁表的物理地址後,就可以用 GVA 中的 bit 位子集作爲 PML4 頁表的索引,得到 PDPE 頁表的 GPA。接下來又是通過一次 nested walk 進行 PDPE 頁表的 GPA->HPA 轉換,然後重複上述過程,依次查找 PD 和 PE 頁表,最終獲得該 GVA 對應的 HPA。
不同於影子頁表是一個進程需要一個 sPT,EPT/NPT MMU 解耦了 GVA->GPA 轉換和 GPA->HPA 轉換之間的依賴關係,一個 VM 只需要一個 nPT,減少了內存開銷。如果 guest VM 中發生了 page fault,可直接由 guest OS 處理,不會產生 vm-exit,減少了 CPU 的開銷。可以說,EPT/NPT MMU 這種硬件輔助的內存虛擬化技術解決了純軟件實現存在的兩個問題。
EPT/NPT MMU 優化
事實上,EPT/NPT MMU 作爲傳統 MMU 的擴展,自然也是有 TLB 的,它在查找 gPT 和 nPT 之前,會先去查找自己的 TLB(前面爲了描述的方便省略了這一步)。但這裏的 TLB 存儲的並不是一個 GVA->GPA 的映射關係,也不是一個 GPA->HPA 的映射關係,而是最終的轉換結果,也就是 GVA->HPA 的映射。
不同的進程可能會有相同的虛擬地址,爲了避免進程切換的時候 flush 所有的 TLB,可通過給 TLB entry 加上一個標識進程的 PCID(x86)/ASID(ARM) 的 tag 來區分。同樣地,不同的 guest VM 也會有相同的 GVA,爲了 flush 的時候有所區分,需要再加上一個標識虛擬機的 tag,這個 tag 在 ARM 體系中被叫做 VMID,在 Intel 體系中則被叫做 VPID。
在最壞的情況下(也就是 TLB 完全沒有命中),gPT 中的每一級轉換都需要一次 nested walk,而每次 nested walk 需要 4 次內存訪問,因此 5 次 nested walk 總共需要(4+1)*5-1=24 次內存訪問(就像一個 5x5 的二維矩陣一樣):
雖然這 24 次內存訪問都是由硬件自動完成的,不需要軟件的參與,但是內存訪問的速度畢竟不能與 CPU 的運行速度同日而語,而且內存訪問還涉及到對總線的爭奪,次數自然是越少越好。
要想減少內存訪問次數,要麼是增大 EPT/NPT TLB 的容量,增加 TLB 的命中率,要麼是減少 gPT 和 nPT 的級數。gPT 是爲 guest VM 中的進程服務的,通常採用 4KB 粒度的頁,那麼在 64 位系統下使用 4 級頁表是非常合適的。
而 nPT 是爲 guset VM 服務的,對於劃分給一個 VM 的內存,粒度不用太小。64 位的 x86_64 支持 2MB 和 1GB 的 large page,假設創建一個 VM 的時候申請的是 2G 物理內存,那麼只需要給這個 VM 分配 2 個 1G 的 large pages 就可以了(這 2 個 large pages 不用相鄰,但 large page 內部的物理內存是連續的),這樣 nPT 只需要 2 級(nPML4 和 nPDPE)。
如果現在物理內存中確實找不到 2 個連續的 1G 內存區域,那麼就退而求其次,使用 2MB 的 large page,這樣 nPT 就是 3 級(nPML4, nPDPE 和 nPD)。
總結
不管是影子頁表還是 EPT/NPT 的優化都能看得出,虛擬化中的地址轉換所依託的還是虛擬內存中的地址轉換方式。相比於影子頁表硬件層面的 EPT/NPT 技術顯然更勝一籌,速度更快,開銷更少,由於虛擬化本身的屬性,它所依託的還是真實的物理機,所以要理解這一部分還是需要先了解 OS 內存管理中的地址轉換是如何進行的。
參考資料
https://zhuanlan.zhihu.com/p/69828213
https://zhuanlan.zhihu.com/p/66971714
https://www.vmware.com/pdf/Perf_ESX_Intel-EPT-eval.pdf
http://developer.amd.com/wordpress/media/2012/10/NPT-WP-1%201-final-TM.pdf
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/A9FtVxzmf2LEPhvR1xm4VQ