与虚拟化相对的一个概念是仿真/模拟,简单说就是伪造硬件。例如 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 是适用于 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 是为了支持 VT-x 而添加的指令:
当 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 基本上存储两种类型的信息:
ol type="1">
更具体地说,VMCS 分为六个部分:
ol type="1">
注意:不同版本的处理器其 VMCS 结构不同,但是各个字段的索引保持不变,因此可以通过 VMCS 的相关读写指令+索引读写不同版本 VMCS 的相同字段。
关于 VMCS,详见虚拟化原理与IA虚拟化扩展 或 Intel 手册。
在硬件虚拟化支持中,暂时忽略了一些内容。在虚拟机内部进行虚拟地址到物理地址的转换是使用 VT-x 的一个名为扩展页表(EPT)的特性完成的。TLB 用于缓存虚拟地址到物理地址的映射,以节省页面表查询。TLB 语义也会因为虚拟机而改变。在物理机器上,高级可编程中断控制器(APIC)负责管理中断。在虚拟机中,这也被虚拟化了,并且有一些虚拟中断可以通过 VMCS 中的某个控制字段进行控制。I/O 是任何计算机操作的一个重要部分。虚拟化 I/O 并没有被 VT-x 所覆盖,通常在用户空间中被模拟,或者通过 VT-d 进行加速。
Kernel-based Virtual Machine (KVM) 是一组 Linux 内核模块,加载后将 Linux 内核转换为 Hypervisor。Linux 仍然以操作系统的形式正常运行,同时为用户空间提供虚拟化管理功能。KVM 模块可以分为两种类型:核心模块和机器特定模块。 是始终需要的核心模块。根据主机 CPU 的不同,可能需要机器特定的模块,例如 或 。KVM 执行 VMLAUNCH/VMRESUME、设置 VMCS、处理 VM 退出等操作。此外,AMD 的虚拟化技术 AMD-V 也有自己的指令,称为 Secure Virtual Machine (SVM)。在 目录下,你会找到名为 和 的文件。它们分别包含处理 AMD 和 Intel 虚拟化功能的代码。
KVM 与用户空间(在本例中是 QEMU)交互的方式有两种:通过设备文件 和 mmap。mmap 用于 QEMU 和 KVM 之间的数据大量传输(作为虚拟机内存)。
是 KVM 提供的主要 API。它支持一组 ,允许 QEMU 管理 VM 并与它们交互。在 KVM 中,最小的虚拟化单元是 vCPU,所有功能都是在其上构建的。 API 是一个三级层次结构:
ol type="1">
创建 vCPU 后,QEMU 继续使用 和 mmap 与它交互。
QEMU 是在 VT-x/KVM/QEMU 栈中唯一的用户空间组件。QEMU 有两种模式:模拟器和虚拟化程序。
作为模拟器(与本文讨论的虚拟化无关),它可以使用二进制翻译技术伪造硬件,可以在 Intel 主机上运行具有 ARM 或 MIPS 核心的虚拟机。QEMU 带有 Tiny Code Generator(TCG)。这可以认为是一种类似于 JVM 的高级语言 VM。例如,它将 MIPS 代码转换为中间字节码,然后在主机硬件上执行。
作为虚拟化程序,它使用上述 与 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 截获页表修改、处理页面失效带来的开销。
ol type="1">
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.mushiming.com/mjsbk/2584.html