RISCV中PLIC和AIA的KVM中断处理
文章目录
- 前言
- PLIC的中断触发响应流程
- 设备侧
- GUEST侧
- AIA的中断触发响应流程
- Linux内核侧AIA初始化
- AIA用户侧设备初始化
- KVM侧运行VCPU时会初始化AIA
- 设备侧触发 AIA 中断
- 不支持 MSI 的设备
- MSI设备触发中断
- imsic 中断设置
- AIA 全链路梳理
- 1) 内核全局启用
- 2) VM / vCPU 默认上下文
- 3) 用户态创建并配置设备
- 4) 内核最终初始化
- 5) 运行期关键点
- 总结
前言
在虚拟化场景下,中断虚拟化一直是性能与架构设计中的关键点。对于 RISC-V 平台而言,最初广泛使用的是 PLIC (Platform-Level Interrupt Controller)
,它负责收集外设中断并分发到各个核。然而,随着 RISC-V
生态的发展,PLIC
在可扩展性和中断延迟方面逐渐暴露出问题。为此,RISC-V
标准引入了 AIA (Advanced Interrupt Architecture)
,通过 IMSIC (Interrupt Message Signaled Interrupt Controller)
和 APLIC (Advanced Platform-Level Interrupt Controller)
的组合,为中断虚拟化提供了更高效的硬件基础。
在 KVM (Kernel-based Virtual Machine)
中,这两套中断架构的支持方式有明显差异。传统的 PLIC
更多依赖用户态虚拟机管理器(如 QEMU/kvmtool)来模拟,而 AIA 则提供了完整的 in-kernel irqchip
模型,能让中断处理下沉到内核,减少 VM-Exit
开销,大幅提升虚拟机对外设中断的响应效率。
PLIC的中断触发响应流程
PLIC
设备本身不支持中断虚拟化,因此需要通过软件模拟的方式为 GUEST
设置中断控制器的实现。
设备侧
PLIC
向下的接口给设备使用,用于接收设备的中断触发响应信号。
static void plic__irq_trig(struct kvm *kvm, int irq, int level, bool edge)
{bool irq_marked = false;u8 i, irq_prio, irq_word;u32 irq_mask;struct plic_context *c = NULL;struct plic_state *s = &plic;if (!s->ready)return;if (irq <= 0 || s->num_irq <= (u32)irq)goto done;mutex_lock(&s->irq_lock);irq_prio = s->irq_priority[irq];irq_word = irq / 32;irq_mask = 1 << (irq % 32);if (level)s->irq_level[irq_word] |= irq_mask;elses->irq_level[irq_word] &= ~irq_mask;/** Note: PLIC interrupts are level-triggered. As of now,* there is no notion of edge-triggered interrupts. To* handle this we auto-clear edge-triggered interrupts* when PLIC context CLAIM register is read.*/for (i = 0; i < s->num_context; i++) {c = &s->contexts[i];mutex_lock(&c->irq_lock);if (c->irq_enable[irq_word] & irq_mask) {if (level) {c->irq_pending[irq_word] |= irq_mask;c->irq_pending_priority[irq] = irq_prio;if (edge)c->irq_autoclear[irq_word] |= irq_mask;} else {c->irq_pending[irq_word] &= ~irq_mask;c->irq_pending_priority[irq] = 0;c->irq_claimed[irq_word] &= ~irq_mask;c->irq_autoclear[irq_word] &= ~irq_mask;}__plic_context_irq_update(s, c);irq_marked = true;}mutex_unlock(&c->irq_lock);if (irq_marked)break;}done:mutex_unlock(&s->irq_lock);
}
PLIC
在根据设备的中断优先级设置好内部寄存器状态之后,需要调用 VCPU
的 KVM_INTERRUPT IOCTL
触发外部中断。
static void __plic_context_irq_update(struct plic_state *s,struct plic_context *c)
{u32 best_irq = __plic_context_best_pending_irq(s, c);u32 virq = (best_irq) ? KVM_INTERRUPT_SET : KVM_INTERRUPT_UNSET;if (ioctl(c->vcpu->vcpu_fd, KVM_INTERRUPT, &virq) < 0)pr_warning("KVM_INTERRUPT failed");
}
Linux
内核内部 KVM
接收用户的 IOCTL
。做如下处理:
// KVM 架构相关的异步 ioctl 处理函数
long kvm_arch_vcpu_async_ioctl(struct file *filp,unsigned int ioctl, unsigned long arg)
{void __user *argp = (void __user *)arg;struct kvm_vcpu *vcpu = filp->private_data; // 从 file 中取出对应的 vCPU 实例// 这里只处理 KVM_INTERRUPT 这个 ioctl,用于用户态注入外部中断if (ioctl == KVM_INTERRUPT) {struct kvm_interrupt irq;// 从用户态拷贝参数 (irq号),失败则返回 -EFAULTif (copy_from_user(&irq, argp, sizeof(irq)))return -EFAULT;// 打印调试信息,显示当前 vCPU id 及中断号kvm_debug("[%d] %s: irq: %d\n", vcpu->vcpu_id, __func__, irq.irq);// 调用核心逻辑函数,真正处理中断注入return kvm_vcpu_ioctl_interrupt(vcpu, &irq);}// 其它 ioctl 未定义,返回错误return -ENOIOCTLCMD;
}// 将一个 irq 号标记到 vCPU 的待处理中断集合中
static inline void kvm_queue_irq(struct kvm_vcpu *vcpu, unsigned int irq)
{// 设置 irq 对应的 pending 位,表示该中断待处理set_bit(irq, &vcpu->arch.irq_pending);// 清除 irq 对应的 clear 位,避免被误认为已清除clear_bit(irq, &vcpu->arch.irq_clear);
}// 注入中断的具体实现
int kvm_vcpu_ioctl_interrupt(struct kvm_vcpu *vcpu, struct kvm_interrupt *irq)
{int intr = (int)irq->irq;if (intr > 0)// 正数表示“触发一个中断”,放入 pending 队列kvm_queue_irq(vcpu, intr);else if (intr < 0)// 负数表示“撤销一个中断”,从 pending 队列移除kvm_dequeue_irq(vcpu, -intr);else {// 0 是非法值,打印错误并返回 -EINVALkvm_err("%s: invalid interrupt ioctl %d\n", __func__, irq->irq);return -EINVAL;}// 关键步骤:kick vCPU// 如果该 vCPU 正在 Guest 模式运行,kick 会通过 IPI/调度机制// 把它拉回内核,使其检查 pending 的中断并在合适位置注入kvm_vcpu_kick(vcpu);return 0;
}
简单来说,就是会设置 VCPU
的 IRQ
的位图同时尝试唤醒该 GUEST,若正在运行则尝试打断让其感知到中断到来。
GUEST侧
GUEST
读取 PLIC
的 MMIO
区域会发生 VMEXIT
。进而退出到 USER
设备模拟的位置进行访问。
// PLIC 的 MMIO 访问回调函数
// 当 Guest 访问 PLIC 寄存器区域时(读写 MMIO),会进入这里
static void plic__mmio_callback(struct kvm_cpu *vcpu,u64 addr, u8 *data, u32 len,u8 is_write, void *ptr)
{u32 cntx;struct plic_state *s = ptr; // 全局 PLIC 状态结构// PLIC 所有寄存器宽度都是 32 位 (4 字节),长度错误直接报错if (len != 4)die("plic: invalid len=%d", len);// 对齐到 4 字节边界addr &= ~0x3;// 去掉基地址偏移,转换为相对偏移地址addr -= RISCV_IRQCHIP;if (is_write) { // Guest 在写 PLIC 寄存器// 写优先级寄存器区间if (PRIORITY_BASE <= addr && addr < ENABLE_BASE) {plic__priority_write(s, addr, data);// 写中断使能寄存器区间} else if (ENABLE_BASE <= addr && addr < CONTEXT_BASE) {// 计算这是哪个 hart 的 contextcntx = (addr - ENABLE_BASE) / ENABLE_PER_HART;// 计算该 hart 内部的相对偏移addr -= cntx * ENABLE_PER_HART + ENABLE_BASE;if (cntx < s->num_context)plic__context_enable_write(s,&s->contexts[cntx],addr, data);// 写 context(claim/complete、阈值等寄存器)} else if (CONTEXT_BASE <= addr && addr < REG_SIZE) {// 计算 context indexcntx = (addr - CONTEXT_BASE) / CONTEXT_PER_HART;// 去掉该 context 的基地址addr -= cntx * CONTEXT_PER_HART + CONTEXT_BASE;if (cntx < s->num_context)plic__context_write(s, &s->contexts[cntx],addr, data);}} else { // Guest 在读 PLIC 寄存器// 读优先级寄存器if (PRIORITY_BASE <= addr && addr < ENABLE_BASE) {plic__priority_read(s, addr, data);// 读中断使能寄存器} else if (ENABLE_BASE <= addr && addr < CONTEXT_BASE) {cntx = (addr - ENABLE_BASE) / ENABLE_PER_HART;addr -= cntx * ENABLE_PER_HART + ENABLE_BASE;if (cntx < s->num_context)plic__context_enable_read(s,&s->contexts[cntx],addr, data);// 读 context(claim/complete、阈值等寄存器)} else if (CONTEXT_BASE <= addr && addr < REG_SIZE) {cntx = (addr - CONTEXT_BASE) / CONTEXT_PER_HART;addr -= cntx * CONTEXT_PER_HART + CONTEXT_BASE;if (cntx < s->num_context)plic__context_read(s, &s->contexts[cntx],addr, data);}}
}
AIA的中断触发响应流程
Linux内核侧AIA初始化
kvm_riscv_aia_init - 初始化 KVM 在 RISC-V 上的 AIA 支持的全局状态
- 检查 CPU 是否支持 SxAIA 扩展,并读取全局 IMSIC 配置(guest 索引位数、guest 可用 MSI IDs 等)。
- 通过探测 HGEIE CSR 的位宽,得到本机每个 hart 可用的 HGEI(Guest External Interrupt)线路数量;再与 IMSIC 的 guest_index_bits 约束取最小,得到最终可用的 HGEI 数量。
- 计算 guest 侧可用的最大 MSI ID(kvm_riscv_aia_max_ids)。
- 初始化每 CPU 的 HGEI 分配位图并为 S 模式的 SGEI 中断注册 per-CPU IRQ 处理。
- 向 KVM 注册 AIA 设备类型,并启用静态分支标记,宣布 AIA 已可用。
返回值:0 成功;负值表示失败。
int kvm_riscv_aia_init(void)
{int rc;const struct imsic_global_config *gc;/* 1) 没有 SxAIA 扩展则直接不支持 */if (!riscv_isa_extension_available(NULL, SxAIA))return -ENODEV;/* 读取全局 IMSIC 配置(可能为 NULL,取决于固件/平台是否暴露) */gc = imsic_get_global_config();/* 2) 探测 HGEIE(Hypervisor Guest External Interrupt Enable)可用位数* 方法:先把 HGEIE 的所有位写 1(-1UL),再读出来,用 fls_long 求最高位索引,* 最后清零(写 0)恢复。注意 bit0 保留不用,所以读到的位数要减 1。*/csr_write(CSR_HGEIE, -1UL);kvm_riscv_aia_nr_hgei = fls_long(csr_read(CSR_HGEIE));csr_write(CSR_HGEIE, 0);if (kvm_riscv_aia_nr_hgei)kvm_riscv_aia_nr_hgei--; /* 排除 bit0,最终得到可用 HGEI 线数 *//** 3) HGEI 可用条数还受到 IMSIC 的 guest_index_bits 约束:* 每个 hart 的 guest interrupt files 数量 = 2^(guest_index_bits),* 其中 guest-index 0 通常保留,所以可用条数最多为 2^bits - 1。* 取探测到的 HGEIE 位数 与 (2^guest_index_bits - 1) 的最小值。* 若 gc 不存在(平台未提供 IMSIC 全局配置),则视为 0。*/if (gc)kvm_riscv_aia_nr_hgei = min((ulong)kvm_riscv_aia_nr_hgei,BIT(gc->guest_index_bits) - 1);elsekvm_riscv_aia_nr_hgei = 0;/* 4) 计算 guest 侧可用的最大 MSI ID(位图需要 +1 容纳 ID0 这个“保留位”) */kvm_riscv_aia_max_ids = IMSIC_MAX_ID; /* 默认上限(平台常量) */if (gc && kvm_riscv_aia_nr_hgei) /* 只有在存在 gc 且确有 HGEI 时才用平台给定值 */kvm_riscv_aia_max_ids = gc->nr_guest_ids + 1;/* 5) 初始化 per-CPU 的 HGEI 分配器,并为 SGEI 建立 per-CPU IRQ 入口 */rc = aia_hgei_init();if (rc)return rc;/* 6) 注册 AIA 设备类型到 KVM(/dev/kvm 的设备控制接口) */rc = kvm_register_device_ops(&kvm_riscv_aia_device_ops,KVM_DEV_TYPE_RISCV_AIA);if (rc) {aia_hgei_exit();return rc;}/* 7) 启用静态分支:让热路径以 AIA 可用的方式工作(性能优化) */static_branch_enable(&kvm_riscv_aia_available);return 0;
}
aia_hgei_init 函数说明
初始化每 CPU 的 HGEI 线路管理,并注册 SGEI per-CPU 中断
详细功能
- 为每个 CPU 初始化一个 HGEI 分配位图(free_bitmap),用于后续把 VS interrupt file 绑定到具体的 HGEI 线路(位 1 表示该 HGEI 线路空闲可分配;bit0 保留清零)。
- 当存在可用 HGEI 时,在 Linux 中断子系统里:
找到本机 INTC 的 irq_domain;
将 RISC-V 的 SGEI(IRQ_S_GEXT)映射成 Linux IRQ 号;
注册 per-CPU 的 SGEI 中断处理函数 hgei_interrupt。这样当硬件通过 IMSIC 触发“Guest External Interrupt”时,宿主 S 模式可收到 SGEI,再由 KVM 把该中断注入到对应 vCPU 的 VS-level interrupt file。
static int aia_hgei_init(void)
{int cpu, rc;struct irq_domain *domain;struct aia_hgei_control *hgctrl;/* 1) 初始化每 CPU 的 HGEI 分配状态(位图 & 自旋锁) */for_each_possible_cpu(cpu) {hgctrl = per_cpu_ptr(&aia_hgei, cpu);raw_spin_lock_init(&hgctrl->lock);if (kvm_riscv_aia_nr_hgei) {/* 构造 (n+1) 位的全 1 位图,再清掉 bit0(保留),* 结果就是 [1..n] 全为可用(free)。*/hgctrl->free_bitmap =BIT(kvm_riscv_aia_nr_hgei + 1) - 1;hgctrl->free_bitmap &= ~BIT(0);} else {/* 没有可用的 HGEI 线路 */hgctrl->free_bitmap = 0;}}/* 若没有任何 HGEI,跳过 SGEI 的注册(SGEI 仅在有 guest 外部中断才有意义) */if (!kvm_riscv_aia_nr_hgei)goto skip_sgei_interrupt;/* 2a) 找到本机的 INTC irq_domain(CPU 内置中断控制器的域) */domain = irq_find_matching_fwnode(riscv_get_intc_hwnode(),DOMAIN_BUS_ANY);if (!domain) {kvm_err("unable to find INTC domain\n");return -ENOENT;}/* 2b) 将 RISC-V 的 SGEI(Supervisor Guest External Interrupt)映射为 Linux IRQ 号* IRQ_S_GEXT 是体系结构定义的 S 模式“Guest 外部中断”源。*/hgei_parent_irq = irq_create_mapping(domain, IRQ_S_GEXT);if (!hgei_parent_irq) {kvm_err("unable to map SGEI IRQ\n");return -ENOMEM;}/* 2c) 注册 per-CPU 的 SGEI 中断处理函数* - hgei_interrupt:当 S 模式收到 SGEI 时调用,驱动 KVM 向对应 vCPU 注入中断。* - &aia_hgei 作为 per-CPU 的回调数据。*/rc = request_percpu_irq(hgei_parent_irq, hgei_interrupt,"riscv-kvm", &aia_hgei);if (rc) {kvm_err("failed to request SGEI IRQ\n");return rc;}skip_sgei_interrupt:return 0;
}
在创建虚拟机之后,会初始化 AIA
的部分数据结构。如下:
/*** kvm_riscv_aia_init_vm - 初始化某个 VM 的 AIA 全局上下文默认值* @kvm: 指向当前虚拟机结构体** 功能介绍:* - 在 KVM 全局已启用 AIA 的前提下,为“该 VM”填入一组合理的* AIA 设备/路由默认参数;此时不做任何内存分配,真正的内存与硬件* 资源准备会在用户态创建/初始化 AIA 设备(ioctl)后进行(参见 aia_init)。* - 根据宿主是否有可用 HGEI 线路来决定 AIA 运行模式(AUTO/EMUL);* 并设置可用的 guest MSI ID 上限、源数、路由编码位数以及缺省的 APLIC 基址等。*/
void kvm_riscv_aia_init_vm(struct kvm *kvm)
{struct kvm_aia *aia = &kvm->arch.aia;/* 如果 KVM 未启用 AIA(静态分支关闭),直接返回,不做任何初始化。 */if (!kvm_riscv_aia_available())return;/** 注意:此处不做内存分配。* 真实的 AIA 设备创建/配置由用户态(/dev/kvm 的设备 ioctl)触发,* 包括与 IMSIC/APLIC 相关的资源准备。详见 aia_init()。*//* 初始化该 VM 的 AIA 全局上下文的默认值 *//* * 根据宿主的 HGEI 条数决定模式:* - 若有可用 HGEI:默认采用 AUTO(尽量使用硬件路径/混合路径);* - 否则:采用 EMUL(纯软件仿真)。*/aia->mode = (kvm_riscv_aia_nr_hgei) ?KVM_DEV_RISCV_AIA_MODE_AUTO : KVM_DEV_RISCV_AIA_MODE_EMUL;/** 可用的 guest MSI ID 数:默认使用全局最大值减去保留的 ID0。* (常见约定:ID0 作为保留位,不作为普通可分配 ID)*/aia->nr_ids = kvm_riscv_aia_max_ids - 1;/* 初始时尚未声明任何中断源(APLIC 未配置/未连接),置 0。 */aia->nr_sources = 0;/** 路由分组相关位数默认置 0,表示未启用分组或等待后续用户态配置。* nr_group_bits:用于编码“组”的位数(如 APLIC 级联/分组场景)。*/aia->nr_group_bits = 0;/** 组字段的“起始偏移”采用最小缺省值(协议/实现允许的最小位移)。* 后续由用户态根据拓扑选择更大的 shift,以便把 group/hart 等字段* 正确地打包到 IMSIC/消息地址编码中。*/aia->nr_group_shift = KVM_DEV_RISCV_AIA_GROUP_SHIFT_MIN;/** 与目标 hart 编码/位宽相关的缺省值,初始置 0,等待后续由用户态* 根据 vCPU 数量/布局来设置。*/aia->nr_hart_bits = 0;/** 与 guest 索引(多来宾文件,多 VM)编码相关的缺省位数,初始置 0,* 后续由用户态或者平台信息决定。*/aia->nr_guest_bits = 0;/** 缺省 APLIC 基址标记为未定义(UNDEF_ADDR)。* 若该 VM 需要 APLIC(软件可编程中断控制器),用户态将设置* 实际的映射地址并触发内核侧的设备注册。*/aia->aplic_addr = KVM_RISCV_AIA_UNDEF_ADDR;
}
创建 VCPU 之后会进行 VCPU 侧的 AIA 初始化,具体如下:
/*** kvm_riscv_vcpu_aia_init - 初始化单个 vCPU 的 AIA 上下文默认值* @vcpu: 目标 vCPU** 功能介绍:* - 若全局未启用 AIA,则不做任何事(返回 0,保持兼容/优雅降级)。* - 不在此处分配任何内存或注册任何设备;真正的资源准备在用户态完成* AIA 设备初始化后进行(见 aia_init() 路径)。* - 为该 vCPU 的 AIA 上下文写入安全的默认值:IMSIC 地址标记为未定义,* hart_index 设为 vCPU 的索引,便于后续路由/编码。** 返回:始终返回 0(即便未启用 AIA,此函数也只是 no-op)。*/
int kvm_riscv_vcpu_aia_init(struct kvm_vcpu *vcpu)
{struct kvm_vcpu_aia *vaia = &vcpu->arch.aia_context;/* 若 KVM 未启用 AIA(静态分支为 false),直接返回。* 这样做可以让同一套代码在有/无 AIA 的平台上都能工作。*/if (!kvm_riscv_aia_available())return 0;/** 注意:这里不做任何内存分配或 MMIO 注册。* 这些动作会在用户态创建设备并下发配置后再进行(参见 aia_init())。*//* 填入 vCPU AIA 上下文的默认值 */vaia->imsic_addr = KVM_RISCV_AIA_UNDEF_ADDR; /* VS-IMSIC 还未映射,标记为未定义 */vaia->hart_index = vcpu->vcpu_idx; /* 记录该 vCPU 的索引,供路由/编码使用 */return 0;
}
AIA用户侧设备初始化
AIA
设备内核提供设备模型,虚拟化工具可直接通过 VM
的 KVM_CREATE_DEVICE
这个 IOCTL
创建 AIA
设备。
void aia__create(struct kvm *kvm)
{int err;struct kvm_create_device aia_device = {.type = KVM_DEV_TYPE_RISCV_AIA,.flags = 0,};if (kvm->cfg.arch.ext_disabled[KVM_RISCV_ISA_EXT_SSAIA])return;err = ioctl(kvm->vm_fd, KVM_CREATE_DEVICE, &aia_device);if (err)return;aia_fd = aia_device.fd;
}
KVM
接收用户的 IOCTL
之后,处理如下。
struct kvm_device_ops kvm_riscv_aia_device_ops = {.name = "kvm-riscv-aia",.create = aia_create,.destroy = aia_destroy,.set_attr = aia_set_attr,.get_attr = aia_get_attr,.has_attr = aia_has_attr,
};
- 调用
AIA
的aia_create
函数,并尝试调用初始化函数ops->init
,这里并未定义。 - 申请可用文件描述符,绑定匿名
INODE
,初始化file
结构体同时绑定文件的ops
,另外设置file
的private_data
属性为该KVM_DEVICE
设备指针,方便OPS
回调时取用:
/** kvm_ioctl_create_device - 通过 /dev/kvm 的 KVM_CREATE_DEVICE ioctl 创建一个 KVM 子设备* @kvm: 目标 VM* @cd : 用户态传入的设备创建参数(type/flags),内核回填 fd** 典型流程:* 1) 校验 type 并从全局表找到对应的 kvm_device_ops;* 2) 若是 TEST 标志则仅测试能力返回 0;* 3) 分配并初始化 kvm_device 核心对象;* 4) 持 kvm->lock 调 ops->create 完成设备专有初始化,并把设备挂到 VM 的设备链表;* 5) 可选调用 ops->init 做延后初始化;* 6) 提升 VM 的引用计数(kvm_get_kvm),并为该设备创建一个匿名 inode 的文件描述符;* 7) 把得到的 fd 回填到用户参数 cd->fd。*/
static int kvm_ioctl_create_device(struct kvm *kvm,struct kvm_create_device *cd)
{const struct kvm_device_ops *ops;struct kvm_device *dev;bool test = cd->flags & KVM_CREATE_DEVICE_TEST; /* 仅测试能力而不真正创建 */int type;int ret;/* 1) 类型越界直接失败 */if (cd->type >= ARRAY_SIZE(kvm_device_ops_table))return -ENODEV;/* 2) 使用 nospec 版索引,缓解 Spectre 类投机攻击带来的越界风险 */type = array_index_nospec(cd->type, ARRAY_SIZE(kvm_device_ops_table));ops = kvm_device_ops_table[type];if (ops == NULL)return -ENODEV; /* 该 type 未注册设备操作集 *//* 3) TEST 模式:只校验是否支持该设备类型,不真正创建内核对象 */if (test)return 0;/* 4) 分配 kvm_device 核心对象(带账户记账标志) */dev = kzalloc(sizeof(*dev), GFP_KERNEL_ACCOUNT);if (!dev)return -ENOMEM;/* 5) 基本字段绑定:设备操作集与所属 VM */dev->ops = ops;dev->kvm = kvm;/* 6) 进入临界区:调用设备专有的 create() 完成内核侧初始化 */mutex_lock(&kvm->lock);ret = ops->create(dev, type);if (ret < 0) {mutex_unlock(&kvm->lock);kfree(dev); /* create 失败,释放已分配对象 */return ret;}/* 7) 把设备挂到 VM 的设备链表,使用 RCU 以支持并发读者 */list_add_rcu(&dev->vm_node, &kvm->devices);mutex_unlock(&kvm->lock);/* 8) 可选的延后初始化钩子(非必须) */if (ops->init)ops->init(dev);/* 9) 提升 VM 的引用计数,确保设备持有期间 VM 不会被释放 */kvm_get_kvm(kvm);/* 10) 为该设备创建一个匿名 inode 的文件描述符* - name: 设备名(用于 /proc 等处显示)* - fops: 文件操作表(读写 ioctl 等)* - priv: 关联到文件的私有数据,这里传 dev* - flags: 可读写 + close-on-exec*/ret = anon_inode_getfd(ops->name, &kvm_device_fops, dev, O_RDWR | O_CLOEXEC);/* 11) 把 fd 回填给用户 */cd->fd = ret;/* 按当前代码逻辑,无论 ret 是否为负都会返回 0(见下面备注) */return 0;
}
之后用户通过一系列 IOCTL
设置 AIA
设备属性。如下:
/** aia__init - 在用户态配置并初始化 KVM 的 RISC-V AIA 设备* @kvm: 该 VM 的 kvm 句柄(含 vCPU 数等信息)** 职责:* 1) 读取/设置 AIA 设备的基础属性(mode、nr_ids、nr_sources、hart_bits)。* 2) 为 APLIC 与每个 vCPU 的 VS-IMSIC 指定 MMIO 基地址。* 3) 建立默认的中断路由。* 4) 发送 INIT 控制指令,令内核侧真正完成 AIA 设备初始化。** 说明:* - 本函数运行在用户态(如 VMM/仿真器)侧,通过 KVM_{GET,SET}_DEVICE_ATTR ioctl* 与内核的 AIA 设备交互。aia_fd 为此前 KVM_CREATE_DEVICE 获得的设备 fd。*/
static int aia__init(struct kvm *kvm)
{int i, ret;u64 aia_addr = 0;/* 用于传参的“地址类”属性:把用户态变量地址传给内核读/写 */struct kvm_device_attr aia_addr_attr = {.group = KVM_DEV_RISCV_AIA_GRP_ADDR, /* 地址相关属性组 */.addr = (u64)(unsigned long)&aia_addr, /* 传入/传出:aia_addr */};/* 用于发送“控制类”属性:INIT 控制命令 */struct kvm_device_attr aia_init_attr = {.group = KVM_DEV_RISCV_AIA_GRP_CTRL, /* 控制相关属性组 */.attr = KVM_DEV_RISCV_AIA_CTRL_INIT, /* 触发初始化 */};/* 预先把几个全局 device_attr 的“addr”字段指向各自的用户态变量。* 之后 ioctl 时,内核会从这些地址读/写对应的值。*/aia_mode_attr.addr = (u64)(unsigned long)&aia_mode; /* AIA 运行模式(AUTO/EMUL 等) */aia_nr_ids_attr.addr = (u64)(unsigned long)&aia_nr_ids; /* 可用 MSI ID 数 */aia_nr_sources_attr.addr = (u64)(unsigned long)&aia_nr_sources; /* APLIC 中断源数量 */aia_hart_bits_attr.addr = (u64)(unsigned long)&aia_hart_bits; /* 编码 hart 需要的位数 *//* 若 AIA 设备尚未创建(fd 无效),什么也不做,直接返回 */if (aia_fd < 0)return 0;/* ---------- 读取/设置 AIA 的基础参数 ---------- *//* 从内核读取当前 AIA mode(用于了解内核端默认或允许的模式) */ret = ioctl(aia_fd, KVM_GET_DEVICE_ATTR, &aia_mode_attr);if (ret)return ret;/* 从内核读取可用的 MSI ID 数(通常内核根据平台能力给出上限) */ret = ioctl(aia_fd, KVM_GET_DEVICE_ATTR, &aia_nr_ids_attr);if (ret)return ret;/* 统计/决定该 VM 将要声明的中断源数量,并写回内核* 这里示例用 irq__get_nr_allocated_lines() 作为默认的“可用源数”*/aia_nr_sources = irq__get_nr_allocated_lines();ret = ioctl(aia_fd, KVM_SET_DEVICE_ATTR, &aia_nr_sources_attr);if (ret)return ret;/* 计算编码 hart 索引所需的位数(如 nrcpus=8,则需 3 bit)。* fls_long(x) 返回最高置位 bit 的索引 + 1;对 (nrcpus-1) 求 fls 即为位宽。*/aia_hart_bits = fls_long(kvm->nrcpus - 1);ret = ioctl(aia_fd, KVM_SET_DEVICE_ATTR, &aia_hart_bits_attr);if (ret)return ret;/* 记录 HART 数(后续生成 FDT/设备树时会用到) */aia_nr_harts = kvm->nrcpus;/* ---------- 为 APLIC 与 VS-IMSIC 指定 MMIO 基地址 ---------- *//* 设置 APLIC 的 MMIO 基地址 */aia_addr = AIA_APLIC_ADDR;aia_addr_attr.attr = KVM_DEV_RISCV_AIA_ADDR_APLIC; /* 选择“APLIC 地址”这个属性项 */ret = ioctl(aia_fd, KVM_SET_DEVICE_ATTR, &aia_addr_attr);if (ret)return ret;/* 为每个 vCPU 设置其 VS-IMSIC 的 MMIO 基地址* 注意:attr 编码中通常包含 vCPU 索引(宏 KVM_DEV_RISCV_AIA_ADDR_IMSIC(i))*/for (i = 0; i < kvm->nrcpus; i++) {aia_addr = AIA_IMSIC_ADDR(i); /* 计算第 i 个 VS-IMSIC 地址 */aia_addr_attr.attr = KVM_DEV_RISCV_AIA_ADDR_IMSIC(i); /* 选择第 i 个 IMSIC 地址属性项 */ret = ioctl(aia_fd, KVM_SET_DEVICE_ATTR, &aia_addr_attr);if (ret)return ret;}/* ---------- 建立默认的中断路由 ---------- *//* 典型地:把 APLIC 源路由到对应 vCPU 的 VS-IMSIC 文件,配置 MSI 路由项等 */aia__irq_routing_init(kvm);/* ---------- 触发内核端完成 AIA 设备初始化 ---------- *//* 发送 INIT 控制命令:内核据此前参数完成实际资源分配/绑定/注册 */ret = ioctl(aia_fd, KVM_SET_DEVICE_ATTR, &aia_init_attr);/* 之后还有你的收尾/错误处理逻辑…… */......
}
最后会调用 KVM_SET_DEVICE_ATTR
这个 IOCTL
调用 aia_init
来初始化。
/*
完成整台 VM 的 AIA 最终初始化:
检查配置、初始化 APLIC、为每个 vCPU 建好 VS-IMSIC,
并将 VM 标记为已初始化。过程中确保所有 vCPU 的 IMSIC 基址布局一致,便于地址编码。
*/
static int aia_init(struct kvm *kvm)
{int ret, i;unsigned long idx;struct kvm_vcpu *vcpu;struct kvm_vcpu_aia *vaia;struct kvm_aia *aia = &kvm->arch.aia;gpa_t base_ppn = KVM_RISCV_AIA_UNDEF_ADDR;/* 该 VM 的 irqchip(AIA) 只能初始化一次,重复初始化直接报忙 */if (kvm_riscv_aia_initialized(kvm))return -EBUSY;/* 防止在 vCPU 创建中途(数量未一致)做初始化,避免并发状态不一致 */if (kvm->created_vcpus != atomic_read(&kvm->online_vcpus))return -EBUSY;/* 中断源数量不能超过可用的 MSI ID 数 */if (aia->nr_ids < aia->nr_sources)return -EINVAL;/* 若存在中断源,则必须提供 APLIC 基地址 */if (aia->nr_sources && aia->aplic_addr == KVM_RISCV_AIA_UNDEF_ADDR)return -EINVAL;/* 初始化 APLIC(软件可编程中断控制器),失败直接返回 */ret = kvm_riscv_aia_aplic_init(kvm);if (ret)return ret;/* 逐个 vCPU 完成 VS-IMSIC 初始化前的检查与准备 */kvm_for_each_vcpu(idx, vcpu, kvm) {vaia = &vcpu->arch.aia_context;/* 每个 vCPU 都必须设置好 VS-IMSIC 的 MMIO 基地址 */if (vaia->imsic_addr == KVM_RISCV_AIA_UNDEF_ADDR) {ret = -EINVAL;goto fail_cleanup_imsics;}/* 所有 vCPU 的 IMSIC 基址在“公共 PPN 前缀”上必须一致 */if (base_ppn == KVM_RISCV_AIA_UNDEF_ADDR)base_ppn = aia_imsic_ppn(aia, vaia->imsic_addr);if (base_ppn != aia_imsic_ppn(aia, vaia->imsic_addr)) {ret = -EINVAL;goto fail_cleanup_imsics;}/* 基于 IMSIC 基址解码并更新该 vCPU 的 hart 索引(路由编码用) */vaia->hart_index = aia_imsic_hart_index(aia,vaia->imsic_addr);/* 为该 vCPU 分配并注册 VS-IMSIC(挂到 KVM 的 MMIO 总线上) */ret = kvm_riscv_vcpu_aia_imsic_init(vcpu);if (ret)goto fail_cleanup_imsics;}/* 成功则标记 AIA 初始化完成 */kvm->arch.aia.initialized = true;return 0;fail_cleanup_imsics:/* 失败回滚:把已初始化的 vCPU VS-IMSIC 逐个清理掉 */for (i = idx - 1; i >= 0; i--) {vcpu = kvm_get_vcpu(kvm, i);if (!vcpu)continue;kvm_riscv_vcpu_aia_imsic_cleanup(vcpu);}/* 同时清理 APLIC */kvm_riscv_aia_aplic_cleanup(kvm);return ret;
}
KVM侧运行VCPU时会初始化AIA
kvm_riscv_vcpu_aia_imsic_update 在 vCPU 迁移到新宿主 CPU 时,切换其 VS-IMSIC 绑定
int kvm_riscv_vcpu_aia_imsic_update(struct kvm_vcpu *vcpu)
{unsigned long flags;phys_addr_t new_vsfile_pa;struct imsic_mrif tmrif;void __iomem *new_vsfile_va;struct kvm *kvm = vcpu->kvm;struct kvm_run *run = vcpu->run;struct kvm_vcpu_aia *vaia = &vcpu->arch.aia_context;struct imsic *imsic = vaia->imsic_state;int ret = 0, new_vsfile_hgei = -1, old_vsfile_hgei, old_vsfile_cpu;/* 纯软件仿真(EMUL)模式:不涉及 VS-IMSIC 切换,直接继续运行 */if (kvm->arch.aia.mode == KVM_DEV_RISCV_AIA_MODE_EMUL)return 1;/* 读取“旧 VS-file”的绑定信息(hgei 槽 & 所在宿主 CPU) */read_lock_irqsave(&imsic->vsfile_lock, flags);old_vsfile_hgei = imsic->vsfile_hgei;old_vsfile_cpu = imsic->vsfile_cpu;read_unlock_irqrestore(&imsic->vsfile_lock, flags);/* 若 vCPU 并未迁核(旧 CPU == 新 CPU),无需切换 VS-IMSIC,继续运行 */if (old_vsfile_cpu == vcpu->cpu)return 1;/* 为“新 CPU”分配一个 VS-IMSIC 槽,并获得其内核映射 VA 与物理地址 PA */ret = kvm_riscv_aia_alloc_hgei(vcpu->cpu, vcpu,&new_vsfile_va, &new_vsfile_pa);if (ret <= 0) {/* 纯硬件加速模式:VS-file 分配失败无法继续,构造 FAIL_ENTRY 退出 */if (kvm->arch.aia.mode == KVM_DEV_RISCV_AIA_MODE_HWACCEL) {run->fail_entry.hardware_entry_failure_reason = CSR_HSTATUS;run->fail_entry.cpu = vcpu->cpu;run->exit_reason = KVM_EXIT_FAIL_ENTRY;return 0;}/* 自动模式:回退。先释放旧 VS-file(若之前已绑定过硬件槽) */if (old_vsfile_cpu >= 0)kvm_riscv_vcpu_aia_imsic_release(vcpu);/* 跳到结尾:不再尝试硬件 VS-file,继续以软件路径运行 */goto done;}new_vsfile_hgei = ret; /* 成功时 ret 返回新分配的 HGEI 槽号 *//** 接下来需要把“中断生产者”(MSI 发起者)迁移到新 VS-file:* 在更新路由前,先把新 VS-file 寄存器内容清零,避免脏状态。*//* 本地清零新 VS-file 的寄存器状态(pending/enable/threshold/claim 等) */imsic_vsfile_local_clear(new_vsfile_hgei, imsic->nr_hw_eix);/* 在 G-stage 将 guest 的 VS-IMSIC GPA 重映射到“新 VS-file”的物理页 */ret = kvm_riscv_gstage_ioremap(kvm, vcpu->arch.aia_context.imsic_addr,new_vsfile_pa, IMSIC_MMIO_PAGE_SZ,true, true);if (ret)goto fail_free_vsfile_hgei;/* TODO: 如需支持设备直通,还要同步更新 IOMMU 映射 *//* 原子更新 imsic 上下文:指向“新 VS-file” */write_lock_irqsave(&imsic->vsfile_lock, flags);imsic->vsfile_hgei = new_vsfile_hgei;imsic->vsfile_cpu = vcpu->cpu;imsic->vsfile_va = new_vsfile_va;imsic->vsfile_pa = new_vsfile_pa;write_unlock_irqrestore(&imsic->vsfile_lock, flags);/** 现在生产者已经指向新 VS-file:* 把“旧 VS-file(或 SW-file)”里的寄存器状态迁移到新 VS-file。*/memset(&tmrif, 0, sizeof(tmrif));if (old_vsfile_cpu >= 0) {/* 从旧 VS-file 读取并清空寄存器状态(true=读取后清除) */imsic_vsfile_read(old_vsfile_hgei, old_vsfile_cpu,imsic->nr_hw_eix, true, &tmrif);/* 旧 VS-file 的硬件槽释放回池子 */kvm_riscv_aia_free_hgei(old_vsfile_cpu, old_vsfile_hgei);} else {/* 若之前没有硬件 VS-file,说明一直用 SW-file:从 SW-file 读取并清空 */imsic_swfile_read(vcpu, true, &tmrif);}/* 将保存的寄存器快照写入“新 VS-file”(完成状态迁移) */imsic_vsfile_local_update(new_vsfile_hgei, imsic->nr_hw_eix, &tmrif);done:/* 更新 guest 的 HSTATUS.VGEIN(VS-IMSIC 入口号)为“新 HGEI 槽” */vcpu->arch.guest_context.hstatus &= ~HSTATUS_VGEIN;if (new_vsfile_hgei > 0)vcpu->arch.guest_context.hstatus |=((unsigned long)new_vsfile_hgei) << HSTATUS_VGEIN_SHIFT;/* 返回 1 告知上层:本次入口可继续执行 vCPU run-loop */return 1;fail_free_vsfile_hgei:/* 若中途失败,释放刚分配的新 VS-file 槽并返回错误码 */kvm_riscv_aia_free_hgei(vcpu->cpu, new_vsfile_hgei);return ret;
}
设备侧触发 AIA 中断
常规线中断设备无法利用 MSI
发起中断,因此需要借助 APLIC
组件。
一般我们会在特定中断控制器上层抽象出 irqchip
。触发中断时,根据特定 chip
的设置,决定触发方式。如下:
void kvm__irq_line(struct kvm *kvm, int irq, int level)
{struct kvm_irq_level irq_level;if (riscv_irqchip_inkernel) {irq_level.irq = irq;irq_level.level = !!level;if (ioctl(kvm->vm_fd, KVM_IRQ_LINE, &irq_level) < 0)pr_warning("%s: Could not KVM_IRQ_LINE for irq %d\n",__func__, irq);} else {if (riscv_irqchip_trigger)riscv_irqchip_trigger(kvm, irq, level, false);elsepr_warning("%s: Can't change level for irq %d\n",__func__, irq);}
}
而支持 MSI
的设备本身则通过如下方式触发中断:
static int irq__default_signal_msi(struct kvm *kvm, struct kvm_msi *msi)
{return ioctl(kvm->vm_fd, KVM_SIGNAL_MSI, msi);
}
AIA
本身 riscv_irqchip_inkernel
为真。故不支持 MSI
的设备使用 KVM_IRQ_LINE
这个 VM IOCTL
触发外部设备中断。Linux
内核做接收并响应。
不支持 MSI 的设备
依据 APLIC 源的触发模式与输入电平,决定是否注入 MSI 到 VS-IMSIC
/*** @kvm: 目标 VM* @source: APLIC 中断源编号(>=1 且 < aplic->nr_irqs)* @level: 本次采样到的源输入电平(true=高电平/上升沿语义,false=低电平/下降沿语义)** 处理流程:* 1) 校验 APLIC/源号有效性,读取域级使能位 IE;* 2) 加锁读取并更新该源的状态机(EDGE/LEVEL 译码),必要时置 PENDING;* 3) 若域级使能且该源 EN(abled)+PENDING 同时为真,则清 PENDING 并标记需注入;* 4) 解锁后根据路由目标 target,调用 aplic_inject_msi() 发出 MSI 到 VS-IMSIC。** 返回:0 表示流程执行完毕(是否实际注入取决于条件);-ENODEV 表示 APLIC/源非法。*/
int kvm_riscv_aia_aplic_inject(struct kvm *kvm, u32 source, bool level)
{u32 target;bool inject = false, ie;unsigned long flags;struct aplic_irq *irqd;struct aplic *aplic = kvm->arch.aia.aplic_state;/* 基本校验:APLIC 必须存在;source 在有效范围内(source>=1 且 <nr_irqs) */if (!aplic || !source || (aplic->nr_irqs <= source))return -ENODEV;/* 获取该源的描述符与域级全局使能位 IE */irqd = &aplic->irqs[source];ie = (aplic->domaincfg & APLIC_DOMAINCFG_IE) ? true : false;/* 进入 per-source 临界区,进行电平/沿译码与状态更新 */raw_spin_lock_irqsave(&irqd->lock, flags);/* 若该源被禁用(D 位),直接跳过处理 */if (irqd->sourcecfg & APLIC_SOURCECFG_D)goto skip_unlock;/* 按源的触发模式进行译码,必要时置 PENDING(避免重复置位) */switch (irqd->sourcecfg & APLIC_SOURCECFG_SM_MASK) {case APLIC_SOURCECFG_SM_EDGE_RISE:/* 上升沿:当前 level=1,且上次 INPUT=0,且尚未 pending */if (level && !(irqd->state & APLIC_IRQ_STATE_INPUT) &&!(irqd->state & APLIC_IRQ_STATE_PENDING))irqd->state |= APLIC_IRQ_STATE_PENDING;break;case APLIC_SOURCECFG_SM_EDGE_FALL:/* 下降沿:当前 level=0,且上次 INPUT=1,且尚未 pending */if (!level && (irqd->state & APLIC_IRQ_STATE_INPUT) &&!(irqd->state & APLIC_IRQ_STATE_PENDING))irqd->state |= APLIC_IRQ_STATE_PENDING;break;case APLIC_SOURCECFG_SM_LEVEL_HIGH:/* 高电平触发:level=1 且未 pending 时置位 */if (level && !(irqd->state & APLIC_IRQ_STATE_PENDING))irqd->state |= APLIC_IRQ_STATE_PENDING;break;case APLIC_SOURCECFG_SM_LEVEL_LOW:/* 低电平触发:level=0 且未 pending 时置位 */if (!level && !(irqd->state & APLIC_IRQ_STATE_PENDING))irqd->state |= APLIC_IRQ_STATE_PENDING;break;}/* 更新输入采样位(用于下一次沿检测) */if (level)irqd->state |= APLIC_IRQ_STATE_INPUT;elseirqd->state &= ~APLIC_IRQ_STATE_INPUT;/* 读取路由目标(目标 VS-IMSIC 的编码) */target = irqd->target;/* 若域级使能,且该源“已使能+已挂起”(ENPEND 组合同时为真),则触发注入* 条件满足时先清掉本次 PENDING,避免重复注入*/if (ie && ((irqd->state & APLIC_IRQ_STATE_ENPEND) ==APLIC_IRQ_STATE_ENPEND)) {irqd->state &= ~APLIC_IRQ_STATE_PENDING;inject = true;}skip_unlock:raw_spin_unlock_irqrestore(&irqd->lock, flags);/* 解锁后执行实际的 MSI 注入到 VS-IMSIC(按 target 路由) */if (inject)aplic_inject_msi(kvm, source, target);return 0;
}
aplic_inject_msi 后续根据 target 提取出 hart 和 guest_idx。
static void aplic_inject_msi(struct kvm *kvm, u32 irq, u32 target)
{u32 hart_idx, guest_idx, eiid;/* 从 APLIC 路由目标 target 解码:* target 位域布局大致为 [ ... | HART_IDX | GUEST_IDX | EIID ]* - HART_IDX:目标 vCPU(hart)的索引* - GUEST_IDX:目标 VS-level interrupt file(guest/虚拟文件索引)* - EIID:External Interrupt ID(要注入的中断号)*//* 取 HART 索引:右移到低位,再用掩码保留有效位 */hart_idx = target >> APLIC_TARGET_HART_IDX_SHIFT;hart_idx &= APLIC_TARGET_HART_IDX_MASK;/* 取 GUEST 索引:右移到低位,再用掩码保留有效位 */guest_idx = target >> APLIC_TARGET_GUEST_IDX_SHIFT;guest_idx &= APLIC_TARGET_GUEST_IDX_MASK;/* 取 EIID(最低若干位保存中断标识),直接掩码即可 */eiid = target & APLIC_TARGET_EIID_MASK;/* 按 (hart_idx, guest_idx, eiid) 三元组把 MSI 注入到对应 VS-IMSIC 文件 */kvm_riscv_aia_inject_msi_by_id(kvm, hart_idx, guest_idx, eiid);
}
kvm_riscv_aia_inject_msi_by_id 按 (hart, guest, iid) 三元组向目标 vCPU 的 VS-IMSIC 注入 MSI
int kvm_riscv_aia_inject_msi_by_id(struct kvm *kvm, u32 hart_index,u32 guest_index, u32 iid)
{unsigned long idx;struct kvm_vcpu *vcpu;/* 仅当 VM 的 AIA 成功初始化后才允许注入 */if (!kvm_riscv_aia_initialized(kvm))return -EBUSY;/* 在该 VM 的所有 vCPU 中查找 hart_index 匹配者并注入 */kvm_for_each_vcpu(idx, vcpu, kvm) {if (vcpu->arch.aia_context.hart_index == hart_index)return kvm_riscv_vcpu_aia_imsic_inject(vcpu,guest_index,0, /* eiinfo=0(可按需扩展) */iid);}/* 没有任何 vCPU 的 hart_index 匹配:不算错误,这里返回 0(未注入) */return 0;
}
MSI设备触发中断
kvm_riscv_aia_inject_msi处理一条发往本 VM 的 MSI,将其注入到匹配的 vCPU VS-IMSIC
/*** 功能说明:* - 解析 MSI 的目标地址,提取出:* 1) 目标 VS-IMSIC 所在的“页号前缀” tppn(去掉页内偏移后);* 2) 目标 guest 文件索引 g(位于 PPN 低位的 nr_guest_bits);* 3) 页内偏移 toff(用作 EIINFO/门铃偏移等按实现定义的信息)。* - 在本 VM 的 vCPU 中查找 VS-IMSIC 基址页号(ippn)与 tppn 匹配的那个 vCPU,* 找到后调用 kvm_riscv_vcpu_aia_imsic_inject(vcpu, g, toff, iid) 完成注入。*/
int kvm_riscv_aia_inject_msi(struct kvm *kvm, struct kvm_msi *msi)
{gpa_t tppn, ippn;unsigned long idx;struct kvm_vcpu *vcpu;u32 g, toff, iid = msi->data; /* data 携带 EIID */struct kvm_aia *aia = &kvm->arch.aia;/* 组合出 64-bit 目标物理地址(MSI doorbell 地址) */gpa_t target = (((gpa_t)msi->address_hi) << 32) | msi->address_lo;/* 仅当 VM 的 AIA 已初始化后才允许注入 */if (!kvm_riscv_aia_initialized(kvm))return -EBUSY;/* 1) 取目标地址的“页号”部分(去掉页内偏移),单位为 IMSIC 页大小 */tppn = target >> IMSIC_MMIO_PAGE_SHIFT;/* 2) 从 PPN 低位剥离出 guest 文件索引 g(宽度为 nr_guest_bits),* 并把这几位清零以得到“共用的 VS-IMSIC 基址页号前缀”。*/g = tppn & (BIT(aia->nr_guest_bits) - 1);tppn &= ~((gpa_t)(BIT(aia->nr_guest_bits) - 1));/* 3) 在所有 vCPU 中寻找 VS-IMSIC 基址页号前缀匹配的那个 vCPU */kvm_for_each_vcpu(idx, vcpu, kvm) {/* vCPU VS-IMSIC 的 GPA 基址页号(同样去掉页内偏移) */ippn = vcpu->arch.aia_context.imsic_addr >>IMSIC_MMIO_PAGE_SHIFT;if (ippn == tppn) {/* 4) 取页内偏移作为 EIINFO/实现相关的附加信息 */toff = target & (IMSIC_MMIO_PAGE_SZ - 1);/* 5) 注入到该 vCPU 的 VS-IMSIC:guest=g, eiinfo=toff, iid=EIID */return kvm_riscv_vcpu_aia_imsic_inject(vcpu, g,toff, iid);}}/* 未匹配到任何 vCPU:返回 0(静默未注入) */return 0;
}
imsic 中断设置
上文我们知道,不管是 APLIC
触发的中断还是直接 MSI
触发的中断,最终都调用函数 kvm_riscv_vcpu_aia_imsic_inject
来触发。
kvm_riscv_vcpu_aia_imsic_inject 向目标 vCPU 的 VS-IMSIC 注入一条 MSI(通过 SETIPNUM“门铃”)
/*** 功能说明:* - 本函数模拟对 VS-IMSIC 的 SETIPNUM 寄存器写入,以“门铃”的方式将 @iid 对应的挂起位置 1;* - 若该 vCPU 已绑定硬件 VS-file(vsfile_cpu >= 0),则直接向其 MMIO 写入并 kick vCPU;* - 否则落到软件影子 SW-file:置位 pending 位并更新外部中断状态(imsic_swfile_extirq_update)。*/
int kvm_riscv_vcpu_aia_imsic_inject(struct kvm_vcpu *vcpu,u32 guest_index, u32 offset, u32 iid)
{unsigned long flags;struct imsic_mrif_eix *eix;struct imsic *imsic = vcpu->arch.aia_context.imsic_state;/* 基本校验:* - 仅支持“每个 vCPU 一个 IMSIC MMIO 页”的模型;* - guest_index 必须为 0(仅一个 VS-file);* - offset 仅允许 SETIPNUM_LE/BE 两种“门铃”寄存器;* - iid 不能为 0(通常 0 号保留)。*/if (!imsic || !iid || guest_index ||(offset != IMSIC_MMIO_SETIPNUM_LE &&offset != IMSIC_MMIO_SETIPNUM_BE))return -ENODEV;/* 若选择的是 BE 端寄存器,需要对写入值进行大小端交换 */iid = (offset == IMSIC_MMIO_SETIPNUM_BE) ? __swab32(iid) : iid;/* 防越界:iid 必须小于该 vCPU VS-IMSIC 支持的 MSI 上限 */if (imsic->nr_msis <= iid)return -EINVAL;/* 进入 VS-file 上下文的读锁(保护 vsfile_cpu/hgei/VA/PA 与 SW-file 并发访问) */read_lock_irqsave(&imsic->vsfile_lock, flags);if (imsic->vsfile_cpu >= 0) {/* 硬件 VS-file 已绑定:* 通过向 SETIPNUM_LE 门铃寄存器写入 iid 来置位对应 pending,* 然后 kick vCPU 以尽快处理该中断。*/writel(iid, imsic->vsfile_va + IMSIC_MMIO_SETIPNUM_LE);kvm_vcpu_kick(vcpu);} else {/* 尚未绑定硬件 VS-file:写入软件影子 SW-file* 计算 EIX 槽(每槽一个 u64 位图),设置对应位为 pending,* 再调用 swfile_extirq_update 通知/刷新外部中断状态。*/eix = &imsic->swfile->eix[iid / BITS_PER_TYPE(u64)];set_bit(iid & (BITS_PER_TYPE(u64) - 1), eix->eip);imsic_swfile_extirq_update(vcpu);}read_unlock_irqrestore(&imsic->vsfile_lock, flags);return 0;
}
如果我们采用硬件加速则直接写入该 IMSIC
的 IMSIC_MMIO_SETIPNUM_LE
。
AIA 全链路梳理
1) 内核全局启用
kvm_riscv_aia_init()
:检测 SxAIA、探测HGEIE
位宽→得出可用 HGEI 数,计算最大 MSI ID;aia_hgei_init()
建 per-CPU HGEI 位图并注册 SGEI 中断;注册 AIA 设备类型并打开静态分支。
2) VM / vCPU 默认上下文
kvm_riscv_aia_init_vm()
:设置 VM 级默认参数(模式、ID 上限、APLIC 地址未定等)。kvm_riscv_vcpu_aia_init()
:设置 vCPU 级默认值(imsic_addr
未定,hart_index = vcpu_idx
)。
3) 用户态创建并配置设备
KVM_CREATE_DEVICE(RISCV_AIA)
→ 得到aia_fd
。aia__init()
:GET/SET_DEVICE_ATTR
读取/设置 mode / nr_ids / nr_sources / hart_bits;写入 APLIC 基址与每个 vCPU 的 IMSIC 基址;建立默认路由;最后CTRL_INIT
触发内核完成资源落地。
4) 内核最终初始化
aia_init()
:校验 nr_ids ≥ nr_sources、APLIC 地址、以及所有 vCPU 的 IMSIC 基址必须有一致的“公共 PPN 前缀”;kvm_riscv_aia_aplic_init()
;逐 vCPU 调kvm_riscv_vcpu_aia_imsic_init()
把 VS-IMSIC 注册到 KVM MMIO 总线;标记 VM 已初始化。
5) 运行期关键点
- vCPU 迁核:
kvm_riscv_vcpu_aia_imsic_update()
在新 CPU 分配 HGEI 槽,清新 VS-file,G-stage 重映射 VS-IMSIC 页(直通设备需同步 IOMMU),迁移寄存器状态,更新HSTATUS.VGEIN
。 - 非 MSI 设备触发:
kvm_riscv_aia_aplic_inject()
按源的沿/电平译码置PENDING
,满足“域使能+已使能+挂起”即调用aplic_inject_msi()
;后者解码{hart_idx, guest_idx, eiid}
,走kvm_riscv_aia_inject_msi_by_id()
→ 目标 vCPU →kvm_riscv_vcpu_aia_imsic_inject()
。 - MSI 设备触发:
kvm_riscv_aia_inject_msi()
解析 MSI doorbell 地址的 目标 PPN 前缀/guest_index/页内偏移,匹配 vCPU 的 IMSIC 基址页号后注入。 - 最终写门铃:
kvm_riscv_vcpu_aia_imsic_inject()
对 VS-IMSIC 的SETIPNUM_{LE,BE}
写入iid
;若无硬件 VS-file 则写 SW-file 位图并更新外部中断状态。
总结
-
AIA = IMSIC + APLIC:IMSIC 负责接收 MSI 并注入 VS-file;APLIC 负责把有线(线/沿/电平)中断转换成 MSI 并路由。
-
两条注入路径并存:
- MSI 设备直达 IMSIC(最佳路径、VM-Exit 少);
- 非 MSI 设备经 APLIC(译码→发一条 MSI→IMSIC)。
-
KVM in-kernel irqchip:AIA 主要在内核侧完成,极大减少用户态模拟和 VM-Exit。
维度 | PLIC | AIA(IMSIC + APLIC) |
---|---|---|
架构定位 | 全局平台中断控制器 | IMSIC 接收 MSI;APLIC 仅为非 MSI设备做“线→MSI”转换 |
触发模型 | 线中断(电平/优先级/Claim/Complete) | MSI 为主;有线中断经 APLIC→MSI→IMSIC |
虚拟化路径 | 用户态模拟 PLIC MMIO,多次 VM-Exit | 内核 irqchip 为主,VM-Exit 显著减少 |
延迟/吞吐 | VM-Exit 频繁、延迟高 | 快路径在内核/硬件,延迟低、可扩展性强 |
设备支持 | 主要面向非 MSI 设备 | 同时支持 MSI 直达 与 APLIC 转 MSI |
路由与注入 | 用户态决定并通过 KVM_INTERRUPT | 内核解析 MSI 地址或 APLIC target,直接注入 VS-IMSIC |
vCPU 迁核 | 与 PLIC 关系弱 | 需要切换 VS-IMSIC 绑定、G-stage 重映射,直通需同步 IOMMU |
代码职责 | 用户态 VMM 负担重 | 内核 AIA 设备模型 + KVM in-kernel irqchip 负担重 |
可扩展性 | 大规模场景吃力 | 设计即面向大规模(多 hart/guest/group) |
完结撒花!!!