1. 一句話總結(jié)
內(nèi)存虛擬化解決虛擬機(jī)里面的進(jìn)程如何訪問物理機(jī)上的內(nèi)存這一問題。
GuestOS本身有虛擬地址空間,用GVA表示。虛擬機(jī)認(rèn)為自己獨(dú)占整個(gè)內(nèi)存空間,用GPA表示。
HostOS本身有虛擬機(jī)地址空間,用HVA表示。宿主機(jī)本身有物理內(nèi)存空間,用HPA表示。
好,內(nèi)存虛擬化的問題變成了GVA-》HPA的映射問題。
GVA-》GPA通過GuestOS頁表映射。HVA-》HPA通過HostOS頁表映射。因此,只要建立GPA-》HVA的映射關(guān)系,即可解決內(nèi)存虛擬化的問題。但,這樣三段逐次映射,效率低下。
影子頁表:GuestOS創(chuàng)建GVA-》GPA頁表的時(shí)候,kvm知道GVA對應(yīng)的HPA,并偷偷記錄下映射關(guān)系GVA-》HPA。后續(xù)需要GVA到GPA映射的時(shí)候,根據(jù)影子頁表就能查到HPA。
EPT頁表:硬件層面引入EPTP寄存器。直接將Guest的CR3加載到宿主機(jī)的MMU中。同時(shí)EPT頁表被載入專門的EPT頁表指針寄存器 EPTP。也就是說GVA-》GPA-》HPA兩次地址轉(zhuǎn)換都由硬件實(shí)現(xiàn)。
2. 概述
我們知道80386引入了保護(hù)模式后,內(nèi)存空間分為虛擬地址空間和物理地址空間。后續(xù)引入頁表機(jī)制,把虛擬機(jī)地址送往mmu,mmu查TLB不中的情況下,依次查頁表就可以找到對應(yīng)的物理地址。
在虛擬化場景下情況略微復(fù)雜,分為以下幾種:
①GuestOS 虛擬地址(guestOS virtual Adress,GVA)
說白了guestos中進(jìn)程使用的虛擬地址就是GVA,也就是程序訪問邏輯存儲(chǔ)器的地址。
②guestOS 物理地址(GuestOS Physical Address,GPA)
Guestos認(rèn)為的物理地址,也是虛擬機(jī)mmu查頁表得出的地址但是他本質(zhì)是一個(gè)邏輯上的地址,是引入虛化后產(chǎn)生的一個(gè)邏輯概念。它必須借助于內(nèi)存虛擬化映射到宿主機(jī)的物理地址上才能訪問內(nèi)存
③主機(jī)虛擬機(jī)地址(Host virtul Address,HVA)
宿主機(jī)中的虛擬地址,宿主機(jī)進(jìn)程使用的虛擬地址空間。
④主機(jī)物理地址(Host Physical Address,HPA)
宿主機(jī)真實(shí)內(nèi)存地址,真實(shí)可以訪問的物理內(nèi)存空間。
至此,在虛擬機(jī)場景下,如何由GVA-》HPA就是內(nèi)存虛擬化的工作。其中,Qemu負(fù)責(zé)管理虛擬機(jī)內(nèi)存大小,記錄內(nèi)存對應(yīng)的HVA地址(因?yàn)镼emu是用戶態(tài)的進(jìn)程,無法管理HPA)想要轉(zhuǎn)化為HPA需要借助于KVM內(nèi)核也就是影子頁表SPT(Shadow Page Table)和EPT(Extent Page Table)
2.1 影子頁表
在Guestos建立頁表的時(shí)候,KVM偷偷的建立了一套指向宿主機(jī)物理地址的頁表??蛻魴C(jī)中的每一個(gè)頁表項(xiàng)都有一個(gè)影子頁表項(xiàng)與之相對應(yīng),就像其影子一樣。
在客戶機(jī)訪問內(nèi)存時(shí),真正被裝入宿主機(jī) MMU 的是客戶機(jī)當(dāng)前頁表所對應(yīng)的影子頁表這樣通過影子頁表就可以實(shí)現(xiàn)真正的內(nèi)存訪問虛擬機(jī)頁表和影子頁表通過一個(gè)哈希表建立關(guān)聯(lián)這樣通過頁目錄/頁表的客戶機(jī)物理地址就可以在哈希鏈表中快速地找到對應(yīng)的影子頁目錄/頁表當(dāng)客戶機(jī)切換進(jìn)程時(shí),客戶機(jī)操作系統(tǒng)會(huì)把待切換進(jìn)程的頁表基址載入 CR3而 KVM 將會(huì)截獲這一特權(quán)指令,進(jìn)行新的處理,也即在哈希表中找到與此頁表基址對應(yīng)的影子頁表基址,載入客戶機(jī) CR3使客戶機(jī)在恢復(fù)運(yùn)行時(shí) CR3 實(shí)際指向的是新切換進(jìn)程對應(yīng)的影子頁表。
2.2 EPT
EPT 技術(shù)在原有客戶機(jī)頁表對客戶機(jī)虛擬地址到客戶機(jī)物理地址映射的基礎(chǔ)上引入了 EPT頁表來實(shí)現(xiàn)客戶機(jī)物理地址到宿主機(jī)物理地址的另一次映射,這兩次地址映射都是由硬件自動(dòng)完成??蛻魴C(jī)運(yùn)行時(shí),客戶機(jī)頁表被載入 CR3,而 EPT 頁表被載入專門的EPT 頁表指針寄存器 EPTP。
在客戶機(jī)物理地址到宿主機(jī)物理地址轉(zhuǎn)換的過程中,由于缺頁、寫權(quán)限不足等原因也會(huì)導(dǎo)致客戶機(jī)退出,產(chǎn)生 EPT異常。對于 EPT 缺頁異常,KVM首先根據(jù)引起異常的客戶機(jī)物理地址,映射到對應(yīng)的宿主機(jī)虛擬地址,然后為此虛擬地址分配新的物理頁最后 KVM 再更新 EPT 頁表,建立起引起異常的客戶機(jī)物理地址到宿主機(jī)物理地址之間的映射。對 EPT 寫權(quán)限引起的異常,KVM 則通過更新相應(yīng)的 EPT 頁表來解決。
由此可以看出,EPT 頁表相對于前述的影子頁表,其實(shí)現(xiàn)方式大大簡化。而且,由于客戶機(jī)內(nèi)部的缺頁異常也不會(huì)致使客戶機(jī)退出,因此提高了客戶機(jī)運(yùn)行的性能。此外,KVM 只需為每個(gè)客戶機(jī)維護(hù)一套 EPT 頁表,也大大減少了內(nèi)存的額外開銷。
3. Qemu到KVM內(nèi)存管理
3.1 設(shè)置鉤子
main(vl.c)==》configure_accelerator==》kvm_init(kvm_all.c)==》memory_listener_register(&kvm_memory_listener,NULL);將kvm_memory_listener添加到memory_listeners鏈表中,將address_spaces和listener建立關(guān)聯(lián)
3.2 內(nèi)存對象初始化
main(vl.c)==》cpu_exec_init_all(exec.c)==》memory_map_init(exec.c)Qemu中系統(tǒng)內(nèi)存system_memory來管理,io內(nèi)存用system_io來管理。static MemoryRegion *system_memory.MemoryRegion可以有子區(qū)域。而memory_lister負(fù)責(zé)處理添加和移除內(nèi)存區(qū)域的管理。
3.3 內(nèi)存實(shí)例化
pc_init1(hwpc_piix.c)==》pc_memory_init這里主要分配整個(gè)內(nèi)存區(qū)域重點(diǎn)關(guān)注memory_region_init_ram方法memory_region_init_ram==》qemu_ram_alloc(獲得內(nèi)存的HVA記錄到)==》qemu_ram_alloc_internal==》ram_block_add(生成一個(gè)RAMBlock添加到ram_list,hva放到host字段)==》phys_mem_alloc==》qemu_anon_ram_alloc==》mmap
3.4 VM-Exit處理
由于mmio導(dǎo)致的退出,相關(guān)處理如下kvm_cpu_exec==》 case KVM_EXIT_MMIO==》 cpu_physical_memory_rw==》 address_space_rw==》 io_mem_write
3.5 qemu到kvm的內(nèi)存調(diào)用接口
前面我們講到注冊過listener,當(dāng)設(shè)置內(nèi)存時(shí)會(huì)調(diào)用到
static MemoryListener kvm_memory_listener = {
.region_add = kvm_region_add,
region_add==》kvm_region_add==》kvm_set_phys_mem
①物理起始地址和長度,在kvm_state中搜索已建立的KVMSlot *mem區(qū)域
②如果沒找到建立一個(gè)slot
==》kvm_set_user_memory_region(通知內(nèi)核態(tài)建立內(nèi)存區(qū)域)==》kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem)
3.6 KVM內(nèi)存處理
kvm_vm_ioctl==》kvm_vm_ioctl_set_memory_region==》kvm_set_memory_region==》__kvm_set_memory_region內(nèi)核態(tài)也維護(hù)了一個(gè)slots,內(nèi)核態(tài)slot的管理策略是根據(jù)用戶空間的slot_id一一對應(yīng)的slot =id_to_memslot(kvm-》memslots, mem-》slot);
①通過用戶態(tài)的slot獲取到內(nèi)核態(tài)對應(yīng)結(jié)構(gòu)
②根據(jù)slot中的值和要設(shè)置的值,決定要操作的類別
③根據(jù)2中的動(dòng)作進(jìn)行操作
a.KVM_MR_CREATE: kvm_arch_create_memslot(做了一個(gè)3級(jí)的頁表)
b.KVM_MR_DELETE OR KVM_MR_MOVE:
申請一個(gè)slots,把kvm-》memslots暫存到這里。首先通過id_to_memslot獲取準(zhǔn)備插入的內(nèi)存條對應(yīng)到kvm的插槽是slot。無論刪除還是移動(dòng),將其先標(biāo)記為KVM_MEMSLOT_INVALID。然后是install_new_memslots,其實(shí)就是更新了一下slots-》generation的值。
4. EPT相關(guān)
4.1 EPT初始化
kvm_arch_init==》 kvm_mmu_module_init
①建立pte_list_desc_cache緩存結(jié)構(gòu)
②建立mmu_page_header_cache緩存結(jié)構(gòu),該結(jié)構(gòu)用于kvm_mmu_page
③register_shrinker(&mmu_shrinker);當(dāng)系統(tǒng)內(nèi)存回收被調(diào)用時(shí)的鉤子
vcpu_create==》vmx_create_vcpu==》init_rmode_identity_map==》alloc_identity_pagetable==》__x86_set_memory_region
4.2 EPT載入
vcpu_enter_guest(struct kvm_vcpu *vcpu)==》 kvm_mmu_reload(Guest的MMU初始化,為內(nèi)存虛擬化做準(zhǔn)備)==》 kvm_mmu_load==》mmu_topup_memory_caches==》mmu_alloc_roots--》mmu_alloc_direct_roots(根據(jù)當(dāng)前vcpu的分頁模式建立 ept頂層頁表的管理結(jié)構(gòu))==》kvm_mmu_sync_roots
4.3 gfn_to_page
該函數(shù)處理GPA的頁號(hào)到HPA的page結(jié)構(gòu):
gfn_to_page==》gfn_to_pfn==》gfn_to_pfn_memslot==》__gfn_to_pfn_memslot==》__gfn_to_hva_many|hva_to_pfn==》hva_to_pfn_fast|hva_to_pfn_slow
4.4 分配頁表
mmu_alloc_roots--》mmu_alloc_direct_roots--》kvm_mmu_get_page--》kvm_mmu_alloc_page
4.5 EPT vm-entry
①KVM_REQ_MMU_RELOAD--》kvm_mmu_unload--》mmu_free_roots
②KVM_REQ_MMU_SYNC--》kvm_mmu_sync_roots--》mmu_sync_roots--》mmu_sync_children--》kvm_sync_page--》__kvm_sync_page
③KVM_REQ_TLB_FLUSH--》kvm_vcpu_flush_tlb--》tlb_flush--》vmx_flush_tlb--》__vmx_flush_tlb--》ept_sync_context--》__invept
進(jìn)入非根模式下,根據(jù)不同事件針對內(nèi)存做相關(guān)處理。
4.6 EPT VM-exit
①設(shè)置cr3
mmu_alloc_direct_roots中會(huì)分配arch.mmu.root_hpavcpu_enter_guest的時(shí)候會(huì)調(diào)用kvm_mmu_load==》 vcpu-》arch.mmu.set_cr3(vcpu,vcpu-》arch.mmu.root_hpa)這個(gè)函數(shù)要申請內(nèi)存,作為根頁表使用。同時(shí)root_hpa指向根頁表的物理地址。然后可以看到,vcpu中cr3寄存器的地址要指向這個(gè)根頁表的物理地址。
②handle_ept_violation
--》kvm_mmu_page_fault--》arch.mmu.page_fault--》tdp_page_fault
__direct_map 這個(gè)函數(shù)是根據(jù)傳進(jìn)來的gpa進(jìn)行計(jì)算,從第4級(jí)(level-4)頁表頁開始,一級(jí)一級(jí)地填寫相應(yīng)頁表項(xiàng)這些都是在for_each_shadow_entry(vcpu, (u64)gfn 《《 PAGE_SHIFT, iterator) 這個(gè)宏定義里面實(shí)現(xiàn)的。這兩種情況是這樣子的:
a.如果當(dāng)前頁表頁的層數(shù)(iterator.level )是最后一層( level )的頁表頁,那么直接通過調(diào)用 mmu_set_spte (之后會(huì)細(xì)講)設(shè)置頁表項(xiàng)。
b.如果當(dāng)前頁表頁 A 不是最后一層,而是中間某一層(leve-4, level-3, level-2)
而且該頁表項(xiàng)之前并沒有初始化(!is_shadow_present_pte(*iterator.sptep) )那么需要調(diào)用kvm_mmu_get_page 得到或者新建一個(gè)頁表頁 B然后通過 link_shadow_page 將其link到頁表頁 A 相對應(yīng)的頁表項(xiàng)中
4.7 EPT遍歷操作
for_each_shadow_entry這個(gè)是定義在mmu.c中的一個(gè)宏,用來不斷的遍歷頁表的層級(jí)。
4.8 影子頁表
init_kvm_mmu==》init_kvm_softmmu
在上述的ept的過程中,根據(jù)參數(shù)不同會(huì)有不同分支大體邏輯保持一致,毋庸贅言。
評論
查看更多