硬件辅助的虚拟化技术,又称为芯片辅助(Chip-Assisted)的虚拟化技术,最初来自于 Intel CPU 实现的 VT(Virtualization Technology)技术,现在已经逐渐发展为以下虚拟化技术扩展分支:
Intel VT-x(Intel Virtualization Technology for x86):使得 VMM(虚拟机监视器)能够在 Intel x86 CPU 上直接运行,而不需要通过 “二进制翻译“ 技术来模拟。它提供了一组 CPU 指令和硬件辅助功能,包括 CPU 虚拟化和内存虚拟化,使得虚拟机能够更好地运行,并提供更好的安全性和隔离性。
Intel VT-d(Intel Virtualization Technology for Directed I/O):是为虚拟化环境中的 I/O 设备提供的硬件加速技术,它能够提高 VM(虚拟机)访问 I/O 设备的性能和安全性。它通过提供硬件辅助来限制 VM 访问 I/O 设备的范围和权限,防止恶意代码窃取敏感信息或破坏系统。
Intel VT-c(Intel Virtualization Technology for Connectivity):是一种网络加速技术,它通过提供硬件辅助来加速网络虚拟化的性能和可扩展性,提高虚拟机网络访问的效率和带宽利用率。它支持多队列、多核心和多路复用等功能,提高了虚拟机的网络吞吐量和响应速度。
基于二进制翻译的全虚拟化技术,是一种完全由软件实现的虚拟化技术,最初诞生于 1998 年,是由 VMware 在 x86 CPU 上实现的 “二进制翻译(Binary Translation)” 技术。其实现原理如下:
特权压缩(Ring Compression):又称为特权降级。当 GuestOS 执行内核态的 Ring0 指令时,VMM 就会将其 “捕获",因为 VMM 运行在 Ring0 上,而 GuestOS 实际是运行在 RIng1 上,所以 VMM 可以捕获 GuestOS 发出的 CPU 指令。捕获后,VMM 调用若干个用户态的 Ring3 指令来进行 “模拟”,从而将 Ring0 的 “降级“ 为 Ring3。如此的,GuestOS 就能够照常执行内核态的指令了。但是,这只能解决内核态的特权指令问题,无法解决用户态的特权指令问题。所以还需要另外的 “陷入模拟“。
陷入模拟(Binary translation):又称为二进制翻译,VMM 会 “捕获“ 所有无法通过 “特权压缩“ 来降级的特权指令,并陷入模拟,将特权指令 “翻译” 成一个只针对 GuestOS 这个进程的、且运行在用户态之上的非特权指令。例如:当 GuestOS 执行了 reboot 指令,VMM 首先将其捕获,然后翻译为 “reboot“,执行 GuestOS 的重启操作。
可见,完全由软件实现的全虚拟化技术需要频繁的捕获这些特权指令,将这些指令进行转换之后,再交给 CPU 执行,导致其性能低下。
半虚拟化(Para-Virtualization)技术,也是一种完全由软件实现的虚拟化技术,最初诞生与 2003 年,是由开源的 Xen 虚拟化项目首先提出。其实现原理如下:
Virtualization Layer(HostOS + VMM)运行在 x86 CPU 的 Ring0(内核态)上,GuestOS(VM 进程)也运行在 Ring0 上,User Application 则运行在 Ring3(用户态)上。
GuestOS 和 HostOS 的代码不一致。GuestOS 需要经过特殊的改造,将需要执行特权指令的代码都修改成对 Virtualization Layer API 的调用,称为 Hypercalls(区别于 Systemcalls)。通过这样的方式,VMM 就不再需要执行捕获了,因为 GuestOS 永远也无法发出特权指令,所以 VMM 在实现上会更加简单,但相对的也无法支持原生的操作系统内核。
另外,Xen 为了进一步提升 VM 的存储和网络 I/O 效率,还提供了一个特殊的 Domain0 VM,作为所有 Normal VMs 的 I/O 中转站。即:VM 通过一个 I/O Front-end(前端模块)与 Domain0 中的 Backend(后端模块)进行交互,并最终由 Domain0 中的 Device drivers 实现物理 I/O。
Domain0 的本质是 Xen 的一个特权虚拟机,运行着被修改过的 Linux kernel,它拥有访问物理 I/O 资源的权限,为其它的 Normal VMs 提供 I/O 外设模拟仿真功能。
硬件辅助的虚拟化技术,顾名思义,是一种由 CPU 硬件来辅助实现的虚拟化技术,最早诞生于 2006 年,由 Intel 实现的 VT-x(Virtualization Technology for x86)技术。其实现原理如下:
CPU 提供了 Root Mode 和 Non-root Mode 这 2 种运行模式。其中,HostOS(VMM)运行在 Root Mode 拥有最高执行权限,而 GustOS(VM)和 User Application 则运行在 Non-root Mode(受控模式),并且这 2 种模式都支持 Ring 0~3。因此 VMM 和 Guest OS 都可以自由选择它们所期望的 CPU 运行级别,因此 “特权压缩“ 的问题迎刃而解。
与二进制翻译的实现方式类型,运行在 Root Mode 上的 CPU 会自动捕获 GuestOS 发出的特权指令(触发异常),然后交由 VMM 来完成翻译,处理后再使用 VMLAUNCH 或 VMRESUME 指令返回到 GuestOS。
这种由硬件辅助来完成的方式显然会比完全由软件实现的方式效率更高。并且 Root Mode 和 Non-root Mode 之间的模式切换时,上下文的保存与恢复也是由 CPU 硬件来完成,也有效提高了陷入(模拟时上下文切换)的效率。
Intel VT-x(Intel Virtualization Technology for x86)技术的另一个核心是为 VMM 提供了一组 CPU 指令和硬件辅助功能。其中,最核心的 CPU 指令集有以下 3 个:
VMCS(Virtual Machine Control Structures,虚拟机控制指令)有两个部分组成:
VMCS 指令集:故名思义,是用于控制 VM 的一系列操作指令。包括:VMREAD、VMWRITE、VMCLEAR 等等。
VMCS Configuration 空间:本质是寄存器,存储了一个 64bits 的指针,指向一个物理的内存空间。以 vCPU 为对象,有多少个 vCPU,就有多少个 VMCS 指针。因为 VMM 和 GuestOS 共享底层的 CPU 资源,所以 CPU 需要一个物理的内存空间来自动保存或恢复彼此切换时的上下文。这块空间包括了 3 个区域:
VMX 切换,即:CPU Root Mode 和 Non-root Mode 之间的切换。CPU 收到 VMM 的控制,在这 2 种模式中进行切换,继而轮流执行 VMM 或 GuestOS 的代码。
VMX 切换,是 VMM 通过调用 VM Entry 和 VM Exit 这 2 个指令集来完成:
VM Entry 指令集:假设当前 CPU 运行在 Root Mode,在执行 VMM 的代码,此时 VMM 可以通过调用 VMLAUNCH 或 VMRESUME 指令将 CPU 的运行模式切换到 Non-root Mode。当 VMM 执行 VM Entry 时,CPU 就会从 Guest State Area 中加载 GuestOS 的上下文,开始执行 GuestOS 的代码。(值得注意的是,VMCS 并不需要保存 VMM 的上下文,因为 CPU 开支执行 VMM 之后,就不会受到 GuestOS 的干扰,只有 VMM 将工作彻底处理完毕后,CPU 才可能切换到执行 GuestOS 的代码。)
VM Exit 指令集:而当 GuestOS 在运行过程中遇到需要 VMM 处理的事件时,例如:缺页异常(Page fault)、I/O 中断、主动调用 VMM 的 VMCALL 指令等,CPU 的运行模式就会从 Root Mode 切换到 Non-root Mode。当 VMM 执行 VM Exit 时,CPU 会自动将 Guest OS 的上下文加载到 Guest State Area 中,挂起 GuestOS,并从 Host State Area 中加载 VMM 的通用事件处理函数的入口地址,开始执行 VMM 的代码。
一个执行 VMX 切换的例子是:GuestOS 发出了 I/O 请求,执行了 VM Exit,然后 VMM 可以直接读取 GuestOS 的内存并将 I/O 操作模拟出来,VMM 完成后,再调用 VMRESUME 指令执行 VM Entry,GuestOS 继续执行。此时在 VM 的角度看来,GuestOS 的 I/O 操作指令就像是直接被 CPU 执行了一样。
VMCS Configuration 空间非常重要,每次发生 VM Exit 时,硬件自动在 VMCS Configuration 中存入丰富的信息,方便 VMM 甄别事件的种类和原因。而当发生 VM Entry 时,VMM 可以方便地为 GuestOS 注入事件(中断和异常),因为 VMCS Configuration 中存有 GuestOS 的中断描述表(IDT)的地址,因此硬件能够自动地调用 GuestOS 的处理程序。
Guest State Area 和 Host State Area 都会存储部分 CPU 物理寄存器的信息,例如:
另外,Guest State Area 还会包含非物理寄存器的内容,例如:一个 32bits 的 Active State 值,以此表明 GuestOS 执行时 CPU 所处于的活跃状态,如果正常执行指令就是处于 Active 状态,如果触发了三重故障(Triple Fault)或其它严重错误就处于 Shutdown 状态等等。同时,Guest State Area 并不强制性包含通用寄存器的数据,这些数据由 VMM 在 VM Exit 的时候来决定是否进行保存,从而提高了系统性能。
Execute State Area 存放可以操控 VM Entry 和 VM Exit 的标志位,包括:
VMCS 还包括一组 BitMap 以提供更好的适应性:
Linux 操作系统采用页表的方式来管理虚拟内存技术,在没有引入 VM 之前,只需要完成 Virtual Memory 和 Physical Memory 之间的地址转换。
在引入了 VM 的同时,虚拟地址转换就增加了一层,形成了一种两级地址映射关系。如下图所示:Host Virtual Address(HVA)=> Host Physical Address(HPA)=> Guest Virtual Address(GVA)=> Guest Physical Address(GPA)。
需要注意的是,VM 的本质是一个 User Process,按理说应该像其他 Normal User Process 一样可以直接访问 HVA,但事实上,由于 VM User Process 运行于 CPU 的 Non-root Mode 中,所以无法直接访问 HVA,而是需要通过 VMM 来完成 GPA 到 HVA 之间的地址转换。即:VMM 需要 intercept (截获)VM 的内存访问指令,然后 virtualize(模拟)为 HVA(e.g. kvm_memory_slot 数据结构)。而 GVA 到 GPA 的地址转换则由 GuestOS 的系统页表完成。
显然,在 VMM 实现的两级地址映射中,VM 的每次内存访问都需要 VMM 介入,并由软件进行多次地址转换,其效率是非常低的。
SPT(Shadow Page Table,影子页表)技术就是一种优化的地址转换过程,可以直接将 GVA 转换为 HPA。
SPT 为 GuestOS 的系统页表设计了一套对应的影子页表,并将影子页表装载到 CPU 的 MMU 中。影子页表的本质也是一个系统页表,从 MMU 的角度来看两者并无差别。只是系统页表是由 Kernel 来维护的,而影子页表是由 VMM 来维护的。并且 VMM 要为每个 VMs 都维护了一套对应的影子页表。
有了 SPT 之后,当 GuestOS 访问内存时,就可以根据 MMU 中的影子页表映射关系,完成 GVA 到 HPA 的直接映射。
虽然 SPT 将 VMM 实现的两级地址映射优化成为了一种基于 MMU 的直接映射,但本质上还是纯软件实现的,效率不高,而且 VMM 承担了太多影子页表的维护工作,设计不好。
为了进一步改善 SPT 内存虚拟化技术,同样需要由 CPT 来进行辅助。
EPT(Extend Page Table,扩展页表)技术最早由 Intel 提出,为 MMU 增加了 SLAT(Second Level Address Translation,二级内存翻译)功能,即:为 VMM 提供了一套专用的页表空间,进而抛弃掉了影子页表。
EPT 的基本原理如上图所示,EPT 在原有 CR3 页表一级映射的基础上,引入了 EPT 页表来实现另一层二级映射,这样就完全由硬件来实现了 GVA => GPA => HPA 之间的两次地址转换。
举例来说,当 GuestOS 中的 User Application 访问内存,CPU 首先会访问 GuestOS 的 CR3 页表来完成 GVA 到 GPA 的转换,如果 GPA 不为空,则 CPU 接着通过 EPT 页表来实现 GPA 到 HPA 的转换(实际上,CPU 首先会查看 Cache 或 TLB,如果没有缓存记录才会进一步查看 EPT 页表)。另外,如果 HPA 为空,那么 CPU 就会抛出 EPT Violation 异常,并交由 VMM 来处理。
另外,CPU 访问 GuestOS 的 CR3 页表发现 GPA 为空的话,那么就会触发缺页异常,并交由 GuestOS 的缺页异常中断处理程序处理。在中断处理程序中会调用 EXIT_REASON_EPT_VIOLATION 指令,VM Exit,VMM 截获到该异常,然后分配 HPA,并建立 GVA 到 HPA 的映射,最后保存到 EPT 中。这样在下次访问的时候就可以完成从 GVA 到 HPA 的转换了。
I/O 虚拟化技术大体上也可以分为 2 种类型:
本文主要讨论第 2 种类型。
从 CPU 的角度看,为了解决 VM 访问物理网卡等 I/O 外设的性能问题,就要避免 VMM 对 CPU 的 I/O 指令进行捕获/翻译、上下文切换、内存拷贝等损耗。使用硬件 Passthrough(直通)的方式来 Bypass 掉 VMM 是一种不错的选择,而这种硬件就被称为 IOMMU(Input/Output Memory Management Unit,I/O 内存管理单元),实际上是一个北桥芯片组(North Bridge,连接 I/O 总线和主存之间的桥梁)。
IOMMU 用于管理 I/O 外设对 Main Memory 的访问,将 NIC 中的 DMA Controller 可以操作的 Device Address 映射到正确的 Physical Address 上,起到保证 Main Memory 访问安全和提升 I/O 效率的作用。
值得注意的是,IOMMU 是一个通用的术语,而 Intel VT-d 和 AMD IOMMU 则是 Intel 和 AMD 公司对 IOMMU 技术的一个特定实现。
从对硬件外设进行抽象的角度来看,每个 I/O 设备都拥有 2 种资源和 2 种能力:
所以要实现 I/O 设备的虚拟化主要就是对上述的 2 种资源和 2 种能力实现隔离、保护和性能着 3 方面的增强。其中,IOMMU 实现了 2 种能力的虚拟化,而 SR-IOV 标准则实现了 2 种资源的虚拟化。
Intel VT-d(Intel Virtualization Technology for Directed I/O)主要在 IOMMU 中实现了下述 2 个方面的功能增强:
通过 Intel VT-d 可以最终实现将 PCIe 网卡 Passthrough(直通)给 VM 使用,而无需 VMM 的参与。
在传统的 I/O MMU 中,它提供了一种集中的方式管理所有的 DMA 地址,通过统一的 ZONE_DMA 内存地址范围来区分不同的 I/O 外设,但却不支持不同 I/O 外设之间的 DMA 隔离。因此 Intel VT-d 在北桥芯片上添加了 DMA Remapping Feature(DMA 重映射)功能,支持地址空间上的隔离,使得 I/O 外设只能访问规定的内存区域。
通过 DMA Remapping Feature,VM 可以直接访问到 Host 中的物理网卡的 DMA 寄存器和 Rx/Tx Queues 空间。
可见,DMA Remapping Feature 主要解决了 2 个问题:
I/O 外设会产生非常多的硬中断请求,在虚拟化场景中,IOMMU 需要正确的将不同的 I/O 硬中断请求分离到对应的 VMs 上处理。
传统的 I/O 硬中断请求路由方式,需要通过 “DMA 写请求“ 指令根据目标内存地址定向地发出一个 MSI(Message signaled interrupts,消息中断)。但由于 “DMA 写请求“ 需要嵌入一个目标内存地址,因此 DMA Controller 就需要访问所有的 ZONE_DMA 内存地址,无法实现中断隔离。
因此,Intel VT-d 实现的 Interrupt Remapping Feature(中断重映射)功能重新定义了 MSI 的格式来解决这个问题。新的 MSI 不再嵌入目标内存地址,取而代之的是一个 Msg ID,并通过维护一个表结构,来完成 Msg ID 和 VM 内存区域的映射。
在 Linux 操作系统中启用 Intel VT-d 的步骤:
# Intel
intel_iommu=on
$ dmesg | grep -e DMAR -e IOMMU
[ 0.000000] DMAR: IOMMU enabled