內存虛擬化到底是咋整的?


1. 一句話總結

內存虛擬化解決虛擬機里面的進程如何訪問物理機上的內存這一問題。

GuestOS本身有虛擬地址空間,用GVA表示。虛擬機認為自己獨占整個內存空間,用GPA表示。

HostOS本身有虛擬機地址空間,用HVA表示。宿主機本身有物理內存空間,用HPA表示。

好,內存虛擬化的問題變成了GVA->HPA的映射問題。

GVA->GPA通過GuestOS頁表映射。HVA->HPA通過HostOS頁表映射。因此,只要建立GPA->HVA的映射關系,即可解決內存虛擬化的問題。但,這樣三段逐次映射,效率低下。

引入軟件模擬的影子頁表和硬件輔助的EPT頁表。

影子頁表:GuestOS創建GVA->GPA頁表的時候,kvm知道GVA對應的HPA,並偷偷記錄下映射關系GVA->HPA。后續需要GVA到GPA映射的時候,根據影子頁表就能查到HPA。

EPT頁表:硬件層面引入EPTP寄存器。直接將Guest的CR3加載到宿主機的MMU中。同時EPT頁表被載入專門的EPT頁表指針寄存器 EPTP。也就是說GVA->GPA->HPA兩次地址轉換都由硬件實現。

2. 概述

我們知道80386引入了保護模式后,內存空間分為虛擬地址空間和物理地址空間。后續引入頁表機制,把虛擬機地址送往mmu,mmu查TLB不中的情況下,依次查頁表就可以找到對應的物理地址。

在虛擬化場景下情況略微復雜,分為以下幾種:

①GuestOS 虛擬地址(guestOS virtual Adress,GVA)

說白了guestos中進程使用的虛擬地址就是GVA,也就是程序訪問邏輯存儲器的地址。

②guestOS 物理地址(GuestOS Physical Address,GPA)

Guestos認為的物理地址,也是虛擬機mmu查頁表得出的地址但是他本質是一個邏輯上的地址,是引入虛化后產生的一個邏輯概念。它必須借助於內存虛擬化映射到宿主機的物理地址上才能訪問內存

③主機虛擬機地址(Host virtul Address,HVA)

宿主機中的虛擬地址,宿主機進程使用的虛擬地址空間。

④主機物理地址(Host Physical Address,HPA)

宿主機真實內存地址,真實可以訪問的物理內存空間。

至此,在虛擬機場景下,如何由GVA->HPA就是內存虛擬化的工作。其中,Qemu負責管理虛擬機內存大小,記錄內存對應的HVA地址(因為Qemu是用戶態的進程,無法管理HPA)想要轉化為HPA需要借助於KVM內核也就是影子頁表SPT(Shadow Page Table)和EPT(Extent Page Table)

2.1 影子頁表

在Guestos建立頁表的時候,KVM偷偷的建立了一套指向宿主機物理地址的頁表。客戶機中的每一個頁表項都有一個影子頁表項與之相對應,就像其影子一樣。

在客戶機訪問內存時,真正被裝入宿主機 MMU 的是客戶機當前頁表所對應的影子頁表這樣通過影子頁表就可以實現真正的內存訪問

虛擬機頁表和影子頁表通過一個哈希表建立關聯這樣通過頁目錄/頁表的客戶機物理地址就可以在哈希鏈表中快速地找到對應的影子頁目錄/頁表當客戶機切換進程時,客戶機操作系統會把待切換進程的頁表基址載入 CR3而 KVM 將會截獲這一特權指令,進行新的處理,也即在哈希表中找到與此頁表基址對應的影子頁表基址,載入客戶機 CR3使客戶機在恢復運行時 CR3 實際指向的是新切換進程對應的影子頁表。

2.2 EPT

EPT 技術在原有客戶機頁表對客戶機虛擬地址到客戶機物理地址映射的基礎上引入了 EPT頁表來實現客戶機物理地址到宿主機物理地址的另一次映射,這兩次地址映射都是由硬件自動完成。客戶機運行時,客戶機頁表被載入 CR3,而 EPT 頁表被載入專門的EPT 頁表指針寄存器 EPTP。

在客戶機物理地址到宿主機物理地址轉換的過程中,由於缺頁、寫權限不足等原因也會導致客戶機退出,產生 EPT異常。對於 EPT 缺頁異常,KVM首先根據引起異常的客戶機物理地址,映射到對應的宿主機虛擬地址,然后為此虛擬地址分配新的物理頁最后 KVM 再更新 EPT 頁表,建立起引起異常的客戶機物理地址到宿主機物理地址之間的映射。對 EPT 寫權限引起的異常,KVM 則通過更新相應的 EPT 頁表來解決。

由此可以看出,EPT 頁表相對於前述的影子頁表,其實現方式大大簡化。而且,由於客戶機內部的缺頁異常也不會致使客戶機退出,因此提高了客戶機運行的性能。此外,KVM 只需為每個客戶機維護一套 EPT 頁表,也大大減少了內存的額外開銷。

3. Qemu到KVM內存管理

3.1 設置鈎子

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建立關聯

3.2 內存對象初始化

main(vl.c)==>cpu_exec_init_all(exec.c)==>memory_map_init(exec.c)Qemu中系統內存system_memory來管理,io內存用system_io來管理。static MemoryRegion *system_memory.MemoryRegion可以有子區域。而memory_lister負責處理添加和移除內存區域的管理。

3.3 內存實例化

pc_init1(hw\pc_piix.c)==>pc_memory_init這里主要分配整個內存區域重點關注memory_region_init_ram方法memory_region_init_ram==>qemu_ram_alloc(獲得內存的HVA記錄到)==>qemu_ram_alloc_internal==>ram_block_add(生成一個RAMBlock添加到ram_list,hva放到host字段)==>phys_mem_alloc==>qemu_anon_ram_alloc==>mmap

3.4 VM-Exit處理

由於mmio導致的退出,相關處理如下kvm_cpu_exec==> case KVM_EXIT_MMIO==> cpu_physical_memory_rw==> address_space_rw==> io_mem_write

3.5 qemu到kvm的內存調用接口

前面我們講到注冊過listener,當設置內存時會調用到

static MemoryListener kvm_memory_listener = { .region_add = kvm_region_add,

region_add==>kvm_region_add==>kvm_set_phys_mem

①物理起始地址和長度,在kvm_state中搜索已建立的KVMSlot *mem區域

②如果沒找到建立一個slot

==>kvm_set_user_memory_region(通知內核態建立內存區域)==>kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem)

3.6 KVM內存處理

kvm_vm_ioctl==>kvm_vm_ioctl_set_memory_region==>kvm_set_memory_region==>__kvm_set_memory_region內核態也維護了一個slots,內核態slot的管理策略是根據用戶空間的slot_id一一對應的slot =id_to_memslot(kvm->memslots, mem->slot);

①通過用戶態的slot獲取到內核態對應結構

②根據slot中的值和要設置的值,決定要操作的類別

③根據2中的動作進行操作

a.KVM_MR_CREATE: kvm_arch_create_memslot(做了一個3級的頁表)

b.KVM_MR_DELETE OR KVM_MR_MOVE:

申請一個slots,把kvm->memslots暫存到這里。首先通過id_to_memslot獲取准備插入的內存條對應到kvm的插槽是slot。無論刪除還是移動,將其先標記為KVM_MEMSLOT_INVALID。然后是install_new_memslots,其實就是更新了一下slots->generation的值。

4. EPT相關

4.1 EPT初始化

kvm_arch_init==> kvm_mmu_module_init

①建立pte_list_desc_cache緩存結構

②建立mmu_page_header_cache緩存結構,該結構用於kvm_mmu_page

③register_shrinker(&mmu_shrinker);當系統內存回收被調用時的鈎子

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初始化,為內存虛擬化做准備)==> kvm_mmu_load==>mmu_topup_memory_caches==>mmu_alloc_roots-->mmu_alloc_direct_roots(根據當前vcpu的分頁模式建立 ept頂層頁表的管理結構)==>kvm_mmu_sync_roots

4.3 gfn_to_page

該函數處理GPA的頁號到HPA的page結構:

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

進入非根模式下,根據不同事件針對內存做相關處理。

4.6 EPT VM-exit

①設置cr3

mmu_alloc_direct_roots中會分配arch.mmu.root_hpavcpu_enter_guest的時候會調用kvm_mmu_load==> vcpu->arch.mmu.set_cr3(vcpu,vcpu->arch.mmu.root_hpa)這個函數要申請內存,作為根頁表使用。同時root_hpa指向根頁表的物理地址。然后可以看到,vcpu中cr3寄存器的地址要指向這個根頁表的物理地址。

②handle_ept_violation

-->kvm_mmu_page_fault-->arch.mmu.page_fault-->tdp_page_fault

__direct_map 這個函數是根據傳進來的gpa進行計算,從第4級(level-4)頁表頁開始,一級一級地填寫相應頁表項這些都是在for_each_shadow_entry(vcpu, (u64)gfn << PAGE_SHIFT, iterator) 這個宏定義里面實現的.這兩種情況是這樣子的:

a.如果當前頁表頁的層數(iterator.level )是最后一層( level )的頁表頁,那么直接通過調用 mmu_set_spte (之后會細講)設置頁表項。

b.如果當前頁表頁 A 不是最后一層,而是中間某一層(leve-4, level-3, level-2)

而且該頁表項之前並沒有初始化(!is_shadow_present_pte(*iterator.sptep) )那么需要調用kvm_mmu_get_page 得到或者新建一個頁表頁 B然后通過 link_shadow_page 將其link到頁表頁 A 相對應的頁表項中

4.7 EPT遍歷操作

for_each_shadow_entry這個是定義在mmu.c中的一個宏,用來不斷的遍歷頁表的層級。

4.8 影子頁表

init_kvm_mmu==>init_kvm_softmmu

在上述的ept的過程中,根據參數不同會有不同分支大體邏輯保持一致,毋庸贅言。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM