RISCV中PLIC和AIA的KVM中断处理
RISCV中PLIC和AIA的KVM中断处理
文章目录
前言
在虚拟化场景下,中断虚拟化一直是性能与架构设计中的关键点。对于 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;
else
s->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号),失败则返回 -EFAULT
if (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 是非法值,打印错误并返回 -EINVAL
kvm_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 的 context
cntx = (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 index
cntx = (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);
else
kvm_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);
else
pr_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;
else
irqd->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) |
完结撒花!!!
更多推荐
所有评论(0)