原文地址:http://blog.csdn.net/ariesjzj/article/details/8745035
記憶體虛擬化是虛擬機器實現中的重要部分。在虛擬機器中,虛擬出來的Guest OS和Host OS用的是相同的實體記憶體,卻不能讓它們相互影響到。具體地說,如果OS跑在裸機上(而非虛擬機器上)的話,只要OS提供頁表,MMU會在訪存時自動做虛擬位址(Virtual address, VA)到物理地址(Physical address, PA)的轉化。而跑在虛擬機器上時,Guest OS經過地址轉化看到的“物理地址”並不是真實實體記憶體上的地址,因此還需要將其轉化為真實實體記憶體地址,稱為機器地址(Machine address,
MA)。也就是說Guest OS要訪問VA需要經過VA=>PA=>MA的轉化。注意在沒有虛擬機器的情況下,物理地址PA就是機器地址MA。如何在虛擬機器中完成這樣的地址轉化呢?本文主要介紹以下幾種主流方案。
1. VTLB(Virtual TLB)
首先介紹一下背景,TLB是由硬體實現的,裡面存放的是虛擬位址到物理地址的映射關係。當系統要訪問一個虛擬位址而TLB中有的話,就不用去頁表裡找了。這樣可以提高效率,因為頁表本身在記憶體中,走頁表也是要訪存的。只有當TLB中沒有MMU才會從頁表中找地址映射關係並把它放到TLB中,這樣下次就可以不用訪問頁表了。
VTLB,顧名思義,就是在虛擬機器的hypervisor中維護一份虛擬“TLB”,而真實的cr3指向的正是這份虛擬TLB而非Guest OS的頁表。VTLB中的“TLB”很容易讓人誤解,因為這份虛擬TLB本身也是一份完整的頁表,它擁有頁表的階層,這和硬體TLB類似於hash表的結構是不同的。稱之為虛擬TLB完全是因為其更新方式與TLB類似。我們知道,作業系統中當中更新了頁表後需手動更新TLB,新的映射關係才會生效。比如本來在頁表和TLB中虛擬位址0x11111111映射到物理地址0x22222222,現在Guest
OS更新了頁表,使虛擬位址0x11111111映射到物理地址0x33333333,如果沒有執行相應的flush TLB指令(invlpg, write cr3等,注意x86不允許顯式修改TLB內容,只能清掉其中的一項或多項),系統看到的仍是0x11111111到0x22222222的映射,因為TLB中的映射關係沒有變(系統先看的是TLB中的映射,沒有的話才看頁表)。因此,作業系統中一旦更新了頁表,一般就會跟著執行flush TLB指令(除了invlpg,load cr3和改變cr4的相關位等也會引起TLB
flush)。因此虛擬機器只要截獲這些指令,然後將相應的地址轉換關係更新到VTLB中即可。比如Guest OS要flush TLB中VA為0x11111111的映射,hypervisor需要做的是尋找Guest OS的頁表中0x11111111對應的物理地址PA,然後通過事先建立好的P2M表(PA=>MA)將之轉化為MA,最後填入VTLB(也就是真實cr3指向的這張頁表)。另外當page fault發生時,hypervisor也需要做類似工作。不同的是,如果page fault發生時在Guest OS頁表中也找不到需要的映射時,hypervisor需要將這個page
fault重新inject到Guest OS中,讓Guest OS先填好自己的頁表。然後再次發生page fault時,hypervisor再根據Guest OS頁表填好VTLB。另外頁表中需要對MMIO作額外處理,MMIO需要始終保持缺頁狀態,因為裝置都是虛擬出來的,缺頁才能在Guest OS訪問裝置時讓hypervisor獲得控制權。
VTLB的優點是實現簡單,只要截獲會引起TLB flush的相關指令即可。同時注意這些指令-invlpg, load cr3 和cr4同時又都是特權指令,這就意味著這種方案下,不用更改Guest OS,只要將Guest OS降到非特權級,它執行這些特權指令時就會因為許可權不夠被hypervisor截獲,全虛擬化就可以實現。其缺點也是顯而易見的,因為每次切換進程時都會write cr3,導致VTLB被清空,於是造成大量的的hidden page fault(即Guest OS的頁表中有映射而VTLB中沒有所造成的page
fault)。而這正是下面方案所要解決的問題。
2. SPT(Shadow page table)
其實前面說的VTLB本質上也是一種Shadow的page table。但是Shadow page table一般特指有多個頁表緩衝的方案。比如hypervisor中存放了4組頁表的緩衝,用pgdir(即cr3值)作為它的key。舉例來說,4組頁表的pgdir地址分別為0x11111000, 0x22222000, 0x33333000, 0x44444000,那麼這4個地址就是這4組頁表的key。當write cr3被截獲時,hypervisor就到這4組頁表緩衝去找,看它們的key中是否有即將要寫入的cr3值。如果有,說明要載入的頁表以前有緩衝,直接拿出來用即可。如果沒有,只能老老實實新建立一個。這裡要注意的是如果Guest
OS更新那些當前不在用的頁表項(不屬於cr3指向的頁表),hypervisor也需要截獲到並且更新到相應的頁表緩衝中。拿前面的例子來說,當前用的如果是0x11111000這份頁表,這時Guest OS更新了0x22222000指向的這份頁表中的某個頁表項,相應的頁表緩衝應該得到更新。這就意味著只截TLB flush指令是不夠的,還需要截獲Guest OS用來更新頁表的操作,幸運地是,這樣的介面已經存在在Linux kernel中,稱為pv_mmu_ops。在SPT中,我們需要截獲其中的set_pte, set_pte_at,
set_pmd, pte_update等操作。至於page fault handler方面,和VTLB是差不多的。
SPT的優點主要來自於效能上的提升。由於時間局部性,系統中經常會是幾個進程之間回來切換,所以哪怕是4組頁表緩衝,其重用率也可達到80~90%。因此和VTLB相比,其效能可以大大提高。其缺點是由於要維護多份頁表緩衝,還是存在一定的額外開銷,並且由於要存放這些緩衝,記憶體上也會有些消耗。下面的PVMMU解決了這個問題。
Linux kernel中的Lguest採用了SPT。
3. PVMMU(aka. Direct paging)
PVMMU和前面兩種方案的主要區別是這裡真實cr3指向的不再是hypervisor中維護的頁表,而是直接指向Guest OS的頁表。不同的是Guest OS頁表中放的不再是VA到PA的映射,而是直接從VA到MA的映射。這就需要我們截獲Guest OS對頁表的幾乎所有訪問。好在前面提到的pv_mmu_ops為我們提供了這樣的介面,我們只要在其中加上我們自己的實現就行了。和SPT相比,我們不權截獲頁表更新的操作,還要截獲頁表讀取的操作。在初始化時,我們需要建立兩張表-P2M表和M2P表。前者完成PA到MA的轉化,後者相反。當Guest
OS需要建立頁表項時(如調用make_pte),我們需要將pte中的PFN(Physical frame number)轉為MFN(Machine frame number),而當Guest OS要讀取頁表項時(如調用pte_val),我們要作相反的轉化,把MFN轉化成PFN再給Guest OS。這樣一來,對於Guest OS而言,就好像是在操作從VA到PA的頁表一樣。至於page fault,除了MMIO外,其它的情況基本可以直接丟給Guest OS處理,因為Guest OS的page fault handler中對頁表的操作也是被截獲掉的。
PVMMU的主要優點是效率高,因為它免除了Guest OS頁表和Shadow頁表間同步所引起的消耗。但它和SPT的共同缺點是需要修改Guest OS,即Guest OS知道自己被虛擬化了,我們稱這種虛擬化方案為半虛擬化(Para-virtualization)。
Xen是這種PVMMU的主要使用者。
4. HAP(Hardware assisted paging)
在這種方案中,Guest完成VA到PA這第一層轉化,硬體幫忙完成PA到MA這第二層轉化。第二層轉化對於Guest OS來說是透明的。Guest OS訪存時做的事和在裸機上跑時一樣,所以可以實現全虛擬化。這種特性Intel和AMD都有支援。Intel稱之為Extended Page Tables (EPT),AMD稱之為Nested Page Tables (NPT)。其優點是hypervisor省了很多活,缺點是需要硬體支援。
相關的實現代碼可以參見Linux kernel中KVM的實現。