- 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
之前,启动代码必须初始化这几个虚拟寄存器。这在裸机程序中尤为重要。