前言

在虚拟化场景下,中断虚拟化一直是性能与架构设计中的关键点。对于 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 在根据设备的中断优先级设置好内部寄存器状态之后,需要调用 VCPUKVM_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;
}

简单来说,就是会设置 VCPUIRQ 的位图同时尝试唤醒该 GUEST,若正在运行则尝试打断让其感知到中断到来。

GUEST侧

GUEST 读取 PLICMMIO 区域会发生 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 支持的全局状态

  1. 检查 CPU 是否支持 SxAIA 扩展,并读取全局 IMSIC 配置(guest 索引位数、guest 可用 MSI IDs 等)。
  2. 通过探测 HGEIE CSR 的位宽,得到本机每个 hart 可用的 HGEI(Guest External Interrupt)线路数量;再与 IMSIC 的 guest_index_bits 约束取最小,得到最终可用的 HGEI 数量。
  3. 计算 guest 侧可用的最大 MSI ID(kvm_riscv_aia_max_ids)。
  4. 初始化每 CPU 的 HGEI 分配位图并为 S 模式的 SGEI 中断注册 per-CPU IRQ 处理。
  5. 向 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 中断
详细功能

  1. 为每个 CPU 初始化一个 HGEI 分配位图(free_bitmap),用于后续把 VS interrupt file 绑定到具体的 HGEI 线路(位 1 表示该 HGEI 线路空闲可分配;bit0 保留清零)。
  2. 当存在可用 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 设备内核提供设备模型,虚拟化工具可直接通过 VMKVM_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,
};
  • 调用 AIAaia_create 函数,并尝试调用初始化函数 ops->init,这里并未定义。
  • 申请可用文件描述符,绑定匿名 INODE,初始化 file 结构体同时绑定文件的 ops,另外设置fileprivate_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;
}

如果我们采用硬件加速则直接写入该 IMSICIMSIC_MMIO_SETIPNUM_LE


AIA 全链路梳理

1) 内核全局启用

  • kvm_riscv_aia_init():检测 SxAIA、探测 HGEIE 位宽→得出可用 HGEI 数,计算最大 MSI IDaia_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_sourcesAPLIC 地址、以及所有 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 并路由

  • 两条注入路径并存

    1. MSI 设备直达 IMSIC(最佳路径、VM-Exit 少);
    2. 非 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)

完结撒花!!!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐