《KVM虚拟化:RISCV架构下Linux源码分析系列文章》
本系列文章将对Linux在RISCV架构下KVM虚拟化的实现方法进行梳理与源码分析,主要侧重点放在RISCV架构下,KVM模块初始化、vcpu虚拟、内存虚拟、以及中断虚拟(AIA)的实现上。
在进行正式介绍之前,有必要对文章引用进行提前说明。本系列文章参考了大量的博客、文章以及书籍:
- Linux虚拟化 - 随笔分类 - LoyenWang - 博客园
- 泰晓科技:RISCV虚拟化
- Sherlock’sblog:虚拟化
- albertxu216/LinuxKernel_Learning/Linux6.5源码注释
KVM虚拟化 | RISCV:(一)RISCV框架下KVM模块初始化流程及Linux6.5源码注释
kvm作为内核模块插入内核中,来实现对CPU, 内存,中断的虚拟化, 其中IO的虚拟化由qemu负责;
本文章将基于Linux6.5内核中 RISCV 架构下kvm实现细节进行源码分析
既然KVM是以内核模块的形式插入内核的,那么我们便可以从module_init()
入手, 在arch/riscv/kvm/main.c
文件涉及到了对该内核模块的声名:
/*kvm 内核模块入口*/
module_init(riscv_kvm_init);
进到该初始化函数中一探究竟, riscv_kvm_init
函数是在RISCV架构下 初始化kvm的入口函数, 他的主要功能是:
-
- 完成体系结构相关的初始化;
- 执行核心函数:
kvm_init()
来初始化真正的KVM模块;
所以本文的主要任务分为以下两点:
- RISCV架构下, 如何进行体系结构相关的检查和初始化的;
kvm_init
函数是如何将kvm模块进行真正的初始化的 (包含了各类回调函数的设置,资源分配,以及设备注册等) ;
1.1 RISCV 体系结构相关初始化
我们先来看一下riscv_kvm_ini()
函数中相关的源码:
arch/ariscv/kvm/main/riscv_kvm_init源码注释
/**
* @brief riscv 中kvm模块的入口函数
**/
static int __init riscv_kvm_init(void)
{
int rc;
const char *str;
/*1. 检查 RISC-V Hypervisor 扩展是否可用
*/
if (!riscv_isa_extension_available(NULL, h)) {
kvm_info("hypervisor extension not available\n");
return -ENODEV;
}
/*2. 检查 SBI 版本
* SBI 是 RISC-V 的标准化接口,用于与底层固件交互
*/
if (sbi_spec_is_0_1()) {
kvm_info("require SBI v0.2 or higher\n");
return -ENODEV;
}
/*3. 检查是否支持 SBI 的 RFENCE 扩展,
* 该扩展用于同步多个核的内存访问
*/
if (!sbi_probe_extension(SBI_EXT_RFENCE)) {
kvm_info("require SBI RFENCE extension\n");
return -ENODEV;
}
/*4. 检测并设置 G-stage 页表模式
*/
kvm_riscv_gstage_mode_detect();//检测并配置二级页表模式
/*5. VMID检测*/
kvm_riscv_gstage_vmid_detect();//检测并配置 VMID(虚拟机标识符)
/*6. 初始化 AIA 以及中断虚拟化的一些全局参数;
* AIA 用于管理更高级的中断功能
*/
rc = kvm_riscv_aia_init();
if (rc && rc != -ENODEV)
return rc;
kvm_info("hypervisor extension available\n");
/*7. 配置 G-stage 页表格式
*/
switch (kvm_riscv_gstage_mode()) {
case HGATP_MODE_SV32X4:
str = "Sv32x4";
break;
case HGATP_MODE_SV39X4:
str = "Sv39x4";
break;
case HGATP_MODE_SV48X4:
str = "Sv48x4";
break;
case HGATP_MODE_SV57X4:
str = "Sv57x4";
break;
default:
return -ENODEV;
}
kvm_info("using %s G-stage page table format\n", str);//打印系统中可用的 VMID 位数
kvm_info("VMID %ld bits available\n", kvm_riscv_gstage_vmid_bits());
/*8. 如果 AIA 可用,打印支持的外部中断数量
*/
if (kvm_riscv_aia_available())
kvm_info("AIA available with %d guest external interrupts\n",
kvm_riscv_aia_nr_hgei);
/*9. 调用 kvm_init 初始化 KVM 核心模块
*/
rc = kvm_init(sizeof(struct kvm_vcpu), 0, THIS_MODULE);
if (rc) {
kvm_riscv_aia_exit();
return rc;
}
return 0;
}
riscv_kvm_init
函数在 RISCV 架构下通过一系列步骤完成体系结构相关的初始化工作。
KVM 初始化逻辑均包括硬件支持检查、页表初始化、中断管理、VMID 检测和字符设备注册等核心步骤;他会先进行硬件方面的检查,再对内存相关部分进行初始化,最重要的一点是进行中断虚拟化的支持;
- 硬件支持检查:
- 检查Hypervisor 扩展是否可用
- 检查 SBI 版本和 RFENCE 扩展支持
- 内存管理初始化:
- 检测和配置 G-stage 页表模式
- 检测和设置 VMID 位数,支持多虚拟机隔离
- 中断虚拟化支持:
- 初始化 AIA 中断架构,支持高级中断管理
1.2 kvm_init KVM初始化
``kvm_init` 的功能是完成 KVM 核心模块的初始化,为虚拟化环境提供运行支持。总体上,它负责建立 KVM 与用户空间的交互接口、初始化虚拟化子系统、分配必要资源,各类回调函数的设置,并确保系统在多核环境下稳定运行。
具体而言,它通过注册 CPU 热插拔回调和性能监控接口,创建 VCPU 缓存和中断管理数据结构,初始化异步页面错误机制和 VFIO 支持,实现对物理设备的虚拟化,并通过注册 /dev/kvm
字符设备为用户空间工具(如 QEMU)提供操作接口,同时具备完善的错误处理和回滚机制,保障初始化过程的可靠性。
Linux6.5/virt/kvm/kvm_main.c /kvm_init源码注释
/*设置虚拟机环境并确保所有相关子系统的正常运行*/
int kvm_init(unsigned vcpu_size, unsigned vcpu_align, struct module *module)
{
int r;
int cpu;
/*1. 设置cpu热插拔时的回调函数*/
#ifdef CONFIG_KVM_GENERIC_HARDWARE_ENABLING
r = cpuhp_setup_state_nocalls(CPUHP_AP_KVM_ONLINE, "kvm/cpu:online",
kvm_online_cpu, kvm_offline_cpu);
if (r)
return r;
/*注册 syscore_ops,用于在系统进入休眠和恢复时处理 KVM 特定操作*/
register_syscore_ops(&kvm_syscore_ops);
#endif
/*2. 创建vcpu缓存,
* 创建一个内存缓存池 kvm_vcpu_cache,
* 用于分配 kvm_vcpu 结构.
*/
if (!vcpu_align)
vcpu_align = __alignof__(struct kvm_vcpu);
kvm_vcpu_cache =
kmem_cache_create_usercopy("kvm_vcpu", vcpu_size, vcpu_align,
SLAB_ACCOUNT,
offsetof(struct kvm_vcpu, arch),
offsetofend(struct kvm_vcpu, stats_id)
- offsetof(struct kvm_vcpu, arch),
NULL);
if (!kvm_vcpu_cache) {
r = -ENOMEM;
goto err_vcpu_cache;
}
/*3. 为每个 CPU 分配一个 kick_mask,
* 用于管理 CPU 中断和调度相关的操作
*/
for_each_possible_cpu(cpu) {
if (!alloc_cpumask_var_node(&per_cpu(cpu_kick_mask, cpu),
GFP_KERNEL, cpu_to_node(cpu))) {
r = -ENOMEM;
goto err_cpu_kick_mask;
}
}
/*4. 创建工作队列, 用于处理VM的shutdown操作*/
r = kvm_irqfd_init();
if (r)
goto err_irqfd;
/*5. 创建用于分配kvm_async_pf的slab缓存*/
r = kvm_async_pf_init();
if (r)
goto err_async_pf;
kvm_chardev_ops.owner = module;
/*6. 设置调度切换时的回调函数*/
kvm_preempt_ops.sched_in = kvm_sched_in;
kvm_preempt_ops.sched_out = kvm_sched_out;
kvm_init_debug();
/*7. VFIO操作初始化*/
r = kvm_vfio_ops_init();
if (WARN_ON_ONCE(r))
goto err_vfio;
/*8. 注册/dev/kvm 字符设备,使用户空间可以通过该字符设备与kvm交互*/
r = misc_register(&kvm_dev);
if (r) {
pr_err("kvm: misc device register failed\n");
goto err_register;
}
...
}
- 回调函数设置:
cpuhp_setup_state_nocall
与CPU的热插拔相关,register_reboot_notifer
与系统的重启相关,register_syscore_ops
与系统的休眠唤醒相关,而这几个模块的回调函数,最终都会去调用体系结构相关的函数去打开或关闭Hypervisor
; - 资源分配:
kmem_cache_create_usercopy
与kvm_async_pf_init
都是创建slab缓存
,用于内核对象的分配; kvm_vfio_ops_init
:VFIO
是一个可以安全将设备I/O
、中断、DMA导出到用户空间的框架,后续在将IO虚拟化时再深入分析;
我们把重点放到字符设备驱动注册上misc_register
,因为该模块用于用户空间与KVM内核模块进行IO交互;
1.2.1 misc_register 注册字符设备 驱动
该函数用于注册字符设备驱动, 使用户空间程序(如 QEMU)能够与 KVM 内核模块进行交互; /dev/kvm
是一个标准的字符设备文件,通过文件操作(如 ioctl
)实现用户空间和内核空间之间的通信。用户空间程序(如虚拟机管理工具)可以通过该接口向 KVM 发送指令或接收数据。
该字符设备的注册, 一共涉及到了三个操作集,设置好了对应的ioctl函数,分别是字符设备操作集、kvm-vm操作集、kvm-vcpu操作集;
kvm
:代表kvm内核模块,可以通过kvm_dev_ioctl
来管理kvm版本信息,以及vm的创建等;vm
:虚拟机实例,可以通过kvm_vm_ioctl
函数来创建vcpu
,设置内存区间,分配中断等;vcpu
:代表虚拟的CPU,可以通过kvm_vcpu_ioctl
来启动或暂停CPU的运行,设置vcpu的寄存器等;