内存虚拟化
内存初始化
qemu的虚拟地址作为guest的物理地地址,guest运行在虚拟的地址空间,但终究是存在物理内存上的,所以要建立虚拟地址空间与物理地址之间的映射。另外,必须要为虚拟机运行程序提供安全的、隔离的运行环境,避免虚拟机之间、虚拟机与宿主机之间的相互干扰。qemu会提前为guest申请好虚拟地址,只有真正需要的时候,通过产生页中断申请物理地址,并且建立页表产生对应关系。
guest对内存的要求
对操作系统从加载到执行的情况来分析,它需要使用一个从0开始的、连续的物理地址空间.在有虚拟机运行的硬件平台上,唯一的物理内存是用于加载、运行宿主机的操作系统的,因此Guest OS是不能直接在物理内存上加载、运行的,唯一可行的办法是为Guest OS 提供一个虚拟的物理内存空间,我们称之为虚拟机物理地址空间,这个虚拟的内存空间是从0开始的、连续的内存空间,也是Guest OS能够感知和管理的虚拟物理内存单元,Guest OS在这个虚拟机物理内存空间的基础上为应用程序分配内存空间,创建页表等;
关键名词:
gva : Guest Virtual Address 虚拟机线性地址
gpa : Guest Physical Address 虚拟机物理地址
gfn : Guest Page Frame Number 虚拟机物理地址空间中一个物理页对应的页帧号
hva : Host Virtual Address 宿主机线性地址
hpa : Host Physical Address 宿主机物理地址
hfn : Host Page Frame Number 宿主机物理地址空间中一个物理页对应的页帧号
pte : Page Table Entry 页表项(包含页表的起始地址及相应访问权限位)
gpte : Guest Page Table Entry 虚拟机页表项
spte : Shadow Page Table Entry EPT表中的页表项
Linux 内核中采用四级页表:
页全局目录 (Page Global Directory, pgd);
页上级目录 (Page Upper Directory, pud);
页中间目录 (Page Middle Directory, pmd);
页表 (Page Table, pt)。
Qemu的内存布局
关键数据结构罗列
AddressSpace、 MemoryRegion、 RAMBlock、 FlatView、 MemoryRegionSection、 KVMSlot、AddressSpaceDispatch、 kvm_userspace_memory_region。全局内存数据结构
AddressSpace:address_space_memory、address_space_io;
MemoryRegion:system_memory、 system_io;- 从数据结构间的关系分析Qemu的内存布局
qemu中用AddressSpace用来表示CPU/设备看到的内存,两个全局 Address_sapce: address_space_memory、 address_space_io,地址空间之间通过链表连接起来;在一个全局地址空间下挂着一个全局的memoryRegion:system_memory、 system_io。在Qemu中通过一棵memoryRegion树的形式管理着不同的内存区间。MemoryRegion又可分为container, alias, subregions,这些类型用来组织管理整个内存空间。另外,涉及到内存的不同用途,mr又可分为ram, rom, mmio等。 AddressSpace构建了虚拟机的一部分地址空间,而挂载在AddressSpace下的根mr及其子树瓜分了该地址空间。利用 Mr树形结构,将地址空间划分不同的内存域进行分域管理。 从代码流程分析qemu内存布局
4.1 地址空间初始化
两个全局AS:address_space_memory, address_space_io;
两个全局MR:system_memory、 system_io4.11 为AS注册回调函数
kvm_memory_listener_register(s, &s->memory_listener,&address_space_memory, 0); memory_listener_register(&kvm_io_listener,&address_space_io);
4.12 初始化地址空间
1
main -> cpu_exec_init_all -> memory_map_init->memory_region_init / address_space_init
为全局 system\_memory、 system\_io MR分配内存,分别建立全局MR和全局AS的连接。初始化MR和AS的相关成员变量,并且为AS添加与MR相关事件(add, commit)的侦听函数,最后把不同的AS分别加入到全局的address\_spaces链表中,最后调用 memory\_region\_transaction\_commit() 提交本次修改.
4.13 申请内存
以申请system_memory为例,描述内存申请与管理的流程。 memory_region_allocate_system_memory - memory_region_init_alias - memory_region_add_subregion | allocate_system_memory_nonnuma | memory_region_init_ram | qemu_ram_alloc | ram_block_add | phys_mem_alloc //static void *(*phys_mem_alloc)(size_t size, uint64_t *align) = qemu_anon_ram_alloc; | qemu_anon_ram_alloc // include/qemu/osdep.h qemu_ram_mmap mmap
说明:memory_region_allocate_\system_memory 为一个mr申请了RAM;
memory\_region\_init\_alias: 将上述mr RAM 分成了低4G的内存和高4G内存的alias. memory\_region\_add\_subregion:将alias挂载到system memory下面。 void pc_memory_init(PCMachineState *pcms, MemoryRegion *system_memory, MemoryRegion *rom_memory, MemoryRegion **ram_memory) { memory_region_allocate_system_memory(ram, NULL, "pc.ram", machine->ram_size);//为mr分配存储空间 *ram_memory = ram; ram_below_4g = g_malloc(sizeof(*ram_below_4g)); memory_region_init_alias(ram_below_4g, NULL, "ram-below-4g", ram,0, pcms->below_4g_mem_size);//将 mr ram从0开始偏移的4G设置成"ram-below-4g" alias也是一个mr,但是其ram block为NULL memory_region_add_subregion(system_memory, 0, ram_below_4g);//将ram_below_4G挂在system memory下 if (pcms->above_4g_mem_size > 0) { ram_above_4g = g_malloc(sizeof(*ram_above_4g)); memory_region_init_alias(ram_above_4g, NULL, "ram-above-4g", ram, pcms->below_4g_mem_size, pcms->above_4g_mem_size); memory_region_add_subregion(system_memory, 0x100000000ULL, ram_above_4g); } }
分配后的内存模型如下:
当在qemu中的内存布局发生变化后,需要将这个变化同步到kvm中,这个过程实在上述函数 memory_region_add_subregion 中实现的。memory_region_add_subregion -> memory_region_add_subregion_common -> memory_region_update_container_subregions -> memory_region_transaction_commit
memory_region_add_subregion_common 将subrigion的container设置为system_memory,置其addr字段为offset,即在全局MR中的偏移。然后就按照优先级顺序把subregion插入到system_memory的subregions链表中。之后调用函数 memory_region_transaction_commit 进行内存拓扑的同步。
memory_region_transaction_commit -> address_space_update_topology -> address_space_update_topology_pass
对于每个address_space,调用address_space_update_topology()执行更新。里面涉及两个重要的函数generate_memory_topology和address_space_update_topology_pass。前者对于一个给定的MR,生成其对应的FlatView,而后者则根据oldview和newview对当前视图进行更新。
struct FlatView { struct rcu_head rcu; unsigned ref; FlatRange *ranges;//flatrange 数组 unsigned nr;//flat range 数目 unsigned nr_allocated; }; /* Range of memory in the global map. Addresses are absolute. */ struct FlatRange { MemoryRegion *mr; hwaddr offset_in_region;//全局mr中的offset,GPA AddrRange addr;//地址区间,HVA uint8_t dirty_log_mask; bool romd_mode; bool readonly; }; FlatView *old_view = address_space_get_flatview(as)//从as的current_map中获取 FlatView *new_view = generate_memory_topology(as->root);
如何生成一个新的FlatView:
generate_memory_topology -> render_memory_region+flatview_simplify
在获取了新旧两个FlatView之后,调用了两次address_space_update_topology_pass()函数,首次调用主要在于删除原来的视图,而后者主要在于添加新的视图。之后设置as->current_map = new_view。并对old_view减少引用,当引用计数为1时会被删除。
address_space_update_topology_pass | MEMORY_LISTENER_UPDATE_REGION//将flatrange转化为 MemoryRegionSection | region_add(kvm_region_add) / region_del(kvm_region_del) | kvm_set_phys_mem// 将 MemoryRegionSection 记录到 KVMSlot中 | kvm_userspace_memory_region//将 slot中的数据记录到 kvm_userspace_memory_region中,然后调用 KVM_SET_USER_MEMORY_REGION 进入kvm中,kvm_userspace_memory_region 这个结构qemu和kvm共同拥有
MemoryRegionSection -> KVMSlot
/**
* MemoryRegionSection: describes a fragment of a #MemoryRegion
*
* @mr: the region, or %NULL if empty
* @address_space: the address space the region is mapped in
* @offset_within_region: the beginning of the section, relative to @mr's start
* @size: the size of the section; will not exceed @mr's boundaries
* @offset_within_address_space: the address of the first byte of the section
* relative to the region's address space
* @readonly: writes to this section are ignored
*/
struct MemoryRegionSection {
MemoryRegion *mr;
AddressSpace *address_space;
hwaddr offset_within_region;
Int128 size;
hwaddr offset_within_address_space;
bool readonly;
};
typedef struct KVMSlot
{
hwaddr start_addr;//gpa
ram_addr_t memory_size;
void *ram;//hva
int slot;
int flags;
} KVMSlot;
MemoryRegionSection -> KVMslot?
根据 region 的起始 HVA(memory_region_get_ram_ptr) + region section 在 region 中的偏移量 (offset_within_region) + 页对齐修正 (delta) 得到 section 真正的起始 HVA,填入 userspace_addr
在 memory_region_get_ram_ptr 中,如果当前 region 是另一个 region 的 alias,则会向上追溯,一直追溯到非 alias region(实体 region) 为止。将追溯过程中的 alias_offset 加起来,可以得到当前 region 在实体 region 中的偏移量。
由于实体 region 具有对应的 RAMBlock,所以调用 qemu_map_ram_ptr ,将实体 region 对应的 RAMBlock 的 host 和总 offset 加起来,得到当前 region 的起始 HVA。
根据 region section 在 AddressSpace 内的偏移量 (offset_within_address_space) + 页对齐修正 (delta) 得到 section 真正的 GPA,填入 start_addr
根据 region section 的大小 (size) - 页对齐修正 (delta) 得到 section 真正的大小,填入 memory_size
KVMSlot -> kvm_userspace_memory_region?
mem.slot = slot->slot | (kml->as_id << 16);
mem.guest_phys_addr = slot->start_addr;
mem.userspace_addr = (unsigned long)slot->ram;
mem.flags = slot->flags;
/* for KVM_SET_USER_MEMORY_REGION */
struct kvm_userspace_memory_region {
__u32 slot;//slot编号
__u32 flags;//标志位,是否需要追踪对该页的写、是否可读等
__u64 guest_phys_addr;//gpa
__u64 memory_size; /* bytes,内存大小 */
__u64 userspace_addr; /* start of the userspace allocated memory ,HVA*/
}//
在qemu中的整个拓扑图如下,至此,内存管理在Qemu中的流程结束。
在Qemu中调用kvm提供的kvm_userspace_memory_region API进行访问,函数链如下:
kvm_vm_ioctl_set_memory_region() -> kvm_set_memory_region(kvm, mem) -> __kvm_set_memory_region(kvm, mem)
两个关键数据结构:
struct kvm_userspace_memory_region {
__u32 slot;//slot编号
__u32 flags;//标志位,是否需要追踪对该页的写、是否可读等
__u64 guest_phys_addr;//gpa
__u64 memory_size; /* bytes,内存大小 */
__u64 userspace_addr; /* start of the userspace allocated memory ,HVA*/
}//
struct kvm_memory_slot {
gfn_t base_gfn;// slot的起始gfn
unsigned long npages;//page数量
unsigned long *dirty_bitmap;//脏页记录表
struct kvm_arch_memory_slot arch;//结构相关,包括 rmap 和 lpage_info 等
unsigned long userspace_addr;//对应的起始 HVA
u32 flags;
short id;
};
__kvm_set_memory_region: kvm_userspace_memory_region -> kvm_memory_slot
常规检查,内存大小是否小于一页等。
调用id_to_memslot来获得kvm->memslots中对应的memslot指针;
设置memslot的base_gfn、npages等域;
处理和已经存在的memslots的重叠;
调用install_new_memslots装载新的memslot;
将会根据新的npages变量和原来的npages变量来判断用户的操作属于哪一类:
KVM_MR_CREATE:现在有页而原来没有,则为新增内存区域,创建并初始化 slot;
KVM_MR_DELETE: 现在没有页而原来有,则为删除内存区域,将 slot 标记为 KVM_MEMSLOT_INVALID;
KVM_MR_FLAGS_ONLY / KVM_MR_MOVE:现在有页且原来也有,则为修改内存区域,如果只有 flag 变了,则为 KVM_MR_FLAGS_ONLY ,目前只有可能是 KVM_MEM_LOG_DIRTY_PAGES ,则根据 flag 选择是要创建还是释放 dirty_bitmap;如果 GPA 有变,则为 KVM_MR_MOVE ,需要进行移动。其实就直接将原来的 slot 标记为 KVM_MEMSLOT_INVALID,然后添加新的.
至此,从qemu到kvm的整个内存管理体系建立起来了,从qemu中将AS以flatview的形式展示出来,然后翻译成为slot,然后以kvm_userspace_memory_region 为中介传递至kvm,该结构记录了GPA->HVA,最后在kvm中以kvm_memory_slot的形式保存下来。但是在申请内存之后并没有建立与物理页的连接,该部分的连接工作将由kvm来处理。guest在访问内存的时候,首先通过guest内部的mmu建立页表转换机制,当发现物理页不可访问的时候发生EPT Violation。
EPT Violation
当 Guest 第一次访问某个页面时,由于没有 GVA 到 GPA 的映射,触发 Guest OS 的 page fault,于是 Guest OS 会建立对应的 pte 并修复好各级页表,最后访问对应的 GPA。由于没有建立 GPA 到 HVA 的映射,于是触发 EPT Violation,VMEXIT 到 KVM。 KVM 在 vmx_handle_exit 中执行 kvm_vmx_exit_handlers[exit_reason],发现 exit_reason 是 EXIT_REASON_EPT_VIOLATION ,因此调用 handle_ept_violation 。
首先看一下调用的函数链,从整体感受一下。
handle_ept_violation -> kvm_mmu_page_fault(gpa) -> vcpu->arch.mmu.page_fault(tdp_page_fault) ;
函数handle_ept_violation中,vmcs_readl(EXIT_QUALIFICATION)获取 EPT 退出的原因,EXIT_QUALIFICATION 是 Exit reason 的补充;vmcs_read64(GUEST_PHYSICAL_ADDRESS)获取发生缺页的 GPA;根据 exit_qualification 内容得到 error_code,可能是 read fault / write fault / fetch fault / ept page table is not present;
tdp_page_fault 函数是主要的处理过程:
static int tdp_page_fault(struct kvm_vcpu *vcpu, gva_t gpa, u32 error_code,
bool prefault)
{
gfn_t gfn = gpa >> PAGE_SHIFT;//得到子机的gfn
force_pt_level = !check_hugepage_cache_consistency(vcpu, gfn,
PT_DIRECTORY_LEVEL);
level = mapping_level(vcpu, gfn, &force_pt_level);//计算gfn在页表中所属level,4kB下通常为1,大页通常为2,物理页为大页时,3级页表
if (fast_page_fault(vcpu, gpa, level, error_code))///尝试快速修复page fault,让CPU重新尝试发生page fault的操作
return RET_PF_RETRY;
mmu_seq = vcpu->kvm->mmu_notifier_seq;
smp_rmb();
if (try_async_pf(vcpu, prefault, gfn, gpa, &pfn, write, &map_writable))
return RET_PF_RETRY;
r = __direct_map(vcpu, write, map_writable, level, gfn, pfn, prefault);
return r;
}
tdp_page_fault()
-> mapping_level //计算页表级别,4kB下通常为1,大页通常为2,物理页为大页时,3级页表
-> fast_page_fault()//尝试快速修复page fault,让CPU重新尝试发生page fault的操作,热迁移时的write protection导致子机的write page fault,快速修复
以下情况下可执行fast page fault:
- many spurious page fault due to tlb lazily flushed
- lots of write-protect page fault (dirty bit track for guest pte, shadow
page table write-protected, frame-buffer under Xwindow, migration, ...)
-> try\_async\_pf() //将gfn转化为pfn,直接请求或者异步请求,关于异步pf,可以选择是否支持,可以通过配置来决定。
-> try_async_pf()-> kvm_vcpu_gfn_to_memslot => __gfn_to_memslot//找到gfn对应的slot
-> __gfn_to_pfn_memslot//寻找gfn对应的pfn,不允许io wait
-> __gfn_to_hva_many -> __gfn_to_hva_memslot//计算gfn对应的起始 HVA,slot->userspace_addr + (gfn - slot->base_gfn) * PAGE_SIZE;
-> hva_to_pfn //计算 HVA 对应的 pfn,同时确保该物理页在内存中
-> hva_to_pfn_fast//
-> hva_to_pfn_slow//申请物理页,即为pfn赋值,可能会sleep,可能会等待swap page载入
-> kvm_arch_setup_async_pf//支持异步
-> __gfn_to_pfn_memslot//允许io wait
-> __direct_map // 更新 EPT,将新的映射关系逐层添加到 EPT 中
-> for_each_shadow_entry//根据传进来的gpa进行计算,level-4页表开始,一级一级地填写相应页表项
-> mmu_set_spte//到达level层后设置页表项
-> is_shadow_present_pte//中间某一层页表页没有初始化时执行下列函数
-> kvm_mmu_get_page//新建一个页表页
-> link_shadow_page //将新建的页表页连接到上一层页表的spetp上
-> mmu_spte_set(sptep, spte);//为当前页表项的值 (*spetp) 设置下一级页表页的 HPA
mmu_page_add_parent_pte(vcpu, sp, sptep)-> pte_list_add//将当前项的地址 (spetp) 加入到下一级页表页的 parent_ptes 中,做反向映射
反向映射:每一级页表的spetp指向下一级页表的HPA,同时下一级页表页的parent_ptes反向指向该级别的页表项。这样,当Host需要将guest的某个GPA对应的page移除时,可直接通过反向索引找到该gfn的相关表项进行修改;
关键函数 try_async_pf 的详细解释:
在该函数中首先会尝试寻找gfn对应的pfn,如果找到就返回;
slot = kvm_vcpu_gfn_to_memslot(vcpu, gfn);
async = false;
//已经找到pfn,不需要异步,返回,执行过程中,如果需要IO wait,则不等待
*pfn = __gfn_to_pfn_memslot(slot, gfn, false, &async, write, writable);`
if (!async)
return false; /* *pfn has correct page already */`
//判断是否能够调用异步机制,如果可以就使用异步机制
if (!prefault && kvm_can_do_async_pf(vcpu)) {//判断vcpu是否允许注入中断
trace_kvm_try_async_get_page(gva, gfn);
if (kvm_find_async_pf_gfn(vcpu, gfn)) {//是否已经为该gfn建立了异步寻页机制呢?
trace_kvm_async_pf_doublefault(gva, gfn);
kvm_make_request(KVM_REQ_APF_HALT, vcpu);//暂停vcp,等待 异步过程完成后在进入guest
return true;
} else if (kvm_arch_setup_async_pf(vcpu, gva, gfn))//建立异步寻页机制,向guest注入中断
return true;
}
//不能使用异步机制就等待为gfn分配pfn,此时允许 IO wait
*pfn = __gfn_to_pfn_memslot(slot, gfn, false, NULL, write, writable);
return false;
kvm_arch_setup_async_pf 、kvm_async_page_present_sync 函数中分别会将 KVM_PV_REASON_PAGE_NOT_PRESENT 和 KVM_PV_REASON_PAGE_READY 中断以 KVM_REQ_EVENT形式注入到子机,子机中会调用 do_async_page_fault 函数处理中断。总的来说,对于swapout mem,可以选择异步模式获取物理页。向guest中发送async page fault 中断,可以帮助子机切换进程,物理页取回来之后再告诉子机把原来的进程调度回来。当子机不能切换进程,此时可以暂停子机。大致流程见下图: