QEMU 之 CPU 虚拟化(二):KVM 模块初始化介绍
最近在阅读李强编著的《QEMU/KVM 源码解析与应用》这本书来学习 Linux 内核虚拟化相关知识,通过读书笔记的方式来提炼和归纳书中重要的知识点。本系列主要内容是关于 QEMU 中 CPU 虚拟化方面的介绍。
2 KVM 模块初始化介绍
关注微信公众号:Linux 内核拾遗
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 为例进行代码分析。
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
来完成实际的初始化流程,后者的代码去掉一些注释、不重要的过程以及错误处理路径之后,其主要的流程如下代码所示:
关键过程包括:
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 模块通信的接口。
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 相关的部分:
首先调用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
来完成处理。
kvm_init
会对每一个在线 CPU 都调用kvm_x86_check_cpu_compat
函数,对应的vmx_check_processor_compat
函数代码如下:
在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
,该设备的定义及其对应的操作如下:
可以看到,该设备只支持 ioctl 系统调用,当然,open 和 close 这些系统调用会被 misc 设备框架处理。
kvm_dev_ioctl
代码如下:
从架构角度看,/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 参考文献
QEMU/KVM 源码解析与应用 - 李强
关注微信公众号:Linux 内核拾遗
版权声明: 本文为 InfoQ 作者【Linux内核拾遗】的原创文章。
原文链接:【http://xie.infoq.cn/article/6fa43a9b435048e005845e422】。文章转载请联系作者。
评论