10.2KVM嵌套虚拟化原理


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设个值就可以了。


  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值