- 1 概述
- 2 虚拟化简介
- 3 AArch64虚拟化
- 4 `Stage-2`地址转换
- 5 指令的陷入和模拟
- 6 虚拟化异常
- 7 虚拟化通用定时器
- 8 虚拟化主机扩展
- 9 嵌套虚拟化
- 10 安全空间的虚拟化
- 11 虚拟化的成本
- 12 小测验
- 13 其它参考文章
- 14 接下来的计划
1 概述
本文描述了ARMv8-64的虚拟化支持。讨论主题包括stage-2地址转换、虚拟异常和陷入。
本文主要介绍基本的虚拟化理论,并给出一些hypervisor如何利用虚拟硬件特性的示例。不会讨论如何写一个具体的hypervisor,或解释如何从头写一个hypervisor。
文章的最后,有一些问题可以用来检测你的学习程度。通过本文,首先,你将学习到两种类型的hypervisor,以及它们与ARM架构的异常级别(EL)的关系。其次,你将能够解释陷入操作,以及如何使用它们模拟操作。最后,你将了解hypervisor能够产生哪些虚拟异常,并描述相关机制。
1.1 准备工作
假设你对虚拟化有一个基本的认识,包括虚拟机是什么,以及hypervisor的角色。还应该熟悉内存管理中的异常模型和地址转换。
2 虚拟化简介
首先,我们引入一些hypervisor和虚拟化理论的入门知识。如果,你已经非常熟悉这些概念,请跳过本段。
在本文中,我们使用术语hypervisor来表示负责创建、管理和调度虚拟机(VM)的软件。
2.1 虚拟化为什么重要?
虚拟化是一项使用广泛的技术,支撑着几乎所有的现代云计算和企业基础设施。通过虚拟化,开发人员可以在单个机器上运行多个操作系统,以便可以在不损害主机环境的情况下测试软件。
虚拟化在服务器中很流行,对虚拟化的支持也是大多数服务器级处理器的要求。因为虚拟化带给了数据中心想要的特性,包括:
隔离:虚拟化的核心是为运行在单个物理系统上的多个虚拟机提供隔离。这种隔离允许互不信任的计算环境共享物理系统。例如,两个竞争对手可以在数据中心共享一台物理机器,但不能访问彼此的数据。高可用性:虚拟化允许在物理机器之间无缝并透明的迁移工作负载。这种常用于将工作负载从故障的硬件平台上迁移出来,以便维护、替换出错的硬件平台。负载均衡:为了优化数据中心的硬件和电力预算,充分利用硬件平台是非常重要的。这可以通过虚拟机的迁移,或在物理机器上托管合理的工作负载实现。这意味着尽可能地挖掘物理机器的容量。基于此,可以为数据中心提供商提供最好的电力预算,也为算力租户提供最佳性能。沙箱: 虚拟机可以为应用程序提供沙箱运行环境。比如,旧应用程序或开发中的软件,都可以运行在虚拟机中。运行在虚拟机中,可以阻止程序的漏洞或缺陷、甚至是恶意程序破坏运行在物理机器上的其它应用程序。
2.2 独立或托管hypervisor
hypervisor可以分为两大类:独立hypervisor,也称为Type-1型hypervisor;托管hypervisor,也称为Type-2型hypervisor。
我们首先看一下Type-2型hypervisor。在Type-2型的配置中,Host OS完全掌控着硬件平台和它的所有资源,包括CPU和物理内存。下图为一个Type-2型hypervisor的示意图:
Virtual Box或VMware就是这种类型的hypervisor。这种hypervisor的好处是,Host OS可以充分利用已有的OS功能管理硬件,也就是不用再开发大量的驱动程序。运行在被托管的虚拟机中的OS,我们称之为Guest OS。
接下来,我们看一下Type-1型hypervisor
从图中可以看出,该设计中没有Host OS的存在。hypervisor直接运行在硬件之上,完全掌控硬件平台及其所有资源,包括CPU和物理内存。与托管型hypervisor一样,独立hypervisor也可以托管虚拟机。这些虚拟机可以运行一个或多个完整的Guest OS。
ARM平台上最常用的两个开源hypervisor是Xen(独立,Type-1型)和KVM(托管,Type-2型)。本文在阐述一些要点时,会用这两个hypervisor作为示例。当然,还有许多其它可用的hypervisor,包括开源或私有的。
2.3 全虚拟化和半虚拟化
虚拟机的经典定义是一个独立的、隔离的计算环境,与真实的物理机器没有区别。尽管可以在ARM平台上完全模拟真实的机器,但这通常不是一种有效的方式。比如,模拟的网卡设备非常慢,因为Guest OS每次访问模拟寄存器,都必须由hypervisor处理。频繁的陷入导致比直接访问物理设备的寄存器,代价高昂的多。
作为替代方案,是修改Guest OS。让运行在虚拟机中的Guest OS意识到,自己是运行在虚拟机中,同时,在hypervisor提供性能更好的虚拟设备,Guest OS可以获得更好的访问性能。(简单地理解,全虚拟化中,每次访问寄存器都需要切换到hypervisor中执行,而半虚拟化中,将多次寄存器访问合并为一次I/O操作,减少hypervisor的切换次数,以提高性能)
Xen就是半虚拟化的代表,也是它推广了半虚拟化这个概念。使用Xen的虚拟化方案,需要修改Guest OS,以便让其可以在虚拟硬件平台上运行,而不是一个物理机器上。这种修改完全是为了提高性能。
在今天,包括ARM在内,大多数架构都支持硬件虚拟化,Guest OS基本上不需要修改就可以运行。除了少数几种I/O设备,比如块存储设备和网络设备,它们使用半虚拟化的设备和驱动程序。这种半虚拟化的I/O设备包括VirtIO和Xen PV Bus。
2.4 虚拟机和虚拟CPU
理解虚拟机(VM)和虚拟CPU(vCPU)的区别是很重要的。虚拟机包含一个或多个vCPU,如下图所示:
VM和vCPU的概念,在我们理解文章中的某些主题时非常有用。比如,一个物理内存页可以被分配给一个VM,那么该VM中所有的vCPU都可以访问这个内存页。但是,一个虚拟中断,只能被传送到目标vCPU上。
严格意义上,应该使用虚拟处理单元(
vPE)的概念,而不是vCPU。对于ARM架构实现的机器来说,PE是通用术语。本文使用vCPU的概念而不是vPE,是因为大部分人对此概念比较熟悉。但是,在ARM架构规范中,使用vPE的术语。
3 AArch64虚拟化
运行在EL2或更高异常级别上的软件,可以访问控制虚拟化:
Stage-2地址转换EL1/0指令和寄存器访问的捕获- 虚拟异常的产生
异常级别(EL),各层上运行的软件以及安全、非安全状态的对应关系,如下图所示:
值得注意的是,安全状态的EL2是灰色的。这是因为Secure EL2的支持并不总是可用的(ARMv8.4扩展)。这将在安全虚拟化一节中讨论。
ARM架构中一些其它的虚拟化扩展特性,包括:
- 安全虚拟化
- 主机虚拟化扩展-支持托管(
Type-2型)hypervisor - 嵌套虚拟化
4 Stage-2地址转换
4.1 Stage-2地址转换概念
Stage-2地址转换允许hypervisor对虚拟机中的内存有一个全局视角。具体来说,就是hypervisor能够控制VM访问的哪些内存映射的系统资源,以及这些资源在VM地址空间中的位置。
能够控制VM的内存访问,对于隔离和沙箱运行是非常重要的。Stage-2地址转换可以保证VM只能看见分配给它的资源,而无法访问分配给其它VM或hypervisor的资源。
对于内存地址转换来说,Stage-2地址转换属于第二阶段。为了支持该功能,需要一组新的地址转换表,称为Stage-2页表。
操作系统(OS)控制一组地址页表,将自己的虚拟地址空间映射到它认为的物理地址空间上。但是,OS想要访问真正的物理地址,还需要经历第二阶段的地址转换。这个第二阶段的地址转换由hypervisor控制。
OS控制的地址转换称为Stage-1地址转换,hypervisor控制的地址转换称为Stage-2地址转换。OS认为的物理内存空间称为中间物理地址(IPA)空间。
Stage-2阶段使用的页表格式与Stage-1类似。但是,页表中某些属性的处理是不同的,比如内存类型是Normal或Device,是直接编码到页表项中的,而不是通过MAIR_ELx寄存器的标志位进行判断。
4.2 虚拟机标识符(VMID)
每个虚拟机都有一个标识符,称为VMID。VMID用来给TLB项进行标记,这样就可以知道相应的项属于哪个VM。通过这种标记的方法,就可以允许同时在TLB中存在不同VM的地址转换。
VMID存储在VTTBR_EL2寄存器中,可以是8位或16位。由VTCR_EL2.VS标志位控制。16位的VMID是在ARMv8.1-A架构扩展中引入的。
EL2和EL3的地址转换不需要使用VMID进行标记,因为它们不属于Stage-2地址转换。
4.3 VMID和ASID的组合使用
我们知道,TLB表项也可以使用地址空间标识符(ASID)进行标记。应用进程由OS指定ASID,该进程中的所有TLB表项都会被该ASID标记。这意味着属于不同应用进程的TLB表项可以在TLB中共存,从而不存在一个应用进程使用了不属于它的TLB表项。
每个VM都有自己的ASID命名空间。比如,两个VM可能都使用了ASID=5,但是对于它们来说,是不同的事物。所以,ASID和VMID的结合是非常重要的。
4.4 内存属性的组合和覆盖
Stage-1和Stage-2地址映射都包含了属性,像内存类型和访问权限。内存管理单元(MMU)会组合两个阶段的属性,给出最终的属性结果。MMU从两者之中选择更严格的属性,如下图所示:
在本示例中,内存的Device类型比Normal类型更严格。因此,最终要访问的就是Device类型内存。如果,我们颠倒两个阶段的内存类型指定,也就是Stage-1是Normal,Stage-2是Device,那么,结果是一样的。
这种属性结合方法适用于大部分情况,但是,有时候,hypervisor可能想要覆盖这种行为。比如,在VM的早期引导启动阶段,
HCR_EL2.CD:强制所有Stage-1阶段的属性都是非缓存的(Non-cacheable)。HCR_EL2.DC:强制Stage-1阶段的属性为回写可缓存的正常内存(Normal、Write-Back Cacheable)。HCR_EL2.FWB:允许Stage-2覆盖Stage-1阶段的属性,而不是前面的常规属性结合方式。(这样,hypervisor可以阻止虚拟机访问某些关键的外设,以防该虚拟机中的Guest OS被恶意破坏后,进一步访问关键设备)。
HCR_EL2.FWB是ARMv8.4-A扩展的引入的。
4.5 模拟MMIO
同真实的物理地址空间一样,一个VM的IPA空间,包含内存和外设,如下图所示:
VM使用IPA地址中的外设区域,访问真实的物理外设(通常是直接分配的外设,也称为直通设备)和虚拟外设。
虚拟外设完全是由hypervisor使用软件模拟的,如下图所示:
已分配的外设是已经分配给VM的真实物理设备,映射到其IPA地址空间中。这就允许运行在VM中的软件可以直接与外设进行交互。
虚拟外设是hypervisor使用软件模拟的一个设备。相应的Stage-2页表项标记为fault。VM中的软件认为它在直接跟外设交互,实际上,每次访问都会触发一个Stage-2的fault异常,hypervisor在异常处理程序中模拟外设的访问。
为了模拟外设,hypervisor不仅需要知道要访问哪个外设,而且需要知道访问外设中的哪个寄存器,是读还是写寄存器,访问的大小,以及传输数据的寄存器。
为了处理异常,异常模型引入了FAR_ELx寄存器。当处理Stage-1的fault异常时,该寄存器会报告触发异常的虚拟地址。但是,此时的虚拟地址对hypervisor是没有用的,因为通常hypervisor不知道Guest OS如何配置它的虚拟地址空间。对于Stage-2阶段的fault异常,有一个额外的寄存器HPFAR_EL2,它将报告发生abort的IPA地址。因为hypervisor可以控制IPA地址空间,所以,它可以使用这个信息确定需要模拟的寄存器。
异常模型展示了ESR_ELx寄存器如何报告异常的信息。对于通用目的寄存器load或store触发的Stage-2阶段的fault异常,会提供额外的信息。这些信息包括访问的大小,源还是目的寄存器,以及允许hypervisor决定对虚拟外设的访问类型。
下图展示了捕获异常,并模拟访问的过程:
这个过程分为三步:
VM尝试访问虚拟外设。在本示例中,访问虚拟UART的接收FIFO。- 这次访问会被阻塞在
stage-2地址转换阶段,产生abort,陷入到EL2。
abort异常会将异常的信息,比如访问的字节数、目标寄存器以及它是load还是store,写入到寄存器ESR_EL2。abort异常还会将异常的IPA地址,写入到寄存器HPFAR_EL2中。
hypervisor读取ESR_EL2和HPFAR_EL2,识别要访问的虚拟外设寄存器。根据这些信息,hypervisor模拟相应的操作。然后,通过ERET指令返回到vCPU。
- 之后的执行从
LDR之后的指令开始。
4.6 系统内存管理单元(SMMU)
到目前为止,我们已经考虑了来自处理器的不同访问类型。系统中的其它主控制器,比如DMA控制器也会被分配给VM使用。我们还需要一些方法,将Stage-2阶段的保护扩展到这些主控制器上。
先考虑不使用虚拟化的系统,和其DMA控制器布局,如下图所示:
该DMA控制器通过内核空间的驱动程序进行访问。该内核驱动程序因为与内核在同一个地址空间中,能够保证OS内存访问不被破坏。也就是,应用程序不能通过DMA访问它不应该访问的内存。
再来考虑相同的系统,但是OS运行在VM中,如下图所示:
在该系统中,hypervisor使用Stage-2地址转换为VM提供隔离。也就是说,虚拟机能够访问的内存完全是由hypervisor控制的Stage-2页表决定的。
如果直接允许VM中的驱动与DMA控制器交互,将会产生两个问题:
隔离:DMA控制器不属于Stage-2页表,可以破坏VM的沙箱。地址空间:由于存在两个阶段的地址转换,导致内核相信PA就是IPA。而DMA控制器仍然能够看见真实的PA,因此,内核和DMA控制器就有了不同的内存视角。为了解决这个问题,hypervisor可以捕获VM和DMA的每次交互,提供必要的模拟行为。当内存碎片化时,这个过程非常低效且是有问题的。
一个替代方案是,扩展Stage-2地址转换机制,让其也能够对其它主控制器对内存的访问进行管理,比如,DMA控制器。也就是为这些主控制器也提供一个MMU管理单元,我们称之为系统内存管理单元(简称为SMMU,有时也称IOMMU)。
hypervisor负责对SMMU进行编程,这样,其它主控制器,比如本例中的DMA,就和VM具有一样的内存视角了。
这个方案解决了我们上面提出的两个问题。SMMU能够增强VM之间的隔离,保证独立的主控制器不会破坏沙箱环境。而且,SMMU也给了VM和分配给VM的主控制器一致的内存视角。
当然了,虚拟化不是SMMU的唯一使用场景。对于其它使用情况不再本文的讨论范围,后续再专门写文章讨论。
5 指令的陷入和模拟
有时候,hypvervisor需要模拟VM中的操作。比如,VM中的软件想要配置跟电源管理或cache一致性有关的一些底层的处理器控制。通常,我们不想VM直接访问这些控制寄存器,因为,它们可能被用来破会隔离,或者影响系统中的其它VM。
当执行给定的操作时,比如读取一个寄存器,陷入会产生异常。hypervisor需要这种能力去捕获VM的操作,就像配置底层的一些控制寄存器一样,而不会影响其它VM。
ARMv8架构提供了这种捕获VM操作并模拟它们的陷入控制标志位。当配置了某种陷入异常之后,VM执行某个特定的操作,将会造成异常,从而陷入到更高级别的异常级(EL)中。进而,hypervisor能够利用这些陷入异常模拟VM中的操作。
比如,执行等待中断(WFI)指令,会将CPU置入低功耗状态。如果设置了HCR_EL2.TWI==1,在EL0或EL1执行WFI指令,就会在EL2产生一个异常。
注意:陷入(
Trap)不仅仅是给虚拟化使用的。在EL3和EL1一样可以控制陷入。但是,陷入对虚拟化软件特别重要。本文仅讨论与虚拟化相关的陷入操作。
在WFI例子中,OS通常在idle循环中执行执行WFI指令。对于虚拟机中的Guest OS,hypervisor能够捕获这种操作,然后调度不同的vCPU执行,如下图所示:
5.1 表示某些寄存器的虚拟值
另一个使用陷入的例子是表示某些寄存器的虚拟值。比如,ID_AA64MMFR0_EL1,表示处理器支持的内存相关的一些特性。尤其是在启动阶段,OS可能会读取这些值,判断内核是否应该使能某些功能。对此,hypervisor可能想给Guest OS表达一个不同的值,称为虚拟值。
为此,hypervisor使能相关陷入标志位。当VM读取该寄存器时,发生陷入异常,hypervisor确定是哪种陷入触发的,然后,模拟该操作。在本例中,hypervisor使用ID_AA64MMFR0_EL1的虚拟值填充目的寄存器,如下图所示:
陷入异常,也可以用于懒惰上下文切换(lazy context switching)。比如,通常情况下,OS在引导启动阶段初始化MMU配置寄存器(TTBR<n>_EL1、TCR_EL1和MAIR_EL1),之后,不会再重新设置。hypervisor可以利用这个习惯优化上下文切换,仅仅在上下文切换时恢复这些寄存器,而不用保存它们。
但是,启动之后,OS也可能会对其重新编程。为了避免造成问题,hypervisor可以设置HCR_EL2.TVM这个陷入使能位。设置之后,任何尝试写MMU相关的寄存器都会产生陷入异常到EL2中,允许hypervisor检测是否需要更新它保存的这些寄存器的副本。
注意:我们使用
陷入(trapping)和路由(routing)表示独立,但是相关的概念。回忆一下,陷入是当执行特定的操作造成异常。路由是指一旦异常产生就会被带到的异常级别。
5.2 MIDR和MPIDR
使用陷入模拟一些操作需要大量的计算。VM的操作产生陷入异常到EL2,hypervisor确定、模拟该操作,然后,返回到Guest OS中。表示特性的寄存器,像ID_AA64MMFR0_EL1,OS不常访问。这意味着,hypervisor模拟这种操作所执行的代码而带来的性能损失是可以接受的。
对于那些需要频繁访问的寄存器,或者性能关键代码中访问的寄存器,就需要避免这种计算负载。这类寄存器和其可能值的示例,如下所示:
MIDR_EL1:处理器类型,比如Cortex-A53。MPIDR_EL1:亲和力寄存器,比如处理器2的核1。
hypervisor希望Guest OS能够看见这些寄存器的虚拟值,但是每次访问都陷入。对于这些寄存器,ARMv8架构提供了代替方案:
VPIDR_EL2:EL1读取MIDR_EL1时返回的值。VMPIDR_EL2:EL1读取MPIDR_EL1时返回的值。
hypervisor可以在进入VM之前,设置这些寄存器。如果VM中的软件读取MIDR_EL1或MPIDR_EL1,硬件自动返回虚拟值,而无需陷入到EL2处理。
注意:
VMPIDR_EL2和VPIDR_EL2没有定义复位值。所以,在第一次进入到EL1之前,启动代码必须初始化这几个虚拟寄存器。这在裸机程序中尤为重要。













