1 概述
2 虚拟化简介
3 AArch64虚拟化
4 `Stage-2`地址转换
5 指令的陷入和模拟
6 虚拟化异常
7 虚拟化通用定时器
8 虚拟化主机扩展
9 嵌套虚拟化
10 安全空间的虚拟化
11 虚拟化的成本
12 小测验
13 其它参考文章
14 接下来的计划
本文描述了ARMv8-64
的虚拟化支持。讨论主题包括stage-2
地址转换、虚拟异常和陷入。
本文主要介绍基本的虚拟化理论,并给出一些hypervisor
如何利用虚拟硬件特性的示例。不会讨论如何写一个具体的hypervisor
,或解释如何从头写一个hypervisor
。
文章的最后,有一些问题可以用来检测你的学习程度。通过本文,首先,你将学习到两种类型的hypervisor
,以及它们与ARM架构的异常级别(EL)的关系。其次,你将能够解释陷入操作,以及如何使用它们模拟操作。最后,你将了解hypervisor
能够产生哪些虚拟异常,并描述相关机制。
1.1 准备工作
假设你对虚拟化有一个基本的认识,包括虚拟机是什么,以及hypervisor
的角色。还应该熟悉内存管理中的异常模型
和地址转换。
首先,我们引入一些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
的术语。
运行在EL2
或更高异常级别上的软件,可以访问控制虚拟化:
Stage-2
地址转换
EL1/0
指令和寄存器访问的捕获
虚拟异常的产生
异常级别(EL
),各层上运行的软件以及安全、非安全状态的对应关系,如下图所示:
值得注意的是,安全状态的EL2
是灰色的。这是因为Secure EL2
的支持并不总是可用的(ARMv8.4
扩展)。这将在安全虚拟化一节中讨论。
ARM架构中一些其它的虚拟化扩展特性,包括:
安全虚拟化
主机虚拟化扩展-支持托管(Type-2
型)hypervisor
嵌套虚拟化
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
的唯一使用场景。对于其它使用情况不再本文的讨论范围,后续再专门写文章讨论。
有时候,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
、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
之前,启动代码必须初始化这几个虚拟寄存器。这在裸机程序中尤为重要。
硬件使用中断发送信号给软件。比如,GPU使用中断通知它已经完成帧的渲染。
在支持虚拟化的系统中,这部分就更为复杂了。某些中断可能是hypervisor
本身处理。其它的中断可能分配到VM
中,由其中的软件进行处理。另外,当接收到中断时,中断的目标VM
可能没在运行中。
这就意味着,你需要一些机制支持hypervisor
处理EL2
上的中断。另外,还需要一些机制,转发中断到特定的VM
或者特定的vCPU
上。
为了使能这些机制,ARMv8
架构支持虚拟中断:vIRQ
、vFIQ
和vSError
。这些虚拟中断的行为与物理中断(IRQ
、FIQ
和SError
类似,但只能在EL0
或EL1
上执行时发出信号。在EL2
或EL3
上执行时,是不可能接收到虚拟中断的。
注意:安全状态的虚拟化支持是在
ARMv8.4-A
扩展中引入的。为了在安全EL0/1
中,发出虚拟中断的信号,需要支持安全EL2
并使能它。否则,在安全状态下是不会发送虚拟中断信号的。
6.1 使能虚拟中断
为了发送虚拟中断到EL0/1
,hypervisor
必须设置HCR_EL2
寄存器中相关的路由标志位。比如,为了使能vIRQ
中断信号,必须设置HCR_EL2.IMO
标志位。这种设置,将物理IRQ
中断路由到EL2
,然后,由hypervisor
使能虚拟中断,发送信号到EL1
。
理论上,可以配置VM
接收物理FIQ
中断和虚拟IRQ
中断。实际上,这是不同寻常的。VM
通常只接收虚拟中断信号。
6.2 产生虚拟中断
产生虚拟中断,有两种机制:
由CPU核内部产生,通过HCR_EL2
中的一些控制位实现。
使用GICv2
或更新架构的中断控制器。(参考另一篇文章《GICv3-软件概述》的第8章)
让我们从机制1
开始。HCR_EL2
中,有3个标志位控制虚拟中断的产生:
VI
:设置该标志位注册一个vIRQ
中断。
VF
:设置该标志位注册一个vFIQ
中断。
VSE
:设置该标志位注册一个vSError
中断。
设置这些标志位,等价于中断控制器产生一个中断信号给vCPU
。产生的虚拟中断收到PSTATE
屏蔽,就像常规中断那样。
这种机制简单易用,但缺点就是,只提供了产生该中断自身的一种方法。hypervisor
需要在VM
中模拟中断控制器的操作。总的来说,通过陷入、模拟的方式涉及到开销问题,对于频繁的操作,尤其是中断,最好避免。
第二种方法是使用ARM
提供的通用中断控制器(GIC
),产生虚拟中断。从GICv2
开始,通过提供物理CPU接口和虚拟CPU接口,中断控制器可以发送物理中断和虚拟中断两种信号。如下图所示:
两种接口是一样的,除了一个发送物理中断信号而另外一个发送虚拟中断信号之外。hypervisor
可以将虚拟CPU接口映射到VM
,这样,VM
中的软件就可以直接和GIC
通信。这种方法的优点是,hypervisor
只需要配置虚拟接口即可,不需要模拟它。这种方法减少了需要陷入到EL2
中执行的次数,因此也就减少了虚拟化中断的开销。
虽然,
GICv2
可以与ARMv8-A
一起使用,但更常见的是使用GICv3
或GICv4
。
6.3 转发中断到vCPU
的示例
到目前为止,我们已经看了虚拟中断是如何被使能和产生的。下面就让我们看一下,将虚拟中断转发到vCPU
的示例。在该例子中,我们假设一个物理外设被分配给VM
,如下所示:
步骤如下:
物理外设发送中断信号到GIC
。
GIC
产生物理中断异常,可以是IRQ
或FIQ
,被路由到EL2
(设置HCR_EL2.IMO/FMO
标志位)。hypervisor
识别外设,并确定已经分配给VM
。然后,判断中断应该被转发到哪个vCPU
。
hypervisor
配置GIC
,将物理中断以虚拟中断的形式转发给vCPU
。然后,GIC
发送vIRQ
或vFIQ
信号。但是,当在EL2
上执行时,处理器会忽略掉这类虚拟中断信号。
hypervisor
将控制权返还给vCPU
。
此时,处理器处于vCPU
中(EL0
或EL1
),就可以接收来自GIC
的虚拟中断。这个虚拟中断同样受制于PSTATE
异常掩码的屏蔽。
该示例展示了一个物理中断,如何被转发为虚拟中断的过程。这个例子对应于在讲解Stage-2
地址转换一节时的直通设备。对于虚拟外设,hypervisor
能够产生虚拟中断,而无需将其连接到一个物理中断上。
6.4 中断掩码和虚拟中断
在异常模型中,我们介绍了PSTATE
中的中断掩码位,PSTATE.I
用于IRQ
,PSTATE.F
用于FIQ
,且PSTATE.A
用于SError
。当在虚拟化环境中工作时,这些掩码的工作方式有些不同。
例如,对于IRQ
,我们已经看到设置HCR_EL2.IMO
做了两件事:
路由物理IRQ
中断到EL2
使能在EL0
和EL1
中的vIRQ
中断信号的发送
此设置还会改变应用PSTATE.I
掩码的方式。当在EL0
和EL1
时,如果HCR_E2.IMO==1
,PSTATE.I
对vIRQ
进行操作,而非pIRQ
。
ARM架构提供了通用定时器,是每个处理器中一组标准化的定时器。通用定时器包含一组比较器,每个比较器与通用系统计数器进行比较。当比较器的值等于或小于系统计数器时,就会产生一个中断。下图中,我们可以通用定时器(橙色),由一组比较器和计数器模块组成。
下图展示了一个具有两个vCPU
的hypervisor
的示例系统:
在示例中,我们忽略
hypervisor
在vCPU
之间执行上下文切换时花费的开销。
4ms
物理时间(挂钟时间)内,每个vCPU
运行了2ms
。如果vCPU0
在T=0
时设置比较器,让其3ms
之后产生中断,中断会按照预期产生吗?
或者,你希望在虚拟时间(vCPU
所经历的时间)2ms
之后中断,还是在挂钟时间2ms
之后中断?
ARM架构提供了这两种功能,具体使用依赖于虚拟化的用途。让我们看一下硬件架构是如何做到的。
运行在vCPU
上的软件可以访问2个定时器:
EL1
物理定时器
EL1
虚拟定时器
EL1
物理定时器与系统计数器产生的计数进行比较。可以使用这个定时器给出挂钟时间
,即物理CPU的执行时间。
挂钟时间,英文名称为
wall-clock time
,也可以理解为物理CPU的执行时间。
EL1
虚拟定时器与虚拟计数进行比较。虚拟计数等于物理计数减去偏移量。hypervisor
在一个寄存器CNTOFF_EL2
中,为当前被调度的vCPU
指定偏移量。这就允许它隐藏该vCPU
未被调度执行时流逝的时间。
为了阐述这个概念,我们扩展前面的示例,如下图所示:
在6ms
的时间周期内,每个vCPU
都运行了3ms
。hypervisor
可以使用偏移量寄存器让虚拟计数仅仅表示vCPU
的运行时间。或者,hypervisor
可以设置偏移量为零,这意味着虚拟时间等于物理时间。
本示例中,展示的系统计数是
1ms
。实际上,这个频率是不现实的。我们推荐系统计数器使用1MHz
到50MHz
之间的频率(也就是1us→20ns
计数时间间隔)。
下图展示了一个软件和异常级别对应关系的简化版本:
可以看到独立hypervisor
和ARM异常级别的对应关系。hypervisor
运行在EL2
上,VM
运行在EL0/1
上。对于托管型hypervisor
这种架构是有问题的。
我们知道,通常情况下,内核运行在EL1
,但是虚拟化的控制操作在EL2
。这意味着,Host OS
内核的大部分代码位于EL1
,一小部分代码运行于EL2
(用于控制虚拟化)。这种设计效率不高,因为它涉及到额外的上下文切换。
想要使内核运行在EL2
,需要处理运行在EL1
和EL2
上的一些差异。但是,这些差异被限制到少数子系统中,比如早期引导阶段。
支持DynamIQ异构技术的处理器(
Cortex-A55
、Cortex-A75
和Cortex-A76
)支持虚拟化主机扩展(VHE
)。
8.1 在EL2
运行Host OS
VHE
由HCR_EL2
寄存器的两个位进行控制:
E2H
:控制是否使能VHE
功能;
TGE
:当使能了VHE
,控制EL0
是Guest
还是Host
。
下表总结了典型的设置:
执行 | E2H |
TGE |
---|---|---|
Guest 内核(EL1 ) |
1 |
0 |
Guest 应用(EL0 ) |
1 |
0 |
Host 内核(EL2 ) |
1 |
1 * |
Host 应用(EL0 ) |
1 |
1 |
当发生异常,从
VM
退出,进入hypervisor
时,TGE
最初为0
。软件必须在运行Host OS
内核主要部分之前设置该位。
典型设置如下图所示:
8.2 虚拟地址空间
下图展示了在引入VHE
之前,EL0/1
的虚拟地址空间布局如下:
在内存管理模型中,EL0/EL1
具有两个区域。习惯上,上面的区域称为内核空间
,下面的区域称为用户空间
。但是,从右侧的图中可以看出,EL2
只有底部的一个地址空间。造成这种差异是因为,一般情况下,hypervisor
不会直接托管应用程序。这意味着,hypervisor
无需划分内核空间和用户空间。
分配上面的区域给内核空间,下面的区域给用户空间,仅仅是约定。ARM架构没有强制这么做。
EL0/1
虚拟地址空间也支持地址空间标识符(ASID
),但是EL2
不支持。这还是因为hypervisor
通常不会托管应用程序。
为了允许EL2
上有效执行Host OS
,我们需要添加第二个区域和ASID
的支持。使能HCR_EL2.E2H
可以解决这个问题,如下图所示:
在EL0
中,HCR_EL2.TGE
控制使用哪个虚拟地址空间:EL1
空间,还是EL2
空间。具体使用哪个空间依赖于应用程序运行在Host OS
(TGE==1
),还是Guest OS
(TGE==0
)。
8.3 重定向寄存器访问
前面我们已经知道,使能VHE
会改变EL2
虚拟地址空间的布局。但是,我们还有一个问题,MMU的配置。这是因为,我们的内核会访问_EL1
寄存器,如TTBR0_EL1
,而不是_EL2
寄存器,如TTBR0_EL2
。
为了在EL2
运行相同的二进制代码,我们需要将对EL1
寄存器的访问重定向到EL2
的等价寄存器上。使能E2H
,就能实现这个功能。如下图所示:
但是,这种重定向给我们带来了新问题。hypervisor
仍然需要访问真实的_EL1
寄存器,以便实现任务切换。为了解决这个问题,一组寄存器别名被引入,后缀为_EL12
或_EL02
。当在EL2
使用时(E2H==1
),访问这些别名寄存器就会访问真实的EL1
寄存器,以便实现上下文切换。如下图所示:
8.4 异常
通常,HCR_EL2.IMO/FMO/AMO
路由标志位控制着物理异常
被路由到EL1
还是EL2
。当在EL0
上执行(TGE==1
)时,所有的物理异常路由到EL2
,除非通过SCR_EL3
寄存器控制路由到EL3
。这种情况下,与HCR_EL2
路由标志位的实际值无关。这是因为应用程序作为Host OS
的子进程在执行,而不是作为Guest OS
。因此,异常应该被路由到运行在EL2
上的Host OS
中。
理论上,hypervisor
还可以运行在一个VM
之中。这个被称为嵌套虚拟化
:
我们称第一个hypervisor
为Host Hypervisor
,在VM
内部的hypervisor
为Guest Hypervisor
。
在ARMv8.3-A
扩展之前,就可以通过在EL0
中运行Guest Hypervisor
而实现在VM
中运行一个Guest Hypervisor
。但是,这要求大量的软件模拟,导致比较差的性能。通过ARMv8.3-A
扩展的特性,可以在EL1
上运行Guest Hypervisor
。添加了ARMv8.4-A
扩展之后,这个过程更加有效率,尽管仍然需要Host Hypervisor
中的一些操作。
9.1 Guest Hypervisor
访问虚拟化控制寄存器
我们不想Guest Hypervisor
直接访问虚拟化控制寄存器。因为直接访问可能潜在允许VM
破坏沙箱,或获取主机平台的信息。这种潜在的问题与我们前面讨论陷入和模拟
一节时面临的问题一样。
Guest Hypervisor
运行在EL1
。HCR_EL2
中新添加的标志位允许Host Hypervisor
捕获Guest Hypervisor
对虚拟化控制寄存器的访问:
HCR_EL2.NV
:硬件嵌套虚拟化总开关
HCR_EL2.NV1
:使能一组额外的陷入(trap)
HCR_EL2.NV2
:使能对内存的重定向
VNCR_EL2
(NV2==1
):指向内存中的一个结构
ARMv8.3-A
添加了NV
和NV1
控制位。从EL1
访问_EL2
寄存器,通常是未定义的,这种访问会造成到EL1
的异常。而NV
和NV1
控制位则将这种异常陷入到EL2
。这就允许运行在EL1
上的Guest Hypervisor
,使用运行在EL2
上的Host Hypervisor
模拟某些操作。NV
标志位还能捕获EL1
的ERET
指令。
下图展示了Guest Hypervisor
设置和进入虚拟机的过程:
Guest Hypervisor
访问_EL2
寄存器会陷入到EL2
。Host Hypervisor
会记录Guest Hypervisor
的配置信息。
Guest Hypervisor
尝试进入它的Guest VM
(Guest
的Guest VM
),这种尝试就是调用ERET
指令,而ERET
指令会被EL2
捕获。
Host Hypervisor
检索Guest
的Guest
的配置,并加载该配置信息到合适的寄存器中。然后,Host Hypervisor
清除NV
标志位,并进入Guest
的Guest
执行。
这种方法的问题是,Guest Hypervisor
每次访问EL2
寄存器都会陷入。在两个vCPU
或VM
之间执行任务切换时,需要访问许多寄存器,导致大量的陷入异常。而异常进入和退出会带来开销。
一个更好的方法是获取EL2
寄存器的配置,只有在调用ERET
指令时陷入到Host Hypervisor
。引入ARMv8.4-A
扩展后,这成为可能。当设置了NV2
标志位后,EL1
访问_EL2
寄存器被重定向到内存中的一个数据结构。Guest Hypervisor
可以根据需要读写这些寄存器,而无需任何陷入。当然,调用ERET
指令仍然会陷入到EL2
,此时,Host Hypervisor
重新检索内存中的配置信息。后面的过程与前面的方法一致,如下图所示:
Guest Hypervisor
访问_EL2
寄存器被重定向到内存中的一个数据结构。数据结构的位置由Host Hypervisor
使用VNCR_EL2
寄存器指定。
Guest Hypervisor
调用ERET
指令,尝试进入它的Guest VM
(Guest
的Guest VM
)。ERET
指令被EL2
捕获。
Host Hypervisor
检索Guest
的Guest
的配置,并加载该配置信息到合适的寄存器中。然后,Host Hypervisor
清除NV
标志位,并进入Guest
的Guest
执行。
这种方法的优点是陷入
更少,因此,进入Host Hypervisor
的次数也更少。
虚拟化是在ARMv7-A
架构引入的。那时的Hyp
模式等价于AArch32
状态的EL2
,只有在非安全状态可用。ARMv8.4-A
扩展添加了对安全EL2
的支持,是一个可选配置。
如果处理器支持安全EL2
,需要在EL3
中使能SCR_EL3.EEL2
标志位。设置该标志位允许进入EL2
,且使能安全状态下的虚拟化。
在安全虚拟化可用之前,EL3
通常运行安全状态切换软件和平台固件
。这是因为我们想要尽量减少EL3
中的软件数量,让EL3
更容易安全。安全虚拟化允许我们将平台固件
移动到EL1
。虚拟化为平台固件
和可信内核
提供单独的安全分区。下图说明了这一点:
10.1 Secure EL2
和两个IPA
空间
ARM
架构定义了两个物理地址空间:Secure
和Non-secure
。在非安全状态中,VM
的Stage-1
地址转换的输出总是非安全的。因此,Stage-2
地址转换只有一个IPA
空间需要处理。
安全状态下,VM
的Stage-1
地址转换的输出可以是安全地址,也可以是非安全地址。地址转换表中描述符中的NS
标志位控制输出是安全,还是非安全地址空间。这意味着对于Stage-2
地址转换有两个IPA
空间需要处理,如下图所示:
与Stage-1
页表不同,Stage-2
页表项中没有NS
位。对于特定的IPA
空间,所有转换都可以产生安全物理地址
或非安全物理地址
。这种转换由一个寄存器位控制。通常,非安全IPA
转换为非安全PA
,而安全IPA
转换为安全PA
。
虚拟化的成本是当hypervisor
需要为VM
服务时,需要在VM
和hypervisor
之间切换时花费的时间。在ARM系统中,这种成本的最低限是:
31
个64
位通用目的寄存器(X0→X30
)
32
个128
位浮点/SIMD
寄存器(V0→V31
)
2
个堆栈指针寄存器(SP_EL0
,SP_EL1
)
通过LDP
和STP
指令,hypervisor
只需要32个指令保存和恢复这些寄存器。
真正的虚拟化性能损失依赖于硬件平台和hypervisor
的设计。
问:Type-1
型hypervisor
和Type-2
型的区别是什么?
答:Type-2
型运行在Host OS
之上,Type-1
型没有Host OS
。
问:安全状态和非安全状态有多少个IPA
空间?
答:安全状态有2个IPA
空间:安全
和非安全
。非安全状态有一个IPA
空间。
问:在哪个异常级别中可以使用虚拟中断?
答:虚拟中断只有在EL0
或EL1
中执行,并且只有设置HCR_EL2
中相应的路由标志位才能启用。
问:SMMU
是什么?如何使用SMMU
进行虚拟化?
答:SMMU
是系统MMU
,为非处理器的主控制器提供地址翻译服务。在虚拟化中,SMMU
可以给主控制器(如DMA
控制器)和VM
一样的内存视角。
问:HCR_EL2.EH2
标志位如何影响MSR TTBRO_EL1,x0
在EL2
上的执行?
答:当E2H==0
,该指令写TTBR0_EL1
寄存器;当E2H==1
,写操作被重定向到TTBR0_EL2
。
问:VMID
是什么?它的作用是什么?
答:VMID
是虚拟机标识符。用来标记VM
的TLB
项,以便来自不同VM
的TLB
项可以在TLB
中共存。
问:陷入(Trap
)是什么?它如何用于虚拟化?
答:陷入
可以造成合法操作触发异常,并将该操作陷入到更高特权级的软件上。在虚拟化中,陷入
允许hypervisor
检测某个操作何时执行,然后模拟这些操作。
与本文相关的一些参考文章:
内存管理
异常模型
ARM虚拟化:性能和架构的意义:关于基于ARM架构的系统虚拟化成本的背景读物
Arm community:ARM官方论坛,可以提问问题,查找文章和博客
下面是一些其它主题的参考内容:
13.1 虚拟化的介绍
Xen项目
KVM的通用知识
13.2 虚拟化概念
GICv3/v4软件概述
Virtio的背景知识
打算开发一个轻量级的hypervisor
,只实现对VM
的分区隔离。hypervisor
本身不参与主动调度VM
的执行。计划如下:
在QEMU模拟器上实现一个hypervisor
,支持裸机程序(EL1)的运行
在QEMU模拟器上实现一个hypervisor
,支持Linux的运行
实现两个虚拟机之间的通信
选择一个硬件平台运行,初步选择RK3399
使用Rust语言重写该hypervisor
另外,读者也可以按照Spawn a Linux virtual machine on Arm using QEMU (KVM) 这篇文章,基于ARM模拟平台建立开源的XEN
和KVM
hypervisor
。
本人才疏学浅,欢迎交流,可以扫描下面二维码,关注本公众号。