6 虚拟化异常
硬件使用中断发送信号给软件。比如,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。
7 虚拟化通用定时器
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计数时间间隔)。
8 虚拟化主机扩展
下图展示了一个软件和异常级别对应关系的简化版本:
可以看到独立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中。
9 嵌套虚拟化
理论上,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的次数也更少。
10 安全空间的虚拟化
虚拟化是在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。
11 虚拟化的成本
虚拟化的成本是当hypervisor需要为VM服务时,需要在VM和hypervisor之间切换时花费的时间。在ARM系统中,这种成本的最低限是:
31个64位通用目的寄存器(X0→X30)32个128位浮点/SIMD寄存器(V0→V31)2个堆栈指针寄存器(SP_EL0,SP_EL1)
通过LDP和STP指令,hypervisor只需要32个指令保存和恢复这些寄存器。
真正的虚拟化性能损失依赖于硬件平台和hypervisor的设计。
12 小测验
- 问:
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检测某个操作何时执行,然后模拟这些操作。
13 其它参考文章
与本文相关的一些参考文章:
- 内存管理
- 异常模型
- ARM虚拟化:性能和架构的意义:关于基于ARM架构的系统虚拟化成本的背景读物
- Arm community:ARM官方论坛,可以提问问题,查找文章和博客
下面是一些其它主题的参考内容:
13.1 虚拟化的介绍
- Xen项目
- KVM的通用知识
13.2 虚拟化概念
- GICv3/v4软件概述
Virtio的背景知识
14 接下来的计划
打算开发一个轻量级的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和KVMhypervisor。

















