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
和KVM
hypervisor
。