KVM虚拟化 | ARM64:(一)Arm64架构下KVM 模块 初始化流程 及 Linux6.5 源码注释

《KVM虚拟化:ARM64架构下Linux源码分析系列文章》

本系列文章将对Linux在ARM64架构下KVM虚拟化的实现方法进行梳理与源码分析,主要侧重点放在ARM64架构下,KVM模块初始化、vcpu虚拟、内存虚拟、以及中断虚拟(vgic)的实现上。

在进行正式介绍之前,有必要对文章引用进行提前说明。本系列文章参考了大量的博客、文章以及书籍:

KVM虚拟化 | ARM64:(一)Arm64架构下KVM 模块 初始化流程 及 Linux6.5 源码注释

kvm作为内核模块插入内核中,来实现对CPU, 内存,中断的虚拟化, 其中IO的虚拟化由qemu负责;

本文章将基于Linux6.5内核中arm架构下kvm实现细节进行源码分析

既然KVM是以内核模块的形式插入内核的,那么我们便可以从module_init()入手, 在arch/arm64/kvm/arm.c文件涉及到了对该内核模块的声名:

/*kvm 内核模块入口*/
module_init(kvm_arm_init);

进到该初始化函数中一探究竟, kvm_arm_init函数是在arm64架构下 初始化kvm的入口函数, 他的主要功能是:

    1. 完成体系结构相关的初始化;
    2. 执行核心函数: kvm_init()来初始化真正的KVM模块;
    3. 在KVM模块初始化后, 将kvm_arm_initialised标记为初始化成功;

所以本文的主要任务分为以下两点:

  • ARM64架构下, 如何进行体系结构相关的检查和初始化的;
  • kvm_init函数是如何将kvm模块进行真正的初始化的 (包含了各类回调函数的设置,资源分配,以及设备注册等) ;

1.1 ARM体系结构相关初始化

我们先来看一下kvm_arch_init()函数中相关的源码:

arch/arm64/kvm/arm.c/kvm_arch_init源码注释

/* 在所有 CPU 上初始化 Hyp 模式并设置内存映射,准备 KVM 的使用 */
static __init int kvm_arm_init(void)
{
	int err;
	bool in_hyp_mode;
	
	/*************************************************************/
	/*  1. 完成体系结构相关的初始化 对应linux5.15中 kvm_arch_init   */
	/*************************************************************/

	/*1.1 调用 is_hyp_mode_available 检测硬件是否支持 Hyp 模式*/
	if (!is_hyp_mode_available()) {
		kvm_info("HYP mode not available\n");
		return -ENODEV;
	}
	
	/*1.2 检查内核命令行是否禁用了 KVM 功能*/
	if (kvm_get_mode() == KVM_MODE_NONE) {
		kvm_info("KVM disabled from command line\n");
		return -ENODEV;
	}
	
	/*1.3 初始化 KVM 使用的系统寄存器表*/
	err = kvm_sys_reg_table_init();
	if (err) {
		kvm_info("Error initializing system register tables");
		return err;
	}
	
	/*1.4 检测当前内核是否运行在 Hyp 模式下
	 *    如果不在 Hyp 模式,后续会尝试切换到 Hyp 模式
	 */
	in_hyp_mode = is_kernel_in_hyp_mode();
	
	/*1.5 检查cpu的相关功能
	 *    cpu错误修复功能
	 */
	if (cpus_have_final_cap(ARM64_WORKAROUND_DEVICE_LOAD_ACQUIRE) ||
	    cpus_have_final_cap(ARM64_WORKAROUND_1508412))
		kvm_info("Guests without required CPU erratum workarounds can deadlock system!\n" \
			 "Only trusted guests should be used on this system.\n");
	
	/*1.6 设置 KVM 的中间物理地址(IPA)限制*/
	err = kvm_set_ipa_limit();
	if (err)
		return err;

	/*1.7 初始化 ARM 的可扩展矢量扩展(SVE)支持*/
	err = kvm_arm_init_sve();
	if (err)
		return err;

	/*1.8 初始化虚拟机标识符(VMID)分配器
	 *    VMID 是 ARM 架构中用于区分不同虚拟机的标识符 
	 */
	err = kvm_arm_vmid_alloc_init();
	if (err) {
		kvm_err("Failed to initialize VMID allocator.\n");
		return err;
	}

	/*1.9 如果当前未运行在 Hyp 模式,
	 *    则尝试通过 init_hyp_mode 切换到 Hyp 模式
	 */
	if (!in_hyp_mode) {
		err = init_hyp_mode();
		if (err)
			goto out_err;
	}

	/*1.10 初始化向量槽,用于管理虚拟机中断向量*/
	err = kvm_init_vector_slots();
	if (err) {
		kvm_err("Cannot initialise vector slots\n");
		goto out_hyp;
	}

	/*1.11 初始化其他子系统(如 I/O 管理等)*/
	err = init_subsystems();
	if (err)
		goto out_hyp;

	/*1.12 根据当前模式,进行日志记录
	 *     a. 受保护的 nVHE 模式
	 *     b. VHE 模式  *****
	 *     c. 普通 Hyp 模式
	 */
	if (is_protected_kvm_enabled()) {
		kvm_info("Protected nVHE mode initialized successfully\n");
	} else if (in_hyp_mode) {
		kvm_info("VHE mode initialized successfully\n");
	} else {
		kvm_info("Hyp mode initialized successfully\n");
	}
	/*************************************************************/
	/*                   体系结构相关的初始化结束                  */
	/*************************************************************/

	/*2. 核心操作: KVM 初始化 */
	/*3. 初始化完成标记*/

}

在这里插入图片描述

kvm_arm_init 函数在 ARM64 架构下通过一系列步骤完成体系结构相关的初始化工作。

  • 首先是对硬件以及内核是否支持虚拟化的检测,包括检测硬件是否支持 Hyp 模式以及内核是否支持KVM;
  • 其次是对架构功能的初始化,包括系统寄存器表的初始化、SVE 支持和 VMID 分配器的初始化,检查 CPU 特性与错误修复,设置中间物理地址(IPA)限制,
  • 最重要的是Hyp模式的切换,以及各子系统的初始化;会在后面重点分析
  • 最后,根据运行模式记录日志,确保 KVM 在 ARM64 架构上的虚拟化环境能够稳定、可靠地运行。

在这里插入图片描述

1.1.1 init_hyp_mode 切换到hyp模式

首先我们看一下init_hyp_mode是如何初始化并切换到hyp模式的, 该函数核心任务是初始化 ARM64 架构下的 Hyp 模式,为 KVM 提供虚拟化支持; 相关细节如下:

  • 检查条件:确保硬件和内存资源满足 Hyp 模式的运行需求。
  • 设置内存管理:初始化页表,配置 Hyp 模式的地址空间和段映射。
  • 分配资源:为每个 CPU 配置独立的栈和 per-CPU 数据。
  • 支持受保护模式**:启用 pKVM 的内存保护和 PSCI 支持。

Linux6.5/arch/arm64/kvm/arm.c/init_hyp_mode源码注释

/* Inits Hyp-mode on all online CPUs */
static int __init init_hyp_mode(void)
{
	u32 hyp_va_bits;
	int cpu;
	int err = -ENOMEM;

	/*1. 确保受保护模式的内存资源可用,
	 *   检查保护模式(pKVM)的内存池是否可用
	 */
	if (is_protected_kvm_enabled() && !hyp_mem_base)
		goto out_err;

	/*2. 初始化 Hyp 页表和 Hyp 地址空间映射
	 *   为 Hyp 模式分配和管理虚拟地址空间
	 */
	err = kvm_mmu_init(&hyp_va_bits);
	if (err)
		goto out_err;

	/*3.  为每个 CPU 分配 Hyp模式下 一页大小的栈空间*/
	for_each_possible_cpu(cpu) {
		unsigned long stack_page;

		stack_page = __get_free_page(GFP_KERNEL);
		if (!stack_page) {
			err = -ENOMEM;
			goto out_err;
		}

		per_cpu(kvm_arm_hyp_stack_page, cpu) = stack_page;
	}

	/*4. 初始化 Per-CPU 的 Hyp 区域*/
	for_each_possible_cpu(cpu) {
		struct page *page;
		void *page_addr;

		page = alloc_pages(GFP_KERNEL, nvhe_percpu_order());
		if (!page) {
			err = -ENOMEM;
			goto out_err;
		}

		page_addr = page_address(page);
		memcpy(page_addr, CHOOSE_NVHE_SYM(__per_cpu_start), nvhe_percpu_size());
		kvm_nvhe_sym(kvm_arm_hyp_percpu_base)[cpu] = (unsigned long)page_addr;
	}
    
	/*5. 创建hyp模式下的 代码段映射*/
	err = create_hyp_mappings(kvm_ksym_ref(__hyp_text_start),
				  kvm_ksym_ref(__hyp_text_end), PAGE_HYP_EXEC);
	if (err) {
		kvm_err("Cannot map world-switch code\n");
		goto out_err;
	}

	/*6. 创建hyp模式下的 只读数据段映射
	 *   涉及到.hyp.rodata段 和 .rodata段
	 */
	err = create_hyp_mappings(kvm_ksym_ref(__hyp_rodata_start),
				  kvm_ksym_ref(__hyp_rodata_end), PAGE_HYP_RO);
	if (err) {
		kvm_err("Cannot map .hyp.rodata section\n");
		goto out_err;
	}
	
	err = create_hyp_mappings(kvm_ksym_ref(__start_rodata),
				  kvm_ksym_ref(__end_rodata), PAGE_HYP_RO);
	if (err) {
		kvm_err("Cannot map rodata section\n");
		goto out_err;
	}

	/*7. 创建hyp模式下的 bss段映射
	 *   涉及 .hyp.bss段 和 .bss段
	 */
	err = create_hyp_mappings(kvm_ksym_ref(__hyp_bss_start),
				  kvm_ksym_ref(__hyp_bss_end), PAGE_HYP);
	if (err) {
		kvm_err("Cannot map hyp bss section: %d\n", err);
		goto out_err;
	}

	err = create_hyp_mappings(kvm_ksym_ref(__hyp_bss_end),
				  kvm_ksym_ref(__bss_stop), PAGE_HYP_RO);
	if (err) {
		kvm_err("Cannot map bss section\n");
		goto out_err;
	}


	/*8. 这段代码的功能是为每个 CPU 创建 Hyp 栈,并通过以下方式增强安全性和可靠性:
	 *8.1. 分配私有虚拟地址空间:每个 CPU 拥有独立的虚拟地址范围。
	 *8.2. 设置保护页:未映射的保护页用于检测栈溢出。
	 *8.3. 保存地址信息:保存栈的物理和虚拟地址,便于后续操作。
	 *8.4. 逐步检查和回滚:确保初始化过程中任何错误都能安全回滚。
     */
	for_each_possible_cpu(cpu) {
		struct kvm_nvhe_init_params *params = per_cpu_ptr_nvhe_sym(kvm_init_params, cpu);
		char *stack_page = (char *)per_cpu(kvm_arm_hyp_stack_page, cpu);
		unsigned long hyp_addr;

		/*8.1 分配 Hyp 栈的虚拟地址空间*/
		err = hyp_alloc_private_va_range(PAGE_SIZE * 2, &hyp_addr);
		if (err) {
			kvm_err("Cannot allocate hyp stack guard page\n");
			goto out_err;
		}

		/*8.2 映射栈页面并设置保护页*/
		err = __create_hyp_mappings(hyp_addr + PAGE_SIZE, PAGE_SIZE,
					    __pa(stack_page), PAGE_HYP);
		if (err) {
			kvm_err("Cannot map hyp stack\n");
			goto out_err;
		}

		/*8.3 保存物理地址和虚拟地址*/
		params->stack_pa = __pa(stack_page);
		params->stack_hyp_va = hyp_addr + (2 * PAGE_SIZE);
	}

	/*9. 创建每 CPU 的 Hyp 数据区域映射*/
	for_each_possible_cpu(cpu) {
		char *percpu_begin = (char *)kvm_nvhe_sym(kvm_arm_hyp_percpu_base)[cpu];
		char *percpu_end = percpu_begin + nvhe_percpu_size();

		err = create_hyp_mappings(percpu_begin, percpu_end, PAGE_HYP);
		if (err) {
			kvm_err("Cannot map hyp percpu region\n");
			goto out_err;
		}

		cpu_prepare_hyp_mode(cpu, hyp_va_bits);
	}
	
	/*10. 初始化符号*/
	kvm_hyp_init_symbols();

	/*11. 初始化保护模式*/
	if (is_protected_kvm_enabled()) {
		if (IS_ENABLED(CONFIG_ARM64_PTR_AUTH_KERNEL) &&
		    cpus_have_const_cap(ARM64_HAS_ADDRESS_AUTH))
			pkvm_hyp_init_ptrauth();

		init_cpu_logical_map();

		if (!init_psci_relay()) {
			err = -ENODEV;
			goto out_err;
		}

		err = kvm_hyp_init_protection(hyp_va_bits);
		if (err) {
			kvm_err("Failed to init hyp memory protection\n");
			goto out_err;
		}
	}
}

我们可以看到该函数完成了PGD页表的分配, 完成了各种段的映射, 完成了异常向量表的映射; 并为每个CPU创建hyp模式下的栈等;

1.1.2 init_subsystems 初始化各个子系统

init_subsystems 函数的主要作用是初始化 KVM 在 Hyp 模式下的一些关键子系统,以确保虚拟化环境的正常运行, 这里包括CPU低功耗通知器模块、VGIC(虚拟化中断控制器)模块、Hyp模式下的Timer模块(用于模拟虚拟化定时中断)等;

    1. 初始化每个CPU的Hyp模式, 使其能够正常进入 EL2(Hypervisor Exception Level);
    2. 注册 Hyp 模式的 CPU 低功耗通知器
    3. 初始化 VGIC(虚拟化中断控制器)模块
    4. 初始化 Hyp 模式下的 Timer
    5. 注册性能监控回调,增强对性能的监控能力;

Linux6.5/arch/arm64/kvm/arm.c/init_subsystems 源码注释

在这里插入图片描述

static int __init init_subsystems(void)
{
	int err = 0;
	/*1. 使能每个cpu, 这样系统才能通过EL2*/
	on_each_cpu(cpu_hyp_init, NULL, 1);

	/*2. 注册hyp模式下 cpu的低功耗通知器*/
	hyp_cpu_pm_init();

	/*3.注册hpy模式下的vgic模块  中断管理相关的模块*/
	err = kvm_vgic_hyp_init();
	switch (err) {
	case 0:
		vgic_present = true;
		break;
	case -ENODEV:
	case -ENXIO:
		vgic_present = false;
		err = 0;
		break;
	default:
		goto out;
	}

	/*4. 初始化hyp模式下的timer*/
	err = kvm_timer_hyp_init(vgic_present);
	if (err)
		goto out;

	kvm_register_perf_callbacks(NULL);

}

这里的重点在 初始化VGIC虚拟化中断控制器,以及初始化 Hyp 模式下的 Timer, 二者都是关于kvm中的虚拟中断实现;

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;
	}

	...
}
  1. 回调函数设置:cpuhp_setup_state_nocall与CPU的热插拔相关,register_reboot_notifer与系统的重启相关,register_syscore_ops与系统的休眠唤醒相关,而这几个模块的回调函数,最终都会去调用体系结构相关的函数去打开或关闭Hypervisor
  2. 资源分配:kmem_cache_create_usercopykvm_async_pf_init都是创建slab缓存,用于内核对象的分配;
  3. kvm_vfio_ops_initVFIO是一个可以安全将设备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的寄存器等;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值