1. 介绍
Arm是移动领域占统治地位的构架,得益于arm在高能效计算方面的经验, 现在arm正进入PC和服务器市场,虚拟化在这些领域非常流行,尤其是服务器领域,75%的服务器都使用虚拟化威廉希尔官方网站
。因此,高效的虚拟化对arm取得成功是必要的。
虚拟化让操作系统对比实际硬件无差别地运行在虚拟化环境。一个让虚拟机如此有吸引力的特征是资源控制:虚拟机管理器总是控制着底下的硬件资源:CPU, 内存和I/O设备。早期CPU比较慢,与之匹配的I/O设备运行也慢,使用programmed I/O的简单通讯方式。当CPU变得比设备更快,使用中断方式的通讯方式被开发出来。中断是直接发送给处理器的电信号。当今处理器都支持中断,操作系统也支持这种通讯方式。因此,虚拟化方案必须提供传送中断给虚拟机的方式,同时虚拟机管理器还保有硬件的控制。
Bhyvearm64是为FreeBSD操作系统设计的type 2 hypervisor,它在arm中断控制器上面实现虚拟中断控制器。Arm将它的中断控制器称之为Generic Interrupt Controller (GIC),我们聚焦在GICv3。与Arm提供的其他构架组件一样,arm小心地设计这个控制器,它具备使虚拟化更加高效的特性。
GICv3有3个独立的组件:Distributor, Redistributor和CPU Interface。Distributor和Redistributor是通过memory-mapped方式访问的,他们通常只在操作系统内核启动时访问。对这两个组件的访问性能不是关键的,我们通过优化的trap-and-emulate(陷入-模拟)方式实现虚拟化,这种方式模拟了I/O寄存器,并且将这些寄存器作为虚拟机上下文的一部分存储在内存中。另一方面,每次中断处理时,CPU Interface频繁被访问,CPU Interface被设计为处理器的一部分,通过快速硬件寄存器(系统寄存器,MSR/MRS方式访问)访问。对于CPU Interface, 我们利用了各种硬件特性,以实现快速虚拟化。
中断控制器被输入输出设备用作与处理器通讯的方式,我们模拟了系统定时器作为使用虚拟中断控制器的第一个设备。Timer对于任何操作系统来说都是必需的。Arm的系统定时器叫做Generic Timer (通用定时器), 它实际上由多个定时器组成:
- Physical(物理)和Virtual(虚拟)定时器,这两个Timer总是存在的;
- Security Physical定时器,它存在于带有安全模式的CPU;
- Physical EL2 定时器,它作为虚拟化扩展的一部分;
操作系统可以自由选择使用物理或是虚拟定时器。我们选择了模拟这两个定时器,这样不会限制操作系统使用哪个定时器。模拟的同时,我们需要考虑到host操作系统必须排他性地访问其中一个定时器,为host操作系统自身所用。
有了对中断控制器和定时器的模拟,bhyvearm64可以成功启动并运行FreeBSD guest, 虽然这个项目的早期有些限制:一个虚拟机仅能运行在一个虚拟CPU上,除了virtio支持仅有比较少的用户空间设备模拟的支持。
2.相关工作
在arm上,第一个利用arm虚拟化扩展的hypervisor是KVM,KVM是Linux内核的一部分。KVM采用了类似bhyvearm64的方式虚拟化中断控制器,充分利用硬件虚拟化,如果不能利用硬件虚拟化时回到trap-and-emulate的方式。
操作系统需要定时器进行如调度,测量逝去的时间这些基本操作,但运行在虚拟环境时,不可能有和物理机器一样的精度,因为虚拟化有与生俱来的开销。
由于历史原因,x86构架实现的定时器有多种用途,并且有时用途重叠:有些定时器被软件用来测量另一个定时器的频率;有些可以产生周期性的中断用于timekeeping。这增加了软件提供一个对逝去时间的虚拟化视角的复杂度, 因为需要模拟多个定时器并且在它们之间保持同步。
相反,在arm平台上的定时器虚拟化相对简单,因为构架强制Generic Timer必须实现,它足够操作系统使用的timekeeping的需求。KVM使用类似bhyvearm64的方式:virtual timer给虚拟机用,需要注意的是timer产生的中断还是需要hypervisor注入,Physical timer由软件模拟,因为它被host使用了。
Arm在armv8.1构架版本创造了一个新的虚拟化模式,叫做Virtual Host Extension (VHE)。当这个功能被使能时,host操作系统运行在与虚拟机不同的CPU执行模式,host访问它自己独立的硬件定时器。然后host可以分配Physical timer和virtual timer给虚拟机,从而去除了软件模拟的需求。在这个场景下,KVM朝着去除软件模拟的方向开发。当前bhyvearm64没有利用VHE,但几乎在不久的将来支持VHE。
3.背景
Bhyvearm64是FreeBSD操作系统上的type-2的hypervisor和虚拟机管理器。它基于现有的x86构架虚拟化方案。图1显示了bhyvearm64的主要部分,大致可以分为用户空间程序和内核代码。用户空间程序包含bhyveload, bhyve和bhyvectl,用户可以利用它们创建,运行和销毁一个虚拟机。与kernel的通讯采用libvmmapi库,它充当对唯一标识虚拟机的特殊设备的系统调用的wrapper。在内核这边,hypervisor实现为一个可加载内核模块 vmm.ko。虚拟中断控制器抽象为叫做VGIC的软件模块,而虚拟timer叫做vtimer。可能与名字相反,虚拟timer实际上是物理硬件timer,而不是软件模拟。VGIC和vtimer都在内核空间模拟来得到更好的性能和更少的开销。
Arm在armv7构架引入了虚拟化扩展,armv8.0顺延了同样的虚拟化模式。对于支持虚拟化的CPU,必需的硬件支持实现在一个独立的处理器执行模式,叫做Exception Level 2 (EL2)。这个hypervisor构架类似于KVM, Linux的hypervisor。EL2创建时针对type-1 hypervisor。 Type-1 hypervisor直接运行在硬件上,它的功能集中在管理虚拟机,而不是像type-2 hypervisor+host内核那样有虚拟机和用户空间程序。不幸地,这样的设计决定使在EL2运行host操作系统不可能,这迫使我们必须分割hypervisor代码在跨两个不同的CPU执行模式运行。
有几个不同的arm中断控制器版本,最常用的是GICv2和GICv3。 GICv2有两个主要的缺陷:
- 最多只支持8个处理器, 这限制了现代的平台,特别是服务器硬件
- 它完全是memory-mapped访问方式,使得寄存器访问昂贵。
GIC v3控制器克服了这些缺陷,它没有CPU数量的严格限制,并且对最频繁使用的寄存器是硬件实现有虚拟化的支持。GICv4控制器从软件角度和GICv3相同,唯一的区别是GICv4加入了虚拟message-based中断的支持。
4. GICv3构架
在描述模拟中断过程之前,有必要熟悉一下GICv3的内部工作方式。中断是发送给处理器异步的,外部的电信号。GICv3控制器实现了4种不同的中断类型:
- Software Generated Interrupts (SGI), 它们由操作系统产生用作处理器之间通讯。在其他构架上它们成为Inter-Processor Interrupt (IPI)
- Private Peripheral Interrupts(PPI). 这类中断由只和一个CPU通讯的设备产生,这个中断只发送给这个CPU。定时器中断时PPI
- Shared Peripheral Interrupts (SPI) 这类中断由I/O设备引起,并可以发送给系统中任何的CPU
- Locality-specific Peripheral Interrupts (LPI), 这类中断用于message-based, 可以被PCIe或其他设备使用。PCIe规范里把它叫做Message-Signaled Interrupt (MSI).
- 除了类型,中断还有其他属性在决定什么时候和怎么发送给处理器方面有重要作
- interrupt group,可以是group0, 或是non-secure group 1, 或是secure group1
- interrupt优先级
中断可以通过IRQ或是FIQ发送给处理器(可通过中断向量的偏移来区分)。中断的安全状态(secure 或是 non-secure)和interrupt group决定中断通过IRQ还是FIQ发送。Group 0的中断总是通过FIQ来发送。FreeBSD配置所有的中断为non-secure group1, 总是通过IRQ来发送。
中断可以基于优先级进行屏蔽。中断优先级也当作多个中断的仲裁:具有最高优先级的中断先被置为有效。在arm构架上有些人试着利用优先级机制实现pseudo Non-Maskable Interrupt (NMI):用于NMI的中断被设置为具有更好优先级,当需要禁止中断时,并不设置PSTATE.I,取而代之的是设置priority mask用来阻挡所有的常规中断,但是NMI依然可以置为有效。
图2是GICv3构架的总览。有3个主要的组件:每个系统有单个Distributor, 每个core都有一个Redistributor和一个CPU Interface。Distributor负责全局中断(SPI)的配置,Redistributor负责core私有的中断(PPI和SGI)的各种属性的配置。CPU Interface负责处理中断的状态更新。Distributor和Redistributor预计只会被偶尔访问,主要是在启动的时候配置中断,相反,CPU Interface在每次中断处理时都会被访问。GIC的实现反映这个使用特点:Distributor和Redistributor是memory-mapped, 访问它们较慢,可以接受,因为访问的次数少;CPU Interface实现为硬件寄存器(系统寄存器),意味着访问更快。
上图中还缺少了一个可选的组件,它是 Interrupt Translation Service (ITS), 它负责message-based Interrupt。它是可选的组件,因为系统集成者可以选择在Redistributor中实现对等的功能。Bhyearm64不支持LPI虚拟化。
5. GICv3 虚拟化
虚拟中断控制器的实现考虑到了GIC组件的特性和FreeBSD如何使用这个中断控制。中断可以配置为group 0, non-secure group 1或是secure group 1。Secure group 1中断总是发送给运行在安全世界的固件处理,这类中断在bhyvearm64里没有实现,因为hypervisor运行在非安全世界。
Group 0中断总是通过FIQ发送,固件可以配置硬件让这些中断发送给安全世界,类似于如果处理secure group 1中断。基于这个原因,FreeBSD和Linux选择配置所有的中断为non-secure group 2中断。
中断控制器的模拟完全在内核空间实现。中断是时间敏感的事件,切换到用户空间模拟,然后再切回到内核空间被证明代价非常高的。但是,为使虚拟机管理器(在用户空间)模拟设备成为可能,我们提供API用来置有效和使无效一个中断。
A. Distributor和Redistributor的模拟
因为Distributor和Redistributors是memory-mapped方式访问并且访问数量较少,我们选择了trap-and-emulate的方式虚拟化。这种方式利用了内存虚拟化的工作方式:对应distributor和Redistributor的guest物理地址不被映射(map)在stage 2 translation table。Stage 2 translation table负责翻译虚拟机发出的guest物理地址(GPA)为真实的物理地址。当GPA没有在stage 2 table中映射时,会导致异常,然后hypervisor可以重构guest指令并相应的模拟它,而不需要将这个异常传播到用户空间。
虚拟Distributor和Redistributor是纯软件结构,作为虚拟机上下文存储在host的内存中。每次guest试图去访问这些虚拟寄存器,hypervisor可以从exception syndrome(异常综合信息)提取到访问的地址。对于每个这样的内存区域,我们维护了一个按起始地址排列的数组,使用二进制搜索,我们可以很快知道虚拟机在访问那个虚拟寄存器。模拟主要由保存guest写的值和返回给guest读的值组成。寄存器的值也用来决定那个中断应该被呈现给虚拟机,与真实硬件工作类似的方式进行。
作为虚拟机上下文一部分的中断控制器寄存器的完整列表可以在表1中找到。
B. CPU Interface虚拟化
CPU Interface寄存器在每次处理中断是都被使用,因此对这些寄存器的快速读写是合理的。CPU Interface实现为CPU寄存器的一部分,并且支持硬件虚拟化。当hypervisor通过置位HCR_EL2.IMO和HCR_EL2.FMO配置所有的group 0/group 1的IRQ发送给EL2处理时,虚拟化被激活。这样配置的目的有两个:
- 所有的物理中断将被发送给host,因而在硬件和guest之间进行强制分离
- 所有对CPU Interface寄存器的访问透明地重定向到一组独立的具有同等功能寄存器组,但是这些访问是控制虚拟中断的处理,而不是物理中断的处理
当使用与处理物理中断访问非虚拟化寄存器一样的方式更新一个虚拟中断的状态时,使用的是虚拟CPU Interface寄存器,因此它们对于hypervisors来说不可写,也不是虚拟CPU上下文的一部分。但是,使用额外的寄存器来使有效一个虚拟中断,这些寄存器只能在EL2访问。
C. 虚拟中断注入
虚拟中断的注入和处理大多由硬件完成。Hypervisor负责选择注入什么中断到guest。中断注入后,它的状态变为pending。当guest被恢复运行时,这个中断被置为有效给guest。剩下的状态转换由virtual CPU Interface处理而不需要hypervisor的参与。
为了注入中断,CPU提供给hypervisor一序列寄存器,叫着List寄存器。每个List寄存器包含将被guest处理的一个虚拟中断的信息:interrupt group, 状态,优先级,中断号,这个虚拟中断是否直接映射到一个物理中断。
一个虚拟中断可以影射一个物理中断,在这种情况下,当guest使无效这个虚拟中断时,对应的物理中断也会被硬件使无效。
List寄存器的数量是有限的,数量和中断控制器实现相关。最大的数量为16个,一个虚拟机pending的虚拟中断数量多于list寄存器的数量是有可能的。为了克服这个限制,我们为pending的中断维护了一个自己的buffer。每次guest被恢复运行时,我们检查这个buffer,并选择最高优先级的中断以注入到guest。
基于guest中断配置选择那个中断被使有效:
- 中断group, 中断类型和中断号:中断必须在Distributor或Redistributor里使能
- 中断要发送到的CPU是否是当前CPU
- 相对于其他pending中断的中断优先级
- 如果两个中断的优先级一样,hypervisor为每个中断维护一个额外的信息。比如,时钟中断总是有更高的优先级,因为这是guest操作系统做timekeeping的方式。
另外一个被设计来帮助虚拟化的硬件特性是一个特殊的中断,叫做maintenance interrupt(维护中断)。这个中断的目的是用来处理hypervisor想注入中断多于可用的List寄存器的场景,或者是处理特定虚拟中断时需要进行特殊的动作。这些情况下,hypervisor使能maintenance interrupt, 触发一个切换到host的行为。然后hypervisor就可以自由地按自己的方式处理这个中断。
6. 定时器虚拟化
定时器对操作系统来说是必须的。操作系统也为周期性任务使用定时器。有必要让虚拟机可以访问虚拟化过的定时器。
A. Generic Timer
Armv8构架提供的定时器叫做Generic Timer. 实现上实际包括至少两个不同的timer, 最多到7个。一个系统可以有一个secure physical timer, 一个non secure physical timer, 通常简称为physical timer, 一个 virtual timer, physical和virtual non-secure EL2 timers, physical和virtual secure EL2 timers. 为虚拟化目的,我们聚焦在一般操作系统使用的timer上,也就是physical timer (它计数逝去的真实时间)和virtual timer(它计数带固定偏移的逝去时间)。
Host操作系统需要排他性地使用一个timer;虚拟机拉慢host是不可取的。Bhyvearm64分配physical timer给host, virtual timer给正在这个core上运行的虚拟机,这样做是基于以下原因:
- 因为virtual timer从一个固定的offset开始计数时间,运行在虚拟机里的guest可以被欺骗地认为定时器与虚拟机在同样的时间开始。
- 当physical timer和virtual timer同时存在且虚拟化没有激活时,FreeBSD和Linux倾向于选择virtual timer而不是physical timer。在没有嵌套式虚拟化支持的虚拟机中,总会是这样的情况。
- Armv8.0构架提供了通过trap读写的方式模拟physical timer的机制,而对virtual timer没有这样的机制。
B. Virtual timer虚拟化
Timer中断是极度时间敏感的。Timer中断以规律性的间隔到来(FreeBSD kernel配置为每1ms一个中断),因为它们这么频繁,因此花太多的时间服务这个中断是极不可取的。这对虚拟中断来说也是适用的:hypervisor在模拟timer上花的时间越少,下一个中断到来前,虚拟机可以利用的CPU时间越多。
为了达到注入timer中断最小化开销的目的,bhyvearm64分配Generic timer的virtual timer部分直接给虚拟机。Guest操作系统可以自由地配置这个timer,而不需要hypervisor的参与。但虚拟timer中断还是需要由hypervisor管理。这是因为更具Popek和Goldberg控制法则,host必须总是控制硬件,这也意味着控制中断的发送。没有硬件机制来选择那些中断需要重定向到虚拟机。当运行guest时,所有的中断都发送给host, host来选择那些需要呈现给虚拟机。
中断天然地是异步的;它们可能在任何时候到来,不过处理器在执行什么程序。这也适用于virtual timer中断:一个虚拟timer中断可以在另一个host程序而不是在编程这个timer的虚拟机运行在CPU上时到来。Virtual Timer需要一个机制,在触发中断之前辨别是否是编程这个timer的虚拟机。为达目标,我们修改了struct pcpu机器相关部分用来保存指向在这个core上最近运行的虚拟CPU的指针,如列表1所示。每次不同的虚拟处理器被运行时,这个虚拟CPU被bhyvearm64机器相关代码修改,当虚拟机被销毁时,设为NULL。
虚拟定时器的正确使用也需要考虑当两个不同的虚拟机共享同一个物理core,因此使用同一个virtual timer的情况。必须注意这种场景下,"共享"意味着CPU的执行在这两个虚拟机见切换。很有可能这两个虚拟机在不同的时间启动,因此它们的虚拟定时器的偏移值须反映这一点;很重要的一点是,两个虚拟机会设置定时器触发的时间在将来不同的时刻。考虑到这些,当切换虚拟机时,保存切换出去的虚拟机定时器状态和恢复切换进来的虚拟机的状态是必需的。
还有一个有关定时器虚拟化重要因素需要考虑:如果虚拟机在定时器中断之后才被调度运行怎么办呢?当我们在Foundation Platform模拟器上的同一个armv8.0 CPU上运行多个虚拟机时,我们经历了这种情况。对于bhyvearm64,为了避免guest内核花太多时间在处理定时器中断上,我们采用了比较保守的方式。当一个虚拟定时器中断变有效时,我们不会无条件地注入这个中断到guest,取而代之,我们会检查是否还有另一个定时器中断处于活跃状态。这有可能发生在中断处理程序中,在guest使能定时器之后并在它标识中断结束之前,如图3所示。这种情况下,我们保存新的中断到中断buffer,当下一次world切换时我们才注入这个中断。因为world切换发生在至少每个host tick,因而guest会损失最多一个完全的host tick。
C. physical timer的模拟
我们发现FreeBSD和Linux倾向于当virtual timer可用时使用virtual timer。但没什么阻止操作系统选择physical timer而不是virtual timer。 因为bhyvearm64让host控制physical timer, 对于Physical timer的虚拟化,我们使用trap-and-emulate的方式。这是通过设置CNTHCTL_EL2.EL1PCEN和CNTHCTL_EL2.EL1PCTEN来实现的,它导致所有对physical timer的访问被陷入到hypervisor.
图3展示了FreeBSD内核处理一个定时器中断的执行步骤。为了得到中断号,代码读取Interrupt Acknowledge Regiter(IAR)。这个中断号是编程在List寄存器里的数值。这使中断状态从pending变为active。 内核然后通过写CNTP_CTL_RL0寄存器禁止定时器,这会导致一个陷入到hypervisor, hypervisor做 in-kernel的模拟。这个写导致hypervisor禁止了所有给guest的pending定时器警报。Guest编程定时器到下一个警报(alarm)时间,我们保存这个值。我们不会编程任何警报去注入一个中断,因为定时器还是在被禁止状态。
仅当guest用另一个写CNTP_CTL_EL0使能定时器之后,我们将其陷入到hypervisor,并使用FreeBSD API编程警报为guest指定的时刻。为结束一个中断处理,内核会写End Of Interrupt Register (EOIR), 它将中断在List寄存器里变为inactive状态。现在,保存这个中断的List寄存器可以被释放,可以重新为注入下一个中断利用。
原作者:修志龙_ZenonXiu