天天看點

KVM位址翻譯流程及EPT頁表的建立過程

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

引:本文多處使用以下術語,聲明如下:

GVA,Guest Virtual Address客戶機程序的線性位址

GPA,Guest Physical Address客戶機的實體位址

HVA,Host Virtual Address主控端程序的線性位址

HPA,Host Physical Address主控端的實體位址

在KVM機制下,客戶系統運作在CPU的非根模式,透明的完成位址翻譯,即對客戶機而言,一條客戶機虛拟位址經MMU翻譯為客戶機實體位址而傳回。但實際過程則稍微複雜,因為每一條客戶機實體位址也都是真實存在于實體記憶體上的,而虛拟機所在的位址空間并不是真實的記憶體實體位址空間,是以客戶域下的GPA需要再經過某種位址翻譯機制完成到HPA的轉化,才能夠取得真實實體記憶體單元中的内容。

KVM提供了兩種位址翻譯的機制,基于軟體模拟的影子頁表機制,以及基于硬體輔助的擴充頁表機制(Intel的EPT,AMD的NPT)。詳細的機制介紹參見Reference,這裡重介紹開啟硬體支援的EPT/NPT位址翻譯情況。

位址翻譯的流程:

與影子頁表的構成略有不同(影子頁表項存儲的是GVA->HPA的映射),基于硬體輔助的位址翻譯采用二維的位址翻譯結構(“two-dimensional”),如以下兩圖所示。客戶系統維護自身的客戶頁表,即完成GVA->GPA的映射,而具體的每一條客戶頁表項、客戶頁目錄項都是真實存儲在實體記憶體中的,是以需要完成GPA->HPA的映射,定位到宿主實體位址空間,以獲得實體記憶體單元中的值。EPT/NPT頁表就負責維護GPA->HPA的映射,并且EPT/NPT頁表是由處理器的MMU直接讀取的,可以高效的實作位址翻譯。總結之,所謂“二維”的位址翻譯結構,即客戶系統維護自己的頁表,透明地進行位址翻譯;VMM負責将客戶機請求的GPA映射到主控端的實體位址,到真實的記憶體單元中取值。

KVM位址翻譯流程及EPT頁表的建立過程
一條完整的位址翻譯流程為,客戶系統加載客戶程序的gCR3,處于非根模式的CPU的MMU查詢TLB,沒有所請求的GPA到HPA的映射,在cache中查詢EPT/NPT,若cache中未緩存,逐層向下層存儲查詢,最終獲得gCR3所映射的實體位址單元内容,作為下一級客戶頁表的索引基址,根據GVA獲得偏移,獲得一條位址用于索引下一級頁表,該位址為GPA,再由VCPU的MMU查詢EPT/NPT,如此往複,最終獲得客戶機請求的客戶頁内容。假設客戶機有m級頁表,主控端EPT/NPT有n級,在TLB均miss的最壞情況下,會産生m*n次記憶體通路,完成一次客戶機的位址翻譯。
KVM位址翻譯流程及EPT頁表的建立過程

雖然影子頁表與EPT/NPT的機制差距甚大,一個用于建立GVA->HPA的影子頁表,另一個用于建立GPA->EPT的硬體尋址頁表,但是在KVM層建立頁表的過程卻十分相似,是以在KVM中共用了建立頁表的這部分代碼。至于究竟建立的是什麼頁表,init_kvm_mmu()會根據EPT支援選項是否開啟,選擇使用哪種方式建立頁表。

EPT頁表的建立過程:

與主控端頁表建立過程相似,EPT頁表結構也是通過對缺頁異常的處理完成的。Guset處在非根模式下運作,加載新的客戶程序時,将VMCS客戶域CR3的值加載到gCR3寄存器(儲存的是GPA),非根模式下的CPU根據EPT表尋址該GPA->HPA的映射,EPT頁表的基址在VMCS域的EPTP儲存。初始情況下客戶頁表、EPT頁表均為空,映射未建立,發生EPT_VIOLATION,切換到根模式下,由KVM負責建立該GPA到宿主記憶體位址HPA的映射,此時映射已建立,取得該記憶體單元中的值(一條GPA),傳回給客戶機,切換到非根模式繼續運作。VCPU的mmu查詢客戶頁表,發現為空,客戶機産生缺頁,不發生VM_Exit,由客戶系統的缺頁處理函數捕獲該異常,建立客戶頁表,根據GVA的偏移産生一條新的GPA,客戶機尋址該GPA,産生缺頁異常,此時該GPA對應的EPT頁表項不存在,發生EPT_VIOLATION,切換到根模式下,由KVM負責建立該GPA->HPA映射,再切換回非根模式,如此往複,直到非根模式下GVA最後的偏移建立最後一級客戶頁表,獲得GPA,缺頁異常退出到根模式建立最後一級EPT頁表項,完成整個EPT頁表的建立。

KVM對客戶機缺頁異常的處理,此處以Intel EPT為例,處理流程如圖:

KVM位址翻譯流程及EPT頁表的建立過程

客戶機運作過程中,首先獲得gCR3的客戶頁幀号(右移PAGE_SIZE),根據其所在memslot區域獲得其對應的HVA,再由HVA轉化為HPA,得到宿首頁幀号。若GPA->HPA的映射不存在,将會觸發VM-Exit,KVM負責捕捉該異常,并交由KVM的缺頁中斷機制進行相應的缺頁處理。kvm_vmx_exit_handlers函數數組中,儲存着全部VM-Exit的處理函數,它由kvm_x86_ops的vmx_handle_exit負責調用。缺頁異常的中斷号為EXIT_REASON_EPT_VIOLATION,對應由handle_ept_violation函數進行處理。

tdp_page_fault是EPT的缺頁處理函數,負責完成GPA->HPA轉化。而傳給tdp_page_fault的GPA是通過vmcs_read64函數(VMREAD指令)獲得的。

gpa = vmcs_read64(GUEST_PHYSICAL_ADDRESS);

gfn_to_pfn函數分析:

GPA到HPA的轉化分兩步完成,分别通過gfn_to_hva、hva_to_pfn兩個函數完成。

-gfn_to_hva首先确定gpa對應的gfn映射到哪一個kvm_memory_slot,通過kvm_memory_slot做一個位址映射(實際就是做一個線性的位址偏移,偏移大小為(gfn - slot->base_gfn) * PAGE_SIZE),這樣就得到了由gfn到hva的映射,實際獲得的是GPA的客戶實體頁号到宿主虛拟位址的映射。

-hva_to_pfn利用獲得的gfn到hva的映射,完成主控端上的虛拟位址(該虛拟位址為gfn對應的虛拟位址)到實體位址的轉換,進而獲得主控端實體頁框号pfn。此轉換可能涉及主控端實體頁缺頁,需要請求配置設定該頁。

__direct_map函數分析:

建立EPT頁表結構的函數為__direct_map,KVM用結構體kvm_mmu_page表示一個EPT頁表項。__direct_map負責将GPA逐層添加到EPT頁表中,若找到最終level的EPT頁表項,調用mmu_set_spte将GPA添加進去,若為各級中間level的頁表項,調用__set_spte将下一級實體位址添加進去。詳細函數分析:

1.for_each_shadow_entry宏:周遊每一級EPT頁表

#define for_each_shadow_entry(_vcpu, _addr, _walker)    \
for (shadow_walk_init(&(_walker), _vcpu, _addr);\
    shadow_walk_okay(&(_walker));\
    shadow_walk_next(&(_walker)))      

先介說明一下結構體struct kvm_shadow_walk_iterator

struct kvm_shadow_walk_iterator {
u64 addr; //尋找的GuestOS的實體頁幀,即(u64)gfn << PAGE_SHIFT
hpa_t shadow_addr;//目前EPT頁表項的實體基位址
int level; //目前所處的頁表級别
u64 *sptep; //指向下一級EPT頁表的指針
unsigned index;//目前頁表的索引
};      

shadow_walk_init負責初始化struct kvm_shadow_walk_iterator結構,

static void shadow_walk_init(struct kvm_shadow_walk_iterator *iterator,
    struct kvm_vcpu *vcpu, u64 addr)
{
//把要索引的位址賦給addr
iterator->addr = addr;

//初始化時,要查找的頁表基址就是目前VCPU的根頁表目錄的實體位址
iterator->shadow_addr = vcpu->arch.mmu.root_hpa;

//說明EPT頁表是幾級頁表
iterator->level = vcpu->arch.mmu.shadow_root_level;

if (iterator->level == PT32E_ROOT_LEVEL) {
iterator->shadow_addr
= vcpu->arch.mmu.pae_root[(addr >> 30) & 3];
iterator->shadow_addr &= PT64_BASE_ADDR_MASK;
--iterator->level;
if (!iterator->shadow_addr)
iterator->level = 0;
}
}      

shadow_walk_okay查詢目前頁表,獲得下一級EPT頁表的基位址,或最終的實體記憶體單元位址,

static bool shadow_walk_okay(struct kvm_shadow_walk_iterator *iterator)
{
//若頁表級數小于1,直接退出
if (iterator->level < PT_PAGE_TABLE_LEVEL)
return false;

//最後一級頁表
if (iterator->level == PT_PAGE_TABLE_LEVEL)
if (is_large_pte(*iterator->sptep))
return false;

//獲得在目前頁表的索引
iterator->index = SHADOW_PT_INDEX(iterator->addr, iterator->level);

//取得下一級EPT頁表的基位址,或最終的實體記憶體單元位址
iterator->sptep= ((u64 *)__va(iterator->shadow_addr)) + iterator->index;
return true;
}      

shadow_walk_next索引下一級EPT頁表

static void shadow_walk_next(struct kvm_shadow_walk_iterator *iterator)
{
iterator->shadow_addr = *iterator->sptep & PT64_BASE_ADDR_MASK;
--iterator->level;
}      

2. mmu_set_spte,設定目前請求level的EPT頁表項,該level值由tdp_page_fault中的level = mapping_level(vcpu, gfn)計算而得,若已經存在該EPT頁表項,即is_rmap_spte(*sptep)為真,說明要更新頁表結構,覆寫掉原來的頁表項内容。

if (is_rmap_spte(*sptep)) {
/*
* If we overwrite a PTE page pointer with a 2MB PMD, unlink
* the parent of the now unreachable PTE.
*/
if (level > PT_PAGE_TABLE_LEVEL &&
   !is_large_pte(*sptep)) {
struct kvm_mmu_page *child;
u64 pte = *sptep;


child = page_header(pte & PT64_BASE_ADDR_MASK);
mmu_page_remove_parent_pte(child, sptep);
__set_spte(sptep, shadow_trap_nonpresent_pte);
kvm_flush_remote_tlbs(vcpu->kvm);
} else if (pfn != spte_to_pfn(*sptep)) {
pgprintk("hfn old %lx new %lx\n",
spte_to_pfn(*sptep), pfn);
drop_spte(vcpu->kvm, sptep, shadow_trap_nonpresent_pte);
kvm_flush_remote_tlbs(vcpu->kvm);
} else
was_rmapped = 1;
}

//設定頁表項後,重新整理TLB
if (set_spte(vcpu, sptep, pte_access, user_fault, write_fault,
     dirty, level, gfn, pfn, speculative, true,
     reset_host_protection)) {
if (write_fault)
*ptwrite = 1;
kvm_mmu_flush_tlb(vcpu);
}      

若對應頁表項不存在,即*iterator.sptep == shadow_trap_nonpresent_pte,則建立該level的EPT頁表項,執行如下:

3. kvm_mmu_get_page,配置設定一個EPT頁表頁,即kvm_mmu_page結構

4. __set_spte,設定頁表項

References:

http://blog.stgolabs.net/2012/03/kvm-virtual-x86-mmu-setup.html

繼續閱讀