您的位置:首页 > 其它

10.2KVM嵌套虚拟化原理

2015-08-15 21:26 1096 查看
10.2.1 KVM嵌套虚拟化简介
KVM使用Intel的vmx来提高虚拟机性能, ,现在如果我们需要多台具备VMX支持的主机, 但是又没有太多物理机器可使用, 那应该怎么办呢? 如果我们的虚拟机能够和物理机一样支持VMX,就能解决这个问题了. 为此,KVM引入了嵌套(nested)虚拟化的支持。也就是能够在第一级的kvm虚拟机(L1
VM)上在启动第二级kvm虚拟机(L2 VM). 在物理机KVM模块加载时添加"nested =1"的选项; 并在第一级虚拟机Qemu启动时设置 "-cpu host"选项,就能开启该功能。

下面看看当nested为1时KVM与普通流程的区别:
(1)hardware_setup
时会调用 nested_vmx_setup_ctls_msrs初始化nested 寄存器相关的变量,这些变量将用于一级虚拟机读取Cpu虚拟化相关能力寄存器时返回:
handle_rdmsr==》 vmx_get_msr ==> vmx_get_vmx_msr
对于MSR_IA32_VMX_BASIC的case会返回VMCS12_REVISION, 该标志用于是L2虚拟机的VMCS.

(2)虚拟化指令的实现,一级虚拟机将会使用vmxon, vmlaunch等指令
[EXIT_REASON_VMCALL] = handle_vmcall,
[EXIT_REASON_VMCLEAR] =handle_vmclear,
[EXIT_REASON_VMLAUNCH] = handle_vmlaunch,
[EXIT_REASON_VMPTRLD] = handle_vmptrld,
[EXIT_REASON_VMPTRST] = handle_vmptrst,
[EXIT_REASON_VMREAD] = handle_vmread,
[EXIT_REASON_VMRESUME] = handle_vmresume,
[EXIT_REASON_VMWRITE] = handle_vmwrite,
[EXIT_REASON_VMOFF] = handle_vmoff,
[EXIT_REASON_VMON] = handle_vmon,
下一小节将按虚拟机初始化的顺序来分析这些函数。

(3)Nested的内存虚拟化与普通模式不同
init_kvm_mmu==> init_kvm_nested_mmu
10.2.2VMX指令虚拟化
10.2.2.1L2虚拟机的创建与运行
L1虚拟机出创建L2虚拟机的指令流程如下:
a) CPU虚拟机能力检测.
b) VMXON
c) VMCLEAR清除VMCS状态
d) VMPTRLD 装载当前VMCS
e) VMWrite 初始化VMCS
f) VMLaunch启动虚拟机
下面我们按照这个顺序来看Host对L1的这几个指令的实现

(1) CPU虚拟机能力检测
该步骤由handle_rdmsr==》 vmx_get_msr ==> vmx_get_vmx_msr
实现。

(2) VMXON
handle_vmon 流程如下:
a. 当前CPU能力与状态检察
b. nested_vmx_check_vmptr 检察VMXON区域
page =nested_get_page(vcpu, vmptr);
if (page == NULL ||
*(u32 *)kmap(page) != VMCS12_REVISION) {
nested_vmx_failInvalid(vcpu);
kunmap(page);
skip_emulated_instruction(vcpu);
return 1;
}
c. 为Vmx-Preemptiontimer机制准备timer和回调用于模拟该机制
hrtimer_init(&vmx->nested.preemption_timer,CLOCK_MONOTONIC,
HRTIMER_MODE_REL);
vmx->nested.preemption_timer.function= vmx_preemption_timer_fn;
d. 设置vmxon 为True
vmx->nested.vmxon =true;
skip_emulated_instruction(vcpu);
nested_vmx_succeed(vcpu);

(3) VMCLEAR
handle_vmclear 流程如下:
vmcs12 = kmap(page);
vmcs12->launch_state= 0; //设子launch为0
kunmap(page);
nested_release_page(page);

(4) VMPTRLD 装载当前VMCS
handle_vmptrld
a) 取得vmptr: nested_vmx_check_vmptr(vcpu,EXIT_REASON_VMPTRLD, &vmptr)
b) 取得vmcs区域内存虚拟地址:
page= nested_get_page(vcpu, vmptr);
new_vmcs12= kmap(page);
c) 存储到vmx结构中去
vmx->nested.current_vmptr= vmptr;
vmx->nested.current_vmcs12= new_vmcs12;
vmx->nested.current_vmcs12_page= page;

(4) VMWrite 初始化VMCS
handle_vmwrite
a) 用get_vmx_mem_address取得要写的地址的gva
b) kvm_read_guest_virt从上面的gva中取出要写的值
c) kvm_register_read取得要写的寄存器的偏移
d) vmcs12_write_any(vcpu,field, field_value);
写入到nested.current_vmcs12对应的位置

(5) VMLaunch 启动虚拟机
handle_vmlaunch ==> nested_vmx_run(vcpu, true);
下面分析nested_vmx_run的流程:
a. 检察vmcs12的状态
b. 调用nested_get_current_vmcs02分配一个vmcs02结构作为L2 VM的VMCS
vmx->nested.vmcs02_pool链表用于管理已经分配的vmcs02.
并调用loaded_vmcs_init 在物理机上对vmcs02执行vmclear
c. enter_guest_mode 设置L1 cpu的arch.hflags |= HF_GUEST_MASK
d. 准备切换vmcs
vmx->loaded_vmcs =vmcs02;
vmx_vcpu_put保存当前hoststate到vmx->host_state
e. vmx_vcpu_load==> vmcs_load(vmx->loaded_vmcs->vmcs);
在物理机上执行vmptrld, 加载vmcs02
然后更新当前vmcs的host state:TSSand GDT与Esp
d. vmcs12->launch_state
e. prepare_vmcs02用于设置vmcs02的vmcs
f. vmx->nested.nested_run_pending= 1;

上面的流程并未结束,当vm-exit后续代码执行时,cpu会回到L2虚拟机eip执行,流程如下:
r = kvm_x86_ops->handle_exit(vcpu); 结束后
__vcpu_run下次循环将重新调用vcpu_enter_guest==》 kvm_x86_ops->run(vcpu);
由于此时loaded_vmcs 已为vmcs02,所以最终将返回到L2 VM上的EIP执行。

小结:vmcs12用于记录L1上管理的L2的VMCS;vmcs02用于记录物理机到L2的vmcs; vmcs01用于管理物理机到L1VM的VMCS.

10.2.2.2 L2虚拟机VM-Exit流程分析
下面以L2虚拟机写CR0为例来分析该流程
vmx_handle_exit 此时由于如下条件成立:
is_guest_mode(vcpu) 成立, 但nested_vmx_exit_handled返回true
所以并不走普通流程而是执行下面代码和返回:
if (is_guest_mode(vcpu)&& nested_vmx_exit_handled(vcpu)) {
nested_vmx_vmexit(vcpu,exit_reason,
vmcs_read32(VM_EXIT_INTR_INFO),
vmcs_readl(EXIT_QUALIFICATION));
return 1;
}

nested_vmx_exit_handled ==》 nested_vmx_exit_handled_cr 将要操作的cr值存储到vmcs12中

nested_vmx_vmexit流程如下:
a. 根据当前vmcs02设置vmcs12的值prepare_vmcs12
b.加载vmcs01(vmcs01为L1在物理机上的vmcs)
vmx->loaded_vmcs =&vmx->vmcs01;
vmx_vcpu_put(vcpu);
vmx_vcpu_load(vcpu,cpu);
b. 更新vmcs01的值
vm_entry_controls_init(vmx,vmcs_read32(VM_ENTRY_CONTROLS));
vm_exit_controls_init(vmx,vmcs_read32(VM_EXIT_CONTROLS));
c. 从vmcs12中load host state到vmcs(vmcs01)

这样当返回时将返回到L1 VM的KVM 代码的VM-Exit处理位置。 L1 KVM来进一步处理L2的vm-exit,最后L1的vmresume指令将会触发,再次进入到物理机的vmx_handle_exit. 下面我们分析该流程。
handle_vmresume==> nested_vmx_run(vcpu, false);该流程与vmlaunch类似; 最终再次进入L2.

本节最后在分析external interrupt导致的vm-exit的例子。
由于nested_vmx_exit_handled返回False,所以直接进入
kvm_vmx_exit_handlers[exit_reason](vcpu) ==》 handle_external_interrupt 然后直接由物理机resume到L2 VM.

10.2.3 EPT内存嵌套虚拟化
10.2.3.1 物理机对L1和 L2的内存管理的区别
本节先从EPT表的切换开始分析内存虚拟化:
KVM 初始化时在kvm_create中
vcpu->arch.mmu.translate_gpa= translate_gpa;
vcpu->arch.nested_mmu.translate_gpa= translate_nested_gpa;

Case 1: VM-Exit 到L2 VM
nested_vmx_run ==> prepare_vmcs02 ==> {
if (nested_cpu_has_ept(vmcs12)) {
kvm_mmu_unload(vcpu);//解除当前的ept映射
nested_ept_init_mmu_context(vcpu);
}
。。。。。。
kvm_set_cr3(vcpu,vmcs12->guest_cr3); //采用L1的EPT映射
kvm_mmu_reset_context(vcpu);//调用init_kvm_nested_mmu
}
这里切换了物理机对内存访问的函数指针。

static void nested_ept_init_mmu_context(struct kvm_vcpu *vcpu)
{
kvm_init_shadow_ept_mmu(vcpu,&vcpu->arch.mmu,
nested_vmx_ept_caps& VMX_EPT_EXECUTE_ONLY_BIT);

vcpu->arch.mmu.set_cr3 = vmx_set_cr3;
vcpu->arch.mmu.get_cr3 = nested_ept_get_cr3;
vcpu->arch.mmu.inject_page_fault= nested_ept_inject_page_fault;
//walk_mmu用于EPT页的查找这里替换为nested_mmu的
vcpu->arch.walk_mmu = &vcpu->arch.nested_mmu;
}

void kvm_init_shadow_ept_mmu(struct kvm_vcpu *vcpu, structkvm_mmu *context,
bool execonly)
{
context->shadow_root_level= kvm_x86_ops->get_tdp_level();

context->nx = true;
context->page_fault= ept_page_fault;
context->gva_to_gpa= ept_gva_to_gpa;
context->sync_page= ept_sync_page;
context->invlpg= ept_invlpg;
context->update_pte= ept_update_pte;
context->root_level= context->shadow_root_level;
context->root_hpa = INVALID_PAGE;
context->direct_map= false;

update_permission_bitmask(vcpu,context, true);
reset_rsvds_bits_mask_ept(vcpu,context, execonly);
}
当物理机VMM Host 为L2 服务时,page_fault等函数处理函数也发生了切换(原来为tdp_page_fault。

kvm_mmu_reset_context ==> init_kvm_nested_mmu 会切换vcpu->arch.nested_mmu ->gva_to_gpa gva_to_gpa 到 xxxx_gva_to_gpa_nested (该函数遍历nested_mmu完成gva到gpa)
当需要在host上访问取虚拟机内存数据时,如kvm_read_guest_virt_system,
其调用流程如下:
kvm_read_guest_virt_system ==》 kvm_read_guest_virt_helper {
gpa_t gpa = vcpu->arch.walk_mmu->gva_to_gpa(vcpu,addr, access,
exception);
unsigned offset =addr & (PAGE_SIZE-1);
unsigned toread =min(bytes, (unsigned)PAGE_SIZE - offset);
int ret;
ret =kvm_read_guest(vcpu->kvm, gpa, data, toread);
}
这时vcpu->arch.walk_mmu->gva_to_gpa会被调用, 该函数会调用
static int FNAME(walk_addr_nested)(struct guest_walker *walker,
struct kvm_vcpu *vcpu, gva_t addr,
u32 access)
{
returnFNAME(walk_addr_generic)(walker, vcpu, &vcpu->arch.nested_mmu,
addr,access);
}
walk_addr_generic ==> mmu->translate_gpa (这里的mmu为nested mmu)
gpa_t translate_nested_gpa(struct kvm_vcpu *vcpu, gpa_t gpa, u32access)
{
access |=PFERR_USER_MASK;
t_gpa = vcpu->arch.mmu.gva_to_gpa(vcpu, gpa,access, &exception);
return t_gpa;
}
vcpu->arch.mmu.gva_to_gpa ==> ept_gva_to_gpa = gva_to_gpa
static gpa_t FNAME(gva_to_gpa)(struct kvm_vcpu *vcpu, gva_tvaddr, u32 access,
struct x86_exception *exception)
{
r =FNAME(walk_addr)(&walker, vcpu, vaddr, access);

if (r) {
gpa = gfn_to_gpa(walker.gfn);
gpa |= vaddr &~PAGE_MASK;
} else if (exception)
*exception =walker.fault;

return gpa;
}
当在物理机上访问L2内存时, 要做两层次搜索, 根据nested_mmu遍历L2的页目录表, 但执行到translate_gpa时会搜索L1的目录表。

Case 2: Vm-Exit退出L2VM到L1 VM:
nested_vmx_vmexit ==> load_vmcs12_host_state ==> {
nested_ept_uninit_mmu_context
kvm_set_cr3(vcpu,vmcs12->host_cr3);
kvm_mmu_reset_context(vcpu); //init_kvm_tdp_mmu 回被调用
}
static void nested_ept_uninit_mmu_context(struct kvm_vcpu *vcpu)
{
vcpu->arch.walk_mmu= &vcpu->arch.mmu; //切回对L1 的EPT管理
}
由于init_kmv_tdp_mmu被调用,因此包括缺页在内的处理方式有变回了default的ept方式。

小结:对于对客户机内存的访问,当为L1时,和普通流程相同。 当为L2是,需要进行两层的转换。

10.2.3.2 L2 EPT异常的处理

L1的EPT处理和3.3同:
kvm_mmu_page_fault ==> vcpu->arch.mmu.page_fault = tdp_page_fault.

下面从L2的EPT异常处理分析嵌套虚拟化的内存管理机制:
VM-Exit ==> nested_vmx_exit_handled 返回0所以会调用到
kvm_vmx_exit_handlers[exit_reason](vcpu) ==》 handle_ept_violation ==> kvm_mmu_page_fault==> vcpu->arch.mmu.page_fault==>
ept_page_fault
该函数用宏实现位于page_tmpl.h, 其流程如下:
(a) 处理mmio case: handle_mmio_page_fault
(b) FNAME(walk_addr)(&walker, vcpu, addr, error_code);根据出错的addr(addr为gva),从客户机页目录表(cr3从得到)开始遍历得到指向该gva的gfn,若gfn不存在则返回让L1 VM处理.

(c) try_async_pf根据gfn的到pfn(若不存在底层的hva_to_pfn_slow会建立物理页)
由此看出对于ept_violation,nest与基于影子页表的内存虚拟化流程基本相同(参考3.4); 不能直接用ept的处理的原因是L2 GVA不能直接对应的L1的GPA(EPT处理L1 GPA->HPA).

接下来kvm_mmu_page_fault会调用x86_emulate_instruction==》 inject_emulated_exception ==》kvm_propagate_fault ==> vcpu->arch.nested_mmu.inject_page_fault==> kvm_inject_page_fault注入异常让L1 VM处理。

最后来看看L2->物理机->L1的内存管理切换开销

下面看看物理机KVM如何管理这vmcs01 和vmcs02对应的EPT:
vcpu_enter_guest 准备进入guest时会先调用:kvm_mmu_reload ==》 kvm_mmu_load 该函数会:
int kvm_mmu_load(struct kvm_vcpu *vcpu)
{
............
r = mmu_alloc_roots(vcpu); //分配root_hpa
kvm_mmu_sync_roots(vcpu);
vcpu->arch.mmu.set_cr3(vcpu,vcpu->arch.mmu.root_hpa);
.........
}
mmu_alloc_roots ==> kvm_mmu_get_page

当虚拟机vmcs切换时,kvm_mmu_reset_context==> kvm_mmu_unload,由此完成了页表的切换

问题,切换时销毁了EPT表, 是否会导致性能问题呢?
原来此时并不会free所有kvm_mmu_page信息,mmu_free_roots相关代码如下:

if(!sp->root_count && sp->role.invalid){
kvm_mmu_prepare_zap_page(vcpu->kvm,sp, &invalid_list);
kvm_mmu_commit_zap_page(vcpu->kvm,&invalid_list);
}
不光引用计数要为0,而且sp->role.invalid要被设置。 如果未销毁,下次分配将从kvm->arch.mmu_page_hash[kvm_page_table_hashfn(_gfn)]中取得,而不需要多次调用kvm_mmu_alloc_page。

当kvm真正销毁时,kvm_destroy_vm ==》 kvm_arch_flush_shadow_all ==》 kvm_mmu_invalidate_zap_all_pages会将该标志设置,之后kvm_mmu_unload是就会是否所有内存了, 而切换时并不会, 仅仅是root_hpa设为空而已,下次换入时为root_hap设个值就可以了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: