QEMU源码全解析 —— CPU虚拟化(10)

123 篇文章 36 订阅 ¥49.90 ¥99.00

接前一篇文章:

本文内容参考:

《趣谈 Linux操作系统 》 —— 刘超, 极客时间

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),代码如下:

  1. void vmx_vcpu_load_vmcs(struct kvm_vcpu *vcpu, int cpu,
  2. struct loaded_vmcs *buddy)
  3. {
  4. struct vcpu_vmx *vmx = to_vmx(vcpu);
  5. bool already_loaded = vmx->loaded_vmcs->cpu == cpu;
  6. struct vmcs *prev;
  7. if (!already_loaded) {
  8. loaded_vmcs_clear(vmx->loaded_vmcs);
  9. local_irq_disable();
  10. /*
  11. * Ensure loaded_vmcs->cpu is read before adding loaded_vmcs to
  12. * this cpu's percpu list, otherwise it may not yet be deleted
  13. * from its previous cpu's percpu list. Pairs with the
  14. * smb_wmb() in __loaded_vmcs_clear().
  15. */
  16. smp_rmb();
  17. list_add(&vmx->loaded_vmcs->loaded_vmcss_on_cpu_link,
  18. &per_cpu(loaded_vmcss_on_cpu, cpu));
  19. local_irq_enable();
  20. }
  21. prev = per_cpu(current_vmcs, cpu);
  22. if (prev != vmx->loaded_vmcs->vmcs) {
  23. per_cpu(current_vmcs, cpu) = vmx->loaded_vmcs->vmcs;
  24. vmcs_load(vmx->loaded_vmcs->vmcs);
  25. /*
  26. * No indirect branch prediction barrier needed when switching
  27. * the active VMCS within a vCPU, unless IBRS is advertised to
  28. * the vCPU. To minimize the number of IBPBs executed, KVM
  29. * performs IBPB on nested VM-Exit (a single nested transition
  30. * may switch the active VMCS multiple times).
  31. */
  32. if (!buddy || WARN_ON_ONCE(buddy->vmcs != prev))
  33. indirect_branch_prediction_barrier();
  34. }
  35. if (!already_loaded) {
  36. void *gdt = get_current_gdt_ro();
  37. /*
  38. * Flush all EPTP/VPID contexts, the new pCPU may have stale
  39. * TLB entries from its previous association with the vCPU.
  40. */
  41. kvm_make_request(KVM_REQ_TLB_FLUSH, vcpu);
  42. /*
  43. * Linux uses per-cpu TSS and GDT, so set these when switching
  44. * processors. See 22.2.4.
  45. */
  46. vmcs_writel(HOST_TR_BASE,
  47. (unsigned long)&get_cpu_entry_area(cpu)->tss.x86_tss);
  48. vmcs_writel(HOST_GDTR_BASE, (unsigned long)gdt); /* 22.2.4 */
  49. if (IS_ENABLED(CONFIG_IA32_EMULATION) || IS_ENABLED(CONFIG_X86_32)) {
  50. /* 22.2.3 */
  51. vmcs_writel(HOST_IA32_SYSENTER_ESP,
  52. (unsigned long)(cpu_entry_stack(cpu) + 1));
  53. }
  54. vmx->loaded_vmcs->cpu = cpu;
  55. }
  56. }
  57. /*
  58. * Switches to specified vcpu, until a matching vcpu_put(), but assumes
  59. * vcpu mutex is already taken.
  60. */
  61. static void vmx_vcpu_load(struct kvm_vcpu *vcpu, int cpu)
  62. {
  63. struct vcpu_vmx *vmx = to_vmx(vcpu);
  64. vmx_vcpu_load_vmcs(vcpu, cpu, NULL);
  65. vmx_vcpu_pi_load(vcpu, cpu);
  66. vmx->host_debugctlmsr = get_debugctlmsr();
  67. }

注意,并不是所有的状态都由CPU自动保存与恢复,还需要考虑效率。以cr2寄存器为例,大多数时候,从Guest退出Host到再次进入Guest期间,Host并不会改变cr2寄存器的值,而且写cr2的开销很大,如果每次VM entry时都更新 一次cr2,除了浪费CPU的算力外,毫无意义。因此,将这些状态交给VMM,由软件自行控制更为合理。

更多内容请看下回。

举报

选择你想要举报的内容(必选)
  • 内容涉黄
  • 政治相关
  • 内容抄袭
  • 涉嫌广告
  • 内容侵权
  • 侮辱谩骂
  • 样式问题
  • 其他