接前一篇文章:
本文内容参考:
《 QEMU /KVM》源码解析与应用 —— 李强,机械工业出版社
《深度探索 Linux 系统虚拟化原理与实现》—— 王柏生 谢广军, 机械工业出版社
特此致谢!
二、x86架构CPU虚拟化
3. VMX
上一回讲解了支持VMX的CPU与将Guest内核运行于用户模式的方式三点不同,本回继续往下讲解。
一个CPU可以分时运行多个任务,每个任务有自己的上下文,由调度器在调度时切换上下文,从而实现一个CPU同时运行多个任务。与之类似,在虚拟化场景下,同一个物理CPU“一人分饰多角”,分时运行着Host及Guest,在不同模式间按需切换,因此,不同模式也需要保存自己的上下文。为此,VMX设计了一个保存上下文的数据结构:VMCS(前文也有提到)。
每一个Guest都有一个VMCS实例,当物理CPU加载了不同的VMCS时,将运行不同的Guest。如下图所示:
VMCS中主要保存着两大类数据 : 一类是状态 ,包括Host的状态和Guest的状态; 另一类是控制Guest运行时的行为 。其中:
1)Guest-state area
保存 虚拟机 状态的区域。当发生VM exit时,Guest的状态将保存在这个区域;当VM entry时,这些状态将被装载到CPU中。这些都是硬件层面的自动行为,无须VMM编码干预。
2)Host-state area
保存宿主机状态的区域。当发生VM entry时,CPU自动将宿主机状态保存到这个区域;当发生VM exit时,CPU自动从VMCS恢复宿主机状态到物理CPU。
3)VM-exit information fields
当虚拟机发生VM exit时,VMM需要知道导致VM exit的原因,然后才能“对症下药”,进行相应的模拟操作。为此,CPU会自动将Guest退出的原因保存在这个区域,供VMM使用。
4)VM-execution control fields
这个区域中的各个字段控制着虚拟机运行时的一些行为。比如,设置Guest运行时访问cr3寄存器时是否触发VM exit;控制VM entry与VM exit时行为的VM-entry control fields和VM-exit control fields。此外还有很多不同功能的区域,这里不一一列举。
在创建VCPU时,KVM模块将为每个VCPU申请一个VMCS,每次CPU准备切入Guest模式时,都将设置其VMCS指针,指向即将切入的Guest对应的VMCS实例。对应的代码在Linux内核源码/arch/x86/kvm/vmx/vmx.c中(笔者的内核版本为6.7),代码如下:
- void vmx_vcpu_load_vmcs(struct kvm_vcpu *vcpu, int cpu,
- struct loaded_vmcs *buddy)
- {
- struct vcpu_vmx *vmx = to_vmx(vcpu);
- bool already_loaded = vmx->loaded_vmcs->cpu == cpu;
- struct vmcs *prev;
-
- if (!already_loaded) {
- loaded_vmcs_clear(vmx->loaded_vmcs);
- local_irq_disable();
-
- /*
- * Ensure loaded_vmcs->cpu is read before adding loaded_vmcs to
- * this cpu's percpu list, otherwise it may not yet be deleted
- * from its previous cpu's percpu list. Pairs with the
- * smb_wmb() in __loaded_vmcs_clear().
- */
- smp_rmb();
-
- list_add(&vmx->loaded_vmcs->loaded_vmcss_on_cpu_link,
- &per_cpu(loaded_vmcss_on_cpu, cpu));
- local_irq_enable();
- }
-
- prev = per_cpu(current_vmcs, cpu);
- if (prev != vmx->loaded_vmcs->vmcs) {
- per_cpu(current_vmcs, cpu) = vmx->loaded_vmcs->vmcs;
- vmcs_load(vmx->loaded_vmcs->vmcs);
-
- /*
- * No indirect branch prediction barrier needed when switching
- * the active VMCS within a vCPU, unless IBRS is advertised to
- * the vCPU. To minimize the number of IBPBs executed, KVM
- * performs IBPB on nested VM-Exit (a single nested transition
- * may switch the active VMCS multiple times).
- */
- if (!buddy || WARN_ON_ONCE(buddy->vmcs != prev))
- indirect_branch_prediction_barrier();
- }
-
- if (!already_loaded) {
- void *gdt = get_current_gdt_ro();
-
- /*
- * Flush all EPTP/VPID contexts, the new pCPU may have stale
- * TLB entries from its previous association with the vCPU.
- */
- kvm_make_request(KVM_REQ_TLB_FLUSH, vcpu);
-
- /*
- * Linux uses per-cpu TSS and GDT, so set these when switching
- * processors. See 22.2.4.
- */
- vmcs_writel(HOST_TR_BASE,
- (unsigned long)&get_cpu_entry_area(cpu)->tss.x86_tss);
- vmcs_writel(HOST_GDTR_BASE, (unsigned long)gdt); /* 22.2.4 */
-
- if (IS_ENABLED(CONFIG_IA32_EMULATION) || IS_ENABLED(CONFIG_X86_32)) {
- /* 22.2.3 */
- vmcs_writel(HOST_IA32_SYSENTER_ESP,
- (unsigned long)(cpu_entry_stack(cpu) + 1));
- }
-
- vmx->loaded_vmcs->cpu = cpu;
- }
- }
-
- /*
- * Switches to specified vcpu, until a matching vcpu_put(), but assumes
- * vcpu mutex is already taken.
- */
- static void vmx_vcpu_load(struct kvm_vcpu *vcpu, int cpu)
- {
- struct vcpu_vmx *vmx = to_vmx(vcpu);
-
- vmx_vcpu_load_vmcs(vcpu, cpu, NULL);
-
- vmx_vcpu_pi_load(vcpu, cpu);
-
- vmx->host_debugctlmsr = get_debugctlmsr();
- }
注意,并不是所有的状态都由CPU自动保存与恢复,还需要考虑效率。以cr2寄存器为例,大多数时候,从Guest退出Host到再次进入Guest期间,Host并不会改变cr2寄存器的值,而且写cr2的开销很大,如果每次VM entry时都更新 一次cr2,除了浪费CPU的算力外,毫无意义。因此,将这些状态交给VMM,由软件自行控制更为合理。
更多内容请看下回。