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 视角走一遍:

  1. 发现:Linux 启动时解析设备树,读到 GIC 的 GICD/GICR 基址与中断号空间。
  2. 初始化配置:GIC 驱动写 GICD_CTLR 使能 distributor,为 SPI 设置类型(GICD_ICFGR)、优先级(GICD_IPRIORITYR)。
  3. 注册并使能:virtio 驱动 request_irq() 注册处理函数,然后写 GICD_ISENABLER 使能这个 SPI。
  4. 接收:网卡有数据时物理中断产生,guest 的 CPU 接口把中断递交给 Linux,CPU 进入 IRQ 异常。
  5. 应答并处理:处理函数读 ICC_IAR1_EL1 得到中断号,调用 virtio 的 handler。
  6. 结束:处理完写 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:

phase5_flow

为什么要介入:三个本质问题

从 hypervisor 视角看,上面那些”介入”背后是三个必须解决的问题:

  1. GIC 是共享硬件。系统中的 GICD(Distributor,全系统共享)和 GICR(Redistributor,每 CPU 一组)只有一份物理实现,多个 guest 不能各自随意读写,否则会互相干扰、甚至破坏 hypervisor 自己的中断配置。
  2. 中断归属需要翻译。物理中断号(pINTID)是全局的,guest 感知的是虚拟中断号(vINTID)。同一块物理设备的中断要能注入到某个 guest 的某个 vCPU,且 guest 的”使能/关闭/优先级”操作不能直达硬件。
  3. 有两种做法。一种是借助 ARM GICv3 提供的硬件虚拟化能力(List Register + VMCR),把要注入的中断直接写进硬件,guest 像用真 GIC 一样收中断;另一种是纯软件模拟——拦截 guest 对 GIC 寄存器的 MMIO 访问,自己维护一份虚拟 GIC 状态。

minos 对 vGICv3 的实现,恰好两种路径都存在:注入走硬件 LR,配置走 MMIO 模拟。本文就沿着这两条路走读代码。

1.2 硬件基础知识

GICv3 基础

为方便后续阅读,这里把本文反复出现、且需对照 ARM GICv3 规范理解的概念列成速查表。不求全面,只覆盖本文实际用到的部分。

GIC 的三个组成部分

以 SPI 为例,中断在三者之间的流向如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
外设中断源(SPI)


┌────────────────────────────────┐
│ GICD Distributor │
│ enable/priority/route SPI │
└──────────────┬─────────────────┘
│ 按亲和性路由
┌──────┴──────┐
▼ ▼
┌────────────────┐ ┌────────────────┐
│ GICR CPU0 │ │ GICR CPU1 │
│ SGI/PPI cfg │ │ SGI/PPI cfg │
└───────┬────────┘ └───────┬────────┘
▼ ▼
┌────────────────┐ ┌────────────────┐
│ ICC_* CPU0 │ │ ICC_* CPU1 │
│ IAR / EOIR │ │ IAR / EOIR │
└───────┬────────┘ └───────┬────────┘
▼ ▼
CPU0 CPU1
  • 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、几位优先级
  • ICH_LR_EL2 —— 注入的中转站

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 共享的,不会不够分。原因是:

  1. LR 是每个物理核一套,不是全局共享ICH_LR<n>_EL2 是 EL2 的 per-CPU 寄存器,每个核各有一套 16 条;而同一时刻一个物理核上只跑一个 vCPU,所以这 16 条只服务”当前这个 vCPU”。
  2. 多 guest 靠 vCPU 调度 + vmodule 保存恢复来复用同一套 LR。切换 vCPU 时,LR 内容随 vmodule 的 state_save/state_restore(第 8 章)一起换进换出,各 guest 的状态互不干扰。
  3. 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-77104-109 的重试逻辑,日志 "VM%d no space to send new irq %d")。硬件其实还提供了维护中断(ICH_HCR_EL2.UIE)在 LR 下溢时主动通知 hypervisor,但 minos 没开。

实际 Linux guest 里,单个 vCPU 同时在途的中断通常远小于 16,所以 16 条绰绰有余;真超了也只是”延迟注入”而非”丢中断”——因为 pending_bitmap 里还记着,下一轮会补上。

  • ICH_HCR_EL2 —— 总开关

最关键的位是 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,即不屏蔽任何优先级。

guest 感知不到真实的 CPU interface 配置,它读到的优先级掩码、中断使能等,来自这个寄存器虚拟出来的值。

其余还有 VEOIM(虚拟 EOI 模式)、VBPR(优先级分组)等控制 guest 侧 EOI 语义的字段,minos 未配置,沿用默认。

  • ICH_VTR_EL2 —— 只读能力报告

hypervisor 启动时读它确定硬件的 LR 数量和优先级位数。vgicv3_initvgicv3.c:1067-1069):

1
2
3
val = read_sysreg32(ICH_VTR_EL2);
gicv3_nr_lr = (val & 0x3f) + 1; // ListRegs 字段
gicv3_nr_pr = ((val >> 29) & 0x7) + 1; // PRIbits 字段

QEMU 的 cortex-a53 上通常报告 16 条 LR;优先级位数由 PRIbits 字段决定。

  • 四者协同的一次完整注入
1
2
3
4
初始化:读 ICH_VTR_EL2(几条 LR)→ 设 ICH_HCR_EL2.En → 设 ICH_VMCR_EL2(VENG1 | VPMR)
注入:hypervisor 写 ICH_LR0_EL2(vINTID=48, state=pending)
交付:硬件投递给 guest → guest 读 IAR 得 48 → 该 LR 的 state 变 active
结束:guest 写 EOIR → 硬件把 LR state 清 inactive → hypervisor 回收该 LR

2. 中断虚拟化的整体框架

先看 minos 里的分层关系:

1
2
3
4
5
6
7
8
9
10
11
12
+----------------------+   virt/virq.c        中断抽象层(virq_desc / 状态机 / 发送)
| virq 抽象层 | 每个 VM 一份 desc,每个 vCPU 一份 bitmap
+----------------------+
│ 调用 ops 表(virq_chip)

+----------------------+ virt/virq_chips/ 虚拟中断控制器实现
| virq_chip 实现 | vgicv3.c / vgicv2.c / bcm_virq.c / vaic.c
+----------------------+
│ 写入
+----------------------+
| 物理 GIC 硬件 | ICH_LR 等 EL2 寄存器
+----------------------+

中断号按 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_descinclude/virt/virq.h:76-88):

1
2
3
4
5
6
7
8
9
10
11
12
13
struct virq_desc {
int32_t flags;
uint16_t vno; /* 虚拟中断号 */
uint16_t hno; /* 物理中断号(非 HW 中断时为 0) */
uint8_t id; /* 在 List Register 中的槽位,无则为 VIRQ_INVALID_ID */
uint8_t state; /* INACTIVE / PENDING / ACTIVE / ACTIVE_AND_PENDING */
uint8_t pr; /* 优先级 */
uint8_t src; /* SGI 来源(发 SGI 的 vCPU) */
uint8_t type; /* 0=LEVEL_HIGH, 1=EDGE_RISING */
uint8_t vcpu_id; /* 目标 vCPU 或 VIRQ_AFFINITY_VCPU_ANY */
uint8_t vmid;
uint8_t padding;
} __packed;

关键字段说明:

  • vno / hno:虚拟号与物理号。硬件路径(HW 中断)下两者都有效,写 LR 时同时填 v_intidp_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
2
3
4
INACTIVE  ──收到中断──▶ PENDING  ──注入 LR/交付给 vCPU──▶ ACTIVE
▲ │
└──────────────guest 处理完并 EO ────────────────────────┘
(EO 时若又 pending,则回到 PENDING)
  • 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_structinclude/virt/virq.h:92-101),保存该 vCPU 所有中断的位图:

1
2
3
4
5
6
7
8
9
10
struct virq_struct {
int nr_lrs; /* 硬件 List Register 数量 */
int last_fail_virq;
atomic_t pending_virq; /* pending 总数(计数) */
uint32_t active_virq; /* active 总数 */
struct virq_desc local_desc[VM_LOCAL_VIRQ_NR]; /* SGI+PPI 的 desc */
unsigned long *pending_bitmap; /* 处于 pending 的 virq 位图 */
unsigned long *active_bitmap; /* 处于 active 的 virq 位图 */
unsigned long lrs_bitmap[BITS_TO_LONGS(VGIC_MAX_LRS)]; /* LR 槽位占用 */
};

数据结构的分工:

  • 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_NRlocal_desc[virq],否则取 vm->vspi_desc[VIRQ_SPI_OFFSET(virq)]

为什么 SGI/PPI 的 desc 是”每 vCPU 一份”而 SPI 是”VM 一份”?因为 SGI/PPI 在硬件上本来就是 per-CPU 的(每个 CPU 各有自己的一份),而 SPI 是全局共享、路由到某个 CPU 的。虚拟化沿用了这个语义。

内存是怎么分配的?virq_create_vmvirt/virq.c:596-642)在 VM 创建时挂 OS_HOOK_CREATE_VM hook,一次性分配:

1
2
3
vspi_desc[](每 VM 一份)
vspi_map(SPI 占用位图,每 VM 一份)
pending_bitmap + active_bitmap(每 vCPU 一份)

各 vCPU 的 virq_struct 本体在 vCPU 创建时由 alloc_vcpuvirt/vm.c:281zalloc,随后 vcpu_virq_struct_initvirt/virq.c:414-439)初始化 local_desc:默认清 HW 标志、置 enable、vcpu_id/vmid = ANYid = VIRQ_INVALID_ID

3.4 SGI/PPI/SPI 的处理差异

  • SGI(软件触发中断):guest 写 ICC_SGI1R_EL1 触发,被 trap 后由 vgicv3_send_sgi 逐目标 vCPU send_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_pervcpuvirt/virq.c:532-563)对每个 vCPU 分别 request。
  • SPI:由 alloc_vm_virqvirt/virq.c:565-579)在 vspi_map 里找一个空闲位,返回 virq + VM_LOCAL_VIRQ_NR

4. virq_chip:抽象 ops 表

不同硬件平台的中断控制器行为不同(GICv2、GICv3、树莓派的 BCM、Apple 的 AIC),所以 minos 把”虚拟中断控制器”抽象成一张 ops 表 struct virq_chipinclude/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_DECLAREinclude/minos/device_id.h:70-75)注册进链接段 .__virqchip。VM 创建时,create_vm_irqchip_ofvirt/resource.c:57-85)根据设备树节点的 compatible 字符串匹配到对应 init 函数(vgicv3_virqchip_init),得到该 VM 的 vm->virq_chip。也就是说,每个 VM 可以有自己的虚拟中断控制器实现

vgicv3_init_virqchipvirt/virq_chips/vgicv3.c:743-762)把上述 ops 全部绑到 vgic 的实现上,前提是 flags & VIRQCHIP_F_HW_VIRT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void vgicv3_init_virqchip(struct virq_chip *vc,
struct vgicv3_dev *dev, unsigned long flags)
{
if (flags & VIRQCHIP_F_HW_VIRT) {
vc->exit_from_guest = vgic_irq_exit_from_guest;
vc->enter_to_guest = vgic_irq_enter_to_guest;
vc->send_virq = gicv3_send_virq;
vc->update_virq = gicv3_update_virq;
vc->get_virq_state = gicv3_get_virq_state;
...
} else {
pr_warn("***WARN***vgicv3 currently only support hard virt mode\n");
}
vc->inc_pdata = dev;
}

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_virqchipvirt/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_EL2(List Register),hypervisor 把要交给 guest 的中断逐条写进 LR,硬件会在 vCPU 运行时直接把 LR 里的中断当成”自己产生的中断”交付给 EL1,完全不需要每次中断都陷入 EL2。guest 对中断的 EO 也由硬件在后台推进 LR 的状态位。

5.1 LR 的结构

include/device/gicv3.h:154-163 用 C 位段声明了 LR 的布局,与 ARM 规范 ICH_LR<n>_EL2 一致(GCC/Clang 在 aarch64 小端下位段自 LSB 依次分配):

1
2
3
4
5
┌───────────────────┬─────────────┬────────┬───────────┬────────┬─────┬────┬───────┐
│ [31:0] │ [41:32] │[47:42] │ [55:48] │[59:56] │[60] │[61]│[63:62]│
│ vINTID │ pINTID │ res0 │ priority │ res1 │grp │ hw │ state │
│ v_intid : 32 │ p_intid : 10│ │ priority: │ │group│ hw │ │
└───────────────────┴─────────────┴────────┴───────────┴────────┴─────┴────┴───────┘
  • 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
2
3
guest 写 GICD_ISENABLER → Stage2 触发 DABT → dataabort_tfl_handler
└─ vdev_mmio_emulation → vgic_mmio_handler → vgic_gicd_mmio_write
└─ virq_enable(置使能标志,绑定物理中断则 irq_unmask)

virq_enablevirt/virq.c:252-265)除了置软件使能标志,对绑定了物理中断的 SPI 还会 irq_unmask(desc->hno)——物理中断从此才允许进 EL2,触发后面那段交付链路。这一阶段的详细模拟逻辑见第 6 章,这里点到为止:使能之后,中断就”就位”了,等着物理设备触发。

第一段:物理中断到来 → 标为 pending

物理设备触发中断 → GIC 硬件把中断路由到某物理核 → 因为 HCR_EL2.IMO 置位,物理中断在 EL2 进入 → do_handle_host_irqcore/irq.c:66-88)调 irq 的 handler。

这个 handler 就是 guest_irq_handlervirt/virq.c:120-138),它是 __request_virqrequest_irq(hwirq, guest_irq_handler, IRQ_FLAGS_VCPU, ...) 注册的(virt/virq.c:477):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int guest_irq_handler(uint32_t irq, void *data)
{
struct virq_desc *desc = (struct virq_desc *)data;

if ((!desc) || (!virq_is_hw(desc))) { ... }

if ((desc->vmid == VIRQ_AFFINITY_VM_ANY) &&
(desc->vcpu_id == VIRQ_AFFINITY_VCPU_ANY))
vcpu = get_current_vcpu();
else
vcpu = get_vcpu_by_id(desc->vmid, desc->vcpu_id);

return send_virq(vcpu, desc);
}

send_virqvirt/virq.c:80-118)做三件事:

  1. 检查 VM 状态(只在 ONLINE/SUSPEND 时接受,SUSPEND 且中断不能唤醒则丢弃)。
  2. __send_virqvirt/virq.c:58-78):在 pending_bitmap 里置位该 virq(test_and_set_bit 保证不重复),atomic_inc(&pending_virq)
  3. virq_kick_vcpukick_vcpu(vcpu, ...) 唤醒/打断目标 vCPU,让它尽快退出 idle 或被抢占状态、重新进入 guest。

注意:到这一步,中断还没有进入硬件 LR,只是记录在内存的 pending_bitmap 里。真正的硬件注入发生在下一次进入 guest 之前。

第二段:进入 guest 前 → 填 LR

vCPU 任务从 EL2 回到 guest 用户态之前,调度器调用 vcpu_return_to_uservirt/vm.c:294-305),它会先 do_hooks(vcpu, regs, OS_HOOK_ENTER_TO_GUEST)

1
2
3
进入 guest:vcpu_return_to_user → OS_HOOK_ENTER_TO_GUEST
└─ virqchip_enter_to_guest(virq_chip.c:26)
└─ vgic_irq_enter_to_guest(vgic.c:42)── 扫描 pending_bitmap,逐条写 LR

vgic_irq_enter_to_guest 是注入的核心(virt/virq_chips/vgic.c:42-112),逻辑:

  1. last_fail_virq 开始遍历 pending_bitmap
  2. 对每个 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)。
  3. 返回 flags:注入了几条、以及是否注入了 FIQ(FIQ_HAS_INJECT 位)。

gicv3_send_virqvirt/virq_chips/vgicv3.c:661-681)真正写 LR:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int gicv3_send_virq(struct vcpu *vcpu, struct virq_desc *virq)
{
uint64_t value = 0;
struct gic_lr *lr = (struct gic_lr *)&value;

if (virq->id >= gicv3_nr_lr) { ... }

lr->v_intid = virq->vno;
lr->p_intid = virq->hno;
lr->priority = virq->pr;
lr->group = 1;
lr->hw = !!virq_is_hw(virq);
lr->state = 1;

gicv3_write_lr(virq->id, value);
return 0;
}

写入后硬件接管:guest 在 EL1 运行时,硬件把 LR 里的 v_intid 当作一个真实的 GIC 中断投递到 vCPU,guest 的中断处理程序照常收中断、EO。这个过程完全不经过 hypervisor——这就是硬件虚拟化的意义:中断交付路径上没有 trap 开销。

第三段:退出 guest 后 → 回收 LR

guest 处理完中断并 EO 后,或者因别的原因退出到 EL2(调度、其他 trap),vcpu_exit_from_uservirt/vm.c:307-318)触发 OS_HOOK_EXIT_FROM_GUEST

1
2
3
退出 guest:vcpu_exit_from_user → OS_HOOK_EXIT_FROM_GUEST
└─ virqchip_exit_from_guest(virq_chip.c:56)
└─ vgic_irq_exit_from_guest(vgic.c:114)── 扫描 active_bitmap,回收已处理的 LR

vgic_irq_exit_from_guestvirt/virq_chips/vgic.c:114-145):

  1. 遍历 active_bitmap 里的每个 virq。
  2. 调用 virqchip_get_virq_stategicv3_get_virq_statevgicv3.c:714-726):读对应 LR 的 [63:62] 位(即代码里的 (value >> 62) & 3),得到硬件反馈的当前状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
static int gicv3_get_virq_state(struct vcpu *vcpu, struct virq_desc *virq)
{
uint64_t value;

if (virq->id >= gicv3_nr_lr)
return 0;

value = gicv3_read_lr(virq->id);
rmb();
value = (value >> 62) & 0x03;

return ((int)value);
}
  1. 若状态为 VIRQ_STATE_INACTIVE(guest 已 EO、硬件已把 LR 清空),则:
    • virqchip_update_virq(vcpu, virq, VIRQ_ACTION_CLEAR)gicv3_update_virqvgicv3.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--
  2. 若状态仍是 pending/active(guest 还没处理完),什么都不做,留待下一轮。

完整链路图

phase5_hw_path

这段链路里最值得体会的是:**”使能”和”配置”是软件路径,”注入”和”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_handlerarch/aarch64/virt/trap.c:241-294)取出故障地址(IPA)后:

1
ret = vdev_mmio_emulation(regs, iswrite, ipa, &value);   // trap.c:272

vdev_mmio_emulationvirt/vdev.c:198-238)遍历当前 VM 的 vdev 链表,用地址匹配 vdev 注册的 iomem 区间:

1
2
3
4
5
6
7
8
9
10
11
12
13
list_for_each_entry(vdev, &vm->vdev_list, list) {
idx = 0;
va = vdev->gvm_area;
while (va) {
if ((address >= va->start) && (address <= va->end)) {
ret = handle_mmio(vdev, regs, write, idx,
address - va->start, value);
...
}
idx++;
va = va->next;
}
}

若没有任何 vdev 认领这个地址,就 trap_vcpu(VMTRAP_TYPE_MMIO, ...) 上报给 host VM(VM0)处理——这是”device passthrough to VM0”的机制。

vGIC 作为 vdev 注册了 5 个地址区间:GICD、GICR、GICC、GICH、GICV(vgicv3_virqchip_initvgicv3.c:832-836)。GICD/GICR 有完整模拟,GICC/GICH/GICV 在 GICv3 下用不到(CPU 接口走系统寄存器而非 MMIO),所以 vgic_mmio_handler 里只实现了 GICD 和 GICR 两类(vgicv3.c:444-447)。

6.2 地址分派

vgic_mmio_handlervgicv3.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/writevgicv3.c:204-293)用 switch 处理各寄存器。两个最有代表性的:

GICD_ISENABLER(使能 SPI,写 1 生效)

1
2
3
4
5
6
case GICD_ISENABLER...GICD_ISENABLER_END:
x = (offset - GICD_ISENABLER) / 4;
y = x * 32;
for_each_set_bit(bit, value, 32)
virq_enable(vcpu, y + bit);
break;

每个 ISENABLER 寄存器管 32 个 SPI,y = x*32 算出这 32 个的起始编号。对每个置位位调用 virq_enablevirt/virq.c:252-265):

1
2
3
4
5
6
7
8
int virq_enable(struct vcpu *vcpu, uint32_t virq)
{
desc = get_virq_desc(vcpu, virq);
virq_set_enable(desc);
if ((virq > VM_LOCAL_VIRQ_NR) && virq_is_hw(desc))
irq_unmask(desc->hno); /* 解开对应的物理中断屏蔽 */
return 0;
}

virq_set_enable 只置软件标志;如果是绑定了物理中断的 SPI,还要 irq_unmask(desc->hno) 真的把物理中断使能——物理中断从此才允许进 EL2 触发 guest_irq_handler。这就是”guest 使能虚拟中断 → 物理中断真正解屏蔽”的联动。

GICD_ICFGR(中断触发类型,边沿/电平)

1
2
3
case GICD_ICFGR...GICD_ICFGR_END:
vgic_set_virq_type(vcpu, offset, *value);
break;

vgic_set_virq_typevgicv3.c:189-202)逐中断调 virq_set_typevirt/virq.c:209-234),后者对 HW 中断会进一步 irq_set_type(desc->hno, ...) 把触发方式同步给物理 GIC。

GICD 模拟的只读寄存器(GICD_CTLR/TYPER/PIDR2/STATUSR)则返回内存里保存的值。vgic_gicd_initvgicv3.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_mmiovgicv3.c:329-387)用 gicr->gicr_enabler0 这个软件影子变量记录状态:

1
2
3
4
5
6
7
8
9
10
case GICR_ISENABLER:
spin_lock(&gicr->gicr_lock);
for_each_set_bit(bit, value, 32) {
if (!(gicr->gicr_enabler0 & BIT(bit))) {
virq_enable(vcpu, bit);
gicr->gicr_enabler0 |= BIT(bit);
}
}
spin_unlock(&gicr->gicr_lock);
break;

GICR_ICPENDR0 的写(清 pending)则调 clear_pending_virqvirt/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_initvgicv3.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_vmvirt/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_guestvgicv2.c:561-578),注入走 32 位的 GICH_LRgicv2_send_virqvgicv2.c:501)。

  • SWE(软件模拟)模式:不绑 enter_to_guestvgicv2.c:564 仅在 HW_VIRT 下绑定),需要额外模拟 CPU 接口 GICCvirt/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:硬编码返回 0xa0vgicv2.c:398-405)。

对比结论(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_initvgicv3.c:1043-1052)填充 vmodule 的 context_size 和四个状态回调,随后 vgicv3_initregister_vcpu_vmodule("gicv3-vmodule", ...)vgicv3.c:1072)把它注册成名为 gicv3-vmodule 的 vmodule:

  • gicv3_state_savevgicv3.c:942-952):dsb() 后批量读 ICH_LR(gicv3_save_lrs)、ICH_AP0R/AP1R(gicv3_save_aprn)、ICC_SRE_EL1ICH_VMCR_EL2ICH_HCR_EL2,存进 struct gicv3_context
  • gicv3_state_restorevgicv3.c:1016-1026):对称写回,最后 dsb()
  • gicv3_state_initvgicv3.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_initvgicv3.c:1054-1075)读 ICH_VTR_EL2gicv3_nr_lr = (val & 0x3f) + 1gicv3_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_handlerarch/aarch64/virt/trap.c:143)捕获后分发:

1
2
3
4
5
case ESR_SYSREG_ICC_SGI1R_EL1:
case ESR_SYSREG_ICC_ASGI1R_EL1:
if (!sysreg->read && (arm_data->sgi1r_el1_trap))
arm_data->sgi1r_el1_trap(vcpu, reg_value); // trap.c:161
break;

sgi1r_el1_trap 在 vgicv3 创建时绑定为 vgicv3_send_sgivgicv3.c:870)。vgicv3_send_sgivgicv3.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. 关键知识点

  1. 中断虚拟化的两条路径:配置(GICD/GICR 寄存器)走 MMIO 软件模拟;交付(写入 LR)走 GICv3 硬件虚拟化。这是 GICv3 相对 GICv2 的核心优势。
  2. virq 编号与物理中断解耦vno(虚拟)与 hno(物理)并存于 virq_deschw 标志决定写 LR 时是否携带 p_intid。使能虚拟中断时(virq_enable)才会真正 irq_unmask 对应物理中断。
  3. 状态机 INACTIVE→PENDING→ACTIVE:pending_bitmap 记录”该给 guest 的”,active_bitmap 记录”已给 guest 在处理的”。同号中断 active 期间再次 pending 会被跳过(vgic.c:67),避免重复注入。
  4. 注入时机在进出 guest 的 hook 里OS_HOOK_ENTER_TO_GUESTvcpu_return_to_user)前批量填 LR;OS_HOOK_EXIT_FROM_GUESTvcpu_exit_from_user)后读 LR 状态回收。LR 不够时记 last_fail_virq 下轮再试。
  5. 纯虚拟中断与 HW 中断的区别:纯虚拟中断 hw=0,EO 由 hypervisor 回收 LR;HW 中断 hw=1,硬件自动 EO 物理中断,hypervisor 只回收 LR 槽位。
  6. 上下文切换靠 vmodulegicv3-vmodule 在 vCPU 调度切换时保存/恢复 ICH_LR、APR、VMCR、HCR。LR 数量来自 ICH_VTR_EL2

11. 函数调用关系总览

把前文分散在各章的调用链汇总成 6 张 UML 时序图,便于快速定位代码。图中消息已细化到函数内部关键步骤,Note 标注了状态变化(文件与行号均已在前文核实)。

11.1 系统初始化

phase5_call1

三个 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_initcore/irq.c:329)遍历设备树,按 compatible = "arm,gic-v3" 匹配到 GIC 节点后调 gicv3_init.init)。它分两半:先配全局共享的 GICD(一次),再配主核自己的 per-CPU 接口。

配置 GICD(全局共享,只需一次):

  1. 解析地址translate_device_address_index 读 dts GIC 节点的 4 段 reg(GICD@0x08000000 / GICR@0x080a0000 / GICC / GICH)存进 array[]io_remap 映射 GICD 与 GICR。
  2. 校验优先级位数:读 ICH_VTR_EL2nr_pr = ((val>>29)&0x7)+1,若不在 5~7 之间则 panic。
  3. 记录每 CPU 的 GICR 基址rbase = GICR起始 + i*128KB,per-CPU 存 gicr_rd_base/gicr_sgi_base(128KB stride 与 64KB 两段划分是 GICv3 规范固定的)。
  4. 关 GICDiowrite32(0, GICD_CTLR)
  5. 读能力GICD_TYPERnr_lines = 32 * (type & 0x1f)(支持的中断线数)。
  6. 触发类型GICD_ICFGR = 0,所有 SPI 默认电平触发、低有效。
  7. 优先级GICD_IPRIORITYR = 0xa0,SPI 默认优先级。
  8. 全部禁用GICD_ICENABLER = 0xffffffff(写 1 清使能)。
  9. 分组GICD_IGROUPR = 0xffffffff,SPI 归 Group1 Non-secure。
  10. 等生效gicv3_gicd_wait_for_rwp(),轮询 GICD_CTLR 的 bit31(RWP)清零。
  11. 使能 GICDGICD_CTLR = 1 | EN_GRP1 | EN_GRP1A | ARE_NS

配置本核 per-CPU 接口:

  1. gicv3_icc_sre_initICC_SRE_EL1/EL2 = 0xf,打开 CPU 接口的系统寄存器访问方式(GICv3 核心前提)。
  2. gicv3_gicr_init:唤醒本核 GICR;SGI 优先级 0x90、PPI 0xa0;SGI 使能、PPI 禁用;归 Group1。
  3. gicv3_gicc_init:记录本核 cpus_affinity[cpu]ICC_BPR1=0ICC_PMR=0xffICC_CTLR=EOImode=1(split 模式)、ICC_IGRPEN1=1
  4. gicv3_hyp_initICH_VMCR_EL2 = VENG1 | 0xff<<24ICH_HCR_EL2 = EN,为 vGIC 的 LR 注入做准备。

主核 vs 从核gicv3_init 只由主核执行一次;每个从核启动时走 gicv3_secondary_initgicv3.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):

  1. 保存地址:把 array 拷贝进全局 vgicv3_info(GICD/GICR/GICC/GICH 的基址与大小),供之后 VM 创建时 get_vgicv3_info 使用。
  2. 校验:GICD/GICR 的基址与大小非 0,否则 panic。
  3. 读硬件能力ICH_VTR_EL2nr_lr = (val&0x3f)+1(List Register 数量)、nr_pr = ((val>>29)&0x7)+1(优先级位数)。
  4. 注册 vmoduleregister_vcpu_vmodule("gicv3-vmodule", gicv3_vmodule_init),让 gicv3 上下文(LR/APR/VMCR/HCR)随 vCPU 调度保存/恢复(见第 8 章)。

11.2 虚拟机初始化

phase5_call2

  • virq_create_vmvspi_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:zalloc virq_structvcpu_virq_struct_init(初始化 local_desc)→ hook 设 nr_lrs

11.3 注册中断

phase5_call3

  • hypervisor 侧:request_hw_virq__request_virq(置 REQUESTED 防重、设 vno/hno、SPI 置 vspi_maprequest_irq(guest_irq_handler)、默认 irq_mask)。
  • guest 侧:写 GICD_ISENABLER → DABT → dataabort_tfl_handlervdev_mmio_emulationvgic_mmio_handlervirq_enable(SPI 且 hw 则 irq_unmask)。

11.4 释放中断

phase5_call4

  • hypervisor 侧:release_vm_virqmemset(virq_desc) + clear_bit(vspi_map)
  • guest 侧:写 GICD_ICENABLER → DABT → vgic_mmio_handlervirq_disableirq_mask

11.5 中断处理:产生与标记 pending

phase5_call5a

  • 物理中断进 EL2 → do_handle_host_irqguest_irq_handlerrequest_irq 注册的 handler)。
  • send_virq 检查 VM 状态 → __send_virqtest_and_set_bit(pending_bitmap) 防重、atomic_inc(pending_virq)、SGI 记 src
  • kick_vcpu 唤醒目标 vCPU;物理侧 irq_eoi/irq_dir。此刻仅 pending,尚未写 LR。

11.6 中断处理:注入与回收

phase5_call5b

  • 进 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。