Minos 虚拟化: 中断虚拟化
本文从 GICv3 硬件机制出发,梳理 Minos 中断虚拟化的完整实现:virq 抽象层如何描述一条虚拟中断的状态,硬件路径如何借助 List Register 直接注入,软件路径又如何模拟 GICD/GICR 的 MMIO 访问,以及 vCPU 切换时中断上下文的保存与恢复。
1. 背景
1.1 guest 如何使用中断
在深入实现之前,先建立两个整体概念:guest 如何使用中断(它眼中的 GIC 长什么样、一个中断的完整生命周期),以及 hypervisor 为什么要介入、介入在哪一步。这样后面读实现代码时,每一处都能对应回”这是 guest 生命周期里的哪一步”。
guest 通过三类接口使用 GIC,对应 GICv3 的三类寄存器:
- GICD / GICR(MMIO 寄存器):配置型寄存器,用于设置中断的使能(
GICD_ISENABLER)、类型(GICD_ICFGR)、优先级(GICD_IPRIORITYR)等。guest 启动时由 Linux 的 GIC 驱动初始化。 - **ICC_*(系统寄存器)**:CPU 接口,用于应答中断(
ICC_IAR1_EL1)和结束中断(ICC_EOIR1_EL1),guest 的中断处理程序直接读写。 - 中断交付:中断真正”到达” guest 的 CPU,靠硬件把 pending 的中断号递交给 EL1 的异常入口。
一个中断的生命周期
以 virtio 网卡中断为例,从 guest 视角走一遍:
- 发现:Linux 启动时解析设备树,读到 GIC 的 GICD/GICR 基址与中断号空间。
- 初始化配置:GIC 驱动写
GICD_CTLR使能 distributor,为 SPI 设置类型(GICD_ICFGR)、优先级(GICD_IPRIORITYR)。 - 注册并使能:virtio 驱动
request_irq()注册处理函数,然后写GICD_ISENABLER使能这个 SPI。 - 接收:网卡有数据时物理中断产生,guest 的 CPU 接口把中断递交给 Linux,CPU 进入 IRQ 异常。
- 应答并处理:处理函数读
ICC_IAR1_EL1得到中断号,调用 virtio 的 handler。 - 结束:处理完写
ICC_EOIR1_EL1,中断回到 inactive,等待下一次。
hypervisor 在哪里介入
这 6 步里,hypervisor(minos)实际只介入两处,其余交给硬件:
- 配置类操作(第 2、3 步)会被拦截:guest 写 GICD/GICR 是 MMIO 访问,而这块地址被标成
VM_GUEST_VDEV类型(include/minos/memattr.h:61,注释为 memory R/W will trapped),guest 一读写就陷入 EL2,由 minos 的软件模拟接手(本文第 6 章)。 - 真实中断的产生(第 4 步)在 EL2 被截获:物理中断号是全局的,GIC 把物理 IRQ 路由给 hypervisor(
HCR_EL2.IMO使物理中断进入 EL2),minos 把物理中断号翻译成这个 guest 的虚拟中断号,再注入回 guest(本文第 5 章)。 - 应答/结束(第 5、6 步)不 trap:GICv3 的硬件虚拟化让 guest 直接读写
ICC_IAR1_EL1/ICC_EOIR1_EL1,硬件自动处理,几乎没有 hypervisor 开销。
一张时序图概括,标注每步是否陷入 EL2:
为什么要介入:三个本质问题
从 hypervisor 视角看,上面那些”介入”背后是三个必须解决的问题:
- GIC 是共享硬件。系统中的 GICD(Distributor,全系统共享)和 GICR(Redistributor,每 CPU 一组)只有一份物理实现,多个 guest 不能各自随意读写,否则会互相干扰、甚至破坏 hypervisor 自己的中断配置。
- 中断归属需要翻译。物理中断号(pINTID)是全局的,guest 感知的是虚拟中断号(vINTID)。同一块物理设备的中断要能注入到某个 guest 的某个 vCPU,且 guest 的”使能/关闭/优先级”操作不能直达硬件。
- 有两种做法。一种是借助 ARM GICv3 提供的硬件虚拟化能力(List Register + VMCR),把要注入的中断直接写进硬件,guest 像用真 GIC 一样收中断;另一种是纯软件模拟——拦截 guest 对 GIC 寄存器的 MMIO 访问,自己维护一份虚拟 GIC 状态。
minos 对 vGICv3 的实现,恰好两种路径都存在:注入走硬件 LR,配置走 MMIO 模拟。本文就沿着这两条路走读代码。
1.2 硬件基础知识
GICv3 基础
为方便后续阅读,这里把本文反复出现、且需对照 ARM GICv3 规范理解的概念列成速查表。不求全面,只覆盖本文实际用到的部分。
GIC 的三个组成部分
以 SPI 为例,中断在三者之间的流向如下:
1 | 外设中断源(SPI) |
- GICD(Distributor):全局共享、仅一份,负责所有 SPI 的使能/优先级/路由。
- GICR(Redistributor):每个 CPU 一组,负责该 CPU 的 SGI/PPI 配置。
- **ICC_*(CPU Interface)**:每个 CPU 一组的系统寄存器,负责应答(IAR)与结束(EOIR)中断。
| 组件 | 全称 | 访问方式 | 作用 |
|---|---|---|---|
| Distributor | GICD | MMIO(系统级共享) | 全局配置:SPI 使能、优先级、触发类型、路由 |
| Redistributor | GICR | MMIO(每 CPU 一组) | 该 CPU 的 SGI/PPI 配置 |
| CPU Interface | ICC_* | 系统寄存器 | 应答中断(IAR)、结束中断(EOIR) |
中断类型与编号空间
| 类型 | 中断号 | 特点 |
|---|---|---|
| SGI | 0-15 | 软件生成,CPU 之间通信用 |
| PPI | 16-31 | 每 CPU 私有 |
| SPI | 32+ | 全局共享,可路由到任意 CPU |
本文涉及的寄存器速查
| 寄存器 | 类别 | 用途 |
|---|---|---|
GICD_CTLR |
MMIO | Distributor 使能 |
GICD_ISENABLER / ICENABLER |
MMIO | 使能/关闭 SPI |
GICD_ICFGR |
MMIO | 中断触发类型(电平/边沿) |
GICD_IPRIORITYR |
MMIO | 中断优先级 |
GICR_ISENABLER / ICENABLER |
MMIO | 使能/关闭 PPI/SGI |
ICC_IAR1_EL1 |
系统寄存器 | 应答中断,读出中断号 |
ICC_EOIR1_EL1 |
系统寄存器 | 结束中断(EOI) |
ICH_LR<n>_EL2 |
EL2 系统寄存器 | List Register,hypervisor 注入虚拟中断 |
ICH_HCR_EL2 |
EL2 系统寄存器 | 使能 List Register 处理 |
ICH_VMCR_EL2 |
EL2 系统寄存器 | 虚拟 CPU interface 控制 |
ICH_VTR_EL2 |
EL2 系统寄存器 | 读取 LR 数量、优先级位数 |
HCR_EL2.IMO / FMO |
EL2 系统寄存器 | 物理 IRQ/FIQ 路由到 EL2 |
中断状态机(硬件视角)
1 | inactive ──中断到来──▶ pending ──CPU 应答(IAR)──▶ active ──EOI──▶ inactive |
- inactive:无中断。
- pending:已产生、等待 CPU 应答。
- active:CPU 已应答、正在处理。
- active and pending:处理过程中又来了新的一笔(边沿触发常见)。
虚拟化关键点:GICv3 的硬件虚拟化能力集中在 ICH_* 寄存器组——这是让”交付不 trap”成为可能的硬件基础,也是理解第 5 章硬件路径的前提,下面单独展开。
ICH_* 寄存器组:硬件虚拟化的关键
GICv3 的硬件虚拟化能力,落在 EL2 的一组”虚拟中断控制”寄存器上。hypervisor 用它们预先准备好要投递给 guest 的中断,之后交付过程完全由硬件完成。核心成员有四个:
| 寄存器 | 作用 |
|---|---|
ICH_LR<n>_EL2 |
List Register,存放一条待投递的虚拟中断:hypervisor 写入,硬件投递,guest 应答后硬件推进其状态 |
ICH_HCR_EL2 |
虚拟化总开关(使能 LR 处理),以及维护中断使能位 |
ICH_VMCR_EL2 |
guest 看到的虚拟 CPU interface 配置(使能、优先级掩码等) |
ICH_VTR_EL2 |
只读能力报告:几条 LR、几位优先级 |
hypervisor 把一个虚拟中断的完整信息打包写进一条 LR:虚拟中断号 vINTID、可选的关联物理中断号 pINTID、优先级,以及 HW(是否绑定物理中断)、state(初始 pending)等字段(完整位段见 5.1)。
写完之后硬件接管。guest 随后的两个动作都被硬件直接映射为 LR 的状态推进,全程不陷入 EL2:
- guest 读
ICC_IAR1_EL1应答 → 硬件从所有 LR 中挑出优先级最高、state=pending 的一条,返回其 vINTID,并把该 LR 的 state 推进到 active。 - guest 写
ICC_EOIR1_EL1结束 → 硬件把对应 LR 的 state 清回 inactive(若处理期间又 pending 则回到 pending+active)。
因此 hypervisor 只在两个时间点介入:注入前写 LR(第 5 章 enter_to_guest),回收时读 LR 状态清空已完成的条目(第 5 章 exit_from_guest)。
疑问:LR 只有 16 条,够多个 guest 用吗?
16 条 LR 并不是所有 guest 共享的,不会不够分。原因是:
- LR 是每个物理核一套,不是全局共享。
ICH_LR<n>_EL2是 EL2 的 per-CPU 寄存器,每个核各有一套 16 条;而同一时刻一个物理核上只跑一个 vCPU,所以这 16 条只服务”当前这个 vCPU”。- 多 guest 靠 vCPU 调度 + vmodule 保存恢复来复用同一套 LR。切换 vCPU 时,LR 内容随 vmodule 的
state_save/state_restore(第 8 章)一起换进换出,各 guest 的状态互不干扰。- LR 是”在途中断数”的上限,不是”总中断数”的上限。中断处理完(EOI)后,
exit_from_guest就把对应 LR 回收复用(vgic.c:135-141)。16 条限制的是”同一 vCPU 同一时刻还挂着(pending/active)的中断数”,而不是它能服务的中断总量。真正的瓶颈只在一种场景:单个 vCPU 同时在途中断超过 16 条。此时 minos 分批注入——LR 装不下就记下
last_fail_virq并 break,等下一轮进出 guest 再补(vgic.c:72-77、104-109的重试逻辑,日志"VM%d no space to send new irq %d")。硬件其实还提供了维护中断(ICH_HCR_EL2.UIE)在 LR 下溢时主动通知 hypervisor,但 minos 没开。实际 Linux guest 里,单个 vCPU 同时在途的中断通常远小于 16,所以 16 条绰绰有余;真超了也只是”延迟注入”而非”丢中断”——因为
pending_bitmap里还记着,下一轮会补上。
最关键的位是 En(bit 0):使能整套 LR 处理机制,不置它 LR 不会生效。minos 在 vmodule 状态初始化时写 ICH_HCR_EL2 = GICH_HCR_EN。
其余位(UIE/LRENPIE/NPIE/VGrp*EIE 等)用于在”LR 用完””LR 清空””guest EOI”等事件发生时产生维护中断(maintenance interrupt),让硬件主动通知 hypervisor 补 LR。minos 目前只开了 En,注入靠进出 guest 时软件轮询 pending_bitmap 完成(对应第 5 章的 enter/exit_from_guest),没有依赖维护中断。
ICH_VMCR_EL2 —— guest 眼中的 CPU interface
- VENG1(bit 1):使能虚拟 Group1 中断投递,不置则 guest 收不到任何虚拟中断。minos 置
GICH_VMCR_VENG1。 - VPMR(bit[31:24]):虚拟优先级掩码,guest 读
ICC_PMR_EL1得到的就是它。minos 置 0xff,即不屏蔽任何优先级。
- VENG1(bit 1):使能虚拟 Group1 中断投递,不置则 guest 收不到任何虚拟中断。minos 置
guest 感知不到真实的 CPU interface 配置,它读到的优先级掩码、中断使能等,来自这个寄存器虚拟出来的值。
其余还有 VEOIM(虚拟 EOI 模式)、VBPR(优先级分组)等控制 guest 侧 EOI 语义的字段,minos 未配置,沿用默认。
hypervisor 启动时读它确定硬件的 LR 数量和优先级位数。vgicv3_init(vgicv3.c:1067-1069):
1 | val = read_sysreg32(ICH_VTR_EL2); |
QEMU 的 cortex-a53 上通常报告 16 条 LR;优先级位数由 PRIbits 字段决定。
1 | 初始化:读 ICH_VTR_EL2(几条 LR)→ 设 ICH_HCR_EL2.En → 设 ICH_VMCR_EL2(VENG1 | VPMR) |
2. 中断虚拟化的整体框架
先看 minos 里的分层关系:
1 | +----------------------+ virt/virq.c 中断抽象层(virq_desc / 状态机 / 发送) |
中断号按 ARM GIC 惯例分成三类,minos 用宏把它们映射到 virq 编号空间(include/virt/virq.h:25-51):
| 类型 | 中断号范围 | minos 的 virq 编号 | 归属 |
|---|---|---|---|
| SGI | 0-15 | 0 ~ VM_SGI_VIRQ_NR-1(默认 16) |
每 vCPU 本地(软件触发) |
| PPI | 16-31 | VM_SGI_VIRQ_NR ~ VM_LOCAL_VIRQ_NR-1 |
每 vCPU 本地 |
| SPI | 32+ | VM_LOCAL_VIRQ_NR + (virq-32) |
整个 VM 共享 |
其中 VM_LOCAL_VIRQ_NR = VM_SGI_VIRQ_NR + VM_PPI_VIRQ_NR = 32,对应 ARM 里的”本地 32 个中断”。SPI 的容量按 VM 类型区分:HVM(host VM)默认 384 个(HVM_SPI_VIRQ_NR),普通 guest VM 默认 64 个(GVM_SPI_VIRQ_NR)。
3. virq 抽象层
3.1 virq_desc:一条中断的全部状态
每个虚拟中断对应一个 struct virq_desc(include/virt/virq.h:76-88):
1 | struct virq_desc { |
关键字段说明:
- vno / hno:虚拟号与物理号。硬件路径(HW 中断)下两者都有效,写 LR 时同时填
v_intid和p_intid并置HW=1;纯虚拟中断(如 vGIC 定时器、vdev 生成的中断)hno 为 0,HW=0。 - flags:一组位标志,最重要的有
VIRQS_ENABLED(guest 已使能该中断)、VIRQS_HW(绑定物理中断)、VIRQS_REQUESTED(已被 request 占用)、VIRQS_FIQ(以 FIQ 注入)。 - state:中断状态机,见下。
3.2 状态机
include/virt/virq.h:10-13 定义了 4 个状态:
1 | INACTIVE ──收到中断──▶ PENDING ──注入 LR/交付给 vCPU──▶ ACTIVE |
VIRQ_STATE_INACTIVE:无事发生。VIRQ_STATE_PENDING:中断已产生、等待交付。VIRQ_STATE_ACTIVE:已交付、guest 正在处理(尚未 End of Interrupt)。VIRQ_STATE_ACTIVE_AND_PENDING:正在处理期间又来了新的一笔。
3.3 virq_struct:每个 vCPU 一份的运行时视图
每个 vCPU 有一个 struct virq_struct(include/virt/virq.h:92-101),保存该 vCPU 所有中断的位图:
1 | struct virq_struct { |
数据结构的分工:
- SGI/PPI(0-31)的 desc 存在
local_desc里,静态内嵌在 virq_struct 中。 - SPI 的 desc 存在
vm->vspi_desc[]数组里,所有 vCPU 共享(SPI 归属 VM 而非某个 vCPU)。 get_virq_desc()(virt/virq.c:38-49)按编号决定去哪个表取:virq < VM_LOCAL_VIRQ_NR取local_desc[virq],否则取vm->vspi_desc[VIRQ_SPI_OFFSET(virq)]。
为什么 SGI/PPI 的 desc 是”每 vCPU 一份”而 SPI 是”VM 一份”?因为 SGI/PPI 在硬件上本来就是 per-CPU 的(每个 CPU 各有自己的一份),而 SPI 是全局共享、路由到某个 CPU 的。虚拟化沿用了这个语义。
内存是怎么分配的?virq_create_vm(virt/virq.c:596-642)在 VM 创建时挂 OS_HOOK_CREATE_VM hook,一次性分配:
1 | vspi_desc[](每 VM 一份) |
各 vCPU 的 virq_struct 本体在 vCPU 创建时由 alloc_vcpu(virt/vm.c:281)zalloc,随后 vcpu_virq_struct_init(virt/virq.c:414-439)初始化 local_desc:默认清 HW 标志、置 enable、vcpu_id/vmid = ANY、id = VIRQ_INVALID_ID。
3.4 SGI/PPI/SPI 的处理差异
- SGI(软件触发中断):guest 写
ICC_SGI1R_EL1触发,被 trap 后由vgicv3_send_sgi逐目标 vCPUsend_virq(见第 9 节)。__send_virq里对vno < VM_SGI_VIRQ_NR的中断会记录src(来源 vCPU,virt/virq.c:74-75)。需注意:src目前在 GICv3 路径上没有任何代码把它暴露给 guest(只有 GICv2 的GICH_LR会用pid携带它),属于预留字段。 - PPI:per-CPU,由
request_virq_pervcpu(virt/virq.c:532-563)对每个 vCPU 分别 request。 - SPI:由
alloc_vm_virq(virt/virq.c:565-579)在vspi_map里找一个空闲位,返回virq + VM_LOCAL_VIRQ_NR。
4. virq_chip:抽象 ops 表
不同硬件平台的中断控制器行为不同(GICv2、GICv3、树莓派的 BCM、Apple 的 AIC),所以 minos 把”虚拟中断控制器”抽象成一张 ops 表 struct virq_chip(include/virt/virq_chip.h:11-25):
| ops | 作用 |
|---|---|
enter_to_guest |
进 guest 前调用,把 pending 的中断”装填”给硬件 |
exit_from_guest |
退出 guest 后调用,回收已处理完的中断 |
send_virq |
把单个 virq 注入硬件(GICv3 写 LR) |
get_virq_state |
读硬件上该 virq 的当前状态(GICv3 读 LR 的 state 位) |
update_virq |
更新/清除硬件上的 virq(GICv3 清 LR) |
generate_virq |
生成 dtb 里的中断描述(供 guest 设备树使用) |
xlate |
把设备树中断说明转成 hwirq |
vcpu_init |
vCPU 初始化时的回调(设置 nr_lrs) |
具体实现通过 VIRQCHIP_DECLARE(include/minos/device_id.h:70-75)注册进链接段 .__virqchip。VM 创建时,create_vm_irqchip_of(virt/resource.c:57-85)根据设备树节点的 compatible 字符串匹配到对应 init 函数(vgicv3_virqchip_init),得到该 VM 的 vm->virq_chip。也就是说,每个 VM 可以有自己的虚拟中断控制器实现。
vgicv3_init_virqchip(virt/virq_chips/vgicv3.c:743-762)把上述 ops 全部绑到 vgic 的实现上,前提是 flags & VIRQCHIP_F_HW_VIRT:
1 | static void vgicv3_init_virqchip(struct virq_chip *vc, |
VIRQCHIP_F_HW_VIRT 这个标志(include/virt/virq_chip.h:9)在 vgicv3_virqchip_init 里被置位(vgicv3.c:866-867,只要 gicd_base != 0)。它表示”走硬件虚拟化”,这也是第 5 节硬件路径存在的前提。inc_pdata 指向 vgicv3_dev(vdev 封装),ops 回调通过它拿到 vgic_gicd/gicr 结构。
值得一提:GICv2 的
vgicv2_init_virqchip(virt/virq_chips/vgicv2.c:561-578)也绑定了同一对vgic_irq_enter_to_guest/exit_from_guest,说明”进出 guest 时装填/回收中断”的框架与具体 GIC 版本无关,差别只在send_virq(GICv2 写 GICH_LR,GICv3 写 ICH_LR)。
5. 硬件路径:GICv3 的 List Register 注入
这是”硬件虚拟化”的核心。GICv3 提供一组 ICH_LR
5.1 LR 的结构
include/device/gicv3.h:154-163 用 C 位段声明了 LR 的布局,与 ARM 规范 ICH_LR<n>_EL2 一致(GCC/Clang 在 aarch64 小端下位段自 LSB 依次分配):
1 | ┌───────────────────┬─────────────┬────────┬───────────┬────────┬─────┬────┬───────┐ |
v_intid[31:0]:交付给 guest 的虚拟中断号。p_intid[41:32]:关联的物理中断号。仅当hw=1时有效。priority[55:48]:中断优先级;group[60]:组别(1=Group1);hw[61]:见下。hw[61]:1 表示这条 LR 关联了一个物理中断。此时硬件在 guest 处理完该中断时自动把物理中断 EO 掉(省一次 trap);hw=0表示纯虚拟中断,guest 的 EO 会通过exit_from_guest由 hypervisor 处理。state[63:62]:0b01=pending,0b10=active,0b11=pending+active。硬件在交付和 EO 时自动更新这两位;gicv3_get_virq_state正是读[63:62]位获取状态(见 5.2)。
注意:C 位段的具体布局属于实现定义(依赖 ABI/编译器)。这里依赖的是 GCC/Clang 在 aarch64 小端下”字段自低位起按声明顺序分配”,正好与 ARM 规范一致;
(value >> 62) & 3能读出 state 位也印证了这一点。
5.2 硬件路径完整链路
一个中断从 guest 使能它,到最终处理完,串起两条路径:先走软件路径完成配置(使能),再走硬件路径完成交付。交付部分又分三段——中断产生侧、注入侧(进 guest 前)、回收侧(退出 guest 后)。
配置阶段:使能中断(软件路径,前置)
在物理中断触发之前,guest 要先”使能”它。Linux 启动时,GIC 驱动写 GICD_ISENABLER(使能 SPI)、GICD_ICFGR(设置触发类型)、GICD_IPRIORITYR(设置优先级)等寄存器。这些都是 MMIO 访问,而 GIC 地址在 Stage2 里被标成陷阱区,guest 一写就陷入 EL2:
1 | guest 写 GICD_ISENABLER → Stage2 触发 DABT → dataabort_tfl_handler |
virq_enable(virt/virq.c:252-265)除了置软件使能标志,对绑定了物理中断的 SPI 还会 irq_unmask(desc->hno)——物理中断从此才允许进 EL2,触发后面那段交付链路。这一阶段的详细模拟逻辑见第 6 章,这里点到为止:使能之后,中断就”就位”了,等着物理设备触发。
第一段:物理中断到来 → 标为 pending
物理设备触发中断 → GIC 硬件把中断路由到某物理核 → 因为 HCR_EL2.IMO 置位,物理中断在 EL2 进入 → do_handle_host_irq(core/irq.c:66-88)调 irq 的 handler。
这个 handler 就是 guest_irq_handler(virt/virq.c:120-138),它是 __request_virq 里 request_irq(hwirq, guest_irq_handler, IRQ_FLAGS_VCPU, ...) 注册的(virt/virq.c:477):
1 | static int guest_irq_handler(uint32_t irq, void *data) |
send_virq(virt/virq.c:80-118)做三件事:
- 检查 VM 状态(只在 ONLINE/SUSPEND 时接受,SUSPEND 且中断不能唤醒则丢弃)。
__send_virq(virt/virq.c:58-78):在pending_bitmap里置位该 virq(test_and_set_bit保证不重复),atomic_inc(&pending_virq)。virq_kick_vcpu:kick_vcpu(vcpu, ...)唤醒/打断目标 vCPU,让它尽快退出 idle 或被抢占状态、重新进入 guest。
注意:到这一步,中断还没有进入硬件 LR,只是记录在内存的 pending_bitmap 里。真正的硬件注入发生在下一次进入 guest 之前。
第二段:进入 guest 前 → 填 LR
vCPU 任务从 EL2 回到 guest 用户态之前,调度器调用 vcpu_return_to_user(virt/vm.c:294-305),它会先 do_hooks(vcpu, regs, OS_HOOK_ENTER_TO_GUEST):
1 | 进入 guest:vcpu_return_to_user → OS_HOOK_ENTER_TO_GUEST |
vgic_irq_enter_to_guest 是注入的核心(virt/virq_chips/vgic.c:42-112),逻辑:
- 从
last_fail_virq开始遍历pending_bitmap。 - 对每个 pending 的 virq:
- 若它同时已在
active_bitmap里(同号中断还在处理中),跳过,等上一笔完成(vgic.c:67-68)。 - 从
lrs_bitmap找一个空闲 LR 槽id;LR 用尽则记last_fail_virq并 break,等下一轮再试(vgic.c:71-77)。 virq->id = id,占住 LR 槽。virqchip_send_virq(vcpu, virq)→gicv3_send_virq,把 LR 写进硬件(见下)。- 状态置
VIRQ_STATE_PENDING,把 virq 加入active_bitmap、从pending_bitmap摘除(vgic.c:88-101)。
- 若它同时已在
- 返回
flags:注入了几条、以及是否注入了 FIQ(FIQ_HAS_INJECT位)。
gicv3_send_virq(virt/virq_chips/vgicv3.c:661-681)真正写 LR:
1 | static int gicv3_send_virq(struct vcpu *vcpu, struct virq_desc *virq) |
写入后硬件接管:guest 在 EL1 运行时,硬件把 LR 里的 v_intid 当作一个真实的 GIC 中断投递到 vCPU,guest 的中断处理程序照常收中断、EO。这个过程完全不经过 hypervisor——这就是硬件虚拟化的意义:中断交付路径上没有 trap 开销。
第三段:退出 guest 后 → 回收 LR
guest 处理完中断并 EO 后,或者因别的原因退出到 EL2(调度、其他 trap),vcpu_exit_from_user(virt/vm.c:307-318)触发 OS_HOOK_EXIT_FROM_GUEST:
1 | 退出 guest:vcpu_exit_from_user → OS_HOOK_EXIT_FROM_GUEST |
vgic_irq_exit_from_guest(virt/virq_chips/vgic.c:114-145):
- 遍历
active_bitmap里的每个 virq。 - 调用
virqchip_get_virq_state→gicv3_get_virq_state(vgicv3.c:714-726):读对应 LR 的[63:62]位(即代码里的(value >> 62) & 3),得到硬件反馈的当前状态。
1 | static int gicv3_get_virq_state(struct vcpu *vcpu, struct virq_desc *virq) |
- 若状态为
VIRQ_STATE_INACTIVE(guest 已 EO、硬件已把 LR 清空),则:virqchip_update_virq(vcpu, virq, VIRQ_ACTION_CLEAR)→gicv3_update_virq(vgicv3.c:683-712)的 CLEAR 分支:gicv3_write_lr(id, 0)显式清空 LR 槽。该函数里还有一个VIRQ_ACTION_REMOVE分支会irq_clear_pending(hno)清物理 pending,但当前代码没有任何调用方传 REMOVE,实际走不到。- 释放 LR 槽(
clear_bit(virq->id, lrs_bitmap)),virq->id = VIRQ_INVALID_ID,从active_bitmap摘除,active_virq--。
- 若状态仍是 pending/active(guest 还没处理完),什么都不做,留待下一轮。
完整链路图
这段链路里最值得体会的是:**”使能”和”配置”是软件路径,”注入”和”EO”是硬件路径**。guest 大部分中断生命周期都不需要陷入 hypervisor。
6. 软件路径:GICD/GICR 的 MMIO 模拟
guest 初始化时(Linux 的 gic_init_bases 等)会读写一大堆 GICD/GICR 寄存器来做配置。这些寄存器不能直接放 guest 碰硬件,所以 minos 把它们”虚拟化”成一个 MMIO 区域:guest 访问时产生 Stage2 数据中止,trap 到 EL2,由模拟器处理。
6.1 从 trap 到模拟器
Stage2 上 GIC 区域没有映射(或映射为 trap),guest 一读写就产生 DABT。上一阶段的 dataabort_tfl_handler(arch/aarch64/virt/trap.c:241-294)取出故障地址(IPA)后:
1 | ret = vdev_mmio_emulation(regs, iswrite, ipa, &value); // trap.c:272 |
vdev_mmio_emulation(virt/vdev.c:198-238)遍历当前 VM 的 vdev 链表,用地址匹配 vdev 注册的 iomem 区间:
1 | list_for_each_entry(vdev, &vm->vdev_list, list) { |
若没有任何 vdev 认领这个地址,就 trap_vcpu(VMTRAP_TYPE_MMIO, ...) 上报给 host VM(VM0)处理——这是”device passthrough to VM0”的机制。
vGIC 作为 vdev 注册了 5 个地址区间:GICD、GICR、GICC、GICH、GICV(vgicv3_virqchip_init,vgicv3.c:832-836)。GICD/GICR 有完整模拟,GICC/GICH/GICV 在 GICv3 下用不到(CPU 接口走系统寄存器而非 MMIO),所以 vgic_mmio_handler 里只实现了 GICD 和 GICR 两类(vgicv3.c:444-447)。
6.2 地址分派
vgic_mmio_handler(vgicv3.c:416-474)先确定这次访问属于哪个子模块:
idx == VGICV3_IDX_GICD→ GICD。idx == VGICV3_IDX_GICR→ 先查当前 vCPU 自己的 GICR(offset_to_gicr_type把 GICR 的 128KB 空间分成 RD(前 64K)和 SGI(后 64K)两个区),若不在自己这组就再查其他 vCPU 的 GICR——因为 master vCPU 可能访问别的 vCPU 的 GICR(vgicv3.c:437-443)。- 定位到类型后调
vgic_gicd_mmio/vgic_gicr_rd_mmio/vgic_gicr_sgi_mmio。
6.3 GICD 模拟:SPI 的使能与配置
vgic_gicd_mmio_read/write(vgicv3.c:204-293)用 switch 处理各寄存器。两个最有代表性的:
GICD_ISENABLER(使能 SPI,写 1 生效):
1 | case GICD_ISENABLER...GICD_ISENABLER_END: |
每个 ISENABLER 寄存器管 32 个 SPI,y = x*32 算出这 32 个的起始编号。对每个置位位调用 virq_enable(virt/virq.c:252-265):
1 | int virq_enable(struct vcpu *vcpu, uint32_t virq) |
virq_set_enable 只置软件标志;如果是绑定了物理中断的 SPI,还要 irq_unmask(desc->hno) 真的把物理中断使能——物理中断从此才允许进 EL2 触发 guest_irq_handler。这就是”guest 使能虚拟中断 → 物理中断真正解屏蔽”的联动。
GICD_ICFGR(中断触发类型,边沿/电平):
1 | case GICD_ICFGR...GICD_ICFGR_END: |
vgic_set_virq_type(vgicv3.c:189-202)逐中断调 virq_set_type(virt/virq.c:209-234),后者对 HW 中断会进一步 irq_set_type(desc->hno, ...) 把触发方式同步给物理 GIC。
GICD 模拟的只读寄存器(GICD_CTLR/TYPER/PIDR2/STATUSR)则返回内存里保存的值。vgic_gicd_init(vgicv3.c:488-516)在 VM 创建时构造 gicd_typer,向 guest 报告 CPU 接口数(vcpu_nr << 5)、SPI 容量(ItLinesNumber = (vspi_nr+32)/32 - 1)、IDbits(9 << 19,表示支持大中断号空间)。
6.4 GICR 模拟:PPI/SGI 的使能
GICR 的 SGI 区(后 64K)里有 GICR_ISENABLER/GICR_ICENABLER 管 PPI/SGI 的使能,vgic_gicr_sgi_mmio(vgicv3.c:329-387)用 gicr->gicr_enabler0 这个软件影子变量记录状态:
1 | case GICR_ISENABLER: |
GICR_ICPENDR0 的写(清 pending)则调 clear_pending_virq(virt/virq.c:343-364),把处于 ACTIVE 状态的 virq 从 active_bitmap 摘除并清掉 LR。
需要注意:vgic_gicr_sgi_mmio 的 switch 只处理了 GICR_CTLR/ISPENDR0/ICPENDR0/ISENABLER/ICENABLER/PIDR2 这几类,没有处理 GICR_ICFGR0/1(PPI/SGI 的触发类型配置)。对比 GICD 那边是模拟了 GICD_ICFGR 的(见 6.3),GICR 的 ICFGR 读返回 0、写被 default 分支忽略——这是 minos 的一个未实现项。对大部分场景无碍:SGI 本来就是边沿触发,PPI(如 vtimer)触发类型固定,guest 通常不依赖 GICR_ICFGR。
GICR 还有个细节:Linux 用 GICR_TYPER 的 bit4(Last)判断哪个 GICR 是最后一个,vgic_gicr_init(vgicv3.c:543-545)在最后一个 vCPU 的 GICR 上置该位。
6.5 软件路径的”纯虚拟中断”注入
软件路径不只用于配置。第 5 节的 gicv3_send_virq 同样服务于纯虚拟中断(hw=0,如虚拟定时器、virtio 的中断):某处调用 send_virq_to_vm / send_virq_to_vcpu → 置 pending_bitmap → 进 guest 时写 LR。区别只在 lr->hw = 0,此时 p_intid 无效,guest 的 EO 由 hypervisor 在 exit_from_guest 里回收 LR 完成。
send_virq_to_vm(virt/virq.c:299-327)只接受 SPI(virq >= VM_LOCAL_VIRQ_NR),且要求该 virq 已使能、非 HW 中断,然后发给它绑定的 vCPU。
7. GICv2 对比:纯软件模拟的 CPU 接口
看一遍 GICv2 的实现能更清楚”硬件路径省了什么”。GICv2 的虚拟化有两种模式:
HWA(硬件虚拟化)模式:与 GICv3 一样,
enter_to_guest/exit_from_guest绑定vgic_irq_enter_to_guest/exit_from_guest(vgicv2.c:561-578),注入走 32 位的GICH_LR(gicv2_send_virq,vgicv2.c:501)。SWE(软件模拟)模式:不绑
enter_to_guest(vgicv2.c:564仅在HW_VIRT下绑定),需要额外模拟 CPU 接口 GICC(virt/virq_chips/vgicv2.c:379-438):- GICC_IAR(读):
get_pending_virq(get_current_vcpu())(virt/virq.c:366-389)——从 pending_bitmap 里取一个当前可投递的 virq,把它从 pending 转成 active 再返回给 guest。这相当于”用软件实现硬件该干的事”。 - GICC_EOIR(写):
clear_pending_virq(...),软件完成 EO。 - GICC_PMR/BPR:返回 zalloc 的 shadow 值(初始 0),写时更新到
vgicc结构;GICC_RPR/HPPIR:硬编码返回0xa0(vgicv2.c:398-405)。
- GICC_IAR(读):
对比结论(GICv3 对应的是 GICv2 的 HWA 模式;SWE 模式是纯软件兜底):
| GICv3(minos 当前 QEMU 平台) | GICv2 HWA | GICv2 SWE | |
|---|---|---|---|
| 中断交付 | 写 ICH_LR,硬件投递给 EL1 | 写 32 位 GICH_LR | guest 读 GICC_IAR 时软件取数 |
| 中断 EO | 硬件推进 LR state,退出后软件回收 | 同左 | guest 写 GICC_EOIR,软件清除 |
| 进 guest 前 | vgic_irq_enter_to_guest 批量填 LR |
同左 | 无 enter_to_guest |
| CPU 接口 | 系统寄存器(ICC_*),不走 MMIO | GICH 寄存器 | GICC 需要 MMIO 模拟 |
8. 上下文切换:vmodule 保存/恢复
中断虚拟化的状态(LR、优先级寄存器、VMCR 等)是 per-vCPU 的,当 vCPU 被调度走、换另一个 vCPU 上这个物理核时,必须保存/恢复,否则 LR 会串台。minos 用 vmodule 机制(include/virt/vmodule.h)统一处理这类”每 vCPU 一份的虚拟化状态”。
gicv3_vmodule_init(vgicv3.c:1043-1052)填充 vmodule 的 context_size 和四个状态回调,随后 vgicv3_init 里 register_vcpu_vmodule("gicv3-vmodule", ...)(vgicv3.c:1072)把它注册成名为 gicv3-vmodule 的 vmodule:
gicv3_state_save(vgicv3.c:942-952):dsb()后批量读 ICH_LR(gicv3_save_lrs)、ICH_AP0R/AP1R(gicv3_save_aprn)、ICC_SRE_EL1、ICH_VMCR_EL2、ICH_HCR_EL2,存进struct gicv3_context。gicv3_state_restore(vgicv3.c:1016-1026):对称写回,最后dsb()。gicv3_state_init(vgicv3.c:1028-1036):初始状态——ICC_SRE_EL1 = 0x7(使能 EL1 访问系统寄存器接口)、ICH_VMCR_EL2 = GICH_VMCR_VENG1 | (0xff << 24)(使能 Group1 虚拟中断 + 设定默认优先级掩码)、ICH_HCR_EL2 = GICH_HCR_EN(使能 LR 处理)。gicv3_state_resume:VM 恢复时重新 init。
这些回调由 vmodule 框架在 vCPU 调度切换时自动调用(vcpu_vmodules_init / save_vcpu_vmodule_state / restore_vcpu_vmodule_state,见 Phase 7)。LR 数量与优先级寄存器数量来自硬件:vgicv3_init(vgicv3.c:1054-1075)读 ICH_VTR_EL2,gicv3_nr_lr = (val & 0x3f) + 1、gicv3_nr_pr = ((val >> 29) & 0x7) + 1(QEMU cortex-a53 上一般 16 条 LR)。vgicv3_init 在物理 GIC 驱动初始化末尾被调用(drivers/irq-chips/gicv3.c:508)。
9. SGI 与系统寄存器 trap
guest 触发 SGI 不走 MMIO,而是写 ICC_SGI1R_EL1 系统寄存器。HCR_EL2 的 TSC/TACR 等位使能系统寄存器 trap(见 Phase 3),access_system_reg_handler(arch/aarch64/virt/trap.c:143)捕获后分发:
1 | case ESR_SYSREG_ICC_SGI1R_EL1: |
sgi1r_el1_trap 在 vgicv3 创建时绑定为 vgicv3_send_sgi(vgicv3.c:870)。vgicv3_send_sgi(vgicv3.c:96-144)解析 SGI 值:
sgi = (value >> 24) & 0xf:SGI 号(0-15)。mode = value & (1UL << 40):目标模式——SGI_TO_LIST(列表)或SGI_TO_OTHERS(除自己外所有 vCPU)。- 对目标列表,解出 aff1/aff2/aff3 和 target list,
affinity_to_logic_cpu换算成 VM 内 vCPU 号;对 others 模式,遍历该 VM 除自己外的所有 vCPU。 - 逐个
send_virq_to_vcpu(target, sgi)——也就是复用第 5 节的注入路径,进入 guest 前写 LR。
注:
access_system_reg_handler只拦截 SGI 的写。guest 读 ICC_SGI1R 是没意义的(SGI 寄存器只写不读),直接放行即可。
10. 关键知识点
- 中断虚拟化的两条路径:配置(GICD/GICR 寄存器)走 MMIO 软件模拟;交付(写入 LR)走 GICv3 硬件虚拟化。这是 GICv3 相对 GICv2 的核心优势。
- virq 编号与物理中断解耦:
vno(虚拟)与hno(物理)并存于virq_desc;hw标志决定写 LR 时是否携带 p_intid。使能虚拟中断时(virq_enable)才会真正irq_unmask对应物理中断。 - 状态机 INACTIVE→PENDING→ACTIVE:pending_bitmap 记录”该给 guest 的”,active_bitmap 记录”已给 guest 在处理的”。同号中断 active 期间再次 pending 会被跳过(
vgic.c:67),避免重复注入。 - 注入时机在进出 guest 的 hook 里:
OS_HOOK_ENTER_TO_GUEST(vcpu_return_to_user)前批量填 LR;OS_HOOK_EXIT_FROM_GUEST(vcpu_exit_from_user)后读 LR 状态回收。LR 不够时记last_fail_virq下轮再试。 - 纯虚拟中断与 HW 中断的区别:纯虚拟中断
hw=0,EO 由 hypervisor 回收 LR;HW 中断hw=1,硬件自动 EO 物理中断,hypervisor 只回收 LR 槽位。 - 上下文切换靠 vmodule:
gicv3-vmodule在 vCPU 调度切换时保存/恢复 ICH_LR、APR、VMCR、HCR。LR 数量来自ICH_VTR_EL2。
11. 函数调用关系总览
把前文分散在各章的调用链汇总成 6 张 UML 时序图,便于快速定位代码。图中消息已细化到函数内部关键步骤,Note 标注了状态变化(文件与行号均已在前文核实)。
11.1 系统初始化
三个 subsys_initcall 注册运行期 hook:virqs_init(CREATE/DESTROY_VM)、virqchip_init(ENTER/EXIT_TO_GUEST)、vcpu_vgic_hook_init(VCPU_INIT)。物理 GIC 与虚拟层的初始化细节见下面两个子小节。
11.1.1 gicv3_init:主核初始化物理 GIC
irq_init(core/irq.c:329)遍历设备树,按 compatible = "arm,gic-v3" 匹配到 GIC 节点后调 gicv3_init(.init)。它分两半:先配全局共享的 GICD(一次),再配主核自己的 per-CPU 接口。
配置 GICD(全局共享,只需一次):
- 解析地址:
translate_device_address_index读 dts GIC 节点的 4 段 reg(GICD@0x08000000 / GICR@0x080a0000 / GICC / GICH)存进array[],io_remap映射 GICD 与 GICR。 - 校验优先级位数:读
ICH_VTR_EL2,nr_pr = ((val>>29)&0x7)+1,若不在 5~7 之间则 panic。 - 记录每 CPU 的 GICR 基址:
rbase = GICR起始 + i*128KB,per-CPU 存gicr_rd_base/gicr_sgi_base(128KB stride 与 64KB 两段划分是 GICv3 规范固定的)。 - 关 GICD:
iowrite32(0, GICD_CTLR)。 - 读能力:
GICD_TYPER→nr_lines = 32 * (type & 0x1f)(支持的中断线数)。 - 触发类型:
GICD_ICFGR = 0,所有 SPI 默认电平触发、低有效。 - 优先级:
GICD_IPRIORITYR = 0xa0,SPI 默认优先级。 - 全部禁用:
GICD_ICENABLER = 0xffffffff(写 1 清使能)。 - 分组:
GICD_IGROUPR = 0xffffffff,SPI 归 Group1 Non-secure。 - 等生效:
gicv3_gicd_wait_for_rwp(),轮询GICD_CTLR的 bit31(RWP)清零。 - 使能 GICD:
GICD_CTLR = 1 | EN_GRP1 | EN_GRP1A | ARE_NS。
配置本核 per-CPU 接口:
gicv3_icc_sre_init:ICC_SRE_EL1/EL2 = 0xf,打开 CPU 接口的系统寄存器访问方式(GICv3 核心前提)。gicv3_gicr_init:唤醒本核 GICR;SGI 优先级 0x90、PPI 0xa0;SGI 使能、PPI 禁用;归 Group1。gicv3_gicc_init:记录本核cpus_affinity[cpu];ICC_BPR1=0、ICC_PMR=0xff、ICC_CTLR=EOImode=1(split 模式)、ICC_IGRPEN1=1。gicv3_hyp_init:ICH_VMCR_EL2 = VENG1 | 0xff<<24、ICH_HCR_EL2 = EN,为 vGIC 的 LR 注入做准备。
主核 vs 从核:gicv3_init 只由主核执行一次;每个从核启动时走 gicv3_secondary_init(gicv3.c:517),只重跑第 12~15 步的 per-CPU 初始化,GICD 是共享的、主核已配好,不重复。
RWP 等待:GICD 寄存器写入是异步的,GICD_CTLR 的 bit31(RWP)为 1 表示硬件还在处理之前的写;wait_for_rwp 忙等它清零,确保配置真正生效后再继续。
11.1.2 vgicv3_init:虚拟层初始化
gicv3_init 末尾调 vgicv3_init(array, 10)(gicv3.c:508):
- 保存地址:把
array拷贝进全局vgicv3_info(GICD/GICR/GICC/GICH 的基址与大小),供之后 VM 创建时get_vgicv3_info使用。 - 校验:GICD/GICR 的基址与大小非 0,否则 panic。
- 读硬件能力:
ICH_VTR_EL2→nr_lr = (val&0x3f)+1(List Register 数量)、nr_pr = ((val>>29)&0x7)+1(优先级位数)。 - 注册 vmodule:
register_vcpu_vmodule("gicv3-vmodule", gicv3_vmodule_init),让 gicv3 上下文(LR/APR/VMCR/HCR)随 vCPU 调度保存/恢复(见第 8 章)。
11.2 虚拟机初始化
virq_create_vm:vspi_nr = vm_max_virq_line()→ 算 size、get_free_pages→ 布局vspi_desc/vspi_map/每 vCPU 的 pending+active bitmap。vgicv3_virqchip_init:读 dts 5 段地址 → 注册 5 个 MMIO 区间 → gicd/gicr 初始化 → 绑 8 个 ops → 挂sgi1r_el1_trap。create_vcpu:zallocvirq_struct→vcpu_virq_struct_init(初始化 local_desc)→ hook 设nr_lrs。
11.3 注册中断
- hypervisor 侧:
request_hw_virq→__request_virq(置 REQUESTED 防重、设 vno/hno、SPI 置vspi_map、request_irq(guest_irq_handler)、默认irq_mask)。 - guest 侧:写
GICD_ISENABLER→ DABT →dataabort_tfl_handler→vdev_mmio_emulation→vgic_mmio_handler→virq_enable(SPI 且 hw 则irq_unmask)。
11.4 释放中断
- hypervisor 侧:
release_vm_virq:memset(virq_desc)+clear_bit(vspi_map)。 - guest 侧:写
GICD_ICENABLER→ DABT →vgic_mmio_handler→virq_disable→irq_mask。
11.5 中断处理:产生与标记 pending
- 物理中断进 EL2 →
do_handle_host_irq调guest_irq_handler(request_irq注册的 handler)。 send_virq检查 VM 状态 →__send_virq:test_and_set_bit(pending_bitmap)防重、atomic_inc(pending_virq)、SGI 记src。kick_vcpu唤醒目标 vCPU;物理侧irq_eoi/irq_dir。此刻仅 pending,尚未写 LR。
11.6 中断处理:注入与回收
- 进 guest:
enter_to_guest遍历 pending → active 跳过 →find_first_zero_bit(lrs_bitmap)分配 LR →gicv3_send_virq填字段写 LR → bitmap 迁移(active++ / pending–);LR 用尽记last_fail_virq。 - guest 运行:硬件投递,IAR 使 LR state
0b01→0b10,EOIR 使0b10→0b00。 - 退出:
exit_from_guest读 LR state[63:62],INACTIVE 则write_lr(0)清 LR、释放槽、摘 active_bitmap。