泰晓科技 -- 聚焦 Linux - 追本溯源,见微知著!
网站地址:https://tinylab.org

基于泰晓RISC-V实验箱的Linux公开课
请稍侯

RISC-V KVM 中断处理的实现(二)

XiakaiPan 创作于 2023/06/30

Corrector: TinyCorrect v0.1 - [tounix spaces tables images urls] Author: XiakaiPan 13212017962@163.com Date: 20230109 Revisor: Walimis walimis@walimis.org Project: RISC-V Linux 内核剖析 Proposal: RISC-V 虚拟化技术调研与分析 Sponsor: PLCT Lab, ISCAS

前言

本文对于 kvmtool 和 KVM 中的中断注入与处理,以及 MMIO 设备的注册与使用,结合代码进行了分析和解读,并主要以流程图的方式呈现其代码实现。

代码版本

SoftwareVersion
Linux Kernel6.0-rc6
kvmtoole17d182ad3f797f01947fc234d95c96c050c534b

KVM 异常处理

RISC-V Trap 类型、编码及其关系

在 RISC-V 中,CSR mcause / scause / vscause 用于记录引发 Trap 的编码,Interrupt 和 Exception 的区分是通过 CSR 最高位作为标志位来实现的,当标志位为 1 时表示当前 Trap 为 Interrupt,为 0 时则是 Exception。

RISC-V 中的中断分为三类:软件中断、计时器中断和外部中断,来自不同特权级的各类中断具有各自的编码。Linux 中对这些中断编码如下:

// arch/riscv/include/asm/csr.h: line 66
/* Exception cause high bit - is an interrupt if set */
#define CAUSE_IRQ_FLAG		(_AC(1, UL) << (__riscv_xlen - 1))

/* Interrupt causes (minus the high bit) */
#define IRQ_S_SOFT		1
#define IRQ_VS_SOFT		2
#define IRQ_M_SOFT		3
#define IRQ_S_TIMER		5
#define IRQ_VS_TIMER		6
#define IRQ_M_TIMER		7
#define IRQ_S_EXT		9
#define IRQ_VS_EXT		10
#define IRQ_M_EXT		11
#define IRQ_PMU_OVF		13

/* Exception causes */
#define EXC_INST_MISALIGNED	0
#define EXC_INST_ACCESS		1
#define EXC_INST_ILLEGAL	2
#define EXC_BREAKPOINT		3
#define EXC_LOAD_ACCESS		5
#define EXC_STORE_ACCESS	7
#define EXC_SYSCALL		8
#define EXC_HYPERVISOR_SYSCALL	9
#define EXC_SUPERVISOR_SYSCALL	10
#define EXC_INST_PAGE_FAULT	12
#define EXC_LOAD_PAGE_FAULT	13
#define EXC_STORE_PAGE_FAULT	15
#define EXC_INST_GUEST_PAGE_FAULT	20
#define EXC_LOAD_GUEST_PAGE_FAULT	21
#define EXC_VIRTUAL_INST_FAULT		22
#define EXC_STORE_GUEST_PAGE_FAULT	23

中断标记前缀为 IRQ(Interrupt ReQuest),异常标记前缀为 EXC(EXCeption)。

KVM 异常处理

KVM 内部处理的是来自于 Guest 的异常,具体来说包括三类:

  • 指令异常:对应 Guest 的虚拟指令异常
  • 内存异常:对应 Guest page-fault
  • 环境调用:对应来自于 Guest 在 VS-mode 的 ecall 指令

详细代码分析参见 此文

KVM 虚拟化相关的中断处理

在 Linux 内核的 arch/riscv/kvm 目录下,实现了对 RISC-V 虚拟化扩展的支持,此节将分析其中有关中断处理的代码实现。据代码可知,KVM 的架构相关的实现中仅包括了 VS-mode 对应的一系列中断的处理,其它中断的处理机制见下一节中断控制器分析。

全局中断基准

如果仅支持 M-Mode,那么默认的中断使能(Interrupt Enable)、Trap 向量、中断请求均以 M-Mode 为基准:

  • CSR 使用 mstatus, mie, mtvec, mcause
  • 状态寄存器标志以 mstatus 的为准:mstatus.mie, mstatus.mpie, mstatus.mpp
  • 中断编码均对应 M-Mode:IRQ_M_SOFT/TIMER/EXT

否则,就以 S-Mode 为基准,如下方代码所示。

// arch/riscv/include/asm/csr.h: line 300
#ifdef CONFIG_RISCV_M_MODE
/* CSR */
# define CSR_STATUS	CSR_MSTATUS
# define CSR_IE		CSR_MIE
# define CSR_TVEC	CSR_MTVEC
# define CSR_SCRATCH	CSR_MSCRATCH
# define CSR_EPC	CSR_MEPC
# define CSR_CAUSE	CSR_MCAUSE
# define CSR_TVAL	CSR_MTVAL
# define CSR_IP		CSR_MIP

/* Status Register Flags */
# define SR_IE		SR_MIE
# define SR_PIE		SR_MPIE
# define SR_PP		SR_MPP

/* Interrupt Cause */
# define RV_IRQ_SOFT		IRQ_M_SOFT
# define RV_IRQ_TIMER	IRQ_M_TIMER
# define RV_IRQ_EXT		IRQ_M_EXT
#else /* CONFIG_RISCV_M_MODE */
# define CSR_STATUS	CSR_SSTATUS
# define CSR_IE		CSR_SIE
# define CSR_TVEC	CSR_STVEC
# define CSR_SCRATCH	CSR_SSCRATCH
# define CSR_EPC	CSR_SEPC
# define CSR_CAUSE	CSR_SCAUSE
# define CSR_TVAL	CSR_STVAL
# define CSR_IP		CSR_SIP

# define SR_IE		SR_SIE
# define SR_PIE		SR_SPIE
# define SR_PP		SR_SPP

# define RV_IRQ_SOFT		IRQ_S_SOFT
# define RV_IRQ_TIMER	IRQ_S_TIMER
# define RV_IRQ_EXT		IRQ_S_EXT
# define RV_IRQ_PMU	IRQ_PMU_OVF
# define SIP_LCOFIP     (_AC(0x1, UL) << IRQ_PMU_OVF)

#endif /* !CONFIG_RISCV_M_MODE */

/* IE/IP (Supervisor/Machine Interrupt Enable/Pending) flags */
#define IE_SIE		(_AC(0x1, UL) << RV_IRQ_SOFT)
#define IE_TIE		(_AC(0x1, UL) << RV_IRQ_TIMER)
#define IE_EIE		(_AC(0x1, UL) << RV_IRQ_EXT)

M/S-Mode 的中断做统一处理,Guest 内部的 VS-Mode 中断将由 KVM 单独处理。下面将对三类中断的实现分别进行分析。

VS-Mode 软件中断

所谓软件中断也称为 IPI(Inter-Processor Interrupt),即处理器间中断。对于 KVM 虚拟机来说,VS-mode 的软件中断是通过 SBI 进行处理的,如下图所示。

具体注入过程如下:

  1. 某个发送 vCPU 通过在 VS-mode 调用 ecall,给另外一个接收 vCPU 发送 IPI 中断。
  2. 此时触发发送 vCPU 所在 pCPU 的 HS-mode 异常,退出到 kvm_riscv_vcpu_exit 中,之后处理流程为:kvm_riscv_vcpu_exit -> kvm_riscv_vcpu_sbi_ecall() -> sbi_ext->handler() -> kvm_sbi_ext_ipi_handler() -> kvm_riscv_vcpu_set_interrupt ()
  3. 最后 kvm_riscv_vcpu_set_interrupt() 函数,把 IPI 注入到接收 vCPU 的标志位上,vcpu->arch.irqs_pendingvcpu->arch.irqs_pending_mask,然后调用 kvm_vcpu_kick() 函数,提醒接收 vCPU,处理 IPI。实际上就是向接收 vCPU 所在的 pCPU 发送 HS-mode IPI(通过函数 smp_send_reschedule() 发送),让接收 vCPU 退出。
  4. 接收 vCPU 退出后,在重新进入运行前,会运行 kvm_riscv_vcpu_flush_interrupts() 函数,把 VS-level software interrupt 写入接收 vCPU 的 vcpu->arch.guest_csr.hvip 里,然后 kvm_riscv_update_hvip() 函数把 vcpu->arch.guest_csr.hvip 写入到 CSR_HVIP,即这个 pCPU 的 HVIP CSR 里。
  5. 接收 vCPU 运行到 VS-Mode 后,VS-level software interrupt 触发,由 VS-Mode 的 Guest OS 处理这个 IPI。
flowchart subgraph arch/riscv/include/asm/csr.h isft[IRQ_VS_SOFT] end subgraph arch/riscv/kvm/main.c hwen[kvm_arch_hardware_enable] end subgraph virt/kvm/kvm_main.c startcpu[kvm_starting_cpu]-->hwennl hwenall[hardware_enable_all]-->hwennl mdl_init[module_kvm_init]--> rv_init[riscv_kvm_init]--> kvm_init[kvm_init]-->ops kvm_exit[kvm_exit]-->ops ops[kvm_syscore_ops]--> resume[kvm_resume]-->hwennl hwennl[hardware_enable_nolock]-->hwen vcpu[kvm_vcpu_ioctl]-->run dev_ioctl[kvm_dev_ioctl]--> dev_create_vm[kvm_dev_ioctl_create_vm]--> cvm[kvm_create_vm]-->hwenall kvm_init-->startcpu kvm_compat[kvm_vcpu_compat_ioctl]-->vcpu exp_exit[EXPORT_SYMBOL_GPL]-->kvm_exit vm[kvm_vm_ioctl]--> cvcpu[kvm_vm_ioctl_create_vcpu]--> vcpu_fd[create_vcpu_fd]--> fops[kvm_vcpu_fops]-->kvm_compat end subgraph arch/riscv/kvm/vcpu_sbi_replace.c ipi[kvm_sbi_ext_ipi_handler] sbi_ipi[vcpu_sbi_ext_ipi]-->ipi end subgraph arch/riscv/kvm/vcpu_sbi.c ecall[kvm_riscv_vcpu_sbi_ecall]--> sbi[sbi_ext]-->sbi_ipi sbi-->sbiv01 end subgraph arch/riscv/kvm/vcpu.c ustint[kvm_riscv_vcpu_unset_interrupt] stint[kvm_riscv_vcpu_set_interrupt] syncint[kvm_riscv_vcpu_sync_interrupts]-->isft run[kvm_arch_vcpu_ioctl_run]-->syncint end subgraph arch/riscv/kvm/vcpu_sbi_v01.c sbiv01[vcpu_sbi_ext_v01]--> v01[kvm_sbi_ext_v01_handler]-->stint v01-->ustint end ipi-->stint stint-->isft ustint-->isft hwen-->isft subgraph arch/riscv/kvm/vcpu_exit.c exit[kvm_riscv_vcpu_exit]-->ecall end

下载由 Mermaid 生成的 PNG 图片

VS-Mode 计时器中断

与 VS-mode 软件中断类似,vCPU 的计时器中断处理接口在 arch/riscv/kvm/vcpu_timer.c 中定义,而这些接口则是通过调用 vcpu.c 中统一的中断处理函数实现的(kvm_riscv_vcpu_has/set/unset_interrupts)。

flowchart LR subgraph arch/riscv/include/asm/csr.h itimer[IRQ_VS_TIMER] end subgraph arch/riscv/kvm/vcpu.c ustint[kvm_riscv_vcpu_unset_interrupt]-->itimer stint[kvm_riscv_vcpu_set_interrupt]-->itimer hasint[kvm_riscv_vcpu_has_interrupts]-->itimer end subgraph arch/riscv/kvm/vcpu_timer.c expired[kvm_riscv_vcpu_hrtimer_expired]-->stint update[kvm_riscv_vcpu_update_hrtimer]-->ustint pending[kvm_riscv_vcpu_timer_pending]-->hasint init[kvm_riscv_vcpu_timer_init]-->expired init-->update init--> vstimer_expired[kvm_riscv_vcpu_vstimer_expired] init--> vstimecmp_update[kvm_riscv_vcpu_update_vstimecmp] end subgraph arch/riscv/kvm/vcpu.c vcpu_create[kvm_arch_vcpu_create]-->init vcpu_pending[kvm_cpu_has_pending_timer]-->pending end subgraph virt/kvm/kvm_main.c check[kvm_vcpu_check_block]-->vcpu_pending end

下载由 Mermaid 生成的 PNG 图片

VS-Mode 外部中断

KVM 中的 ioctl

ioctl

从 Kernel 到 VM:调用 ioctl 注册 KVM 虚拟机并为其申请资源。具体实现可以参见 此文 中有关 kvmtool 创建 VM 的部分。

kvmtool 作为用户态程序,对于 VM 的所有访问都是通过 ioctl 完成的,例如 kvm_cpu__arch_init 初始化 VM、vCPU 和内存:

struct kvm_cpu *kvm_cpu__arch_init(struct kvm *kvm, unsigned long cpu_id)
{
	// ...
	/* 创建 vCPU */
	vcpu->vcpu_fd = ioctl(kvm->vm_fd, KVM_CREATE_VCPU, cpu_id);

	// ...
	/* 获取 VM 的寄存器 */
	if (ioctl(vcpu->vcpu_fd, KVM_GET_ONE_REG, &reg) < 0)

	// ...
}

ioctl 函数自身定义如下:

#include <sys/ioctl.h>

int ioctl(int fd, unsigned long request, ...);
kvm_*_ioctl

从 VM 到 Kernel:VM 内部触发 IO 控制,调用 kvm_*_ioctl 进行处理

flowchart subgraph kvm direction LR i e fops end subgraph i[kvm_*_ioctl] vcpu/device/vm/dev end i-->e subgraph fops[kvm_*_fops] vcpu/device/vm/chardev end subgraph e[elements_in_fops] ... ui[unlocked_ioctl] end e-->fops-->vfs vfs[vfs_ioctl]--> ept(EXPORT_SYMBOL) vfs-->dvfs[do_vfs_ioctl] dvfs-->d3(SYSCALL_DEFINE3) dvfs-->cd3(COMPAT_SYSCALL_DEFINE3) subgraph fs/ioctl vfs dvfs end subgraph include/linux subgraph syscalls d3 cd3 end subgraph export ept end end

下载由 Mermaid 生成的 PNG 图片

外部中断

kvm_vcpu_ioctl 函数作为 kvm_vcpu_fops.unlocked_ioctl 在 KVM 初始化之时就已经被注册。当发生对 /dev/kvmioctl 调用时,就会通过如上节所述的 vfs_ioctl 方法调用 filp->f_op->unlocked_ioctlkvm_vcpu_ioctl 进行处理。

KVM 内部与 VS-Mode 外部中断相关的调用如下图所示:

flowchart LR subgraph arch/riscv/include/asm/csr.h ext[IRQ_VS_EXT] end subgraph arch/riscv/kvm/vcpu.c async[kvm_arch_vcpu_async_ioctl]--> int[kvm_riscv_vcpu_set/unset_interrupt]-->ext end subgraph virt/kvm/kvm_main.c vcpu[kvm_vcpu_ioctl]-->async end

下载由 Mermaid 生成的 PNG 图片

kvm_arch_vcpu_async_ioctl 内部实现依据具体的中断类型采取对应的操作:

// arch/riscv/kvm/vcpu.c: line 569

long kvm_arch_vcpu_async_ioctl(struct file *filp,
			       unsigned int ioctl, unsigned long arg)
{
	struct kvm_vcpu *vcpu = filp->private_data;
	void __user *argp = (void __user *)arg;

	if (ioctl == KVM_INTERRUPT) {
		struct kvm_interrupt irq;

		// 将用户态的由 argp 所指向的中断信息复制到 irq 中
		if (copy_from_user(&irq, argp, sizeof(irq)))
			return -EFAULT;

		// 根据 irq 的中断操作类型,对指定的 vcpu 进行中断操作(set, unset)
		if (irq.irq == KVM_INTERRUPT_SET)
			return kvm_riscv_vcpu_set_interrupt(vcpu, IRQ_VS_EXT);
		else
			return kvm_riscv_vcpu_unset_interrupt(vcpu, IRQ_VS_EXT);
	}

	return -ENOIOCTLCMD;
}

RISC-V 中断在 Linux 中的实现

Timer 驱动

参考 此文 对 RISC-V 计时器在 Linux 内核中的实现的分析,Linux Timer 的实现包含两个驱动文件:

  • 无 MMU 的 drivers/clocksource/timer-riscv.c:运行于 M-mode 下,可直接读取 mtime CSR 获取当前时间、通过 mtimecmp CSR 设置中断,考虑到虚拟化对于特权级的需求,该实现并不会在虚拟化系统中被调用。
  • 有 MMU 的 drivers/clocksource/timer-clint.c:支持 S-mode (S/HS/VS) 下的时钟访问,但因为权限问题,需要借助于 CSR 读写指令达成。在不支持 SSTC 扩展的情况下,需要通过 SBI 写入 mtimecmp 实现计时器中断。

在添加了虚拟化扩展之后,VS-mode 的计时器中断操作需要通过 SBI 进入 HS-mode 再进入 M-mode,访问 htimedeltamtimecmp 等 CSR,开销较大。后续有望通过添加 SSTC 扩展 实现对 vstimecmp 的直接访问进而简化虚拟情况下的中断开销。

中断驱动与 PLIC 控制器

这篇文章 基于一个 RTC(Real Time Clock)例程分析了 RISC-V 中断的申请、产生、处理流程。

Linux 内核中涉及 RISC-V 中断相关的处理机制如下图所示,从左到右依次为 PLIC、INTC(INTerrupt Controller)和内核中断处理。

flowchart e[arch/riscv/kernel/entry.S]-->ghai subgraph kernel/irq/handle.c ghai[generic_handle_arch_irq] shi[set_handle_irq] end subgraph kernel/softirq.c ghai-->ie[irq_exit] ghai-.->so[others] end subgraph other end ghai-.->other subgraph drivers/irqchip/irq-riscv-intc.c direction ii[IRQCHIP_DECLARE:riscv_intc_init]-->shi--> rii[riscv_intc_irq] idm[[intc_domain]] end subgraph kernel/irq/irqdesc.c ghdi[generic_handle_domain_irq] end subgraph include/linux/irqdomain.h al[irq_domain_add_linear] end ii-->al-.return..->idm rii-->ghdi idm-.arg..->ghdi subgraph drivers/irqchip/irq-sifive-plic.c direction TB epid[IRQCHIP_DECLARE: plic_edge_init]-->pei[plic_edge_init]-->pi pid[IRQCHIP_DECLARE: plic_init]--> tpi[__plic_init]-->pi[plic_init]-->phi[plic_handle_irq] end

下载由 Mermaid 生成的 PNG 图片

小结

结合本节和上一节中有关 Linux 以及 KVM 对 RISC-V 中断的分析可知,KVM 内实现了将虚拟机内部 VS-mode 的中断与外部中断处理控制器的绑定,同时实现了特定于 VS-mode 的中断处理功能,从而完成了对于 RISC-V 虚拟化的支持。

MMIO 虚拟化

KVM

通过用户态程序(如 kvmtool)创建了 vCPU 之后,vcpu 内部就包含了 MMIO 相关的项,如下图所示。如此,便实现了虚拟机 MMIO 的管理。所以 Guest 的 MMIO 操作都是基于下图所示的数据结构实现的。

flowchart BT subgraph v[kvm_vcpu] subgraph va[kvm_vcpu_arch] md[kvm_mmio_decode] vao[other arch states, ...] end subgraph r[kvm_run] m[mmio] ro[other run states, ...] end end

下载由 Mermaid 生成的 PNG 图片

mmio 在 Host 一端的注册与销毁如下图所示:

flowchart LR subgraph kvm_main.c cv[kvm_create_vm] cd[kvm_destroy_vm] ... end subgraph coalseced_mmio.c mi[kvm_coalesced_mmio_init] mf[kvm_coalesced_mmio_free] ..., end cv-->mi cv-->mf cd-->mf

下载由 Mermaid 生成的 PNG 图片

KVM 中的 MMIO 的访存操作有如下三个对应处理函数:

// arch/riscv/include/asm/kvm_vcpu_insn.h: line 40
int kvm_riscv_vcpu_mmio_load(struct kvm_vcpu *vcpu, struct kvm_run *run,
			     unsigned long fault_addr,
			     unsigned long htinst);
int kvm_riscv_vcpu_mmio_store(struct kvm_vcpu *vcpu, struct kvm_run *run,
			      unsigned long fault_addr,
			      unsigned long htinst);
int kvm_riscv_vcpu_mmio_return(struct kvm_vcpu *vcpu, struct kvm_run *run);

下图展示了 MMIO 访存操作的具体实现,可以发现 LAOD/STORE 操作最终是通过调用 IO 设备中注册好的读写函数来实现的:

flowchart LR subgraph vi[arch/riscv/kvm/vcpu_insn.c] l[kvm_riscv_vcpu_mmio_load]-->r s[kvm_riscv_vcpu_mmio_store]-->r r[kvm_riscv_vcpu_mmio_return] end subgraph m[virt/kvm/kvm_main.c] rd[kvm_io_bus_read] wr[kvm_io_bus_write] end l-->rd s-->wr subgraph dv[include/kvm/iodev.h] subgraph iodev subgraph ops frd[*read] fwr[*write] end end end rd-.->frd wr-.->fwr

下载由 Mermaid 生成的 PNG 图片

kvmtool 中断注入及 MMIO 创建

在 kvmtool 中 MMIO 是作为 VIRTIO 设备之一连带着中断处理函数一起被注册的。整个过程可以分为两个部分:

  • PLIC,设备树初始化
  • MMIO/PCI 等设备与 PLIC 以及中断处理函数的绑定
  • Console/Net 等设备与初始化时与 MMIO/PCI 设备的绑定

执行完整个 Console 的创建过程就完成了 Guest 的 PLIC、IRQ 与设备的绑定,即实现了虚拟机的中断注入机制与 MMIO 创建。

下图中左上的 virtio_dev_init:virtio_console__init 表示以 KVM 指定的方式初始化设备完成绑定。

右边 RISC-V 模块左下方的 late_init:setup_fdt 则表示包含有 PLIC 的设备树的初始化。

flowchart LR subgraph riscv subgraph irq.c il[kvm__irq_line] it[kvm__irq_trigger] end subgraph plic.c pit[plic__irq_trig] pnd[pci__generate_fdt_nodes] end il-->pit it-->pit subgraph fdt.c li[late_init:setup_fdt] end li-->pnd end subgraph virtio subgraph unified_devices subgraph console.c cdi[virtio_dev_init:virtio_console__init] end subgraph net.c bdi[virtio_dev_init:virtio_net__init] end udo[other unified_devices, ...] end cdi-->vi bdi-->vi udo-.->vi subgraph pci.c pvq[virtio_pci__signal_vq]-->it pvq-->il pcfg[virtio_pci__signal_config]-->it po[other functions, ...] end subgraph mmio.c vq[virtio_mmio_signal_vq]-->it cfg[virtio_mmio_signal_config]-->it mo[other functions, ...] end pm[pci-modern.c]-->il pl[pci-legacy.c]-->il subgraph core.c vi[virtio_init: case VIRTIO_*] cm[mmio] cp[pci] end end cm-.->mmio.c cp-.->pci.c subgraph hw i8[i8042.c]-->il sr[serial.c]-->il end

下载由 Mermaid 生成的 PNG 图片

总结

RISC-V 中断通过 PLIC,CLINT 等驱动和控制器来实现,KVM 模块对于虚拟化的支持体现在两方面,一方面是 KVM 实现了与 Guest 外部的中断控制相关联的 VS-mode 的中断处理,另一方面则是通过为用户态程序如 kvmtool 提供接口,支持了虚拟机内部的设备与中断处理函数的注册与绑定,也实现了虚拟机与内核态的绑定,这使得 Guest 的 MMIO 访存等操作顺利进行。

参考资料



Read Album:

Read Related:

Read Latest: