网站开发综合设计报告,成全视频免费观看在线看第6季,做个人博客的网站,网站设计与网页制作团队文章目录 1 简介2 arm64 虚拟化相关硬件支持2.1 arm64 cpu 虚拟化基本原理及硬件支持2.2 系统寄存器捕获和虚拟寄存器支持2.3 VHE 特性支持2.4 内存虚拟化支持2.5 IO 虚拟化支持2.6 DMA 虚拟化支持2.7 中断虚拟化支持2.8 定时器虚拟化支持 3 arm64 kvm 初始化流程3.1 初始化总体… 文章目录 1 简介2 arm64 虚拟化相关硬件支持2.1 arm64 cpu 虚拟化基本原理及硬件支持2.2 系统寄存器捕获和虚拟寄存器支持2.3 VHE 特性支持2.4 内存虚拟化支持2.5 IO 虚拟化支持2.6 DMA 虚拟化支持2.7 中断虚拟化支持2.8 定时器虚拟化支持 3 arm64 kvm 初始化流程3.1 初始化总体流程3.2 aarch64 架构初始化总体流程 4 cpu虚拟化和 vcpu 运行流程5 arm64 内存虚拟化流程6 arm64 中断虚拟化7 arm64 timer 虚拟化 1 简介
虚拟化类型
hypervisorguest 管理机 又称为 VMMvirtual machine monitor
1软件虚拟化
不借助硬件支持通过软件完整的模拟一台虚拟机 qemu tcg。
2硬件虚拟化
根据 VMM 实现不同硬件虚拟化包含 type1 hypervisor 和 type2 hypervisor 两种类型。
type1XENsel4 hypervisor 直接运行在硬件上并管理系统中所有的硬件和虚拟机资源。
type2KVMhypervisor 会运行在一个 host os 上host os 负责管理和控制系统的硬件资源。
type2host os 和 hypervisor 运行在不同的异常级别通过异常方式实现功能调用相对于 type1 效率更低为了解决这个问题arm8.1 增加了 VHE 拓展使得 host os 和 hypervisor 都可以运行在 el2 级别此时他们之间通过函数调用效率与 type1 基本相同。
2.1半虚拟化
全虚拟化是指虚拟机上 guest os 不需要修改任何代码其执行方式与真实硬件上完全相同但是由于每次 guest os 的每次 io 都需要返回 hypervisor中间涉及异常上下文切换等效率比较低。
半虚拟化为了提高虚机 IO 能力而实现通过修改 guest os 中驱动代码实现与 hypervisor 之间更高效的 IO 交互从而提升虚机整体性能。典型方案是 virtio定义了 guest os 驱动与 hypervisor 中模拟设备之间的 io 数据通信协议通过 channel 方式管理数据传输避免频繁的 io 操作。
虚拟化包含的内容
让每个虚拟机 vm 都拥有完整的硬件视角需要包含cpu内存io 设备中断控制器定时器等。因此 hypervisor 需要提供相关的虚机化实现如捕获到虚机 cpu 的敏感指令并为其提供模拟实现。vm 视角下的物理内存是 hypervisor 的虚拟内存因此 hypervisor 需要为其建立与实际物理内存的映射页表。捕获 vm 的 IO 访问并提供对应的模拟实现。
2 arm64 虚拟化相关硬件支持
2.1 arm64 cpu 虚拟化基本原理及硬件支持
cpu 一般包含至少两个运行级别用户级和特权级。用户级用于执行用户空间代码具有较低的执行权限特权级可执行操作系统内核代码具有较高的执行权限。当特权级执行特殊指令时触发异常并被 hyperviosr 捕获hypervisor 负责模拟或者处理该异常。比如 guest os 执行 wfiwfe 进入低功耗状态对于管理机来说不会让你占用硬件资源因此该类指令为敏感指令当geust os 执行时将会触发异常进入 hypervisorhypervisor 捕获该异常并将该 vcpu 调度出去由其他 vcpu 使用。
因此 armv8 在虚拟化中提供了敏感指令和异常的捕获功能可以通过配置 hypervisor 控制寄存器 hcr_el2 实现它将 vm 执行的敏感指令或异常路由到 el2 中hypervisor 捕获到相关异常后可为其模拟相关功能。
hcr_el2 相关 bit 描述如下
1FMO配置该位会将fiq中断路由到EL2 2IMO配置该位会将irq中断路由到EL2 3AMO配置该位会将serror异常路由到EL2 4TWI配置该位会将WFI指令路由到EL2 5TWE配置该位会将WFE指令路由到EL2 6TGE该位只有在支持VHE的架构下才有效。当配置该位后所有需要路由到EL1的异常都会被路由到EL2。因此只要设置该位后不再设置以上这些位其对应的异常也会被路由到EL2下
2.2 系统寄存器捕获和虚拟寄存器支持
有些系统寄存器的值在 vmm 和 guest os 视角下并不相同如 ID_AA64MMFR0_EL1 寄存器用于提供 cpu 支持的内存特性其支持的最大物理地址范围支持的 ASID 位数大小端支持情况等。对于这类寄存器每个 guest os 可以拥有其自身的属性值因此 vmm 需要通过异常捕获 guest os 对其的访问并在异常处理流程中为其提供一个虚拟值。
这类寄存器的访问频率并不高因此捕获异常对性能影响有限。而对于高频访问的寄存器如 MIDR_EL1MPIDR_EL1如果使用异常方式则对 vm 性能造成很大影响。
因此 arm 为该类问题提供了优化即为该类寄存器专门提供了虚拟化版本如为 MIDR_EL1 定义了 VMIDR_EL2为MPIDR_EL1定义了VMPIDR_EL2。vmm 在切换到 vcpu 之前将其值写入这些虚拟寄存器中当 guest os 访问 MIDR_EL1 时其实际获取到的即为 VMIDR_EL2 中的值。通过这种方式guest os 对这类寄存器的访问将不再需要触发异常。
2.3 VHE 特性支持
通过上面提到的方式可以解决特殊指令等访问效率问题但对于 kvm type2 类型的 hypervisor 设计来说hypervisor 只是 host os 的一个组件由于 host os 本身是一个操作系统linux他被定义运行在 el1。
若需要让其在 el2 运行代码上会改动非常多如在 linux 中使用 vbar_el1 访问异常向量表基地址寄存器此时需要修改为 vbar_el2。并且 el1 支持两个页表基地址寄存器 ttbr0_el1 和 ttbr1_el1而在 armv8.0 的 el2 异常等级中只包含一个 ttbr0_el2。
因此为了不修改 host os 本身代码最初的 type2 方案中的 host os 被设计为运行在 el1而 hypervisor 作为它的一个模块运行在 el2。
此时由于 hypervisor 和 host os 位于不同异常级别因此它们之间的调用都需要通过异常完成该流程需要执行上下文的保存和恢复因此会影响 hypervisor 的效率为了解决该问题arm 在 armv8.1 架构之后增加了 VHE 特性从而使得 host os 不经过改动就能运行在 el2 中。
VHE 主要是为了支持将 host os 运行在 EL2 中以提高 hypervisor 和虚拟机之间的切换代价。
其主要包含以下特性
1vhe 扩展了 EL2 的内存映射能力。在不支持 vhe 的 armv8.0 中 EL2 只有一个页表基地址寄存器 ttbr0_el2因此其能映射的地址范围为0x0000 0000 0000 0000 – 0x000f ffff ffff ffff。
而 linux 内核要求的地址映射范围为 0xfff0 0000 0000 0000 – 0xffff ffff ffff ffff且由于 EL0 和 EL1 都支持通过 ASID 将地址与进程关联以避免在进程切换时刷新 tlb从而减少进程切换的开销而 armv8.0 的 EL2 并不支持 ASID因此若不进行改造EL2 在页表层级并不支持运行 host os。
为此 vhe 特性增加了 ttbr1_el2 寄存器以及对 asid 的支持从而使 EL2 具有了与 EL0 和 EL1 相同的页表能力。
2由于 os 内核如 linux被设计为访问 EL1异常等级下的系统寄存器因此为了在不修改代码的情况下使其实际访问 EL2 寄存器vhe 实现了寄存器重定向功能。即在使能 vhe 之后所有 EL1寄存器的访问操作都被硬件转换为对相应 EL2 寄存器的访问如 OS 访问ttbr0_el1寄存器则会按下图方式被重定向到 ttbr0_el2。
el2: msr ttbr0_el1, x0E2H 1 - ttbr0_el2E2H 0 - ttbr0_el1这里还有一个问题hypervisor 实际上还是有访问实际 el1寄存器的需求因此 vhe 也对其做了扩展当访问实际 el1寄存器时则需要使用新的特殊指令。
el2: msr ttbr0_el2, x0E2H 1 - ttbr0_el13当支持 vhe 且 hcr.tge 被设置后不管hcr_el2.imo/fmo/amo是否被设置所有 EL0 和 EL1 的异常都会被路由到 EL2 中。
有了以上扩展之后host os 就可以不经任何修改而像运行在 EL1 中一样运行于 EL2 中。
2.4 内存虚拟化支持
首先有一下几个地址范围定义
1HVAhost virtual addresshost视角下的虚拟地址 2HPAhost physical addresshost视角下的物理地址 3GVAguest virtual addressguest视角下的虚拟地址 4GPAguest physical addressguest视角下的物理地址 5IPAintermediate physical address它与gpa含义相同是arm的对guest物理地址的一种命名方式
gva - translation table 1 - gpa - translation table 2 - hpaguest os 通过 stage1 访问物理地址IPA硬件通过 stage2 映射访问实际物理地址PA影子页表
不支持硬件虚拟化的系统中由于只有一个 mmu若其被 guest os 用于 GVA - GPA 的转换则由于无法访问到 HPA从而导致内存访问失败。为了解决 GPA - HPA 之间的内存转换引入影子页表。
影子页表是 hvpervisor 将 GVA - GPA 和 GPA - HPA 两级页表转换关系合并成一张表以实现 guest 内存访问的一种机制。
1hypervisor 为每个 vm 中的每个进程都维护一张合并了 GVA–GPA 和 GPA–HPA 两级页表关系的 shadow 页表。当 guest os 执行页表切换操作时hypervisor 将截获该操作并用这张合并后的页表替换掉 guest os 本身的页表。它就像影子一样覆盖掉了 guest os 的页表因此其被称为影子页表
2guest os 在自身页表中建立 GVA 到 GPA 的映射关系
3当 guest os 通过 GVA 访问内存时若该 GVA 在 shadow 页表中已建立则可正常通过 MMU 访问内存否则会引起缺页异常
4由于 hypervisor 在页表替换时知道被替换 guest os 页表的基地址因此在缺页异常处理流程中可通过遍历其对应的页表查询 GVA 对应的 GPA
5GPA 是在虚拟机初始化内存条时设置并与一段 host 中的用户态虚拟地址 HVA 绑定因此 hypervisor 可以通过 GPA 获取其对应的 HVA
6此时可通过 host 内存管理模块的 HVA 获取到其对应的 HPA
7最后对以上流程进行合并计算得到 GVA–HPA 之间的关系并将其填到影子页表中
显然以上流程中页表创建的开销是比较大的因此对虚拟机的性能会有比较大的影响。为此硬件引入了两级页表通过第一级页表完成 GVA–GPA 的转换并且通过第二级页表完成 GPA–HPA 的转换从而提高了地址转换的效率。
Armv8 两级页表
在硬件支持两级页表后不需要 hypervisor 执行页表合并这个复杂操作。一级页表由 guest os 创建用于 GVA - GPAIPAstage 1的换转而二级页表由 hypervisor 创建用于执行 GPAIPAstage 2 - HPA 转换同样对 guest os 是透明的。
与 ASID 类似armv8 为每个虚机提供了 VMID用于在 TLB 中标识地址是属于哪个虚机的。有了 VMID 之后当 cpu 执行虚机切换操作时就不需要失效 tlb 中的内容当下次该 vm 再次被切换回来时若 tlb 中包含了该 vm 先前的 entry则这些 entry 依然有效因此可以提高虚机切换的效率和运行性能。
由于 tlb entry 中每个 vm 含有自身的 VMID tag而每个 VM 中的每个进程又含有对应的 ASID tag因此在实际的页表转换时需要将它们进行合并。
同样由于 stage 1 页表和 stage 2 页表都可以设置各自的属性因此在实际页表转换时 MMU 需要选择两级页表中访问限制更严格的属性如
stage 1devicestage 2 normal此时选择 device。2.5 IO 虚拟化支持
虚机除了模拟物理内存空间还需要模拟外设的地址空间以用于其对模拟设备的访问。由于虚机视角的物理地址是 IPA因此 IPA 需要提供虚机对外设的访问能力。对于物理地址只需要为 IPA 分配实际的物理页框并为其建立 stage 2 页表即可而对于 IO 地址的目的是为了操作设备的功能因此需要将其与设备操作相关联。
如对于一个 vm 直通的外设IPA 地址与实际设备的 IO 地址建立页表即可使 VM 具备了对实际设备的控制能力。而在虚拟化系统中大部分设备都并不是某一个 VM 独享的这种情况下 hypervisor 需要捕获 VM 的 IO 地址访问操作并根据 IO 访问信息模拟相关设备的能力。此时不需要为这些 IO 建立 stage 2 映射而是当 vm 访问这些地址时就会产生缺页异常vmm 再根据 HPFAR_EL2 寄存器获取到异常地址并根据 ESR_EL2 获取产生异常原因最后再根据这些信息执行实际的设备模拟流程。
2.6 DMA 虚拟化支持
系统除了 cpu 外dma 也是需要访问内存的 master 设备。在 guest os 视角中只能感知 IPA 地址存在因此 guest 操作 DMA 时会以 IPA 地址作为 DMA 数据搬移的源地址和目的地址。
由于 IPA 地址并非总线上的实际物理地址因此若直接执行 DMA 操作会导致地址访问错误。同样若没有硬件拓展vmm 可以捕获所有 DMA 控制器的 IO 操作当捕获到 DMA 地址设置操作时vmm 为 dma 地址分配实际物理内存并将物理地址写入实际的 DMA 寄存器同时建立 IPA 到 HPA 的内存映射。
上述方法效率低从而引入 SMMU通过 SMMU 为 master 设备创建 stage 2 映射从而使 DMA 可以通过 IPA 直接看到 PA。
vmm 只需要为 DMA 创建 SMMU 页表将其 IPA 转换为 PA 即可实现 DMA 的内存访问操作。
2.7 中断虚拟化支持
如果没有硬件支持中断虚拟化时此时 guest os 正常的初始化中断控制器异常向量表注册中断函数。vmm 需要为 vm 模拟一个虚拟的中断控制器捕获 guest os 中断相关的设置并将其转发给虚拟中断控制器处理。若虚拟中断与物理中断关联还需要根据与实际中断的映射关系获取其对应的物理中断信息并将其设置到物理中断控制器中。
当中断触发时首先由 hypervisor 捕获物理中断然后根据其物理中断与虚拟中断的映射关系设置虚拟中断控制器的状态信息以向 vcpu 注入中断。在切换到 vcpu 之前 hypervisor 检查虚机中断是否被触发并确定是否需要跳转到 vcpu 的中断处理入口。
为了提高效率和简化虚拟中断注入流程GIC 在硬件层面提供了虚拟中断注入功能。如 GICv3 提供了一组 list register寄存器用于hypervisor为vcpu注入虚拟中断并且为vcpu在硬件层面提供了vIRQ和vFIQ中断信号用于响应注入到list register中的中断。最后GICv3还为vcpu提供了一组虚拟cpu interface寄存器vcpu可以像处理普通中断一样操作这组寄存器完成中断信息读取、应答和结束等流程。
在增加硬件支持以后armv8架构虚拟中断的流程就会变为如下这个样子
1hypervisor 为 GICv3 模拟虚拟 distributor 和虚拟 redistributor。由于虚拟cpu interface已经由硬件支持因此不再需要hypervisor模拟
2guest os的中断配置信息由hypervisor捕获并发送给虚拟distributor和虚拟redistributor处理
3hypervisor捕获实际的物理中断查找其对应的虚拟中断信息并将虚拟中断信息写入list register寄存器中以向vcpu注入虚拟中断
4hypervisor调度中断对应的vcpu运行由于virq/vfiq已经被触发vcpu进入中断处理流程。此时其对cpu interface的操作实际上都会被GICv3转换为对虚拟cpu interface的操作并直接作用于GIC硬件。
2.8 定时器虚拟化支持
Armv8架构的每个cpu都包含一个generic timer定时器该定时器可以通过比较器与系统计数器比较以确定定时器是否到期。当定时器到期时可以产生一个PPI中断以触发定时器事件。
但是在支持虚拟化之后由于物理cpu是被vcpu共享的因此它们可能需要在物理cpu上交替运行。
对于交替运行的两个 vcpu定时器如何共享
为了解决该问题armv8为 vm 同时提供了虚拟定时器和物理定时器。
Physical Count Virtual offsetcntpct_el0 cntoff_el2Virtual Count(cntvct_el0)其中虚拟计数器提供了一个与物理计数器之间的 offset 偏移量该值可以由 vmm 进行设置。当不同 vcpu 运行时为其设置不同的 offset。
3 arm64 kvm 初始化流程
kvm是基于linux内核实现的一种type 2虚拟化方案它作为内核的一个模块负责虚拟化环境初始化虚拟机和虚拟cpu模拟以及IO捕获与转发等功能。在kvm中虚拟机和虚拟cpu分别通过host os的进程和线程实现并且由host os的调度器对其进行调度。
除了像中断控制器之类的关键设备kvm 不会模拟设备工作因此它通常与 qemu 结合使用。由 qemu 执行实际的 IO 设备模拟以及虚拟机创建和参数配置功能。kvm 提供 ioctl 向用户导出一组虚拟机管理接口。
3.1 初始化总体流程
kvm 初始化的主要目的是为虚拟机的创建和运行提供必要的软硬件环境总体流程如下
arm_init - kvm_init- kvm_arch_init- kvm_irqfd_init- alloc_workqueue- cpuhp_setup_state_nocalls- kvm_starting_cpu- kvm_dying_cpu- register_reboot_notifier- blocking_notifier_chain_register- kmem_cache_create_usercopy- misc_register- register_syscore_ops- kvm_init_debug- debugfs_create_dir- kvm_vfio_ops_init- kvm_register_device_ops可以看到 kvm 初始化流程主要包括以下几个部分
1架构相关的初始化
2电源管理接口注册回调处理kvm在电源管理流程中的行为
3为 kvm 注册字符设备提供 ioctl 接口
4其他一些辅助功能
1电源管理接口注册回调
可以热插拔的 cpu 和休眠的系统上cpu可以进行 online/offline 的状态转换。
kvm_syscore_ops提供了系统挂起和恢复的回调。
register_reboot_notifier提供了在系统reboot后下线时的通知回调这里对应调用架构自己实现的 kvm_arch_hardware_disable。
而cpuhp_setup_state_nocalls则提供了cpu重新上线/下线的回调上线则对应 reboot 时的kvm_arch_hardware_enable
下线同样对应kvm_arch_hardware_disable。
2ioctl 接口注册
kvm 的 ioctl 接口注册在 misc_register 中完成提供了kvm_dev_ioctl回调接口。
对应的该 ioctl 接口可以分为三个类型kvm ioctlvm ioctlvcpu ioctl。
kvm ioctl 全局控制 kvmvm ioctl 对英语特定 vm 虚机控制vcpu ioctl 特定于 vcpu 控制。
vm ioctl 通过全局的 KVM_CREATE_VM 命令创建一个 vm fd并且绑定匿名 inode该匿名 inode 对应 kvm_vm_fops ioctl 回调。
vcpu ioctl 通过上面的 vm ioctl 命名 KVM_CREATE_VCPU 命名创建一个 vcpu fd并绑定匿名 inode该匿名 inode 对应 kvm_vcpu_fops ioctl 回调。
3其他一些辅助功能
kvm_irqfd_init为 eventfd 创建了一个全局的工作队列它用于在虚机被关闭时关闭所有与其相关的 irqfd并等待该操作完成。kvm_init_debug用于为 kvm 创建 debugfs 相关接口kvm_vfio_ops_init用于为 vfio 注册设备回调函数
4架构相关初始化 kvm_arch_init
armv8的虚拟化方案有两种方式nvhe 和 vhe。
vhe 实现虚拟支持 VHE 拓展该实现中 host os 和 guest os 都运行在 el2此时 host os 和 hypervisor 公用所有的 el2 寄存器且 host 可以直接通过函数调用的方式调用hypervisor接口因此 vhe 模式下的 kvm 模块初始化流程更简单主要包括一些 host context 初始化以及虚拟 gic 和 timer 的初始化。
nvhe 实现相对则是更早期的实现只需要硬件支持虚拟化即可不需要 vhe 拓展。nvhe 的 host os 和 hypervisor 运行在不同异常级别hypervisor 处于 el2host os 处于 el1。比如 host os 需要通过异常hvc的方式进入 hypervisor并且为 el2 的 hypervisor 独立设置异常处理函数映射代码段数据段等因此初始化相对复杂见下面。
3.2 aarch64 架构初始化总体流程
kvm_arch_init- is_hyp_mode_available- is_kernel_in_hyp_mode- !in_hyp_mode kvm_arch_requires_vhe- check_kvm_target_cpu- kvm_target_cpu- init_common_resources- kvm_get_vmid_bits- kvm_set_ipa_limit- init_hyp_mode (nvhe 模式 true)- kvm_mmu_init- per_cpu(kvm_arm_hyp_stack_page, cpu) stack_page;- create_hyp_mappings- kvm_map_vectors- init_subsystems- _kvm_arch_hardware_enable- hyp_cpu_pm_init- kvm_vgic_hyp_init- kvm_timer_hyp_init- kvm_perf_init- kvm_coproc_table_init1is_hyp_mode_available由于管理机需要运行在 el2所以这里通过判断启动是否是在el2来判断是否支持hypervisor可以参考之前的 arm64 启动流程分析中关于 el2_setup 的解析。
2is_kernel_in_hyp_mode 判断当前所处的异常级别判断自己是否支持 vhe如果支持 VHE 那么 host 此时是处于 el2 的那么 is_kernel_in_hyp_mode 返回 ture。
3!in_hyp_mode kvm_arch_requires_vhe如果没有支持 vhe但是 kvm_arch_requires_vhe 表示我们需要 VHE 这个特性那么此处如果不成立则 kvm 不可用直接返回 -ENODEV。
4check_kvm_target_cpu该函数通过 smp_call_function_single ipi 在每个 cpu 上执行通过读取 cpu 的型号并判断是否合法。
5init_common_resources获取 vmid bits以及通过读取 SYS_ID_AA64MMFR0_EL1并根据支持的大小页判断当前系统配置的 page size 能否被硬件支持通过解析出能够支持的最大物理地址长度。
6init_hyp_mode如果没有支持 vhe那么则会进入该流程下面详细说明。
init_hyp_mode
1kvm_mmu_init
之前说过nvhe 模式 hypervisor 和 host os 处于不同级别此时我们处于 el1我们需要为 el2 的 hypervisor 创建页表映射
int kvm_mmu_init(void)
{int err;hyp_idmap_start kvm_virt_to_phys(__hyp_idmap_text_start);hyp_idmap_start ALIGN_DOWN(hyp_idmap_start, PAGE_SIZE);hyp_idmap_end kvm_virt_to_phys(__hyp_idmap_text_end);hyp_idmap_end ALIGN(hyp_idmap_end, PAGE_SIZE);hyp_idmap_vector kvm_virt_to_phys(__kvm_hyp_init);
...merged_hyp_pgd (pgd_t *)__get_free_page(GFP_KERNEL | __GFP_ZERO);if (!merged_hyp_pgd) {kvm_err(Failed to allocate extra HYP pgd\n);goto out;}__kvm_extend_hypmap(boot_hyp_pgd, hyp_pgd, merged_hyp_pgd,hyp_idmap_start);} else {err kvm_map_idmap_text(hyp_pgd);if (err)goto out;}io_map_base hyp_idmap_start;return 0;
...
}static int kvm_map_idmap_text(pgd_t *pgd)
{int err;/* Create the idmap in the boot page tables */err __create_hyp_mappings(pgd, __kvm_idmap_ptrs_per_pgd(),hyp_idmap_start, hyp_idmap_end,__phys_to_pfn(hyp_idmap_start),PAGE_HYP_EXEC);if (err)kvm_err(Failed to idmap %lx-%lx\n,hyp_idmap_start, hyp_idmap_end);return err;
}内核中类似 .hyp.idmap.text, ax __hyp_text 标记段则是需要被映射到 el2 的 hypervisor 的文本段并通过上述 __create_hyp_mappings 首先完成 idmap 映射其余的段会在后续继续在 hyp_pgd中建立映射。而创建完成的 hyp_pgd pgd 页表将会在 _kvm_arch_hardware_enable 中通过 kvm_call_hyp 调用一同传递到 el2
ENTRY(__kvm_call_hyp)
alternative_if_not ARM64_HAS_VIRT_HOST_EXTNhvc #0ret
alternative_else_nop_endifb __vhe_hyp_call
ENDPROC(__kvm_call_hyp)2hypervisor栈和percpu内存分配
该流程主要为 hypervisor所有物理cpu运行时分配对应的栈 /** Allocate stack pages for Hypervisor-mode*/for_each_possible_cpu(cpu) {unsigned long stack_page;stack_page __get_free_page(GFP_KERNEL);if (!stack_page) {err -ENOMEM;goto out_err;}per_cpu(kvm_arm_hyp_stack_page, cpu) stack_page;}同样的 kvm_arm_hyp_stack_page 在 cpu_init_hyp_mode 中通过 kvm_call_hyp 一并传递到 el2。
3create_hyp_mappings。。。在这里将会完成 hyp textdatabss vector段stack 的映射 /** Map the Hyp-code called directly from the host*/err create_hyp_mappings(kvm_ksym_ref(__hyp_text_start),kvm_ksym_ref(__hyp_text_end), PAGE_HYP_EXEC);if (err) {kvm_err(Cannot map world-switch code\n);goto out_err;}err create_hyp_mappings(kvm_ksym_ref(__start_rodata),kvm_ksym_ref(__end_rodata), PAGE_HYP_RO);if (err) {kvm_err(Cannot map rodata section\n);goto out_err;}err create_hyp_mappings(kvm_ksym_ref(__bss_start),kvm_ksym_ref(__bss_stop), PAGE_HYP_RO);if (err) {kvm_err(Cannot map bss section\n);goto out_err;}err kvm_map_vectors();if (err) {kvm_err(Cannot map vectors\n);goto out_err;}/** Map the Hyp stack pages*/for_each_possible_cpu(cpu) {char *stack_page (char *)per_cpu(kvm_arm_hyp_stack_page, cpu);err create_hyp_mappings(stack_page, stack_page PAGE_SIZE,PAGE_HYP);if (err) {kvm_err(Cannot map hyp stack\n);goto out_err;}}到这里 nvhe 模式的映射相关初始化完成。
init_subsystems
1_kvm_arch_hardware_enable有如下流程
cpu_hyp_reinit- cpu_hyp_reset- if is_kernel_in_hyp_mode- kvm_timer_init_vhe- else- cpu_init_hyp_mode- kvm_arm_init_debug- kvm_vgic_init_cpu_hardwarecpu_hyp_reset 如果host是 nvhe 模式则通过 hvc 调用HVC_RESET_VECTORS ENTRY(__hyp_stub_vectors)
...
...
el1_sync:
...
...
3: cmp x0, #HVC_RESET_VECTORSbeq 9f // Nothing to reset!/* Someone called kvm_call_hyp() against the hyp-stub... */ldr x0, HVC_STUB_ERReret9: mov x0, xzreret
ENDPROC(el1_sync)目前无事可做。 kvm_timer_init_vhe 如果支持 vhe 则在这里初始化 vhe timer:
/** On VHE system, we only need to configure trap on physical timer and counter* accesses in EL0 and EL1 once, not for every world switch.* The host kernel runs at EL2 with HCR_EL2.TGE 1,* and this makes those bits have no effect for the host kernel execution.*/
void kvm_timer_init_vhe(void)
{/* When HCR_EL2.E2H 1, EL1PCEN and EL1PCTEN are shifted by 10 */u32 cnthctl_shift 10;u64 val;/** Disallow physical timer access for the guest.* Physical counter access is allowed.*/val read_sysreg(cnthctl_el2);val ~(CNTHCTL_EL1PCEN cnthctl_shift);val | (CNTHCTL_EL1PCTEN cnthctl_shift);write_sysreg(val, cnthctl_el2);
}如果是 nvhe那么调用 cpu_init_hyp_mode
static void cpu_init_hyp_mode(void *dummy)
{phys_addr_t pgd_ptr;unsigned long hyp_stack_ptr;unsigned long stack_page;unsigned long vector_ptr;/* Switch from the HYP stub to our own HYP init vector */__hyp_set_vectors(kvm_get_idmap_vector());pgd_ptr kvm_mmu_get_httbr();stack_page __this_cpu_read(kvm_arm_hyp_stack_page);hyp_stack_ptr stack_page PAGE_SIZE;vector_ptr (unsigned long)kvm_get_hyp_vector();__cpu_init_hyp_mode(pgd_ptr, hyp_stack_ptr, vector_ptr);__cpu_init_stage2();
}后续的kvm_vgic_hyp_initkvm_timer_hyp_init等均首先在如 irq-gic-v3/arch_timer 的初始化中检测出与虚拟化相关的支持信息并在这里对应做虚拟化相关的硬件初始化未详细分析。
到这里kvm 的初始化流程基本结束后续就可以通过 ioctl 来创建虚机vcpu 了。
psarch 相关的初始化比如虚拟化 gictimer的初始化这里没有进行详细分析。
4 cpu虚拟化和 vcpu 运行流程
后续基于支持 VHE 特性的虚拟化进行分析目前大多数设备都支持 VHE。
linux cpu 上下文切换
在linux 中一个从一个任务切换到另一个任务主要通过 context_switch 进行切换其中涉及到了 mm 切换cpu 切换这主要看下 cpu 上下文切换直接看代码
/** Register switch for AArch64. The callee-saved registers need to be saved* and restored. On entry:* x0 previous task_struct (must be preserved across the switch)* x1 next task_struct* Previous and next are guaranteed not to be the same.**/
ENTRY(cpu_switch_to)mov x10, #THREAD_CPU_CONTEXTadd x8, x0, x10mov x9, spstp x19, x20, [x8], #16 // store callee-saved registersstp x21, x22, [x8], #16stp x23, x24, [x8], #16stp x25, x26, [x8], #16stp x27, x28, [x8], #16stp x29, x9, [x8], #16str lr, [x8]add x8, x1, x10ldp x19, x20, [x8], #16 // restore callee-saved registersldp x21, x22, [x8], #16ldp x23, x24, [x8], #16ldp x25, x26, [x8], #16ldp x27, x28, [x8], #16ldp x29, x9, [x8], #16ldr lr, [x8]mov sp, x9msr sp_el0, x1ret
ENDPROC(cpu_switch_to)
NOKPROBE(cpu_switch_to)将正在执行的任务1的 cpu 寄存器保存到其上下文中然后将任务2上一次调度时保存的上下文恢复到 cpu 寄存器中即可完成一次切换切换主要包括 x18-x28fpsppc 寄存器。至于为什么是这些寄存器是根据规范定义的调用约定决定的具体可以查阅相关资料。
vcpu entry
vcpu 的运行与进程上下文切换类似当 host os 需要切换到 vcpu 运行时就将 vcpu 对应的 guest 上下文恢复到对应的物理 cpu 中。当 guest 退出时则将 cpu 状态保存到 vcpu 上下文中。
不过实际上与任务切换还是存在一些区别。首先任务是 host os 内的管理实体拥有相同的系统寄存器配置切换时不需要改变系统寄存器的值。而 vcpu 上运行的是 guest os它所有的寄存器配置可能与 host os 不同因此在进行上下文切换时需要保存和恢复的寄存器更多比如包含系统寄存器等。
另一个差异是任务的切换都是在 host os 内核中而 guest os 与管理机运行在不同异常等级级别因此其切换流程还需要伴随着异常等级的改变。即切换前后都位于 os 所处的 el2 级别而切换后 guest os 处于 el1 级别。 补充host os 进入 guest os 是host在 el2 通过 eret 指令返回到 el1 实现具体可以查阅代码
vcpu exit
在不考虑 el3 安全拓展时非虚拟化环境下 host os 可以控制所有系统资料包括内存io 地址中断处理以及所有指令的执行。但在虚拟化环境下guest os 一般不能直接访问 io敏感指令如 wfiwfe以及物理中断等。
比如管理机host os kvm可以配置 guest 在执行到敏感指令时触发异常并且该异常被路由到 el2 的管理机中。同样的 io 地址在 stage 2 也表设置为不指向实际物理地址时则 guest os 一旦访问 io 地址则会触发缺页异常并该异常路由到 el2 的管理机中。
因此el2 管理机通过捕获这些异常就可以为 guest os 模拟相关操作并将模拟结果返回而 guest os 自身不感知这些行为。 补充guest os 退出回到 guest os 是 guest 发生了访问 io访问敏感指令中断等触发异常返回到 el2 vcpu 调度
上面说到 guest os 只有在触发异常产生中断时才会退出 guest os返回 host os处理完成后再次返回 guest os。在 vcpu 视角下除了管理机代理其处理了异常外物理cpu像被其独占。
实际上站在 host os 角度vcpu 只是 host os 创建的一个普通线程因此受到 host os 的调度器管理。当 vcpu 线程时间片用尽时则会被调度出去让出物理cpu。这里涉及两个上下文
1vcpu 所在的普通线程上下文
2vcpu guest 和 host 之前切换的上下文
在 schedule中有 fire_sched_in_preempt_notifiers 和 fire_sched_out_preempt_notifiers。vcpu 运行前向 preempt_notifiers 通知链注册一个通知该通知包含其线程应该被 sched in 和 sched out 时需要执行的回调。
当 vcpu 正在运行 guest 程序时host 触发 timer 中断并在中断中检测到需要调度程序。因此触发 schedule_irq。
调度器在调度前通过preempt_notifiers通知链中所有回调。
回调执行到 vcpu 切出操作在该操作中先保存 guest 上下文然后恢复 vcpu 对应线程的上下文到这里完成 vcpu 退出并且 vcpu 对应的线程也即将被调度出去。
当该 vcpu 对应线程再次被调度回来运行时则同样通过preempt_notifiers执行通知回调 vcpu entry恢复 vcpu 的 guest 上下文。
vcpu 生命周期
1vcpu创建并初始化完成后host的用户空间将通过KVM_RUN的ioctl进入内核空间切入guest运行。在切换流程中先将切前的cpu寄存器保存到host上下文中然后将guest上下文恢复到cpu寄存器中
2物理cpu切换到guest状态开始执行guest指令
3当guest遇到敏感指令或IO操作等需要切换回host处理的流程时将退出guest
4host根据退出原因判断其应该是由内核态还是用户态处理
5若其由内核态处理则直接执行处理流程并在处理完成后重新切入guest。guest则从上次被中断的位置继续向下运行
6若其需要用户态执行则需要先切换到用户态。用户态执行完相关流程后再次通过ioctl进入内核态并切入guest运行
7vcpu的运行流程即是以上这些步骤的循环往复直到虚拟机结束执行为止
vcpu创建流程
入口在 vm ioctl KVM_CREATE_VCPU 中
kvm_vm_ioctl_create_vcpu- kvm_arch_vcpu_create- kvm_vcpu_init- kvm_timer_vcpu_init- kvm_arm_reset_debug_ptr- kvm_vgic_vcpu_init- create_hyp_mappings- preempt_notifier_init- kvm_create_vcpu_debugfs- create_vcpu_fd- kvm_arch_vcpu_postcreate1preempt_notifier_initvcpu 的 preempt_notifiers用于任务的 vcpu 切换。
2create_vcpu_fd创建 vcpu ioctl 相关
3kvm_vcpu_init主要逻辑在这里
对应架构相关的 vcpu 创建初始化流程主要初始化一些与 cpu 相关的硬件状态如 timerpmuvgic 的 redistributor和 sgippi 中断等。
vcpu 的运行
vcpu 在运行时需要处理中断信号pmutimer等状态这里主要分析一些重要流程入口是 vcpu ioctl kvm_arch_vcpu_ioctl_run
kvm_arch_vcpu_ioctl_run- kvm_vcpu_first_run_init- kvm_handle_mmio_return- vcpu_load- preempt_notifier_register- kvm_arch_vcpu_load- kvm_vgic_load- kvm_timer_vcpu_load- kvm_vcpu_load_sysregs- kvm_arch_vcpu_load_fp- update_vttbr- check_vcpu_requests- guest_enter_irqoff- kvm_arm_vhe_guest_enter- kvm_vcpu_run_vhe- sysreg_save_host_state_vhe- __activate_vm- __activate_traps- sysreg_restore_guest_state_vhe- do {/* Jump in the fire! */exit_code __guest_enter(vcpu, host_ctxt);/* And were baaack! */} while (fixup_guest_exit(vcpu, exit_code));- sysreg_save_guest_state_vhe- __deactivate_traps- sysreg_restore_host_state_vhe- kvm_arm_vhe_guest_exit- guest_exit- handle_exit_early- handle_exit (如果 host os 能够处理则不用退回到 user 去直接返回 update_vttbr 继续准备进入 vcpu 执行)- vcpu_put5 arm64 内存虚拟化流程
内存虚拟化基本概念 Guest需要一块或者多块物理内存并且物理内存地址由 guest 自己指定并且为了实现多任务和内存权限管理还能够通过页表实现虚拟地址和物理地址的转换。 Host新增一级页表可以将 guest 所看到的物理地址映射到 host 实际的物理地址上。 Armv8硬件通过两级页表映射实现该机制。
guest hyp [host os] phys虚拟地址 - stage1 table - [IPA] - stage2 table - [PA]根据上述流程有如下命名
1GVAguest 视角下的虚拟地址
2GPAguest 视角下的物理地址host 视角下的 IPA 地址
3HPAhost 视角下的物理地址
4HVAhost 视角下的虚拟地址
host只需要管理和维护 HVA 到 GPA 的关系即可不需要像影子页表一样自己维护 mmu 映射。
HVA 到 GPA 触发映射流程如下
当 guest 建立虚拟地址到物理地址映射时GVA - GPA由于实际上 host 没有给 guest 的 GPA 分配实际的物理地址因此 guest 一旦访问 GPA 时cpu 则会触发缺页异常该异常将被 host 捕获。
此时 hyphost os根据异常开始为 HVA 到 GPA 建立一个维护数据并为其实际映射物理地址stage2映射完成返回到 guest 后guest 即可顺利访问实际映射的物理地址。
虚拟机内存设置
为虚拟机分配 GPA 内存是通过 host 的 vm ioctl 发起完成的因此 HVA 在这里实际是用户态程序qemu的虚拟地址。为了描述它们之间的关系kvm 定义了如下结构
/** Note:* memslots are not sorted by id anymore, please use id_to_memslot()* to get the memslot by its id.*/
struct kvm_memslots {u64 generation;struct kvm_memory_slot memslots[KVM_MEM_SLOTS_NUM];/* The mapping table from slot id to the index in memslots[]. */short id_to_index[KVM_MEM_SLOTS_NUM];atomic_t lru_slot;int used_slots;
};struct kvm_memory_slot {gfn_t base_gfn;unsigned long npages;unsigned long *dirty_bitmap;struct kvm_arch_memory_slot arch;unsigned long userspace_addr;u32 flags;short id;
};用户态程序通过 kvm_vm_ioctl_set_memory_region 设置一块 guest 内存描述到 struct kvm_memory_solt 中。
kvm_vm_ioctl_set_memory_region- kvm_set_memory_region- __kvm_set_memory_region一块内存描述如下
/* for KVM_SET_USER_MEMORY_REGION */
struct kvm_userspace_memory_region {__u32 slot;__u32 flags;__u64 guest_phys_addr;__u64 memory_size; /* bytes */__u64 userspace_addr; /* start of the userspace allocated memory */
};内存设置完成后并没有建立实际的 stage2映射当 guest 访问 IPA 会触发缺页异常此时 host 将会实际建立 stage2 映射流程如下
__kvm_hyp_vector- el1_sync- __guest_exit- handle_exit- handle_trap_exceptions (ARM_EXCEPTION_TRAP)- kvm_get_exit_handler- kvm_handle_guest_abort// io abort- io_mem_abort- user_mem_abort// 流程很多没有详细分析- ...6 arm64 中断虚拟化
一般 kvm 不模拟设备不过中断控制器和timer由内核完成了模拟它们处于虚拟运行的关键路径上armv8 目前最常用的中断控制器是 GICv3它支持中断虚拟化以及额外中断虚拟化拓展包括安全拓展vcpu interface 虚拟化。gicv3 布局大致如下
外设将中断电平线连接到 GICv3cpu 可以通过GICv3的寄存器设置中断的触发方式优先级以及路由方式等属性。当中断被触发后则通过 cpu 与 gic 之间的中断信号线 IRQ 或者 FIQ 通知 CPU。随后cpu 通过中断向量表进入中断处理流程在中断处理完成后通过 GIC 的 EOI 寄存器通知中断处理完成GIC内部修改其状态机进行下一次的中断分发。
备注使用 irq 还是 fiq 根据 gic 配置决定大致有如下配置
即 group0 的中断都是 fiqel3 的都是 fiq 中断另外 安全世界的 el1/el0如果gic配置为se group1 则是 irqno se group1 是 fiq。
非安全世界 el0/el1/el2se group1 是fiqno se group1 是 irq。linux 处于 non secure el1/el2gicv3 为 non-serucre group1 所有 linux 收到的都是 irq 中断。
除了 IRQ 和 FIQCPU 和 GIC 之间还有 vIRQ 和 vFIQ。它们是GICv3为了支持虚拟化拓展而设计的主要目的是用于向 vcpu 发送中断信号。
gic 组件
为了实现中断的配置接收仲裁和路由功能GICv3设计了如下组件包含 SPIPPISGI 和 LPI 四种中断类型以及分发器重分发器ITS 和 cpu interface 四大组件关系如下
分发器主要包含两部分功能
1作为可编程接口可以配置 GICD 寄存器对中断控制器的全局属性以及 SPI 类型中断属性进行配置
2根据配置可以设定中断分组优先级以及亲和性并且将 SPI 和 SGI 中断路由到特定的重分发器和 cpu interface 上。
重分发器位于分发器和cpu interface之间包含如下两部分功能
1可以配置GICR寄存器对 PPI 和 SGI 类型中断属性进行配置设定中断触发类型中断使能以及中断优先级等还包括了电源管理LPI中断管理。
2能将最高优先级的 pending 中断发送到与其对应的 cpu interface 上。
cpu interface 可以用于物理中断虚拟中断处理以及可为管理机提供虚拟机控制接口包括 SGI 中断产生PPISGI 的优先级设置最高 pending优先级读取以及应答deactivate完成操作执行等。
ITS 是 gicv3 的高级特性作用是将一个来自设备的输入 eventID转换为 LPI 中断号并确定该中断将被发送的目的 PE。
虚拟中断原理
虚拟中断的实现需要cpu和gic共同配合首先GICv3提供了一组名为list registerLR的寄存器host可以通过将中断信息写入该寄存器来触发虚拟中断。其次cpu在原先irq和fiq两条中断线的基础上又增加了两条虚拟中断线virq和vfiq以用于虚拟中断的接收。
在虚拟中断被触发后guest需要通过cpu interface执行中断的应答、结束等操作。为了提高中断处理效率GICv3又在硬件上支持了一组虚拟cpu interface寄存器。使guest可以直接操作这些寄存器从而避免了需要host模拟cpu interface设备
由于物理设备的中断通常都需要由host处理因此若guest的中断与物理中断关联则应该先路由给host处理。armv8可以通过hcr_el2寄存器配置该能力 1若未使能vhe可通过设置hcr_el2.FMO和hcr_el2.IMO以将EL1的guest中断路由到EL2 2若使能了vhe则可通过设置hcr_el2.TGE将EL1的guest中断路由到EL2
Host执行相应处理后就可以通过设置lr寄存器将其以虚拟中断的方式注入到给guest。下图为其总体框图 1外设触发 irq 中断gic 的分发器接收中断通过分发器仲裁确定应该发送到 cpu
2target cpu 确定以后将中断发送给该 cpu 的重分发器
3重分发器将中断发送到其对应的 cpu interface 上
4该中断被路由到 el2 的管理机中断入口
5管理机不处理该中断但是触发异常 vcpu exit退回到 host在host handle exit 中重新使能该中断中断再次触发并有 host 中断处理函数进行中断处理
6host 通过写 lr 向 guest 注入虚拟中断并切入 guest 执行
7虚拟中断触发 virq并进入 guest 的中断入口函数
8guest 执行中断处理流程并通过虚拟 cpu interface 执行中断应答和 EOI 操作。
9若该虚拟中断与物理中断关联则 EOI 实际是到实际的物理中断上。
10中断处理完成。
更详细的例子
根据之前 cpu 进入 guest 的流程
kvm_arch_vcpu_ioctl_run- __kvm_vcpu_run_vhe- __guest_enter在 __kvm_vcpu_run_vhe 之前有 __activate_traps操作
static void activate_traps_vhe(struct kvm_vcpu *vcpu)
{u64 val;val read_sysreg(cpacr_el1);val | CPACR_EL1_TTA;val ~CPACR_EL1_ZEN;if (!update_fp_enabled(vcpu)) {val ~CPACR_EL1_FPEN;__activate_traps_fpsimd32(vcpu);}write_sysreg(val, cpacr_el1);write_sysreg(kvm_get_hyp_vector(), vbar_el1);
}static inline void *kvm_get_hyp_vector(void)
{return kern_hyp_va(kvm_ksym_ref(__kvm_hyp_vector));
}也就是说将 guest 的异常处理向量表设置为了 host的 __kvm_hyp_vector那么当 guest 触发中断后会进入 __kvm_hyp_vector而在这个里面根据之前的流程知道会执行 __guest_exit那么返回到__guest_enter后面的代码并且通过 __deactivate_traps恢复原来的异常向量表并且后续流程有local_irq_enable();由于中断实际未被处理电平信号存在再次触发中断此时进入实际的中断处理中处理中断。
那么此时物理中断如何调用到guest虚拟中断的中断处理函数以 arch timer 为例
kvm_timer_hyp_init- err request_percpu_irq(host_vtimer_irq, kvm_arch_timer_handler,kvm guest timer, kvm_get_running_vcpus());
kvm_arch_timer_handler- kvm_timer_update_irq- kvm_vgic_inject_irq- vgic_queue_irq_unlock- list_add_tail(irq-ap_list, vcpu-arch.vgic_cpu.ap_list_head);处理完成后vcpu 继续运行进入下一次 vcpu 循环此时有
kvm_vgic_flush_hwstate- vgic_flush_lr_state- vgic_populate_lr- vgic_v3_populate_lr- vcpu-arch.vgic_cpu.vgic_v3.vgic_lr[lr] val;将 ap_list 中上的虚拟中断写到 vgic_lr 寄存器context中并且再次切换回 guest 流程中此时由于 lr 寄存器写入了中断信息gic 会确保使 virq 中断线触发guest将实际触发该中断并开始guest的中断处理。
除了硬件中断触发上述流程以外host 还可以主动向 LR 注入虚拟中断。这种用于模拟设备的触发中断以及 vcpu 之间发送 ipi 中断。
另一方面由 guest 发起的配置 cpu interface分发器重分发器流程
GICv3 只实现了 cpu interface 虚拟化那么 guest 配置 cpu interface可以直接配置但是GICv3没有实现分发器和重分发器的虚拟化所以需要 host 来实现 分发器和重分发器的模拟当 guest 访问 GICv3的分发器和重分发器时将会触发 IO 异常并返回 host由host 来执行对应寄存器的模拟工作。
GICv3 只实现 cpu interface 虚拟化理由很简单cpuinetface 直接和 cpu 硬件模块连接cpu 可以直接通过 msrmrs 访问系统寄存器可以加快 cpu interface 寄存器的访问因此中断触发cpu 需要频繁访问 GICC_SGRI 相关寄存器来读取中断号写 EOI 结束中断处理为了提高性能是有必要去实现虚拟化的而分发器和重分发器只有在初始化配置时会访问没有必要去实现虚拟化。
arm64 kvm中vgic 分发器和重分发器lr寄存器组件的模拟实现
分发器
分发器和重分发器的虚拟设备模拟在内核中实现但是 io 基地址分发器重分发器基地址中断数量需要用户空间配置因此也是通过匿名inode来实现控制这些配置的。
路径在 vm ioctl 的 kvm_ioctl_create_device中。
static int kvm_ioctl_create_device(struct kvm *kvm,struct kvm_create_device *cd)
{struct kvm_device_ops *ops NULL;struct kvm_device *dev;bool test cd-flags KVM_CREATE_DEVICE_TEST;int ret;if (cd-type ARRAY_SIZE(kvm_device_ops_table))return -ENODEV;ops kvm_device_ops_table[cd-type];if (ops NULL)return -ENODEV;if (test)return 0;dev kzalloc(sizeof(*dev), GFP_KERNEL);if (!dev)return -ENOMEM;dev-ops ops;dev-kvm kvm;mutex_lock(kvm-lock);ret ops-create(dev, cd-type);if (ret 0) {mutex_unlock(kvm-lock);kfree(dev);return ret;}list_add(dev-vm_node, kvm-devices);mutex_unlock(kvm-lock);if (ops-init)ops-init(dev);kvm_get_kvm(kvm);ret anon_inode_getfd(ops-name, kvm_device_fops, dev, O_RDWR | O_CLOEXEC);if (ret 0) {kvm_put_kvm(kvm);mutex_lock(kvm-lock);list_del(dev-vm_node);mutex_unlock(kvm-lock);ops-destroy(dev);return ret;}cd-fd ret;return 0;
}主要根据 kvm_device_ops_table 来决定是创建哪一类设备有如下设备可用
enum kvm_device_type {KVM_DEV_TYPE_FSL_MPIC_20 1,
#define KVM_DEV_TYPE_FSL_MPIC_20 KVM_DEV_TYPE_FSL_MPIC_20KVM_DEV_TYPE_FSL_MPIC_42,
#define KVM_DEV_TYPE_FSL_MPIC_42 KVM_DEV_TYPE_FSL_MPIC_42KVM_DEV_TYPE_XICS,
#define KVM_DEV_TYPE_XICS KVM_DEV_TYPE_XICSKVM_DEV_TYPE_VFIO,
#define KVM_DEV_TYPE_VFIO KVM_DEV_TYPE_VFIOKVM_DEV_TYPE_ARM_VGIC_V2,
#define KVM_DEV_TYPE_ARM_VGIC_V2 KVM_DEV_TYPE_ARM_VGIC_V2KVM_DEV_TYPE_FLIC,
#define KVM_DEV_TYPE_FLIC KVM_DEV_TYPE_FLICKVM_DEV_TYPE_ARM_VGIC_V3,
#define KVM_DEV_TYPE_ARM_VGIC_V3 KVM_DEV_TYPE_ARM_VGIC_V3KVM_DEV_TYPE_ARM_VGIC_ITS,
#define KVM_DEV_TYPE_ARM_VGIC_ITS KVM_DEV_TYPE_ARM_VGIC_ITSKVM_DEV_TYPE_MAX,
};当我们创建 gicv3 设备时,kvm_arm_vgic_v3_ops回调将被调用
struct kvm_device_ops kvm_arm_vgic_v3_ops {.name kvm-arm-vgic-v3,.create vgic_create,.destroy vgic_destroy,.set_attr vgic_v3_set_attr,.get_attr vgic_v3_get_attr,.has_attr vgic_v3_has_attr,
};vgic_v3_set_attr- vgic_set_common_attr
static int vgic_set_common_attr(struct kvm_device *dev,struct kvm_device_attr *attr)
{int r;switch (attr-group) {case KVM_DEV_ARM_VGIC_GRP_ADDR: {u64 __user *uaddr (u64 __user *)(long)attr-addr;
...case KVM_DEV_ARM_VGIC_GRP_NR_IRQS: {u32 __user *uaddr (u32 __user *)(long)attr-addr;
...case KVM_DEV_ARM_VGIC_GRP_CTRL: {
...}return -ENXIO;
}
vgic_v3_set_attr可以配置分发器的基地址支持的中断数量初始化分发器。
分发器配置完成后还需要将其对应抽象的 io设备和 io region 注册到上面提到的 kvm_bus 总线管理中用于实际的 guest 访问设备模拟设备操作这个会在首次运行 vcpu 时被调用
kvm_arch_vcpu_ioctl_run- kvm_vcpu_first_run_init- kvm_vgic_map_resources- vgic_v3_map_resources- vgic_register_dist_iodev注册包括设置 io region 的模拟函数设置 region 数量。
而对应的 vgic_v3_dist_registers数组有如下寄存器注册进去
static const struct vgic_register_region vgic_v3_dist_registers[] {REGISTER_DESC_WITH_LENGTH_UACCESS(GICD_CTLR,vgic_mmio_read_v3_misc, vgic_mmio_write_v3_misc,NULL, vgic_mmio_uaccess_write_v3_misc,16, VGIC_ACCESS_32bit),REGISTER_DESC_WITH_LENGTH(GICD_STATUSR,vgic_mmio_read_rao, vgic_mmio_write_wi, 4,VGIC_ACCESS_32bit),REGISTER_DESC_WITH_BITS_PER_IRQ_SHARED(GICD_IGROUPR,vgic_mmio_read_group, vgic_mmio_write_group, NULL, NULL, 1,VGIC_ACCESS_32bit),
...
...
};完成注册后那么guest就可以访问分发器器并操作了。比如guest访问GICD_CTLR寄存器时将会最终在host调用到vgic_mmio_read_v3_misc或者vgic_mmio_write_v3_misc并返回对应结果。
重分发器
重分发器和分发器类似不重复说明
list register
该流程比较简单不需要用户配置只需要在kvm初始化流程中初始化即可
kvm_arch_init- init_subsystems- _kvm_arch_hardware_enable- cpu_hyp_reinit- kvm_vgic_init_cpu_hardware- kvm_call_hyp(__vgic_v3_init_lrs);void __hyp_text __vgic_v3_init_lrs(void)
{int max_lr_idx vtr_to_max_lr_idx(read_gicreg(ICH_VTR_EL2));int i;for (i 0; i max_lr_idx; i)__gic_v3_set_lr(0, i);
}可以看到首先从 GICv3 获取到 lr 的数量并且依次将其全部初始化为 0。
中断注入
spi 和 ppi 中断注入流程一致的sgi 稍微不同。
kvm 包含三种虚拟中断注入形式
1与实际硬件中断绑定的虚拟中断
2由模拟设备产生的中断
3vcpu 之间发送的 ipi 中断
为了管理这些虚拟中断每个 vcpu 结构体都有一个 ap_list 链表。该链表表示已经被注入到特定 vcpu 中单尚未写到 list register 中的虚拟中断ap 表示 active 和 pending。
硬件中断注入
上面已经演示
模拟设备的中断注入
模拟中断注入通过 irqfd 实现基于evnetfd因此要模拟中断注入需要先建立 eventfd再创建irqfd
kvm_vm_ioctl- kvm_ioeventfd- kvm_assign_ioeventfd- ioeventfd_ops// 用户态注入中断
ioeventfd_ops- ioeventfd_write- eventfd_signal// 唤醒 irqfd 对应的工作队列完成中断注入- wake_up_locked_pollipi 注入
这个最简单host 只需要捕获到 guest 写 gicv3的ipi寄存器异常然后在host完成 vcpu 注入即可。
7 arm64 timer 虚拟化
主要是在进入和退出 vcpu 时恢复和保存 vcntl 相关的寄存器数据比较简单跳过。