设为首页 收藏本站
查看: 2356|回复: 0

[经验分享] 10.2KVM嵌套虚拟化原理

[复制链接]
累计签到:1 天
连续签到:1 天
发表于 2015-10-10 13:09:26 | 显示全部楼层 |阅读模式



10.2.1 KVM嵌套虚拟化简介

KVM使用Intel的vmx来提高虚拟机性能, ,现在如果我们需要多台具备VMX支持的主机, 但是又没有太多物理机器可使用, 那应该怎么办呢? 如果我们的虚拟机能够和物理机一样支持VMX,就能解决这个问题了. 为此,KVM引入了嵌套(nested)虚拟化的支持。也就是能够在第一级的kvm虚拟机(L1VM)上在启动第二级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、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-125115-1-1.html 上篇帖子: KVM性能调优 下篇帖子: kvm 桥接网卡设置
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表