虚拟化技术简介——以 Intel VT-x/KVM/QEMU 为例
虚拟化简介
与虚拟化相对的一个概念是仿真/模拟,简单说就是伪造硬件。例如 QEMU 依靠模拟 ARM 处理器在 x86 处理器系统中创建一个 ARM 处理器的虚拟机。
而虚拟化的(大部分)指令是直接跑在真实的 CPU 上的,虚拟机内的指令集与宿主机相同,执行速度接近原生(宿主机)执行的速度。
系统虚拟化体系结构分为宿主型和独立监控型两种类型:
- 独立监控型 (Type-1) 是直接运行于硬件层之上的虚拟化类型。
- Xen、Hyper-V、Vmware ESXi
- 宿主型 (Type-2) 是需要运行在 Host OS 之上,并由其提供驱动程序与硬件通信的虚拟化类型。
- VMware Workstation、VirtualBox
KVM 虚拟化属于 Type-1 还是 Type-2 有所争议(参考什么是虚拟机监控程序? - Red Hat、Hypervisor - Wikipedia),但 Type-1 还是 Type-2 并不是虚拟化技术本质的区别,只是一种形式上的区分。
虚拟化技术则分为以下几种:
- 基于二进制辅助翻译的全虚拟化
- 使用了一种叫做二进制转换(Binary Translation)的技术。其核心是让 Hypervisor 运行在 Ring 0 上,由它来负责管理底层的硬件。而虚拟机的操作系统运行在权限较低的 Ring 1 上,普通指令直接转交 CPU 执行,调用特权指令的时候,Ring 0 的 VMM 使用二进制转换技术将这些指令调用拦截下来,并负责指令的后续工作。
- 优点:兼容性好,无需修改操作系统内核,无需额外硬件支持
- 缺点:软件拦截机制,性能开销大
- VMware Workstation 早期版本
- 半虚拟化
- 使得虚拟机的操作系统仍然可以运行在 Ring 0 上,但是需要修改操作系统的内核,把其中对特权指令的调用都改成对 Hypervisor 的调用,这种调用叫做 Hypercall,半虚拟化的典型代表是 Xen。这样,当在 Ring 0 上的虚拟机的操作系统调用特权指令的时候,会转成对 Hypervisor 的 Hypercall 调用,依然是由 Hypervisor 来统一的对系统硬件资源进行管理。
- 优点:性能开销较小,效率高
- 缺点:需要修改 Guest OS 内核
- Xen-PV
- 基于硬件辅助的全虚拟化
- 需要 CPU 对虚拟化技术的支持。CPU 提供了 root 模式与 non-root 模式,各有 Ring 0 到 Ring 3。虚拟机的操作系统运行在 non-root 模式的 0 环上,在操作系统调用特权指令的时候,通过硬件的机制将特权指令调用通过类似于中断的机制转到在处在 root 模式的 Hypervisor 上,由 Hypervisor 完成对硬件的统一管理。
- 优点:性能开销比基于二进制辅助翻译的全虚拟化小,且无需修改操作系统内核,兼容性好
- 缺点:需要有硬件支持(如Intel VT, AMD SVM)
- VMware Workstation 后期版本、Xen-HVM、KVM
接下来谈论的虚拟化指的是硬件辅助虚拟化,且以 VT-x/KVM/QEMU 技术栈为例。VT-x 是英特尔 CPU 虚拟化技术的名称。KVM 是利用 VT-x 技术的 Linux 内核的虚拟化组件。而 QEMU 则是一个用户空间的应用程序,允许用户创建虚拟机。QEMU 利用 KVM 实现高效虚拟化。
典型的虚拟机架构如下所示:
在最低层是支持虚拟化的硬件。在它的上方是虚拟机监控器(Hypervisor,VMM)。KVM 模块是加载到 Linux 内核中的,换句话说,KVM 是一组内核模块,当加载到 Linux 内核中时,它将内核转换为 Hypervisor。在 Hypervisor 的上方,位于用户空间的是虚拟化应用程序,最终用户直接与之交互,如 QEMU 等。然后这些应用程序创建虚拟机,虚拟机在 Hypervisor 的协助下运行自己的操作系统。
VT-x
VT-x 是适用于 Intel 64 和 IA-32 体系结构的 CPU 虚拟化技术。对于 I/O 虚拟化,有 VT-d。AMD 也有自己的虚拟化技术称为 AMD-V。
在 VT-x 下,CPU 运行在两种模式之一下:根模式 (root) 和非根模式 (non-root),这两者是对称的,它与实模式、保护模式、长模式等以及特权级(0-3)是正交(相互组合)的。具体说来,就是无论是在根模式还是非根模式,都分别有自己的实模式、保护模式、长模式等以及特权级(0-3)。
Hypervisor 在根模式下运行,而 VM 在非根模式下运行。在非根模式下,指令大多以与在根模式下运行时相同的方式执行,这意味着 VM 的 CPU 相关的操作大多以原生的速度运行。
特权指令是 CPU 上所有可用指令的一个子集。这些是只有在 CPU 处于更高特权状态下(如 CPL = 0)才能执行的指令。这些特权指令中有一些指令称为“全局状态变更”指令,可以影响 CPU 整体状态,例如修改时钟或中断寄存器的指令,而非根模式无法执行这个敏感指令的子集。
VMX 和 VMCS
VMX 是为了支持 VT-x 而添加的指令:
Intel 注记符 | 描述 |
---|---|
INVEPT | 使处理器中的高速缓存扩展页表(EPT)映射无效2 |
INVVPID | 基于虚拟处理器ID(VPID)使地址转换的缓存映射无效2 |
VMCALL | 在非根模式中调用 VMM 功能。引发一个 VM Exit 事件,返回到 VMM |
VMCLEAR | 清除一个 VMCS |
VMFUNC | 在非根模式中调用 VMM 功能,不产生 VM Exit 事件 |
VMLAUNCH | 创建一个 VM 实例并进入非根模式。 |
VMRESUME | 进入现有 VM 实例的非根模式。 |
VMPTRLD | 从内存加载 VMCS |
VMPTRST | 保存 VMCS 到内存 |
VMREAD | 读取 VMCS 某字段 |
VMWRITE | 写入 VMCS 某字段 |
VMXOFF | 退出虚拟化 |
VMXON | 进入 VMX 操作。在执行此指令之前,不存在根模式与非根模式的概念。CPU 的操作就像没有虚拟化一样。必须执行 VMXON 才能进入虚拟化。在 VMXON 之后,CPU 处于根模式。 |
当 VM 尝试执行在非根模式下禁止的指令时,CPU 立即以类似于陷阱的方式切换到根模式。这称为 VM 退出(VM exit)。
综合上述信息:CPU 从普通模式开始,执行 VMXON 以在根模式下启动虚拟化,执行 VMLAUNCH 以创建和进入 VM 实例的非根模式,VM 实例运行其自己的代码,就好像原生运行一样,直到它尝试执行禁止的操作,这会导致 VM 退出并切换到根模式。在根模式下运行的是 Hypervisor,Hypervisor 处理 VM 退出的原因,然后执行 VMRESUME 以重新进入该 VM 实例的非根模式,使 VM 实例恢复其操作。根模式和非根模式之间的这种交互是硬件虚拟化支持的本质。
上述描述还有一些空缺。例如,Hypervisor 如何知道 VM 退出发生的原因?一个 VM 实例与另一个 VM 实例有何不同?这就是 VMCS 的作用。VMCS 代表虚拟机控制结构(Virtual Machine Control Structure),其中包含上述过程所需的信息。这些信息包括 VM 退出的原因以及每个 VM 实例的唯一信息,决定 CPU 处于非根模式时运行的是哪个 VM 实例。
在 QEMU 或 VMWare 中可以设置每个虚拟机拥有多少个 CPU。每个这样的 CPU 称为虚拟 CPU 或 vCPU。每个 vCPU 都有一个 VMCS。这意味着 VMCS 存储 CPU 级别的信息,而不是 VM 级别的信息。要读取和写入特定的 VMCS 字段,需要根模式(Hypervisor)使用 VMREAD 和 VMWRITE 指令。非根 VM 可以执行 VMWRITE,但不能写入实际的 VMCS,而是写入一个“影子”VMCS(用于嵌套虚拟化加速)——这不是我们目前所关注的。
还有一些针对整个 VMCS 的操作指令。切换 vCPU 时可以使用这些指令。VMPTRLD 用于从内存加载 VMCS,VMPTRST 用于保存 VMCS 到内存。可以有多个 VMCS 实例,但在任何时刻一个逻辑 CPU 只有一个被标记为当前的且活动的。VMPTRLD 将特定的 VMCS 标记为活动(active)状态。然后,当执行 VMRESUME 时,非根模式 VM 使用该活动的 VMCS 实例来知道它正在执行的特定 VM 和 vCPU。
在这里值得注意的是,以上所有 VMX 指令都需要 CPL 级别为 0,因此它们只能从内核内部执行。
VMCS 基本上存储两种类型的信息:
- 上下文信息(Context Info),其中包括在根和非根之间转换期间要保存和恢复的 CPU 寄存器值。
- 控制信息(Control Info),确定 VM 在非根模式下的行为。
更具体地说,VMCS 分为六个部分:
- Guest State Area(客户机状态)存储 VM 退出时 vCPU 的状态。在 VMRESUME 时,vCPU 状态从此处恢复。
- Host State Arrea(主机状态)存储主机 CPU 状态。在 VM 退出时,主机 CPU 状态从此处恢复。
- Control Fields(VM 执行控制字段)确定 VM 在非根模式下的行为。这里面的控制字段为 1 表示某功能生效(带 exiting 就表示当发生时 VM 退出),通过合理的配置它,就能在审查与性能间达到很好的平衡。
- VM Exit Control Fields(VM 退出控制字段)确定 VM 退出的行为,表示在退出时由硬件保存哪些上下文。
- VM Entry Control Fields(VM 进入控制字段)确定 VM 进入的行为,如寄存器加载,事件注入。这是 VM 退出控制字段的对应部分。
- VM Exit Information Fields(VM 退出信息字段)告诉 Hypervisor 为什么会有 VM 退出,并提供其他信息。
注意:不同版本的处理器其 VMCS 结构不同,但是各个字段的索引保持不变,因此可以通过 VMCS 的相关读写指令+索引读写不同版本 VMCS 的相同字段。
关于 VMCS,详见虚拟化原理与IA虚拟化扩展 或 Intel 手册。
在硬件虚拟化支持中,暂时忽略了一些内容。在虚拟机内部进行虚拟地址到物理地址的转换是使用 VT-x 的一个名为扩展页表(EPT)的特性完成的。TLB 用于缓存虚拟地址到物理地址的映射,以节省页面表查询。TLB 语义也会因为虚拟机而改变。在物理机器上,高级可编程中断控制器(APIC)负责管理中断。在虚拟机中,这也被虚拟化了,并且有一些虚拟中断可以通过 VMCS 中的某个控制字段进行控制。I/O 是任何计算机操作的一个重要部分。虚拟化 I/O 并没有被 VT-x 所覆盖,通常在用户空间中被模拟,或者通过 VT-d 进行加速。
KVM
Kernel-based Virtual Machine (KVM) 是一组 Linux 内核模块,加载后将 Linux 内核转换为 Hypervisor。Linux 仍然以操作系统的形式正常运行,同时为用户空间提供虚拟化管理功能。KVM 模块可以分为两种类型:核心模块和机器特定模块。kvm.ko
是始终需要的核心模块。根据主机 CPU 的不同,可能需要机器特定的模块,例如 kvm-intel.ko
或 kvm-amd.ko
。KVM 执行 VMLAUNCH/VMRESUME、设置 VMCS、处理 VM 退出等操作。此外,AMD 的虚拟化技术 AMD-V 也有自己的指令,称为 Secure Virtual Machine (SVM)。在 arch/x86/kvm/
目录下,你会找到名为 svm.c
和 vmx.c
的文件。它们分别包含处理 AMD 和 Intel 虚拟化功能的代码。
KVM 与用户空间(在本例中是 QEMU)交互的方式有两种:通过设备文件 /dev/kvm
和 mmap。mmap 用于 QEMU 和 KVM 之间的数据大量传输(作为虚拟机内存)。
/dev/kvm
是 KVM 提供的主要 API。它支持一组 ioctl
,允许 QEMU 管理 VM 并与它们交互。在 KVM 中,最小的虚拟化单元是 vCPU,所有功能都是在其上构建的。/dev/kvm
API 是一个三级层次结构:
- 系统级别:调用此 API 可以操作整个 KVM 子系统的全局状态。其中之一是用于创建 VM。
- VM 级别:此 API 处理特定 VM。通过调用此 API 创建 vCPU。
- vCPU 级别:这是最低粒度的 API,处理特定的 vCPU。由于 QEMU 为每个 vCPU 分配一个线程(请参见下面的 QEMU 部分),因此在用于创建 vCPU 的线程中调用此 API。
创建 vCPU 后,QEMU 继续使用 ioctl
和 mmap 与它交互。
QEMU
QEMU 是在 VT-x/KVM/QEMU 栈中唯一的用户空间组件。QEMU 有两种模式:模拟器和虚拟化程序。
作为模拟器(与本文讨论的虚拟化无关),它可以使用二进制翻译技术伪造硬件,可以在 Intel 主机上运行具有 ARM 或 MIPS 核心的虚拟机。QEMU 带有 Tiny Code Generator(TCG)。这可以认为是一种类似于 JVM 的高级语言 VM。例如,它将 MIPS 代码转换为中间字节码,然后在主机硬件上执行。
作为虚拟化程序,它使用上述 ioctl
与 KVM 通信,为每个 VM 创建一个进程。对于每个 vCPU,QEMU 创建一个线程。这些是常规线程,它们像任何其他线程一样由操作系统调度。随着这些线程进入运行时,QEMU 为其 VM 内运行的软件创建多个 vCPU。
由于 QEMU 的起源于模拟,它可以模拟 I/O,这是 KVM 可能不完全支持的东西,比如主机上没有特定串行端口的虚拟机,因此很多外设 I/O 操作都可以让 QEMU 接管来模拟实现。当 VM 内的软件执行 I/O 时,VM 退出到 KVM。 KVM 查看原因,并将控制权与指向有关 I/O 请求的信息的指针传递给 QEMU。 QEMU 为该请求模拟 I/O 设备,并将控制传递回 KVM。 KVM 执行 VMRESUME 以使该 VM 继续进行。
以下图表总结了总体情况:
内存虚拟化
有了虚拟机之后,虚拟机内系统也有自己的页表,而 VMM 也有自己的页表,因此虚拟机每次访问内存需要经过两轮内存地址的转换
- 机器地址:真实硬件的机器地址(HPA, host physical address)
- 物理地址:Guest OS 所见的伪物理地址(GPA, guest physical address)
- 虚拟地址:Guest OS 提供给应用程序使用的线性地址(GVA, guest virtual address)
地址转换:虚拟地址 --- Guest OS ---> 物理地址 --- VMM ---> 机器地址
- MMU 泛虚拟化方法
- VMM 将虚拟地址到机器地址的映射直接写入 Guest OS 的页表中,替换原来虚拟地址 --> 物理地址的映射
- 影子页表方法(全虚拟化)
- 为 Guest OS 的每个页表维护一个影子页表,并将合成后的虚拟地址到机器地址的映射关系写入到“影子”中,Guest OS 的页表内容保持不变。最后,VMM 将影子页表交给 MMU 进行地址转换。
- 硬件辅助内存虚拟化
- VMX 在 PCID(进程上下文标识符)之外又引入了 VPID(虚拟处理器标识),用于区分不同虚拟机的 TLB 条目。可以通过给 TLB 条目标注与 VMM/虚拟机对应的 VPID 防止每次 VM exit 和 VM entry 导致的 TLB 清空,减少内存中页表访问次数,增加效率。
- Intel 扩展页表 EPT,通过硬件完成 虚拟地址->物理地址->机器地址 的两次地址转换操作,提高转换效率的同时避免 VMM 截获页表修改、处理页面失效带来的开销。