第5章 鲲鹏虚拟化 前几章介绍了Intel x86架构中硬件辅助虚拟化的设计与实现。本章将目光聚焦于另一种主流架构——ARMv8的虚拟化硬件支持。本章同样涵盖虚拟化的四个基本要素: CPU、内存、I/O和时钟。5.1节简要介绍ARM虚拟化架构,5.2和5.3节分别介绍ARMv8 CPU虚拟化以及中断虚拟化,5.4节简要介绍ARMv8架构下的内存虚拟化,5.5节主要介绍ARMv8架构下的I/O虚拟化,5.6节简要介绍ARMv8架构下的时钟虚拟化。 5.1鲲鹏虚拟化框架 鲲鹏系列处理器是基于ARMv8架构设计的面向服务器市场的处理器,下面先简要介绍ARMv8对于虚拟化的支持。 5.1.1鲲鹏虚拟化简介 由于ARMv8需要对ARMv7进行兼容和升级,所以先介绍ARMv7的特权级和工作模式,并和ARMv8以及x86进行对比。 ARMv7是ARM的A系列处理器对应的32位ARM架构。该架构提供了7种工作模式,分别是用户(USR)模式、系统(SYS)模式、一般中断(IRQ)模式、快速中断(FIQ)模式、管理(SVC)模式、中止(ABT)模式和未定义指令终止(UND)模式。七种模式中,用户模式称为非特权模式,其他模式称作特权模式,有权访问所有的系统资源。特权模式中除系统模式外的其余五种模式又称为异常模式。在这七种模式中用户模式的特权级最低,用户的应用程序运行在此模式下,无权访问硬件资源,只能通过模式切换、软中断或者异常等方式使CPU进入特权模式对硬件资源进行间接访问。ARMv8架构在ARMv7的基础上进行了升级,支持64位架构并增加了异常级。 类似于x86架构的Ring0~Ring3特权级,ARMv8提供了EL0~EL3四个异常级(Exception Level)。不同的是,在ARM中EL0的权限级别最低,而EL3权限级别最高。相比于ARMv7只有非特权模式和特权模式的两种特权级别,ARMv8的四个异常级实际上是对特权级的一种扩充,用户程序运行在EL0异常级,操作系统则运行在EL1异常级。为了兼容v7运行模式,v8将v7中的特权模式映射在EL1异常级。而更高的异常级EL2和EL3则分别运行Hypervisor和安全监视器(Secure Monitor)。ARMv8整体异常级架构与对应的模式分布如图51所示。 图51ARMv8异常级与工作模式分布 ARMv8还为各异常级增加了对应的寄存器(如*_EL1、*_EL2等),并将Hypervisor运行在比客户机操作系统更高一级的EL2异常级上,为ARM虚拟化提供了硬件支持。EL2异常级的存在也实现了x86架构特权级“1级”的预想方案。 5.1.2EL2虚拟化框架 类似于x86的虚拟化框架模型,ARMv8虚拟化也有两种类型: Type Ⅰ和Type Ⅱ。Type Ⅰ类似于Hypervisor模型,Type Ⅱ类似于宿主机模型。 (1) Type Ⅰ: 在这种模型中,Hypervisor运行在EL2异常级,控制运行在EL1异常级中的虚拟机,对虚拟机进行隔离和必要的资源共享管理,如图52所示。在这种模式下,虚拟机要想获取全局资源就需要产生异常从而陷入位于EL2中的Hypervisor进行处理。这样虽然对虚拟机进行了有效的隔离,但是特权级切换时引入的上下文切换造成了虚拟机的性能损耗。ARM为此设计了VHE(Virtualization Host Extensions,虚拟化主机拓展)技术,也就是Type Ⅱ模型。 图52ARMv8的Type Ⅰ虚拟化模型 (2) Type Ⅱ: 与Type Ⅰ不同的是,在Type Ⅱ模型中EL2异常级上运行的是宿主机操作系统,Hypervisor则依赖于宿主机操作机系统,利用宿主机操作系统运行环境来管理虚拟机。当发生异常时,异常级会直接由EL0切换到EL2,略去了到EL1的切换步骤,节省了上下文切换的开销,提升了虚拟化性能,详见5.2.3小节VHE虚拟化。 5.2鲲鹏CPU虚拟化 第2章提出了CPU虚拟化所面临的三个挑战以及Intel VTx相应的解决方案,在某种程度上,这也是ARMv8硬件辅助虚拟化需要解决的问题。本节将介绍ARMv8的解决方案以及在设计上与Intel VTx的不同之处。 5.2.1CPU虚拟化 如前所述,CPU虚拟化本质上通过创建QEMU线程来创建vCPU。那么vCPU是如何运行以达到和真正的CPU一样的效果呢?鲲鹏处理器使用VHE扩展,将KVM和Linux内核代码直接运行在EL2,管理控制运行在EL0中的QEMU线程,对外呈现出独立的CPU特性,其具体实现如下。 首先是Hypervisor初始化,设置一些与虚拟化相关的寄存器并开启虚拟化模式,然后开始运行vCPU。vCPU的主要任务是模拟真实的CPU,在鲲鹏虚拟化中采取的是QEMU和KVM相结合的方式。为了隔离多个虚拟机的运行环境,vCPU执行的指令将受到Hypervisor的管理与限制。但是如果每一条指令都要经过Hypervisor处理必然会造成性能损耗。因此将指令分为敏感指令和非敏感指令。非敏感指令交由运行在EL0级别的QEMU线程直接执行,敏感指令则交由Hypervisor进行处理。敏感指令的识别交由硬件完成,CPU自动识别指令是否是敏感指令。对于普通的指令(用户ISA),CPU可以直接运行,而对于敏感指令的执行(系统ISA),vCPU处理较为复杂,下面举例说明。当执行到一条敏感指令时, CPU会自动识别这条指令,然后触发指令陷入。由于鲲鹏处理器支持VHE技术,所以敏感指令将会直接陷入EL2级别的Hypervisor中。在陷入的过程中,CPU需要依次完成如下工作。 (1) 保存上下文。在虚拟机退出之前要保存上下文,以便执行完敏感指令之后继续运行虚拟机。 (2) 执行敏感指令。为了保证虚拟机之间的隔离性,敏感指令不能在EL2级直接执行,而是通过模拟的方式执行。以关机、休眠等指令为例,Hypervisor会通过模拟的方式使虚拟机关机或者休眠,而不会影响主机。 (3) 恢复上下文。将第一步中保存的上下文恢复到相应的寄存器中,具体过程不再赘述。 经过上述三个步骤,vCPU可以模拟敏感指令的执行。然而,CPU虚拟化技术不仅仅是提供指令的执行单元这么简单,还可以使vCPU数目超过物理CPU的数量。Hypervisor通过调度不同的vCPU来使用物理CPU完成对CPU的虚拟,类似于操作系统调度任务时采用的分时复用思路。vCPU的数量可以超出真实CPU的数量,因为vCPU就是QEMU中的一个线程。但是如果vCPU的数量过多,反而会降低整个虚拟机的性能,因为Hypervisor在调度vCPU时,上下文切换会带来一定的时间损耗。 5.2.2EL2异常级 继Intel和AMD相继推出硬件辅助虚拟化拓展后,ARM于2012年推出了自己的硬件虚拟化拓展。与Intel VT-x类似,ARMv8硬件辅助的CPU虚拟化旨在解决“虚拟化漏洞”问题,使所有的敏感指令都能触发虚拟机下陷,从而使Hypervisor能够截获并模拟敏感非特权指令的执行。Intel VTx通过引入根模式与非根模式两种操作模式,并改变非根模式下敏感非特权指令的语义使其触发VMExit,解决了“虚拟化漏洞”问题。ARMv8则引入了EL2来解决上述问题,ARMv8异常级架构如图53所示。 图53ARMv8异常级架构 为了保障系统的安全,ARMv8引入了安全状态(Security State)这一概念。处理器可以处于安全态(Secure State)或非安全态(Nonsecure State),二者都有EL0~EL2ARMv8.4引入了安全态EL2(Secure EL2),将SCR_EL3.EEL2置1开启安全态EL2。异常级以及独立的物理地址空间,安全状态与异常级别共同组成了处理器的当前状态。对于安全态,它拥有完整的物理资源控制权限,可以访问两种状态下的地址空间以及系统寄存器,而非安全态只能访问自己的地址空间以及部分系统寄存器。通常将可信的操作系统和应用运行在安全态,普通的操作系统和应用,如Linux和Android操作系统,则运行在非安全态,从而避免了不可信应用带来的安全隐患。异常级则类似于x86中特权级的概念,其中EL0异常级最低,EL3认为EL3始终处于安全态,安全态的切换由EL3完成。异常级最高。当ARMv8未开启虚拟化拓展时,应用程序运行在EL0,操作系统则运行在EL1,有权访问所有的硬件资源。而当开启了虚拟化拓展后,Hypervisor运行在EL2,客户机操作系统运行在EL1,客户机应用程序运行在EL0。ARMv8提供了HCR_EL2寄存器,用于控制虚拟机行为,类似于x86 VMCS中的VMExecution控制域。以HCR_EL2.TWI为例,在EL0和EL1中执行WFI指令会导致其陷入EL2中。在物理环境下,WFI指令会使当前CPU进入低功耗状态; 而在虚拟环境下执行WFI指令将会令Hypervisor调度另一个vCPU运行。尽管硬件辅助虚拟化已大大减少了虚拟化的开销,但是频繁的虚拟机下陷引起的异常级切换仍会引入巨大的虚拟化开销。ARMv8对操作系统频繁访问的寄存器进行了进一步优化,如MIDR_EL1和MPIDR_EL1寄存器。MIDR_EL1保存着处理器的类型,MPIDR_EL1则保存着处理器亲和性相关的信息。对于这两个寄存器,Hypervisor更希望客户机操作系统能够直接读取到虚拟机中相应寄存器的值,而无须陷入Hypervisor中。故ARMv8提供了VPIDR_EL2和VMPIDR_EL2寄存器,Hypervisor在运行虚拟机之前配置好这两个寄存器的值,当虚拟机读取MIDR_EL1/MPIDR_EL1时,会自动返回VPIDR_EL2/VMPIDR_EL2的值。此外,不同于Intel VTx使用VMCS保存虚拟机上下文,ARMv8直接为EL1和EL2提供了两套系统寄存器,这样虚拟机下陷时便无须保存虚拟机寄存器状态,降低了上下文切换的开销。而运行在EL2中的Hypervisor可以直接读写EL0/EL1中的寄存器。 即便如此,当运行在EL2的Hypervisor需要操作系统内核服务时,仍需切换到EL1异常级进行操作,这样还是会有大量的异常级切换损耗。为此在ARMv8.1中新增了虚拟化主机扩展VHE。 5.2.3VHE 硬件层面上,VHE主要增加了以下几个部分。 (1) 在EL2级别的Hypervisor配置寄存器中增加了用于指示是否开启VHE的控制位E2H。 (2) 在EL2级别新增了TTBR1_EL2、CONTEXTIDT_EL2寄存器供宿主机操作系统使用。 (3) 增加了新的虚拟计时器。 当宿主机内核启动后,首先会调用stext,stext调用el2_setup根据内核是否配置了CONFIG_ARM64_VHE来决定是否开启了VHE。开启VHE后,宿主机操作系统会直接运行在EL2。当产生虚拟机下陷时,由于宿主机运行在EL0(宿主机应用程序)和EL2(宿主机操作系统)两个异常级,所以会直接从虚拟机运行的EL0异常级直接切换为EL2异常级。 图54展现了ARMv8虚拟化架构,Hypervisor运行在EL2异常级,虚拟机则运行在EL0/EL1异常级,但是该架构只支持Type Ⅰ类型的虚拟机,不支持Type Ⅱ类型的虚拟机。这是因为Linux等操作系统开发时假定其运行在EL1异常级,部分EL1的寄存器在EL2中并不存在; 而在Type Ⅱ类型的虚拟机中,Hypervisor很大程度上依赖于宿主机操作系统提供的接口,因此就出现了宿主机操作系统运行在EL1,而Hypervisor运行在EL2,二者异常级不一致的问题。以KVM为例,为了解决上述问题,系统开发人员提出将KVM划分为上层监控器(Highvisor)和底层监控器(Lowvisor)两部分,其中底层监控器运行在EL2,上层监控器运行在EL1,架构如图54所示。 注: ①超级调用(hypercall); ②系统调用(syscall)。 图54ARM KVM架构图 当发生虚拟机下陷时,首先会进入EL2中,底层监控器进行必要的处理,当它需要使用Linux内核的功能时,则切换到EL1中的上层监控器进行处理。通常底层监控器相对精简,只进行必需的处理,将大部分工作留给上层监控器处理。分离模式虚拟化解决了在ARMv8上运行Type Ⅱ类型虚拟机的问题,但是这种分层模式造成了大量的上下文切换,严重影响虚拟化性能,于是VHE应运而生,它允许宿主机操作系统运行在EL2,从硬件层面解决了上述问题。引入VHE前后Type Ⅱ类型虚拟机运行如图55所示。VHE由HCR_EL2. E2H和HCR_EL2. TGE控制,E2H用于使能VHE,而TGE用于在使能VHE时区分虚拟机应用程序和物理机应用程序。 注: ①超级调用(hypercall); ②系统调用(syscall)。 图55引入VHE前后的虚拟化架构 在ARMv8的虚拟化中,虚拟化组件有两个: 一个是运行在EL0异常级的QEMU,另一个是运行在EL2异常级的KVM。类似于x86下的KVM架构,ARM的虚拟化也是通过QEMU线程来模拟vCPU,通过ioctl命令在QEMU和KVM之间交互。在QEMU需要获取全局资源的时候,执行VMExit操作切换到EL2异常级进行全局资源的处理。同时ARM在虚拟化设计中也有自己独特之处。 在VHE中,由于宿主机操作系统需要运行在EL2异常级,所以就需要访问EL2的寄存器。但是由于现有的操作系统都是运行在EL1异常级上面的,它们会默认访问EL1的寄存器。为了能够不加修改地在EL2级运行现有的操作系统内核,就需要对宿主机操作系统进行寄存器重定向操作。具体的方法为: 根据是否开启VHE的标志位E2H,来判断是否需要对寄存器进行重定向。当E2H为1时,运行在EL2异常级的指令进行寄存器重定向,而当E2H为0时则不用重定向。 但是重定向又引入了一个新的问题: 如果运行在EL2异常级的Hypervisor确实需要访问EL1的寄存器,那么重定向就会将其定向到EL2的寄存器上。为此ARM架构引入了新的别名机制: 以_EL12或者_EL02结尾。当访问这些别名时,就可以正常访问EL1的寄存器。 5.3鲲鹏中断虚拟化 由于鲲鹏服务器搭载ARM架构的芯片,所以其使用的中断控制器为ARM架构采用的GIC(Generic Interrupt Controller, 通用中断控制器)。GIC发展至今已历经四代,本节主要侧重于最新的GICv3/GICv4架构,将不会深入讲述GICv1/GICv2架构。 5.3.1GICv1 GICv1是ARM最早推出的中断控制器,现在已经弃用,其架构如图56所示。GICv1最多支持8个PE(Processing Element,处理器单元)和1020个中断源。中断控制器的加入使得处理器可以及时地响应外部设备发送的请求。当外围设备发送中断请求时,中断控制器可以及时捕获并在经过仲裁后将中断信号发送给CPU,然后等待CPU对于中断的处理结果。遗憾的是GICv1并不支持中断虚拟化。 图56GICv1架构 5.3.2GICv2 GICv2增加了对中断虚拟化的支持,但是仍只支持8个PE,其架构如图57所示。 图57GICv2架构 上面的中断控制器主要分为以下几个部分: 中断分发器(Distributor)、CPU接口(CPU Interface)和CPU虚拟接口(CPU Virtual Interface),其中CPU虚拟接口主要用于中断的虚拟化。各部分的主要功能如下。 (1) 分发器: 主要用于处理中断的优先级,并将中断分发给对应的CPU接口。 (2) CPU接口: 主要用来处理中断相关事务,如中断优先级屏蔽、中断抢占以及与CPU之间的通信等。每个CPU核都有一个CPU接口。 (3) CPU虚拟接口: 主要用在虚拟化环境,是虚拟CPU的CPU接口。在虚拟化场景中,需要将HCR_EL2.IMO设置为1,此时所有的中断信号都将会陷入Hypervisor中,并由Hypervisor判断是否将该中断信号插入vCPU中。 GICv2共支持1020个中断源,根据中断的编号将中断分为以下三类。 (1) SGI(Software Generated Interrupt,软件生成中断): 由编号为0到编号为15的中断源组成。这种中断由CPU直接写对应的寄存器触发,而非硬件触发,所以叫作软件产生的中断。这种中断主要用于ARM核间通信。 (2) PPI(Private Peripheral Interrupt,私有设备中断): 由编号为16到编号为31的中断源组成。该中断源为CPU私有的中断源,类似于x86中的LAPIC。 (3) SPI(Shared Peripheral Interrupt,共享设备终端): 由编号为32到编号为1019的中断源组成。该中断源是所有CPU共享的中断源,类似于x86中的IOAPIC。而编号为1020到编号为1023的中断源预留做其他用途。 5.3.3GICv3/GICv4 相较于GICv2,GICv3增加了许多新功能,而GICv4相较于GICv3则变化不大。在后文中,未经特殊说明,GIC都是指GICv3架构。图58中不同颜色的矩形表示GIC架构中的组件,箭头则表示中断传递的流程。GICv3架构主要包括四个组件: 中断分发器(Distributor)、中断再分发器(Redistributor)、CPU接口(CPU Interface)和ITS。在正式介绍这四个组件的功能之前,需要先了解GIC中的一些基本概念和机制。 注: ①可能存在0个或多个ITS; ②SGIs由PE产生,由中断分发器路由。 图58GICv3中断架构 1. 中断类型 除了GICv2定义的三种中断类型外,GICv3还引入了一种新的中断类型LPI(Locality-specific Peripheral Interrupt,特殊设备中断)。LPI是GICv3引入的一种新的基于消息的中断类型,可以兼容PCIe总线的MSI和MSI-X机制。 2. 中断ID GIC为每个中断指定一个INTID(Interrupt ID,中断ID),类似于x86的中断向量号。但是不同于x86中段向量号暗含中断优先级,GIC显式地为每个中断都指定了一个中断优先级号。通常中断优先级号越小,优先级越高。GIC允许高优先级中断抢占低优先级中断。GIC将中断优先级号分为两部分: 优先级组号(Group Priority)和次优先级号(SubPriority),中断抢占需要满足以下两个条件。 (1) 阻塞中断的优先级组号小于CPU当前的运行优先级组号,即当前正在被处理的最高优先级中断的优先级组号。 (2) 阻塞中断的优先级组号小于当前CPU的屏蔽优先级组号(Priority Mask)。 SGI/PPI类型中断的优先级号保存在中断再分发器的相关寄存器中,SPI类型中断的优先级号保存在中断分发器的相关寄存器中,LPI类型中断的优先级号则保存在内存中的LPI配置表中。 3. 中断分组 GIC还引入了中断分组(Interrupt Grouping)机制使得特定的中断只能被特定的异常级处理,从而保障系统安全。为了兼容如图51所示的异常级架构,GICv3引入了中断分组机制,它将物理中断分为三组。 (1) 组0(Group 0): ARMv8期望这些中断在EL3处理。 (2) 安全组1(Secure Group 1): ARMv8期望这些中断在安全态EL1(Secure EL1)中处理。 (3) 非安全组1(Nonsecure Group 1): 在虚拟化环境下,ARMv8期望这些中断在非安全态(Nonsecure EL2)中处理; 在非虚拟化环境下,ARMv8期望这些中断在非安全态EL1(Non-secure EL1)中处理。 SGI/PPI类型中断的分组保存在中断再分发器的相关寄存器中,SPI类型中断的分组保存在中断分发器的相关寄存器中,而LPI类型中断一定属于非安全组1。 4. ITS ITS是GICv3新引入的组件,它输出LPI类型的物理中断。外设只需要提供设备号(Device ID)和事件号(Event ID)就能触发一个LPI中断,其中事件号需要写入ITS中的GITS_TRANSLATER寄存器,而设备号的传输是由架构具体实现所定义的。ITS在内存中维护了四种类型的表: DT(Device Table,设备表)、ITT(Interrupt Translation Table,中断翻译表)、CT(Collection Table,集合表)和vPE Table(Virtual PE Table,虚拟PE表)。其中vPE Table是GICv4添加的支持,使得ITS还可以输出LPI类型的虚拟中断,且无需Hypervisor介入便可以将虚拟LPI中断注入vCPU。各表之间的关联如图59所示。 图59ITS表 各类型表的主要功能如下。 (1) 设备表: 维护了设备号和中断翻译表基地址的映射,ITS为每个设备单独维护一个中断翻译表。 (2) 中断翻译表: 中断翻译表可以维护两种类型的映射。对于物理环境而言,中断翻译表通过事件号得到该事件对应的物理LPI中断的INTID和ICID(Interrupt Collection ID,中断集合ID),然后ITS用ICID索引集合表得知该中断的目的中断再分发器。对于虚拟环境而言,中断翻译表通过事件号得到该事件对应的虚拟LPI中断的INTID和vPEID,然后ITS用vPEID索引虚拟PE表得知该虚拟中断的目的vCPU,将该虚拟中断发送至vCPU所在物理CPU对应的中断再分发器。若当前vCPU没有运行,ITS将给物理CPU发送一个门铃中断(Doorbell Interrupt),使其调度vCPU运行。 (3) 集合表: 如上所述,维护ICID和中断再分发器的映射。 (4) 虚拟PE表: 如上所述,维护vPEID和中断再分发器的映射。 5. GIC组件 前面提到,GIC中断架构主要包括四个部分: 中断分发器、中断再分发器、CPU接口和ITS。其中中断分发器、中断再分发器和ITS实现在GIC内部,故CPU通过MMIO的方式访问其内部寄存器。而CPU接口则位于CPU核内部,可以直接通过系统寄存器访问。中断分发器和ITS由所有CPU核共享,中断再分发器则与CPU核一一对应,每一个CPU接口都有一个中断再分发器与其相连,各部分具体功能如下。 (1) 中断分发器: 主要维护SPI类型中断的相关信息,如中断优先级、中断分组、中断路由信息和中断状态等。此外,SGI类型中断也通过中断分发器路由。GICv3引入了亲和性路由(Affinity Routing)机制,使得SPI路由至指定CPU或指定CPU集合中的某一个,而SGI则会被路由至指定CPU集合中的每一个CPU。 (2) 中断再分发器: 维护PPI/SGI类型中断的相关信息,如中断状态、中断优先级、中断触发方式等。 (3) ITS: ITS是GICv3新引入的组件,它输出LPI类型的中断。外设只需要向ITS的GITS_TRANSLATER寄存器写入一个事件号(EventID)就能触发一个LPI中断。而在GICv4架构中,ITS还提供了Virtual LPI直接注入机制,类似于APICv提供的分布中断机制,无需Hypervisor参与便能直接注入虚拟中断。 (4) CPU接口: 主要负责中断优先级检测、优先级仲裁(选择优先级最高的中断处理)和中断应答等。 6. GIC中断处理 在GIC中断架构中,中断存在以下四种状态。 (1) 非活跃态(Inactive): 当前没有待处理或正在处理的中断。 (2) 阻塞态(Pending): 当前中断正在等待CPU处理。 (3) 活跃态(Active): 当前中断已经被CPU响应,该中断正在被CPU处理。 (4) 活跃阻塞态(Active and Pending): 当前中断正在被处理时,又收到一个相同INTID的中断。 四种中断状态在中断处理过程中的变化如下所述。 (1) 中断产生: 外部设备或系统程序通过中断连接线或写入GIC相关寄存器触发中断,此时中断从非活跃态变为阻塞态。 (2) 中断分发: GIC通过前述寄存器或内存控制结构确定该中断的优先级、中断分组等信息,并通过亲和性路由机制将中断发送给目标CPU接口。 (3) 中断交付: CPU接口将中断递交给CPU。 (4) 中断激活: CPU应答该中断,表明该中断正在被处理,该中断由阻塞态变为活跃态。在此过程中如果有相同INTID的中断到达,则中断变为活跃阻塞态。 (5) 运行优先级降低: CPU处理完该中断后,首先修改当前的运行优先级,使得低优先级中断可以被响应,此时中断仍处于活跃态。 (6) 中断无效: 将当前中断状态置为非活跃态,使得后续处于阻塞态的同INTID中断能够被处理。 5.3.4GICv3/GICv4中断虚拟化 GIC为中断虚拟化提供了以下硬件支持: 虚拟CPU接口寄存器直接访问、虚拟中断注入以及虚拟LPI中断直接注入。 1. 虚拟CPU接口寄存器直接访问 在早期GIC架构中,CPU接口也位于GIC内部,同中断分发器等组件一样,CPU通过MMIO的方式访问其内部寄存器。而在虚拟环境中,为了避免虚拟机直接写入CPU接口中的寄存器,Hypervisor通常会将接口寄存器映射区域设置为不可访问,从而截获所有访问并陷入Hypervisor中进行相应的模拟。这一处理方式同前述早期x86中断控制器的访问类似。而GICv3将CPU接口移入CPU内部并提供相关的系统寄存器(ICC_*)供CPU使用,大大提升了中断响应速度。而为了应对虚拟化环境,GICv3还为这些系统寄存器提供了相应的虚拟CPU接口寄存器(ICV_*),当Hypervisor将HCR_EL2.IMO和HCR_EL2.FMO设置为1时,运行在EL1中的虚拟机操作系统对CPU接口系统寄存器(ICC_*)的访问将被重定向到相应的ICV_*寄存器,从而避免了中断处理过程中访问CPU接口寄存器造成的虚拟机下陷。 2. 虚拟中断注入 GICv3可以配置使得所有的物理中断路由到EL2,此时Hypervisor对该中断进行检查,若该物理中断的目标为Hypervisor,则Hypervisor按照前述流程处理该物理中断; 若该中断目标为vCPU,则Hypervisor会向vCPU中注入一个虚拟中断。GICv3提供了寄存器(ICH_LR<n>_EL2)保存虚拟中断的INTID、中断优先级、中断状态以及相关联的物理中断INTID等。当vCPU恢复运行时,硬件将根据这些寄存器中的信息向vCPU注入一个虚拟中断,并调用相应的中断处理函数。 3. 虚拟LPI中断直接注入 GICv4引入了虚拟LPI中断注入机制,无需Hypervisor参与便可以将虚拟LPI中断注入vCPU,这是通过前述GIC的ITS组件完成的。ITS通过查询设备表、中断翻译表和虚拟PE表得到虚拟中断目的vCPU所处的物理CPU所对应的中断再分发器,将虚拟LPI中断信息插入位于内存中的虚拟LPI配置表(Virtual LPI Configuration Table)和虚拟LPI状态表(Virtual LPI Pending Table)中。然后vCPU会将表中记录的虚拟LPI中断与前述ICH_LR<N<_EL2寄存器中记录的虚拟中断进行比较,选择最高优先级的中断进行处理。 5.4鲲鹏内存虚拟化 ARM架构借鉴了Intel系列架构演进过程的“前车之鉴”,在ARMv7架构中就包含了对双层页表的支持,保证了内存虚拟化的性能; 而Intel在较晚的虚拟化版本中才支持扩展页表,在此之前只能使用软件实现的影子页表,性能较差。ARM架构也为内存管理增加了更多的寄存器,用于保存多类页表的基地址。Intel系列仅有一个CR3,并使用VMCS结构保存EPT页表基地址; 而ARM架构为操作系统的用户态和内核态各准备了一个寄存器,用于保存页表基地址,增强了用户态空间和内核态空间的隔离性。下面从与Intel架构类似的地址翻译概念讲起,并逐步介绍ARM中出现的新概念。 5.4.1VMSAv864架构概述 在ARM处理器中,VMSA(Virtual Memory System Architecture,虚拟内存系统架构)给在ARMv8处理器的AArch64模式下运行的PE提供了虚拟内存管理功能。具体而言,VMSAv864为PE提供了MMU,当PE进行内存访问时,MMU可以完成地址翻译、权限检查以及内存区域类型判断等功能。对于PE而言,它访问内存时使用的是VA,MMU可以: ①将VA映射到PA,用于访问物理内存系统中的资源。完成地址映射时,MMU会使用保存着页表基地址的寄存器; ②如果由于一些原因,无法将VA映射到PA,则会引起异常(Exception),称为MMU异常(MMU Fault)。系统寄存器负责保存引起MMU 异常的原因,供软件使用。 ARM内存管理中的概念和Intel系列处理器中的大致相同。但如5.1.2节所述ARM异常级架构的介绍,ARM中PE执行的异常级主要分为EL0、EL1、EL2等,此处仅考虑非安全状态下操作系统以及Hypervisor所运行的异常级。相应的,地址翻译系统相对于Intel系列有所改变。ARM架构中提出了一个通用的概念,即翻译流程(Translation Regime),用来概括物理机环境和虚拟化环境下的内存翻译流程。ARM中包括两类翻译流程: ①单阶段的地址翻译,即VA翻译为PA,是在物理机上运行的操作系统中发生的地址翻译流程; ②两个连续阶段的地址翻译,即客户机虚拟地址翻译为客户机物理地址,进而翻译为宿主机物理地址,而ARM架构为了与前一种翻译流程的说法统一,将客户机虚拟地址依然称为VA,客户机物理地址称为IPA(Intermediate Physical Address,中间物理地址),宿主机物理地址称为PA,阶段1的地址翻译将VA翻译为IPA(Stage1),阶段2的地址翻译将阶段1得到的IPA作为输入,翻译为PA(Stage2)。一个翻译流程定义了某异常级下地址翻译使用的页表基地址寄存器和控制寄存器,每个异常级各有其如下翻译流程。 (1) EL0&EL1: 关闭EL2时,系统中不运行Hypervisor,也不存在虚拟化的概念。EL0&EL1异常级上的内存访问共用同一个单阶段的翻译流程,只进行单阶段的地址翻译,将VA翻译为PA。EL1运行着操作系统内核,EL0运行着应用程序,均使用VA访问内存,需要翻译为PA。此时在TLB中查找地址翻译缓存时,VMSA系统需要根据ASID进行匹配,ASID为每个进程标识了其独占的TLB表项,于是进程切换时无须清空TLB,加快了地址翻译的速度。 (2) EL0&EL1: 开启EL2时,运行在EL1的操作系统成为客户机操作系统,于是EL0&EL1异常级上的内存访问共用一个两阶段的翻译流程,即前文所述的VA→IPA→PA。在TLB中查找地址翻译缓存时,VMSA首先匹配相同的VMID,筛选当前客户机独占的TLB缓存,在vPE切换时无须清空TLB; 然后匹配ASID,筛选当前客户机进程的TLB缓存。 (3) 其他异常级(如EL2、EL3)也可以使用VA访问内存。此时没有客户机的概念,故只需要单阶段的翻译流程将VA翻译为PA。 如5.1.2节所述,与异常级正交的概念是安全状态,即每个EL0、EL1、EL2异常级都有安全态和非安全态,但与地址翻译的关系不大,故不做赘述,详细内容请参看ARMv8架构说明手册。ARM架构在不开启虚拟化和开启虚拟化的情况下使用了类似的名词描述地址翻译过程,即阶段1和阶段2,而Intel架构使用EPT指代阶段2使用的页表。ARM提供了更多寄存器用于保存页表起始地址,这也和Intel架构形成了鲜明对比,下文将介绍。 5.4.2地址空间与页表 在ARM虚拟化设计中,虚拟机应用要想访问物理内存必须经过两级转换: VM维护一套地址转换表,Hypervisor控制最终的转换结果。在第一级转换的过程中,虚拟机将其虚拟地址(VA)转化为虚拟机视角下的物理地址(IPA),然后由Hypervisor最终控制转化为实际的物理内存地址。因此Hypervisor可以控制虚拟机访问特定大小的内存,并且指定被访问内存的空间位置。而其他的内存对于虚拟机来说都是不可见的,虚拟机无法访问。这种设计增强了虚拟机之间的隔离,保证了虚拟机的安全性。 IPA作为中间物理地址的时候,不仅存在内存区,它还包含了外围设备区域。虚拟机可以通过IPA的外围设备区域来访问虚拟机可见设备。而设备又包括两种: 直通设备和虚拟的外围设备。当一个直通设备被分配给虚拟机以后,该设备就会被映射到IPA地址空间,然后VM通过IPA直接访问物理设备。而当虚拟机需要使用虚拟的外围设备时,在地址转换的阶段2,也就是从IPA转换到设备空间的时候会触发错误。当错误被Hypervisor捕获后由Hypervisor对设备进行模拟。 在实际的使用中,会给每个虚拟机都分配一个ID称为VMID,用以标记特定TLB项和VM之间的对应关系。这样不同的VM就可以使用同一块TLB缓存。除此之外,TLB也可以使用ASID(Address Space Identification,地址空间标识符)来标记。每个应用分配一个ASID,使得不同的应用之间也可以共享同一块TLB缓存。 VMSA中支持三种类型的地址: VA、IPA以及PA。在AArch64执行模式下,内存访问的地址VA共64位,但查询页表时,仅使用其中的48位,故虚拟地址空间共48位。有了Intel架构中用户、内核页表隔离需要软件实现的前车之鉴即KPTI补丁,由Linux内核实现,为了修复Intel x86处理器的Meltdown漏洞,性能开销较大。,VMSA为VA提供两套页表,支持两个VA地址范围的地址翻译。由于查询页表仅使用VA中的前48位,剩余的16位可以用于标记属于两个VA地址范围中的哪一个。其中,在内核VA的范围内(Kernel Space),所有VA的高16位均设为1,故内核可以使用的范围是0xffff000000000000~0xffffffffffffffff; 在用户VA的范围内(User Space),所有VA的高16位均设为0,故用户空间可以使用的范围是0x0000000000000000~0x0000ffffffffffff。 VMSA为内核VA空间和用户VA空间都提供了一个页表基地址寄存器,分别命名为TTBR1_ELx和TTBR0_ELx,即翻译表基地址寄存器(Translation Table Base Register),后缀代表在ELx异常级下的PE有权限操作。这样,每当PE访问了一个VA,首先需要确定其高16位是否为0,MMU才能确定使用哪个寄存器作为页表基地址寄存器。然而只有EL0和EL1拥有两段VA空间,并具有TTBR{0,1}_EL{0,1}; 对于EL2和EL3而言,只有较低段的VA空间,以及TTBR0_EL{2,3},故EL2、EL3只能访问0x0000000000000000~0x0000ffffffffffff的VA。 如5.1.2节所述,由于ARM提供了EL0、EL1、EL2的异常级模式,操作系统运行在EL1,Hypervisor运行在EL2,这样软件上只能实现Type Ⅰ类型的Hypervisor。而依赖于Linux内核的KVM这类Type Ⅱ类型的Hypervisor将无法正确运行在EL2,于是只能实现为上层监控器结合底层监控器的模式,这样上下文切换会过于频繁,影响性能。为此,ARMv8.1 提出了VHE,使得宿主机操作系统可以经过最少的修改量即可运行在EL2。在内存管理方面,VHE为EL2引入了用户态、内核态两套页表,以及TTBR{0, 1}_EL2。对于操作系统原有的访问TTBR{0, 1}_EL1的代码,VHE将该访问重定向到TTBR{0, 1}_EL2。于是操作系统可以在EL2中透明地访问EL1寄存器,减少了操作系统的复杂度。 介绍完阶段1中的两段VA机制,下面继续介绍阶段2页表与相关寄存器。根据上文对于翻译流程的介绍,当运行在EL2的Hypervisor开启了阶段2的地址翻译时,所有EL0、EL1中的内存翻译均需要两个阶段,其中阶段1完成VA到IPA的翻译,使用的页表基地址寄存器为TTBR{0, 1}_EL{0, 1},阶段2完成IPA到PA的翻译,使用的寄存器为VTTBR0_EL2,保存着第二层页表的基地址。与Intel架构相同,两阶段地址翻译使用的两层页表在TLB 不命中时也会产生大量的内存访问。 ARMv8中实现的翻译流程以及使用的页表基地址寄存器如图510所示。其中,有两个阶段的翻译流程使用了TTBR{0, 1}_EL1以及VTTBR0_EL2,可以实现内存虚拟化; 单阶段的翻译流程仅使用TTBR{0, 1}_EL{0, 1}以及TTBR0_EL{2, 3},可以实现虚拟内存以及用户、内核虚拟内存隔离。在EL2运行的Hypervisor将HCR_EL2的第0位设置为1时,则开启了第二阶段地址翻译,此后在EL0和EL1执行的访存指令中的VA都要经过两阶段的地址翻译,从而获得对应的IPA。在开启第二阶段地址翻译之前,Hypervisor需要将第二阶段页表的基地址写入VTTBR0_EL2中,从而正确地完成第二阶段地址翻译。 注: ①VA→IPA→PA; ②③④VA→PA。 图510ARM KVM架构图 除了页表基地址寄存器外,ARMv8还提供了翻译控制寄存器用于控制MMU地址翻译的行为。阶段1的地址翻译由TCR_EL{0, 1, 2, 3}(Translation Control Register,翻译控制寄存器)控制,而阶段2的地址翻译由VTCR_EL2(Virtual Translation Control Register,虚拟翻译控制寄存器)控制。这些地址翻译控制寄存器的作用包括指定当前的ASID/VMID、地址翻译的粒度大小以及各类地址的宽度,详细内容请查阅ARMv8架构说明手册。 5.4.3内存属性、访问权限与缺页异常 对于所有的翻译流程,完成页表的查询后,可以得到三个结果: VA对应的PA、PA对应的内存区域的内存属性以及访问权限,具体如下所述。 (1) 访问权限大致包括可读、可写和可执行,这和Intel x86架构中页表项包含的访问权限类似。其区别是,每个访问权限是针对一个特定的异常级的,如标记为只能被运行在EL2的PE执行的内存区域,则不能被EL0和EL1异常级执行。 (2) 内存属性包括对缓存行为的控制信息以及内存类型的信息,其中缓存行为包括: 该段内存对应的缓存采用写穿(WriteThrough)方式更新、或使用写回(WriteBack)方式更新、或该段内存不可缓存。 (3) 内存类型包括普通内存,包括共享(Sharable,可在多个PE间共享)和不可共享(Nonsharable,只能被单一的PE访问)的普通内存,它支持预取、乱序访问、非对齐访问。例如,RAM、闪存等均属于普通内存。另一种类型是设备内存,即映射到物理地址空间内的外围设备内存,或称为MMIO对应的物理内存区域。在图510中,外围设备代表了这类内存区域: 它不支持预取、乱序访问、非对齐访问。回忆在第3章讲述QEMU内存虚拟化源代码时的MemoryRegion,ARM中实现的不同内存区域的内存类型和MemoryRegion的类型相似,即分为普通内存和设备内存。其他的内存属性和访存顺序相关,ARM中实现了一种较弱的内存模型,需要控制PE的访存顺序,这部分内容较为复杂,本节不做讲述。 下面介绍地址翻译时可能出现的缺页异常。当MMU查询页表时,也会存在查询不成功的情况,此时会产生MMU异常,包括同步异常和异步异常。产生异常后,CPU将跳转到相关的异常处理函数中,可以在处理函数中完成页表的填充。发生异常的原因将会记录在ESR_EL{0, 1, 2}(Exception Syndrome Register,异常特征寄存器)中,它提供了异常的类型等信息,供软件使用。除ESR外,系统中还有FAR_EL{0,1,2}(Fault Address Register,错误地址寄存器),其作用和Intel x86架构下的CR2相同,它记录了导致缺页异常的VA。 对于两阶段的翻译流程,MMU异常可能发生在两个阶段中的任何一个阶段,而阶段1中引起的异常则会跳转到客户机异常处理函数,无须退出到Hypervisor; 阶段2中引起的异常则会退出到Hypervisor,这和Intel架构下的扩展页表原理相类似。对于阶段2中的缺页异常,ARMv8提供了HPFAR_EL2(Hypervisor IPA Fault Address Register,Hypervisor IPA错误地址寄存器),它记录了引起阶段2缺页异常的IPA,而Intel架构将引起EPT缺页异常的GPA记录在VMCS中,这有所不同。 第二阶段的缺页异常还被用来模拟MMIO,详见5.5.1节的介绍。 5.4.4MPAM MPAM(Memory System Resource Partitioning and Monitoring,内存系统资源分割和监控)是一种通过确定性流控针对 CPU 访存系统资源隔离的技术手段,旨在解决大规模云部署时由于共享资源竞争带来的性能下降问题。下面将具体介绍MPAM在虚拟化中对于访存资源的隔离与应用。 MPAM的系统架构可参考图511,从图中可以看到,L3缓存是被各个CPU所共享的。在L3高速缓存容量有限的情况下,如果某台虚拟机使用了过多的缓存,则会导致其他的虚拟机的缓存较少,使得其他虚拟机发送TLB命中失败的概率增大,从而影响其他虚拟机的访存性能。为了解决这个问题,在鲲鹏虚拟化中使用了MPAM来对访存资源进行隔离。MPAM对访存的隔离有如图512所示的两种配置方式。 图511MPAM系统框架 图512MPAM配置方式 第一种(图512(b))是通过优先级来配置,这种情况会给不同的虚拟机配置不同的优先级。优先级高的虚拟机可以优先取得对于共享缓存资源的使用权。这种配置方式可以确保运行重要任务的虚拟机能够优先使用共享缓存资源。 第二种(图512(a))是以高速缓存的路(Cache Way)为粒度,使用位图来对资源进行分割,隔离不同虚拟机对于访存资源的使用。这也是目前鲲鹏920所使用的方案。 MPAM在对不同业务流/虚拟机的访存控制上可以确保有一个明确的上下限。当虚拟机对访存资源的使用超过上限时则限制虚拟机的访存到上限以下; 当虚拟机对于访存资源的使用低于下限时则赋予其对于访存资源使用的优先权。在鲲鹏服务器的实际使用中,MPAM能够有效降低在CPU访存过程中因虚拟机竞争带来的性能下降。 5.5鲲鹏I/O虚拟化 5.5.1MMIO的模拟 与x86架构不同的是,ARM架构的CPU对外设的访问只存在MMIO这一种方式。在ARM-V8架构下,当虚拟机向设备发起MMIO访问请求时,由于物理内存空间对虚拟机透明,虚拟机使用的是中间物理地址(IPA),该地址并不能直接用于外设的访问,此时就需要Hypervisor的介入。Hypervisor会对IPA进行阶段2的地址转换,将IPA转换为能够用于MMIO访问的真实物理地址。 虚拟机通常可以拥有两种类型的外设,一种是直接分配给虚拟机的物理设备,一种是Hypervisor提供的虚拟设备,Hypervisor会采用不同的方式来实现对这两种设备的MMIO访问模拟。对分配给虚拟机的物理设备的MMIO访问的处理过程比较简单,如图513所示,阶段2的页表项中会包含物理设备的物理空间地址与虚拟机IPA之间的映射,运行在虚拟机中的驱动程序可以通过IPA直接访问该物理设备。与访问直接分配的物理设备不同的是,每次虚拟机向虚拟设备发起MMIO访问时,都会触发阶段2缺页异常,之后Hypervisor会在异常处理程序中对该MMIO访问进行模拟。 图513两种串口设备MMIO过程 在模拟MMIO访问之前,Hypervisor需要知道虚拟机访问的具体虚拟设备,并定位该设备中被访问的寄存器,同时Hypervisor还需要知道与访问相关的信息,例如方式是读还是写、访问的大小以及用于传输数据的寄存器。图513展示了虚拟机访问虚拟串口设备的模拟过程,具体如下所述。 (1) vCPU执行指令LDR x0, [virt_uart_rx_reg],向虚拟串口发起MMIO读访问。 (2) 该访问在阶段2翻译过程中会产生缺页异常,并触发中止(abort)异常。中止异常会将IPA地址填充到HPFAR_EL2寄存器中,并将上文提到的访问相关信息[Read,4 bytes, x0]填充到ESR_EL2寄存器中。 (3) 由于Hypervisor负责分配和管理虚拟机的中间物理地址空间,Hypervisor可以通过HPFAR_EL2中记录的IPA来确定vCPU访问的虚拟设备。Hypervisor还可以通过特定函数获取ESR_EL2中记录的用于模拟MMIO访问的相关信息,如图513中的identify_reg函数会返回访问的虚拟设备寄存器。接下来,Hypervisor会利用IPA信息和寄存器信息调用emulate_access函数,完成MMIO的模拟。之后,Hypervisor通过ERET指令将控制流返回给vCPU,vCPU会继续执行下一条指令。 5.5.2DMA重映射——SMMUv3 常用的I/O虚拟化框架主要有两种: 一种是半虚拟化场景下使用的virtio框架,该虚拟化框架是一种纯软件实现,与硬件无关,前面章节已经讨论过,这里就不再赘述; 另一种就是设备直通的虚拟化方案。设备直通的一个关键点就是要解决DMA重映射问题,本节主要介绍ARM平台中的 SMMU(System Memory Management Unit,系统内存管理单元)。 在非虚拟化环境下,运行在内核态的设备驱动程序通过DMA相关的API接口完成DMA数据传输。在发起DMA请求时,驱动程序会在操作系统层面对访问的内存地址加以限制,确保应用程序内存访问的安全性。 然而在虚拟化环境下,虚拟机内的设备驱动可以与分配给该虚拟机的物理设备直接交互,但是驱动程序和物理设备却拥有不同的内存视图,驱动程序会将中间物理地址空间视为“真实”的物理地址空间,同时物理设备访问的则是实际主机物理地址空间,这就会给DMA传输带来问题。如图514所示,由于DMA控制器并不受内存阶段2翻译控制,物理设备会将驱动程序传入的IPA当作主机物理地址进行DMA传输,导致的后果是虚拟机可能会读写到属于其他虚拟机的物理内存地址甚至是Hypervisor所在的物理内存区域,破坏了系统的安全性和隔离性。 图514非虚拟化与虚拟化环境下的地址翻译过程 为了解决DMA地址翻译问题,将DMA重映射到对应主机物理地址,各大厂商都推出了各自的硬件解决方案。4.2节中介绍了Intel提出的VTd技术,ARM同样也提出了SMMU。物理设备可以使用虚拟地址、IPA或其他总线地址执行DMA,SMMU可以通过类似于PE中内存地址阶段2转换的方式将这些地址转换为PA。 在ARM架构不断演进的过程中,ARM推出了很多新的特性,为了支持这些特性,SMMU也在不断演进。SMMUv1主要支持ARMv7的页表格式,使用寄存器配置少量的设备流。SMMUv2对ARMv8.1A的页表格式提供了支持,并扩展了SMMUv1,支持64位地址,同样使用寄存器配置少量的设备流。SMMUv1和SMMUv2将传入的设备流映射到某个基于寄存器的上下文,该上下文会指向要使用的转换表和转换配置。该上下文还可以指示第二个上下文,用于阶段2的嵌套翻译。受寄存器数量的限制,使用基于寄存器的配置限制了上下文的可扩展性,并且不可能支持数千个并发的上下文。为了解决这一问题,SMMUv3使用基于内存的配置结构。相较于使用寄存器,基于内存的配置结构可以支持大量的设备流,这也是SMMUv3相较于SMMUv1和SMMUv2最大的不同点。本节将对最新的SMMUv3进行介绍。 SMMU的设计理念与VTd有很多相似之处,在直通设备分配给虚拟机之前,Hypervisor会为该设备建立阶段2地址翻译页表。阶段2地址翻译页表与VTd中第二级(Secondlevel)地址翻译页表的作用类似,但SMMU与VTd的不同之处是,SMMU与MMU共用一套阶段2页表,而VTd使用的是专用的I/O页表。Hypervisor在阶段2地址翻译页表中会建立IPA与主机PA的映射关系,并限制设备所能访问的物理地址范围。图515展示了与SMMU相关的数据结构以及各配置结构之间的关系,下面对各配置结构分别进行介绍。 图515SMMU使用的配置结构示例 SMMU内有一个名为流表(Stream Table)的表,用于记录与每个设备阶段1和阶段2翻译页表基地址相关的信息。流表由Hypervisor维护,并且Hypervisor会将流表的基地址保存在SMMU_STRTAB_BASE寄存器中。流表由多个STE(Stream Table Entry,流表项)构成,每个表项STE会记录一个发起DMA传输的设备的相关信息。值得注意的是,在SMMU中,阶段1转换和阶段2转换的使用是相互独立的,即SMMU可以只进行某一阶段翻译,也可以同时进行两个阶段翻译,这由STE中保存的配置信息决定。 STE中有三个主要的成员: VMID、S2TTB(Stage2 Translate Table Base,阶段2转换表基)和S1上下文指针(S1ContextPtr)。VMID属性代表设备所属的虚拟机。S2TTB属性会指向此设备所属虚拟机的阶段2转换页表基地址。由于多个设备可能会同属于一个虚拟机,因此多个STE可以共享同一个阶段2转换表。此外,虚拟机中的多个进程可能同时使用虚拟机的某个设备,设备需要获得当前与自己交互的进程的相关信息,例如每个进程各自的阶段1页表。CD(Context Descriptor,上下文描述符)正是为描述进程信息而引入的数据结构。CD中的ASID属性用于标识进程的地址空间,TTB0和TTB1分别保存用户空间阶段1页表基地址和内核空间阶段1页表基地址(与AArch64中的寄存器TTBR0和TTBR1类似)。同一设备对应的全部CD构成了一个CD表,STE中的S1ContextPtr成员会保存一个指向该CD表基地址的指针。 SMMU使用流ID(StreamID)来索引流表,并规定StreamID的大小为0~32位,但是StreamID使用的位数以及StreamID的构成都要依据具体实现决定。对于PCI设备来说,一般情况下SMMU会使用PCI设备标识符来填充StreamID的低16位,如果系统中存在多个根组件(Root Complex),则会使用高于16的位来扩展现有的16位StreamID。在开启阶段1地址转换的情况下,SMMU规定使用下一级流编号(SubstreamID)识别发起DMA请求的进程,并用于索引上下文描述表来选择使用的阶段1翻译页表。与StreamID类似,SubstreamID被SMMU限定了大小范围为0~20位,SubstreamID使用的位数以及StreamID的构成同样也要依据具体实现来决定。在PCIe系统中,SubstreamID等价于PASID,这与VTd的可扩展模式中使用PASID获取第一级(Firstlevel)翻译页表类似。 图516线性流表结构 SMMU中允许流表拥有如图516和图517所示的两种不同类型的组织形式。图516展示了所有SMMU都支持的流表的线性结构。线性流表是一个连续的STE数组,通过StreamID从0进行索引。线性流表包含2n个STE,其中n最大可取到SMMU中支持的StreamID位数。图517展示了StreamID为10位这一条件下的两级流表结构。其中第一级流表包含多个描述符,使用StreamID的最高两位[9∶8]检索第一级流表,每个描述符指向包含STE的第二级线性流表,StreamID的低8位[7∶0]用于索引第二级流表。每个第二级流表包含的STE数量可以根据需要灵活配置,以达到减少连续内存空间占用的目的。当线性流表中包含超过64个STE时,流表大小会超过4KB,这意味着无法将流表存放在一个单独的内存页。所以SMMU中规定,当StreamID的位数大于6位时,必须使用两级流表结构。 图517两级流表结构 与流表类似,SMMU同样支持线性CD 表和两级CD 表结构。如图518所示,在使用两级CD 表的情况下,STE中的S1ContextPtr指向的是由多个L1CD(Level 1 Context Descriptor,第一级上下文描述符)组成的第一级CD 表。 图518两级CD 表 SMMU使用子流ID(SubstreamID)的高位数据索引第一级CD 表中的L1CD。每个L1CD中的L2Ptr成员会指向某个第二级线性CD 表的基地址。SMMU使用SubstreamID的低位数据索引第二级线性CD 表中的CD,进而获取阶段1的地址转换页表。 5.5.3SMMUv3中的缓存机制 4.2.4节中介绍了Intel VTd技术通过引入翻译缓存(Translation Cache)机制缓存与重映射地址转换相关的数据结构来加速地址转换过程,ARM在SMMU中同样也引入了缓存机制。 SMMU中TLB机制与前文介绍的MMU中的TLB机制类似,但不同之处在于SMMU查询TLB的过程除了VMID、ASID、地址之外,还需要流世界(Stream World)参与。流世界主要用来描述设备数据流的安全状态和控制设备的进程所处的异常级。流世界的引入可以区分在不同异常级上运行的以及拥有不同安全状态的进程在SMMU中对应的TLB条目。例如,在输入地址都为0x1000、ASID都等于3的情况下,流世界可以判断发起本次访问的是运行在EL1中的安全进程还是非安全进程,进而查找到不同的TLB条目。关于流世界的更详细介绍,读者可以自行查阅ARM技术手册。 如图519展示了SMMU中在TLB参与下的地址翻译过程。该过程主要包含配置信息读取(步骤1~3)和DMA地址翻译(步骤4~5)两个阶段,流程如下。 图519SMMU地址翻译过程 (1) SMMU从I/O 事务中获取设备标识符,即StreamID。 (2) SMMU从SMMU_STRTAB_BASE寄存器中获取流表的基地址,并通过StreamID获取对应的STE。 (3) 在开启阶段1转换的情况下,通过SubstreamID定位到对应的CD,进而获取ASID和阶段1页表基地址。在开启阶段2转换的情况下,在STE中获取VMID和阶段2页表基地址以及流世界配置信息。 (4) SMMU根据DMA地址、ASID、VMID和流世界查询TLB。如果TLB命中,可以直接获得目标物理地址以及访问权限信息。如果TLB未命中,则需要根据DMA地址通过相应地址翻译过程获得对应的目标物理地址,并将映射关系填充到TLB中。 (5) 设备根据目标物理地址进行数据传输。 SMMU中缓存机制的引入使得在TLB命中的情况下,SMMU可以直接从TLB中获取目标物理地址,同时并不需要经历“漫长”的阶段1和阶段2页表查询过程,从而提升SMMU的地址转换效率。 5.6鲲鹏时钟虚拟化 在操作系统和应用程序中,对于时间的获取是非常必要且频繁的操作。如操作系统需要获取时间来提供日期显示、需要通过计时器来进行进程之间的调度、也需要通过周期性的时钟信号来实现看门狗功能等,所以时钟虚拟化尤为重要。下面将通过对比x86平台和鲲鹏的时钟虚拟化来理解时钟虚拟化。 与x86平台不同,ARM平台对于时钟的虚拟化设计更为灵活。ARM的时钟名称为ARM通用计时器(ARM Generic Timer)。该计时器直接加入了对时钟虚拟化的支持,由两部分组成: 一个是由多个处理器共享的位于SoC(System on Chip,片上系统)的系统计数器(System Counter); 另一个是每个处理器上的计时器。通用计时器由一系列的比较器组成,与公共的系统计数器进行比较。当比较器中的数据小于或者等于系统计数器的时候,比较器就会产生一个中断。图520展示了鲲鹏的通用计数器的组成部分和工作原理。 图520鲲鹏通用计数器组成 由于在实际使用中可能存在一个物理机上运行多个虚拟机的情况,当进行vCPU调度的时候,被调度的vCPU就会处于挂起状态,那么处于挂起状态的vCPU是如何计算并获取实时时钟的呢? 假设Hypervisor调度vCPU的时间可以忽略不计。如图521所示,在4ms的时间里,vCPU0和vCPU1各自运行了2ms。但是如果vCPU0在T=0时刻设置了一个3ms的计时器,而此时Hypervisor已经通过调度将运行环境和运行权限交给了vCPU1,那么此时的中断将会如何触发?除此之外还有一个问题: 由于Hypervisor的调度,vCPU0和vCPU1各自只有一半的时间在使用CPU资源,那么设置的3ms计时器是vCPU0里的虚拟3ms还是实际中 图521vCPU调度 的3ms(也就是wallclock时间里面的3ms)? 为了支持虚拟化,ARM又加入了两个新的概念: 虚拟计数器(Virtual Counter)和虚拟计时器(Virtual Timer)。也就是说,在鲲鹏中,既有物理计数器(Physical Counter)又有虚拟计数器。值得注意的是,虽然名为虚拟计数器,但是该计数器确是实际存在的计数器而不是软件模拟。由于是两种不同的计数器,所以在访问计数器的数据的时候所读取的寄存器也不同。对于物理计数器,读取数据时要访问的寄存器为CNTPCT,虚拟计数器对应的寄存器则为CNTVCT。vCPU中的程序可以访问两个计数器: EL1 物理计数器和EL2 虚拟计数器。其中EL1的物理计数 图522虚拟计数器与物理计数器关系 器与系统计数器产生的计数做比较,虚拟计数器则是物理计数器减去一个偏移量。由于这个偏移量保存在寄存器中,Hypervisor通过寄存器来读取偏移量的大小,这样就可以在vCPU被调度器挂起的时候将时间透传给vCPU。虚拟计数器和物理的计数器关系如图522所示 那么这样的设计在实际调度中是如何工作的呢?可以用图523来表示。 图523时钟虚拟化在vCPU调度时的工作原理 这幅图清晰地展示了鲲鹏使用虚拟计数器来保证多个虚拟机计时器时间同步的工作原理。如图523所示,第一行展示了两个虚拟CPU: vCPU0和vCPU1的实际调度时间; 第二行展示了物理计时器以1ms的周期记录时间的流逝; 第三行则是偏移量的值,由于是两个虚拟的CPU,所以需要两个不同的偏移量: vCPU0的offset0(图523中偏移量中浅色的值)和vCPU1的offset1(图523中偏移量中深色的值)。在T=0时刻,由于虚拟机刚刚启动,所以offset0和offset1均为0。T=1时切换到vCPU1运行,由于第1ms都是vCPU0在占用,所以offset1的值为1。T=2时由于vCPU1也只运行了1ms ,所以offset0的值为1,后面的offset同理。 有了偏移量的存在,vCPU在切换的时候也可以获取到实时的时间,保证了多个vCPU运行场景中对于时间的正确获取。 这种使用虚拟计数器的设计给鲲鹏的时钟虚拟化带来了较强的竞争力。在实际使用中,Hypervisor会被配置去使用物理计时器,而虚拟机则被设置去使用虚拟计时器。这样就可以使Hypervisor和虚拟机使用不同的计时器,在使用中就不存在冲突了。在虚拟机使用计时器的时候,不用通过陷出到EL2异常级访问计时器的数值,减少了异常级切换带来的损耗,提高了时钟虚拟化的效率。但是值得注意的是,如果在一个平台上运行了多台虚拟机,在虚拟机切换的时候还是要将虚拟计时器的数据保存起来。 下面结合代码深入分析鲲鹏虚拟化的实现。 首先是虚拟时钟的初始化,初始化动作在内核启动时进行。初始化时会创建一个arch_timer_kvm_info结构体来记录中断和计时器,代码如下Linux kernel 5.10源码下载地址: https://github.com/torvalds/linux,下载时须选用v5.10标签。。 linux-5.10/include/clocksource/arm_arch_timer.h struct arch_timer_kvm_info { struct timecounter timecounter; int virtual_irq; int physical_irq; }; 然后在KVM启动时,调用kvm_timer_hyp_init函数初始化中断号和读取虚拟计数器的函数。之后就是在QEMU创建虚拟机时调用kvm_timer_init函数来读取CPU的虚拟计数器,重点是初始化vCPU。上面提到过,在多个虚拟机的场景下,如果需要切换虚拟机,就要对时钟的上下文环境进行保存。所以在初始化vCPU时,每个vCPU都会为自己维护一个和时钟虚拟化相关的结构体,用来保存时钟虚拟化的上下文环境,代码如下。 linux-5.10/include/kvm/arm_arch_timer.h struct arch_timer_cpu { struct arch_timer_context timers[NR_KVM_TIMERS]; /* Background timer used when the guest is not running */ struct hrtimerbg_timer; /* Is the timer enabled */ boolenabled; }; 然后在第一次运行vCPU的时候通过调用vgic函数创建中断映射,此时时钟虚拟化的初始化动作完成。完成初始化之后虚拟时钟就可以正常工作了。 本章小结 本章主要围绕鲲鹏平台介绍了ARM虚拟化的一些相关原理,包括CPU、内存、I/O、中断以及时钟虚拟化。前面的第2、3、4章分别介绍了CPU、内存和I/O在x86下的虚拟化实现以及x86和ARM下的通用实现方式,本章则着重介绍鲲鹏平台中ARM虚拟化特有的虚拟化技术,如CPU部分的GIC中断、内存的阶段2转换、I/O部分的SMMU以及鲲鹏的时钟虚拟化。