写点什么

QEMU 之 CPU 虚拟化(二):KVM 模块初始化介绍

  • 2023-09-02
    浙江
  • 本文字数:6714 字

    阅读完需:约 22 分钟

QEMU之CPU虚拟化(二):KVM模块初始化介绍

最近在阅读李强编著的《QEMU/KVM 源码解析与应用》这本书来学习 Linux 内核虚拟化相关知识,通过读书笔记的方式来提炼和归纳书中重要的知识点。本系列主要内容是关于 QEMU 中 CPU 虚拟化方面的介绍。

2 KVM 模块初始化介绍

关注微信公众号:Linux 内核拾遗

文章来源:https://mp.weixin.qq.com/s/fmr_T1F9mnENz_zya_xM8g


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.clapic.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.kokvm-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 模式的步骤简单概括如下:


  1. CPUID 检测 CPU 是否支持 VMX;

  2. CPUID.1:ECX.VMX[bit 5]=1表示 CPU 支持 VMX。

  3. 检测 CPU 支持的 VMX 能力,通过读取与 VMX 能力相关的 MSR 寄存器完成;

  4. IA32_VMX_BASIC 寄存器:基本 VMX 能力信息;

  5. IA32_VMX_PINBASED_CTLS 和 IA_32_VMX_PROCBASED_CTLS 寄存器:表示 VMCS 区域中 VM-execution 相关域能够设置值。

  6. 分配一段 4KB 对齐的内存作为 VMXON 区域;

  7. IA32_VMX_BASICMSR 寄存器:表示 VMXON 区域大小。

  8. 初始化 VMXON 区域的版本标识;

  9. 确保当前 CPU 运行模式的 CR0 寄存器符合进入 VMX 的条件;

  10. 即 CR0.PE=1,CR0.PG=1;

  11. 其他需要满足的设置通过 IA32_VMX_CR0_FIXED0 和 IA32_VMX_CR0_FIXED1 寄存器报告。

  12. 开启 VMX 模式;

  13. 设置 CR4.VMXE 为 1;

  14. 其他 CR4 需要满足的设置通过 IA32_VMX_CR4_FIXED0 和 IA32_VMX_CR4_FIXED1 报告。

  15. 确保 IA32_FEATURE_CONTROL 寄存器被正确设置,其锁定位(0 位)为 1,这个 MSR 寄存器通常由 BIOS 编程。

  16. 使用 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 个部分:


  1. kvm_is_vmx_supported:检测是否支持并且已经开启了 VMX 模式,否则后面的初始化流程就没有意义了;

  2. kvm_x86_vendor_init:完成架构特定的初始化流程,传参&vmx_init_ops包含了 Intel VT-x 具体实现的各种初始化回调函数;

  3. 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(); ...}
复制代码


关键过程包括:


  1. kvm_mmu_vendor_module_init:完成内存虚拟化中跟 MMU 架构相关部分的初始化,但大部分的初始化过程将被推迟到供应商模块(kvm-intel.ko或者kvm-amd.ko)加载的时候完成,因为许多掩码/值会被 VMX 或者 SVM 修改;

  2. kvm_init_pmu_capability:PMU 能力的初始化,如果开启了 pmu 能力(模块参数enable_pmu),将会在这一步完成struct x86_pmu_capability kvm_pmu_cap的初始化,它主要记录了 KVM pmu 的版本信息、计数器数量(num_counters_gpnum_counters_fixex)、计数器位宽(bit_width_gpbit_width_fixed)以及 pmu 事件掩码(events_maskevents_mask_len)等;

  3. ops->hardware_setup():用来创建一些跟启动 KVM 密切相关的数据结构以及初始化一些硬件特性,里面涵盖的内容比较多,其中包括 MMU 和扩展页表(EPT)的设置和初始化、嵌套虚拟化的配置以及 CPU 支持特性列表的设置等等,具体的可以直接去查看arch/x86/kvm/vmx/vmx.c#hardware_setup(void)方法;

  4. kvm_ops_update:将初始化方法列表kvm_x86_init_ops中的运行时方法kvm_x86_runtime_ops和 pmu 相关方法kvm_pmu_ops复制到虚拟化统一接口列表kvm_x86_ops中,在 KVM 完成初始化后,将通过kvm_x86_ops来完成用户程序的接口调用请求;

  5. kvm_x86_check_cpu_compat:对每个 CPU,会调用该方法来检测所有 CPU 的特性是否一致;

  6. kvm_timer_init:时钟初始化;

  7. 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); ...}
复制代码


  1. kvm_vcpu_cache:创建 VCPU 结构体的 cache 赋值给 kvm_vcpu_cache,之后就能比较快地分配 VCPU 空间;

  2. kvm_irqfd_init:初始化 irqfd 相关的数据,主要是创建一个线程kvm-irqfd-cleanup

  3. kvm_async_pf_init:初始化 async_pf 相关的数据,主要是创建一个async_pf_cache缓存结构;

  4. kvm_sched_inkvm_sched_out:设置kvm_preempt_opssched_insched_out,当虚拟机 VCPU 所在线程被抢占或者被调度时会调用这两个函数;

  5. kvm_vfio_ops_init:注册kvm_vfio_ops接口;

  6. 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_opshardware_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 %d\n", cpu); return -EIO; } ... if (memcmp(&vmcs_config, &vmcs_conf, sizeof(struct vmcs_config))) { pr_err("Inconsistent VMCS config on CPU %d\n", 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_VERSIONKVM_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 参考文献

  1. QEMU/KVM 源码解析与应用 - 李强


关注微信公众号:Linux 内核拾遗

文章来源:https://mp.weixin.qq.com/s/fmr_T1F9mnENz_zya_xM8g

发布于: 刚刚阅读数: 7
用户头像

聚沙成塔 2023-01-12 加入

分享Linux内核开发相关的编程语言、开发调试工具链、计算机组成及操作系统内核知识、Linux社区最新资讯等

评论

发布
暂无评论
QEMU之CPU虚拟化(二):KVM模块初始化介绍_Linux_Linux内核拾遗_InfoQ写作社区