[置顶] 泰晓 RISC-V 实验箱,配套 30+ 讲嵌入式 Linux 系统开发公开课
RISC-V KVM 中断处理的实现(二)
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 设备的注册与使用,结合代码进行了分析和解读,并主要以流程图的方式呈现其代码实现。
代码版本
Software | Version |
---|---|
Linux Kernel | 6.0-rc6 |
kvmtool | e17d182ad3f797f01947fc234d95c96c050c534b |
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 进行处理的,如下图所示。
具体注入过程如下:
- 某个发送 vCPU 通过在 VS-mode 调用 ecall,给另外一个接收 vCPU 发送 IPI 中断。
- 此时触发发送 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 ()
- 最后
kvm_riscv_vcpu_set_interrupt()
函数,把 IPI 注入到接收 vCPU 的标志位上,vcpu->arch.irqs_pending
和vcpu->arch.irqs_pending_mask
,然后调用kvm_vcpu_kick()
函数,提醒接收 vCPU,处理 IPI。实际上就是向接收 vCPU 所在的 pCPU 发送 HS-mode IPI(通过函数smp_send_reschedule()
发送),让接收 vCPU 退出。 - 接收 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 里。 - 接收 vCPU 运行到 VS-Mode 后,VS-level software interrupt 触发,由 VS-Mode 的 Guest OS 处理这个 IPI。
VS-Mode 计时器中断
与 VS-mode 软件中断类似,vCPU 的计时器中断处理接口在 arch/riscv/kvm/vcpu_timer.c
中定义,而这些接口则是通过调用 vcpu.c
中统一的中断处理函数实现的(kvm_riscv_vcpu_has/set/unset_interrupts
)。
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, ®) < 0)
// ...
}
ioctl
函数自身定义如下:
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
kvm_*_ioctl
从 VM 到 Kernel:VM 内部触发 IO 控制,调用 kvm_*_ioctl
进行处理
外部中断
kvm_vcpu_ioctl
函数作为 kvm_vcpu_fops.unlocked_ioctl
在 KVM 初始化之时就已经被注册。当发生对 /dev/kvm
的 ioctl
调用时,就会通过如上节所述的 vfs_ioctl
方法调用 filp->f_op->unlocked_ioctl
即 kvm_vcpu_ioctl
进行处理。
KVM 内部与 VS-Mode 外部中断相关的调用如下图所示:
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,访问 htimedelta
,mtimecmp
等 CSR,开销较大。后续有望通过添加 SSTC 扩展 实现对 vstimecmp
的直接访问进而简化虚拟情况下的中断开销。
中断驱动与 PLIC 控制器
这篇文章 基于一个 RTC(Real Time Clock)例程分析了 RISC-V 中断的申请、产生、处理流程。
Linux 内核中涉及 RISC-V 中断相关的处理机制如下图所示,从左到右依次为 PLIC、INTC(INTerrupt Controller)和内核中断处理。
小结
结合本节和上一节中有关 Linux 以及 KVM 对 RISC-V 中断的分析可知,KVM 内实现了将虚拟机内部 VS-mode 的中断与外部中断处理控制器的绑定,同时实现了特定于 VS-mode 的中断处理功能,从而完成了对于 RISC-V 虚拟化的支持。
MMIO 虚拟化
KVM
通过用户态程序(如 kvmtool)创建了 vCPU 之后,vcpu 内部就包含了 MMIO 相关的项,如下图所示。如此,便实现了虚拟机 MMIO 的管理。所以 Guest 的 MMIO 操作都是基于下图所示的数据结构实现的。
mmio 在 Host 一端的注册与销毁如下图所示:
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 设备中注册好的读写函数来实现的:
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 的设备树的初始化。
总结
RISC-V 中断通过 PLIC,CLINT 等驱动和控制器来实现,KVM 模块对于虚拟化的支持体现在两方面,一方面是 KVM 实现了与 Guest 外部的中断控制相关联的 VS-mode 的中断处理,另一方面则是通过为用户态程序如 kvmtool 提供接口,支持了虚拟机内部的设备与中断处理函数的注册与绑定,也实现了虚拟机与内核态的绑定,这使得 Guest 的 MMIO 访存等操作顺利进行。
参考资料
猜你喜欢:
- 我要投稿:发表原创技术文章,收获福利、挚友与行业影响力
- 知识星球:独家 Linux 实战经验与技巧,订阅「Linux知识星球」
- 视频频道:泰晓学院,B 站,发布各类 Linux 视频课
- 开源小店:欢迎光临泰晓科技自营店,购物支持泰晓原创
- 技术交流:Linux 用户技术交流微信群,联系微信号:tinylab
支付宝打赏 ¥9.68元 | 微信打赏 ¥9.68元 | |
请作者喝杯咖啡吧 |
Read Album:
- TinyBPT 和面向 buildroot 的二进制包管理服务(3):服务端说明
- TinyBPT 和面向 buildroot 的二进制包管理服务(2):客户端说明
- TinyBPT 和面向 buildroot 的二进制包管理服务(1):设计简介与框架
- RISC-V Linux 内核及周边技术动态第 118 期
- RISC-V Linux 内核及周边技术动态第 117 期