2 KVM模块初始化介绍
关注微信公众号:Linux内核拾遗
文章来源:mp.weixin.qq.com/s/fmr_T1F9m…
KVM是一种基于内核的虚拟机监控器,其架构简单清晰,充分复用了Linux内核的诸多功能。下面将对KVM模块的初始化流程进行介绍。
2.1 KVM源码组织
KVM在Linux内核树中的代码组织主要包括通用部分代码和架构相关代码这两部分。
2.2.1 通用部分代码
KVM本质上是一个虚拟化的统称方案,当前主流的处理器架构,包括x86,ARM和RISCV等都有自己的虚拟化架构实现方案,而KVM作为抽象层,屏蔽了底层虚拟化架构实现的差异,为用户态程序(主要是QEMU)提供了统一的接口。
KVM的主体代码位于内核树virt/kvm
目录下面,表示所有CPU架构的公共代码,这也是kvm.ko对应的源码。
2.2.2 架构相关代码
CPU架构代码位于arch/
目录下面,例如x86的架构相关代码在arch/x86/kvm
下。进一步地,同一个架构可能会有多种不同的实现,例如x86架构下就有Intel和AMD两家的CPU实现,所以在x86目录下面就有多种实现代码:
arch/x86/vmx/
目录:主要是vmx.c
代码,对应Intel的VM-X方案,最终编译成kvm-intel.ko
;arch/x86/svm/
目录:主要是svm.c
代码,对应AMD的AMD-V方案,最终编译成kvm-amd.ko
。
此外,CPU架构代码下还包括了像中断控制器(ioapic.c
和lapic.c
)、性能监控单元(pmu.c
)以及CPUID(cpuid.c
)等虚拟化代码。
题外话,熟悉Linux内核的开发者应该能立刻发现,这种源码组织架构也常见于Linux内核中的其他子系统。
KVM的所有虚拟化实现(Intel和AMD)都会向KVM模块注册一个kvm_x86_ops
结构体,这样KVM中的一些函数就仅仅作为一个外壳,它可能首先会调用kvm_arch_xxx
函数,表示的是调用CPU架构相关的函数,而如果kvm_arch_xxx
函数需要调用到实现相关的代码,则会调用kvm_x86_ops结构中的相关回调函数。
2.2.3 KVM内核模块
KVM的通用部分和架构相关部分代码都单独编译成Linux内核模块,因此在使用的时候也需要同时进行加载,在Intel平台上就是kvm.ko
和kvm-intel.ko
两个内核模块。
kvm.ko
初始化代码不做任何事情,相当于只是把代码加载到了内存中;kvm-intel.ko
负责完成KVM的开启和关闭。
KVM初始化完成后,会向用户空间呈现KVM的接口(即前面讲的虚拟化统一接口),这些接口都是由kvm.ko
导出的,当用户程序调用这些接口时,kvm.ko
中的通用代码反过来会调用kvm-intel.ko
架构中的相关代码。调用关系如下所示:
2.2 KVM模块初始化
KVM模块的初始化主要包括初始化CPU与架构无关的数据以及设置与架构相关的虚拟化支持。
2.2.1 开启VMX模式
在Intel平台上,VMM只有在CPU处于保护模式并且开启分页时才能进入VMX模式。开启VMX模式的步骤简单概括如下:
CPUID检测CPU是否支持VMX;
CPUID.1:ECX.VMX[bit 5]=1
表示CPU支持VMX。
检测CPU支持的VMX能力,通过读取与VMX能力相关的MSR寄存器完成;
- IA32_VMX_BASIC寄存器:基本VMX能力信息;
- IA32_VMX_PINBASED_CTLS和IA_32_VMX_PROCBASED_CTLS寄存器:表示VMCS区域中VM-execution相关域能够设置值。
分配一段4KB对齐的内存作为VMXON区域;
- IA32_VMX_BASICMSR寄存器:表示VMXON区域大小。
初始化VMXON区域的版本标识;
确保当前CPU运行模式的CR0寄存器符合进入VMX的条件;
- 即CR0.PE=1,CR0.PG=1;
- 其他需要满足的设置通过IA32_VMX_CR0_FIXED0和IA32_VMX_CR0_FIXED1寄存器报告。
开启VMX模式;
- 设置CR4.VMXE为1;
- 其他CR4需要满足的设置通过IA32_VMX_CR4_FIXED0和IA32_VMX_CR4_FIXED1报告。
确保IA32_FEATURE_CONTROL寄存器被正确设置,其锁定位(0位)为1,这个MSR寄存器通常由BIOS编程。
使用VMXON区域的物理地址作为操作数调用VMXON指令,执行完成后,如果RFLAGS.CF=0则表示VMXON指令执行成功。
进入VMX模式之后,在VMX root的CPL=0时执行VMXOFF,RFLAGS.CF和RFLAGS.ZF均为0则表示CPU关闭了VMX模式。
2.2.2 KVM初始化流程
KVM的初始化流程在kvm-intel.ko
的模块注册函数vmx_init
中完成。下面以最新的Linux内核v6.5为例进行代码分析。
static int __init vmx_init(void)
{
int r, cpu;
if (!kvm_is_vmx_supported())
return -EOPNOTSUPP;
...
r = kvm_x86_vendor_init(&vmx_init_ops);
...
/*
* Common KVM initialization _must_ come last, after this, /dev/kvm is
* exposed to userspace!
*/
r = kvm_init(sizeof(struct vcpu_vmx), __alignof__(struct vcpu_vmx),
THIS_MODULE);
...
}
vmx_init
函数大体上分成3个部分:
kvm_is_vmx_supported
:检测是否支持并且已经开启了VMX模式,否则后面的初始化流程就没有意义了;kvm_x86_vendor_init
:完成架构特定的初始化流程,传参&vmx_init_ops
包含了Intel VT-x具体实现的各种初始化回调函数;kvm_init
:完成KVM通用部分的初始化流程。2.2.2.1 kvm_x86_vendor_init
kvm_x86_vendor_init
函数在获取了相关的锁之后,最终调用__kvm_x86_vendor_init
来完成实际的初始化流程,后者的代码去掉一些注释、不重要的过程以及错误处理路径之后,其主要的流程如下代码所示:
struct kvm_x86_init_ops {
int (*hardware_setup)(void);
unsigned int (*handle_intel_pt_intr)(void);
struct kvm_x86_ops *runtime_ops;
struct kvm_pmu_ops *pmu_ops;
};
void kvm_pmu_ops_update(const struct kvm_pmu_ops *pmu_ops)
{
memcpy(&kvm_pmu_ops, pmu_ops, sizeof(kvm_pmu_ops));
...
}
static inline void kvm_ops_update(struct kvm_x86_init_ops *ops)
{
memcpy(&kvm_x86_ops, ops->runtime_ops, sizeof(kvm_x86_ops));
...
kvm_pmu_ops_update(ops->pmu_ops);
}
static int __kvm_x86_vendor_init(struct kvm_x86_init_ops *ops)
{
int r, cpu;
...
r = kvm_mmu_vendor_module_init();
...
kvm_init_pmu_capability(ops->pmu_ops);
r = ops->hardware_setup();
...
kvm_ops_update(ops);
for_each_online_cpu(cpu) {
smp_call_function_single(cpu, kvm_x86_check_cpu_compat, &r, 1);
if (r < 0)
goto out_unwind_ops;
}
/*
* Point of no return! DO NOT add error paths below this point unless
* absolutely necessary, as most operations from this point forward
* require unwinding.
*/
kvm_timer_init();
...
kvm_init_msr_lists();
...
}
关键过程包括:
kvm_mmu_vendor_module_init
:完成内存虚拟化中跟MMU架构相关部分的初始化,但大部分的初始化过程将被推迟到供应商模块(kvm-intel.ko
或者kvm-amd.ko
)加载的时候完成,因为许多掩码/值会被VMX或者SVM修改;kvm_init_pmu_capability
:PMU能力的初始化,如果开启了pmu能力(模块参数enable_pmu
),将会在这一步完成struct x86_pmu_capability kvm_pmu_cap
的初始化,它主要记录了KVM pmu的版本信息、计数器数量(num_counters_gp
和num_counters_fixex
)、计数器位宽(bit_width_gp
和bit_width_fixed
)以及pmu事件掩码(events_mask
和events_mask_len
)等;ops->hardware_setup()
:用来创建一些跟启动KVM密切相关的数据结构以及初始化一些硬件特性,里面涵盖的内容比较多,其中包括MMU和扩展页表(EPT)的设置和初始化、嵌套虚拟化的配置以及CPU支持特性列表的设置等等,具体的可以直接去查看arch/x86/kvm/vmx/vmx.c#hardware_setup(void)
方法;kvm_ops_update
:将初始化方法列表kvm_x86_init_ops
中的运行时方法kvm_x86_runtime_ops
和pmu相关方法kvm_pmu_ops
复制到虚拟化统一接口列表kvm_x86_ops
中,在KVM完成初始化后,将通过kvm_x86_ops
来完成用户程序的接口调用请求;kvm_x86_check_cpu_compat
:对每个CPU,会调用该方法来检测所有CPU的特性是否一致;kvm_timer_init
:时钟初始化;kvm_init_msr_lists
:初始化KVM支持的MSRs列表。2.2.2.2 kvm_init
kvm_init
将完成KVM通用部分的初始化,该过程完成后,KVM模块就将/dev/kvm
暴露给了用户态,作为用户态程序(QEMU)与KVM模块通信的接口。
int kvm_init(unsigned vcpu_size, unsigned vcpu_align, struct module *module)
{
int r;
int cpu;
...
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);
...
r = kvm_irqfd_init();
if (r)
goto err_irqfd;
r = kvm_async_pf_init();
if (r)
goto err_async_pf;
kvm_chardev_ops.owner = module;
kvm_preempt_ops.sched_in = kvm_sched_in;
kvm_preempt_ops.sched_out = kvm_sched_out;
...
r = kvm_vfio_ops_init();
...
/*
* Registration _must_ be the very last thing done, as this exposes
* /dev/kvm to userspace, i.e. all infrastructure must be setup!
*/
r = misc_register(&kvm_dev);
...
}
kvm_vcpu_cache
:创建VCPU结构体的cache赋值给kvm_vcpu_cache,之后就能比较快地分配VCPU空间;kvm_irqfd_init
:初始化irqfd相关的数据,主要是创建一个线程kvm-irqfd-cleanup
;kvm_async_pf_init
:初始化async_pf相关的数据,主要是创建一个async_pf_cache
缓存结构;kvm_sched_in
和kvm_sched_out
:设置kvm_preempt_ops
的sched_in
和sched_out
,当虚拟机VCPU所在线程被抢占或者被调度时会调用这两个函数;kvm_vfio_ops_init
:注册kvm_vfio_ops
接口;misc_register(&kvm_dev)
:调用misc_register
创建kvm_dev
这个misc设备,即/dev/kvm
设备文件。2.2.3 KVM初始化重要过程
2.2.3.1 hardware_setup
kvm_init
调用的第一个重要函数是ops->hardware_setup()
,它是实现相关的vmx_init_ops
的hardware_setup
成员。该函数代码如下,我们只看跟VMCS相关的部分:
static __init int hardware_setup(void)
{
int r;
...
if (setup_vmcs_config(&vmcs_config, &vmx_capability) < 0)
return -EIO;
...
r = alloc_kvm_area();
...
}
static __init int alloc_kvm_area(void)
{
int cpu;
for_each_possible_cpu(cpu) {
struct vmcs *vmcs;
vmcs = alloc_vmcs_cpu(false, cpu, GFP_KERNEL);
if (!vmcs) {
free_kvm_area();
return -ENOMEM;
}
...
per_cpu(vmxarea, cpu) = vmcs;
}
return 0;
}
首先调用setup_vmcs_config
用于设置一个全局变量vmcs_config
,该函数根据查看CS的特性支持情况来填写vmcs_config
(对应开启条件2),之后在创建虚拟CPU的时候用这个配置来初始化VMCS。
然后调用alloc_kvm_area
,为每一个物理CPU分配一个vmcs
结构并且放到vmxarea
这个percpu
变量中(对应开启条件3和4)。
2.2.3.2 kvm_x86_check_cpu_compat
kvm_init
调用的第二个重要函数是kvm_x86_check_cpu_compat
,它通过调用kvm_x86_check_processor_compatibility
方法,最终调用vmx_check_processor_compat
来完成处理。
static struct kvm_x86_ops vmx_x86_ops __initdata = {
...
.check_processor_compatibility = vmx_check_processor_compat,
...
}
static int kvm_x86_check_processor_compatibility(void)
{
int cpu = smp_processor_id();
struct cpuinfo_x86 *c = &cpu_data(cpu);
/*
* Compatibility checks are done when loading KVM and when enabling
* hardware, e.g. during CPU hotplug, to ensure all online CPUs are
* compatible, i.e. KVM should never perform a compatibility check on
* an offline CPU.
*/
WARN_ON(!cpu_online(cpu));
if (__cr4_reserved_bits(cpu_has, c) !=
__cr4_reserved_bits(cpu_has, &boot_cpu_data))
return -EIO;
return static_call(kvm_x86_check_processor_compatibility)();
}
static void kvm_x86_check_cpu_compat(void *ret)
{
*(int *)ret = kvm_x86_check_processor_compatibility();
}
kvm_init
会对每一个在线CPU都调用kvm_x86_check_cpu_compat
函数,对应的vmx_check_processor_compat
函数代码如下:
static int vmx_check_processor_compat(void)
{
int cpu = raw_smp_processor_id();
struct vmcs_config vmcs_conf;
struct vmx_capability vmx_cap;
...
if (setup_vmcs_config(&vmcs_conf, &vmx_cap) < 0) {
pr_err("Failed to setup VMCS config on CPU %dn", cpu);
return -EIO;
}
...
if (memcmp(&vmcs_config, &vmcs_conf, sizeof(struct vmcs_config))) {
pr_err("Inconsistent VMCS config on CPU %dn", cpu);
return -EIO;
}
return 0;
}
在hardware_setup
函数中调用setup_vmcs_config
是用当前运行的物理CPU的特性构造出一个vmcs_config
,这里对所有物理CPU构造出vmcs_conf
,然后与全局的vmcs_config
比较,确保所有的物理CPU的vmcs_conf
一样,这样才能够保证VCPU在物理CPU上调度的时候不会出现错误。
2.2.3.3 misc_register(&kvm_dev)
kvm_init
的最后一个重要工作是创建一个misc设备/dev/kvm
,该设备的定义及其对应的操作如下:
static struct file_operations kvm_chardev_ops = {
.unlocked_ioctl = kvm_dev_ioctl,
.llseek = noop_llseek,
KVM_COMPAT(kvm_dev_ioctl),
};
static struct miscdevice kvm_dev = {
KVM_MINOR,
"kvm",
&kvm_chardev_ops,
};
可以看到,该设备只支持ioctl系统调用,当然,open和close这些系统调用会被misc设备框架处理。
kvm_dev_ioctl
代码如下:
static long kvm_dev_ioctl(struct file *filp,
unsigned int ioctl, unsigned long arg)
{
int r = -EINVAL;
switch (ioctl) {
case KVM_GET_API_VERSION:
if (arg)
goto out;
r = KVM_API_VERSION;
break;
case KVM_CREATE_VM:
r = kvm_dev_ioctl_create_vm(arg);
break;
case KVM_CHECK_EXTENSION:
r = kvm_vm_ioctl_check_extension_generic(NULL, arg);
break;
case KVM_GET_VCPU_MMAP_SIZE:
if (arg)
goto out;
r = PAGE_SIZE; /* struct kvm_run */
#ifdef CONFIG_X86
r += PAGE_SIZE; /* pio data page */
#endif
#ifdef CONFIG_KVM_MMIO
r += PAGE_SIZE; /* coalesced mmio ring page */
#endif
break;
case KVM_TRACE_ENABLE:
case KVM_TRACE_PAUSE:
case KVM_TRACE_DISABLE:
r = -EOPNOTSUPP;
break;
default:
return kvm_arch_dev_ioctl(filp, ioctl, arg);
}
out:
return r;
}
从架构角度看,/dev/kvm
设备的ioctl接口分为两类:
- 一类为通用接口,如
KVM_API_VERSION
和KVM_CREATE_VM
; - 另一类为架构相关接口,ioctl由
kvm_arch_dev_ioctl
函数处理。
从内容角度看,KVM的ioctl处理整个KVM层面的请求,如:
KVM_GET_API_VERSION
返回KVM的版本号;KVM_CREATE_VM
创建一台虚拟机;KVM_CHECK_EXTENSION
检查KVM是否支持一些通用扩展;KVM_GET_VCPU_MMAP_SIZE
返回VCPU中QEMU和KVM共享内存的大小。
2.2.4 总结
这就是kvm_init
的主要工作。可以看到,KVM模块的初始化过程主要是对硬件进行检查,分配一些常用结构的缓存,创建一个/dev/kvm
设备,得到vmcs的一个配置结构vmcs_config
,并根据CPU特性设置一些全局变量,给每个物理CPU分配一个vmcs结构。
值得注意的是,这个时候CPU还不在VMX模式下,因为在vmx_init
初始化的过程中并没有向CR4.VMXE
写入1,也没有分配VMXON
区域。这其实也是一种惰性策略,毕竟如果加载了KVM模块,却一个虚拟机也不创建,那也就没有必要让CPU进入VMX模式。所以VMX模式的真正开启是在创建第一个虚拟机的时候。
2.3 参考文献
关注微信公众号:Linux内核拾遗
文章来源:mp.weixin.qq.com/s/fmr_T1F9m…