Linux虚拟化KVM-Qemu分析(三)之KVM源码(1)kvm_init

【KVM】 专栏收录该内容
33 篇文章 8 订阅

Table of Contents

1. 概述

2. KVM初始化

2.1 kvm_arch_init

2.1.1 init_hyp_mode

2.1.2 init_subsystems

2.2 misc_register

3. 总结


 

KVM版本:5.9.1

QEMU版本:5.0.0

用QEMU构建嵌入式LINUX系统

Linux虚拟化KVM-Qemu分析(一)

Linux虚拟化KVM-Qemu分析(二)之ARMv8虚拟化

Linux虚拟化KVM-Qemu分析(三)之KVM源码(1)

在CentOS上进行虚拟化:QEMU、Xen、KVM、LibVirt、oVirt

ARM SMMU原理与IOMMU技术(“VT-d” DMA、I/O虚拟化、内存虚拟化)

OpenVZ,Xen,KVM等:虚拟化解决方案

KVM Virtio: An I/O virtualization framework for Linux(Linux虚拟IO框架)

 

1. 概述

  • 从本文开始将开始source code的系列分析了;

  • KVM作为内核模块,可以认为是一个中间层,向上对接用户的控制,向下对接不同架构的硬件虚拟化支持;

  • 本文主要介绍体系架构初始化部分,以及向上的框架;

 

2. KVM初始化

  • 贝多芬曾经说过,一旦你找到了代码的入口,你就扼住了软件的咽喉;

  • 我们的故事,从module_init(arm_init)开始,代码路径:arch/arm64/kvm/arm.c

老规矩,先来一张图(图片中涉及到的红色框函数,都是会展开描述的):

//kvm_main.c
//kvm_main.c	virt\kvm	98184	2/20/2019	2546
int kvm_init(void *opaque, unsigned vcpu_size, unsigned vcpu_align,
		  struct module *module)
{

	r = kvm_arch_init(opaque);
	r = kvm_irqfd_init();
	r = cpuhp_setup_state_nocalls(CPUHP_AP_KVM_STARTING, "kvm/cpu:starting",
				      kvm_starting_cpu, kvm_dying_cpu);

	register_reboot_notifier(&kvm_reboot_notifier);
	kvm_vcpu_cache =
		kmem_cache_create_usercopy("kvm_vcpu", vcpu_size, vcpu_align,
					   SLAB_ACCOUNT,
					   offsetof(struct kvm_vcpu, arch),
					   sizeof_field(struct kvm_vcpu, arch),
					   NULL);


	r = kvm_async_pf_init();

	r = misc_register(&kvm_dev);

	register_syscore_ops(&kvm_syscore_ops);
	kvm_init_debug();
	r = kvm_vfio_ops_init();
	WARN_ON(r);

	return 0;
}
EXPORT_SYMBOL_GPL(kvm_init);
  • 内核的功能模块,基本上的套路就是:1)完成模块初始化,向系统注册;2)响应各类请求,这种请求可能来自用户态,也可能来自异常响应等;

  • kvm的初始化,在kvm_init中完成,既包含了体系结构相关的初始化设置,也包含了各类回调函数的设置,资源分配,以及设备注册等,只有当初始化完成后,才能响应各类请求,比如创建虚拟机等;

  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虚拟化时再深入分析;
  • 图片中红色的两个函数,是本文分析的内容,其中kvm_arch_init与前文ARMv8硬件虚拟化支持紧密相关,而misc_register与上层操作紧密相关;

 

2.1 kvm_arch_init

在4.20.11中是这样的

//arm.c	virt\kvm\arm	38004	2/20/2019	1030

/**
 * Initialize Hyp-mode and memory mappings on all CPUs.
 */
int kvm_arch_init(void *opaque)
{
	int err;
	int ret, cpu;
	bool in_hyp_mode;

	if (!is_hyp_mode_available()) {
		kvm_info("HYP mode not available\n");
		return -ENODEV;
	}

	if (!kvm_arch_check_sve_has_vhe()) {
		kvm_pr_unimpl("SVE system without VHE unsupported.  Broken cpu?");
		return -ENODEV;
	}

	for_each_online_cpu(cpu) {
		smp_call_function_single(cpu, check_kvm_target_cpu, &ret, 1);
		if (ret < 0) {
			kvm_err("Error, CPU %d not supported!\n", cpu);
			return -ENODEV;
		}
	}

	err = init_common_resources();
	if (err)
		return err;

	in_hyp_mode = is_kernel_in_hyp_mode();

	if (!in_hyp_mode) {
		err = init_hyp_mode();
		if (err)
			goto out_err;
	}

	err = init_subsystems();
	if (err)
		goto out_hyp;

	if (in_hyp_mode)
		kvm_info("VHE mode initialized successfully\n");
	else
		kvm_info("Hyp mode initialized successfully\n");

	return 0;

out_hyp:
	if (!in_hyp_mode)
		teardown_hyp_mode();
out_err:
	return err;
}

is_hyp_mode_available用于判断ARMv8的Hyp模式是否可用,实际是通过判断__boot_cpu_mode的值来完成,该值是在arch/arm64/kernel/head.S中定义,在启动阶段会设置该值:

/* 在4.20.11中是这样的
 * Sets the __boot_cpu_mode flag depending on the CPU boot mode passed
 * in w0. See arch/arm64/include/asm/virt.h for more info.
 */
set_cpu_boot_mode_flag:
	adr_l	x1, __boot_cpu_mode
	cmp	w0, #BOOT_CPU_MODE_EL2
	b.ne	1f
	add	x1, x1, #4
1:	str	w0, [x1]			// This CPU has booted in EL1
	dmb	sy
	dc	ivac, x1			// Invalidate potentially stale cache line
	ret
ENDPROC(set_cpu_boot_mode_flag)

/*
 * These values are written with the MMU off, but read with the MMU on.
 * Writers will invalidate the corresponding address, discarding up to a
 * 'Cache Writeback Granule' (CWG) worth of data. The linker script ensures
 * sufficient alignment that the CWG doesn't overlap another section.
 */
	.pushsection ".mmuoff.data.write", "aw"
/*
 * We need to find out the CPU boot mode long after boot, so we need to
 * store it in a writable variable.
 *
 * This is not in .bss, because we set it sufficiently early that the boot-time
 * zeroing of .bss would clobber it.
 */
ENTRY(__boot_cpu_mode)
	.long	BOOT_CPU_MODE_EL2
	.long	BOOT_CPU_MODE_EL1
  • is_kernel_in_hyp_mode,通过读取ARMv8的CurrentEL,判断是否为CurrentEL_EL2

  • ARM架构中,SVE的实现要求VHE也要实现,这个可以从arch/arm64/Kconfig中看到,SVE的模块编译:depends on !KVM || ARM64_VHESVE(scalable vector extension),是AArch64下一代的SIMD(single instruction multiple data)指令集,用于加速高性能计算。其中SIMD如下:

  • init_common_resources,用于设置IPA的地址范围,将其限制在系统能支持的物理地址范围之内。stage 2页表依赖于stage 1页表代码,需要遵循一个条件:Stage 1的页表级数 >= Stage 2的页表级数;

 

2.1.1 init_hyp_mode

在 4.20.11中是这样的


/**
 * Inits Hyp-mode on all online CPUs
 */
static int init_hyp_mode(void)
{
	err = kvm_mmu_init();

	for_each_possible_cpu(cpu) {
		stack_page = __get_free_page(GFP_KERNEL);
	}
	err = create_hyp_mappings(kvm_ksym_ref(__hyp_text_start),
				  kvm_ksym_ref(__hyp_text_end), PAGE_HYP_EXEC);

	err = create_hyp_mappings(kvm_ksym_ref(__start_rodata),
				  kvm_ksym_ref(__end_rodata), PAGE_HYP_RO);

	err = create_hyp_mappings(kvm_ksym_ref(__bss_start),
				  kvm_ksym_ref(__bss_stop), PAGE_HYP_RO);

	err = kvm_map_vectors();

	/*
	 * Map the Hyp stack pages
	 */
	for_each_possible_cpu(cpu) {
		err = create_hyp_mappings(stack_page, stack_page + PAGE_SIZE,
					  PAGE_HYP);
	}
	for_each_possible_cpu(cpu) {
		err = create_hyp_mappings(cpu_ctxt, cpu_ctxt + 1, PAGE_HYP);
	}
	err = hyp_map_aux_data();

	return 0;
}

放眼望去,init_hyp_mode解决的问题就是各种映射,最终都会调用到__create_hyp_mappings,先来解决这个映射问题:

  • 看过之前内存管理子系统的同学,应该熟悉这个页表映射建立的过程,基本的流程是给定一个虚拟地址区间和物理地址,然后从pgd开始逐级往下去建立映射。ARMv8架构在实际映射过程中,P4D这一级页表并没有使用。

让我们继续回到init_hyp_mode的正题上来,这个函数完成了PGD页表的分配,完成了IDMAP代码段的映射,完成了其他各种段的映射,完成了异常向量表的映射,等等。此外,再补充几点内容:

  1. ARMv8异常向量表

ARMv8架构的AArch64执行态中,每种EL都有16个entry,分为四类:Synchronous,IRQ,FIQ,SError。以系统启动时设置hypervisor的异常向量表__hyp_stub_vectors为例:

  • 当从不同的Exception Level触发异常时,根据执行状态,去选择对应的handler处理,比如上图中只有el1_sync有效,也就是在EL1状态触发EL2时跳转到该函数;

  1. pushsection/popsection

  • init_hyp_mode函数中,完成各种段的映射,段的定义放置在vmlinux.lds.S中,比如hyp.idmap.text

在4.10.11中是这样的

#define IDMAP_TEXT							\
		ALIGN_FUNCTION();					\
		__idmap_text_start = .;					\
		*(.idmap.text)						\
		__idmap_text_end = .;					\
		. = ALIGN(PAGE_SIZE);					\
		__hyp_idmap_text_start = .;				\
		*(.hyp.idmap.text)					\
		__hyp_idmap_text_end = .;
  • 可以通过pushsection/popsection来在目标文件中来添加一个段,并指定段的属性,比如"ax"代表可分配和可执行,这个在汇编代码中经常用到,比如hyp-init.S中,会将代码都放置在hyp.idmap.text中:

  • 除了pushsection/popsection外,通过#define __hyp_text __section(.hyp.text) notrace __noscs的形式也能将代码放置在指定的段中;

  1. Hypervisor相关寄存器

  • 讲几个关键的相关寄存器:1)sctlr_el2(System Control Register):可以用于控制EL2的MMU和Cache相关操作;2)ttbr0_el2(Translation Table Base Register 0):用于存放页表的基地址,上文中提到分配的hyp_pgd就需要设置到该寄存器中;3)vbar_el2(Vector Base Address Register):用于存放异常向量表的基地址;

我们需要先明确几点:

  1. Hyp模式下要执行的代码,需要先建立起映射;

  2. 映射IDMAP代码段和其他代码段,明确这些段中都有哪些函数,这个可以通过pushsection/popsection以及__hyp_text宏可以看出来;

  3. 最终的目标是需要建立好页表映射,并安装好异常向量表;

貌似内容比较零碎,最终的串联与谜题留在下一小节来解答。

 

2.1.2 init_subsystems

先看一下函数的调用流程:

  • VGICtimer,以及电源管理相关模块在本文中暂且不深入分析了,本节主要关心cpu_hyp_reinit的功能;

  • 绿色框中的函数,会陷入到EL2进行执行;


static int init_subsystems(void)
{
	int err = 0;

	/*
	 * Enable hardware so that subsystem initialisation can access EL2.
	 */
	on_each_cpu(_kvm_arch_hardware_enable, NULL, 1);

	/*
	 * Register CPU lower-power notifier
	 */
	hyp_cpu_pm_init();

	/*
	 * Init HYP view of 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;
	}

	/*
	 * Init HYP architected timer support
	 */
	err = kvm_timer_hyp_init(vgic_present);
	if (err)
		goto out;

	kvm_perf_init();
	kvm_coproc_table_init();

out:
	on_each_cpu(_kvm_arch_hardware_disable, NULL, 1);

	return err;
}

看图中有好几次异常向量表的设置,此外,还有页表基地址、栈页的获取与设置等,结合上一小节的各类映射,是不是已经有点迷糊了,下边这张图会将这些内容串联起来:

  • 在整个异常向量表创建的过程中,涉及到三个向量表:__hyp_stub_vectors__kvm_hyp_init, __kvm_call_hyp,这些代码都是汇编实现;

  • 在系统启动过程中(arch/arm64/kernel/head.S),调用到el2_setup函数,在该函数中设置了一个临时的异常向量表,也就是先打一个桩,这个从名字也可以看出来,该异常向量表中仅实现了el2_synchandler处理函数,可以应对两种异常:1)设置新的异常向量表;2)重置异常向量表,也就是设置回__hyp_stub_vectors

  • kvm初始化时,调用了__hyp_set_vectors来设置新的异常向量表:__kvm_hyp_init。这个向量表中只实现了__do_hyp_init的处理函数,也就是只能用来对Hyp模式进行初始化。上文提到过idmap段,这个代码就放置在idmap段,以前分析内存管理子系统时也提到过idmap,为什么需要这个呢?idmap: identity map,也就是物理地址和虚拟地址是一一映射的,防止MMU在使能前后代码不能执行;

  • __kvm_call_hyp函数,用于在Hyp模式下执行指定的函数,在cpu_hyp_reinit函数中调用了该函数,传递的参数包括了新的异常向量表地址,页表基地址,Hyp的栈地址,per-CPU偏移等,最终会调用__do_hyp_init函数完成相应的设置。

到此,页表和异常向量表的设置算是完成了。

 

2.2 misc_register

misc_register    -    register a miscellaneous device

misc_register用于注册字符设备驱动,在kvm_init函数中调用此函数完成注册,以便上层应用程序来使用kvm模块

  • 字符设备的注册分为三级,分别代表kvmvmvcpu,上层最终使用底层的服务都是通过ioctl函数来操作;

  • kvm:代表kvm内核模块,可以通过kvm_dev_ioctl来管理kvm版本信息,以及vm的创建等;

  • vm:虚拟机实例,可以通过kvm_vm_ioctl函数来创建vcpu,设置内存区间,分配中断等;

  • vcpu:代表虚拟的CPU,可以通过kvm_vcpu_ioctl来启动或暂停CPU的运行,设置vcpu的寄存器等;


/**
 *	misc_register	-	register a miscellaneous device
 *	@misc: device structure
 *
 *	Register a miscellaneous device with the kernel. If the minor
 *	number is set to %MISC_DYNAMIC_MINOR a minor number is assigned
 *	and placed in the minor field of the structure. For other cases
 *	the minor number requested is used.
 *
 *	The structure passed is linked into the kernel and may not be
 *	destroyed until it has been unregistered. By default, an open()
 *	syscall to the device sets file->private_data to point to the
 *	structure. Drivers don't need open in fops for this.
 *
 *	A zero is returned on success and a negative errno code for
 *	failure.
 */

int misc_register(struct miscdevice *misc)
{
	dev_t dev;
	int err = 0;
	bool is_dynamic = (misc->minor == MISC_DYNAMIC_MINOR);

	INIT_LIST_HEAD(&misc->list);

	mutex_lock(&misc_mtx);

	if (is_dynamic) {
		int i = find_first_zero_bit(misc_minors, DYNAMIC_MINORS);

		if (i >= DYNAMIC_MINORS) {
			err = -EBUSY;
			goto out;
		}
		misc->minor = DYNAMIC_MINORS - i - 1;
		set_bit(i, misc_minors);
	} else {
		struct miscdevice *c;

		list_for_each_entry(c, &misc_list, list) {
			if (c->minor == misc->minor) {
				err = -EBUSY;
				goto out;
			}
		}
	}

	dev = MKDEV(MISC_MAJOR, misc->minor);

	misc->this_device =
		device_create_with_groups(misc_class, misc->parent, dev,
					  misc, misc->groups, "%s", misc->name);
	if (IS_ERR(misc->this_device)) {
		if (is_dynamic) {
			int i = DYNAMIC_MINORS - misc->minor - 1;

			if (i < DYNAMIC_MINORS && i >= 0)
				clear_bit(i, misc_minors);
			misc->minor = MISC_DYNAMIC_MINOR;
		}
		err = PTR_ERR(misc->this_device);
		goto out;
	}

	/*
	 * Add it to the front, so that later devices can "override"
	 * earlier defaults
	 */
	list_add(&misc->list, &misc_list);
 out:
	mutex_unlock(&misc_mtx);
	return err;
}

Qemu的使用为例:

  1. 打开/dev/kvm设备文件;

  2. ioctl(xx, KVM_CREATE_VM, xx)创建虚拟机对象;

  3. ioctl(xx, KVM_CREATE_VCPU, xx)为虚拟机创建vcpu对象;

  4. ioctl(xx, KVM_RUN, xx)让vcpu运行起来;

 

3. 总结

本文主要从两个方向来介绍了kvm_init

  1. 底层的体系结构相关的初始化,主要涉及的就是EL2的相关设置,比如各个段的映射,异常向量表的安装,页表基地址的设置等,当把这些准备工作做完后,才能在硬件上去支持虚拟化的服务请求;

  2. 字符设备注册,设置好各类ioctl的函数,上层应用程序可以通过字符设备文件,来操作底层的kvm模块。这部分内容深入的分析,留到后续的文章再展开了;

实际在看代码过程中,一度为很多细节绞尽乳汁,对不起,是绞尽脑汁,每有会意,便欣然忘食,一文也无法覆盖所有内容,草率了。

 

  • 0
    点赞
  • 0
    评论
  • 5
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 代码科技 设计师:Amelia_0503 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值