2. KVM模块初始化


  • 1. 原理介绍
  • 2. 整体流程
  • 3. kvm_init: 初始化 kvm 框架
    • 3.1. 架构初始化: kvm_arch_init
      • 3.1.1. CPU特性支持检查
      • 3.1.2. kmem_cache分配(x86_fpu)
      • 3.1.3. percpu kvm_shared_msrs
      • 3.1.4. mmu 模块相关处理
      • 3.1.5. 设置全局的 kvm_x86_ops
      • 3.1.6. 设置 MMU 的shadow PTE masks
      • 3.1.7. timer初始化
      • 3.1.8. lapic初始化
    • 3.2. workqueue?
    • 3.3. 架构相关的硬件设置
      • 3.3.1. vmx/svm 的硬件配置 hardware_setup()
        • 3.3.1.1. 设置全局 vmcs_config 和 vmx_capability
        • 3.3.1.2. 一堆检查
        • 3.3.1.3. posted 中断的 wakeup 处理函数设置
        • 3.3.1.4. percpu的VMXON region
      • 3.3.2. msr保存到全局变量msrs_to_save[]数组
    • 3.4. 检查每个处理器对vmx的兼容性
    • 3.5. 注册CPU状态变化的通知函数
    • 3.6. 注册reboot时候的通知函数
    • 3.7. 给kvm_vcpu分配cache
    • 3.8. 赋值file_operations的模块名
    • 3.9. 注册设备文件/dev/kvm
    • 3.10. 动作注册
    • 3.11. debugfs初始化
    • 3.12. vfio操作初始化
  • 4. 参考

1. 原理介绍

KVM模块分为三个主要模块:kvm.ko、kvm-intel.ko和kvm-amd.ko,这三个模块在初始化阶段的流程如图5-4所示。

?图5-4 KVM模块初始化阶段:

2019-07-05-21-29-03.png

KVM模块可以编译进内核中,也可以作为内核模块在Linux系统启动完成之后加载。加载时,KVM 根据主机所用的体系架构是 Intel的 VMX技术还是AMD的SVM技术,会采用略微不同的加载流程。

Linux的子模块入口通常通过module_init宏进行定义,由内核进行调用。KVM的初始化流程如图5-5所示。
?
图5-5 KVM的初始化流程:

2019-12-11-11-04-37.png

KVM的初始化步骤分为以下三步。

1)在平台相关的KVM模块中通过module_init宏正式进入KVM的初始化阶段,并且执行相关的硬件初始化准备。(arch/x86/kvm/vmx.c)

2)进入kvm_main.c中的kvm_init函数进行正式的初始化工作,期间进行了一系列子操作。(virt/kvm/kvm_main.c)

注: vmx.c中定义了一个结构体vmx_x86_ops, 这是函数指针集合(各种操作的集合). 初始化很多动作以及iotcl调用也都会用到这些函数. 作为参数传给kvm_init(), 再传给kvm_arch_init(), 在传递到全局变量kvm_x86_ops. 可以见4的说明

  • 通过kvm_arch_init函数初始化KVM内部的一些数据结构:注册全局变量kvm_x86_ops初始化MMU等数据结构、初始化Timer定时器架构。(arch/x86/kvm/x86.c)
  • 分配KVM内部操作所需要的内存空间
  • 调用kvm_x86_ops的hardware_setup函数进行具体的硬件体系结构的初始化工作。
  • 注册sysfsdevfs等API接口信息。
  • 最后初始化debugfs的调试信息。

3)进行后续的硬件初始化准备操作。

2. 整体流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
vmx_init()                               // 初始化入口
 ├─ kvm_init(KVM_GET_API_VERSION)        // 初始化KVM框架
 |   ├─ kvm_arch_init()                  // 架构相关初始化
 |   |   ├─ cpu_has_kvm_support()         // CPU是否支持kvm
 |   |   ├─ disabled_by_bios()            // bios是否禁用vt
 |   |   ├─ boot_cpu_has()                // CPU是否支持一些特性
 |   |   ├─ kmem_cache_create("x86_fpu")  // x86_fpu kmem_cache
 |   |   ├─ alloc_percpu()                // kvm_shared_msrs
 |   |   ├─ kvm_mmu_module_init()         // mmu模块初始化
 |   |   ├─ kvm_mmu_set_mask_ptes()       // shadow pte mask设置
 |   |   ├─ kvm_timer_init()              // 时钟初始化
 |   |   ├─ kvm_lapic_init()              // lapic初始化
 |   ├─ kvm_irqfd_init()                 //
 |   ├─ kvm_arch_hardware_setup()        //
 |   |   ├─ kvm_x86_ops->hardware_setup() // CPU是否支持kvm
 |   |   |  ├─ rdmsrl_safe()              // 读msr
 |   |   |  ├─ store_idt()                // 保存idt
 |   |   |  ├─ setup_vmcs_config()        // 建立vmcs_config和vmx_capability
 |   |   |  ├─ boot_cpu_has()             // CPU特性支持
 |   |   |  ├─ cpu_has_vmx_vpid()         // cpu是否支持vpid
 |   |   |  ├─ cpu_has_vmx_invvpid()      // cpu是否支持invvpid
 |   |   |  ├─ cpu_has_vmx_ept()          // cpu是否支持ept
 |   |   |  ├─ kvm_configure_mmu()       // mmu相关硬件判断和全局变量
 |   |   |  ├─ cpu_has_vmx_XXX()          // cpu是否有XXX
 |   |   |  ├─ vmx_enable_tdp()           // ept支持时开启tdp
 |   |   |  ├─ kvm_disable_tdp()          // 关闭tdp
 |   |   |  ├─ kvm_set_posted_intr_wakeup_handler()     // posted intr wakeup handler
 |   |   |  └─ alloc_kvm_area()           // 给每个cpu分配一个struct vmcs
 |   |   └─ kvm_init_msr_list()           // 将msr保存到全局变量msrs_to_save[]数组
 |   ├─ smp_call_function_single()       // 对每个online cpu进行兼容性检查
 |   ├─ cpuhp_setup_state_nocalls()      // 注册cpu状态变化的回调函数
 |   ├─ register_reboot_notifier()       // 注册reboot时候的通知函数
 |   ├─ kvm_cache_create_usercopy()      // 创建vcpu 的 kmem cache, 对象大小是sizeof(struct vcpu_vmx)
 |   ├─ kvm_async_pf_init()              // 异步
 |   ├─ misc_register(&kvm_dev)          // 注册字符设备文件/dev/kvm
 |   ├─ register_syscore_ops()           // 注册系统核心函数, 这里是suspend和resume
 |   ├─ kvm_init_debug()                 // 初始化debugfs
 |   └─ kvm_vfio_ops_init()              // vfio的操作初始化
 ├─ vmx_setup_l1d_flush()                //
 └─ vmx_check_vmcs12_offsets()

3. kvm_init: 初始化 kvm 框架

vmx.c定义了vmx的架构下的操作函数vmx_x86_ops, 其他架构也有自己的定义

vmx_init()将自己的vmx_x86_ops作为参数传了进去

1
r = kvm_init(&vmx_x86_ops, sizeof(struct vcpu_vmx), __alignof__(struct vcpu_vmx), THIS_MODULE);

正式进行KVM框架初始化

3.1. 架构初始化: kvm_arch_init

使用传入的vmx_x86_ops参数, 注意这是void *类型的

1
r = kvm_arch_init(opaque);

主要是架构相关的检查和初始化

3.1.1. CPU特性支持检查

1
2
3
4
5
6
// CPUID.1:ECX.VMX[bit 5] -> VT 必须支持
ops->cpu_has_kvm_support();
// bios必须支持
ops->disable_by_bios();
// FPU和FXSR必须支持
!boot_cpu_has(x86_FEATURE_FPU) || !boot_cpu_has(X86_FEATURE_FXSR);

3.1.2. kmem_cache分配(x86_fpu)

1
2
3
4
5
// arch/x86/kvm/x86.c
struct kmem_cache *x86_fpu_cache;
EXPORT_SYMBOL_GPL(x86_fpu_cache);

x86_fpu_cache = kmem_cache_create("x86_fpu", ...)

3.1.3. percpu kvm_shared_msrs

1
2
static struct kvm_shared_msrs __percpu *shared_msrs;
shared_msrs = alloc_percpu(struct kvm_shared_msrs);

3.1.4. mmu 模块相关处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// arch/x86/kvm/x86.c
kvm_mmu_module_init();

// arch/x86/kvm/mmu.c
static struct kmem_cache *pte_list_desc_cache;
static struct kmem_cache *mmu_page_header_cache;
static struct percpu_counter kvm_total_used_mmu_pages;
int kvm_mmu_module_init(void)
{
    kvm_mmu_reset_all_pte_masks();
    // cache 初始化
    pte_list_desc_cache = kmem_cache_create("pte_list_desc", ...)
    // cache 初始化
    mmu_page_header_cache = kmem_cache_create("kvm_mmu_page_header", ...)
    // percpu的mmu pages使用计数
    percpu_counter_init(&kvm_total_used_mmu_pages, 0, GFP_KERNEL);
    // 预分配
    register_shrinker(&mmu_shrinker);
}

3.1.5. 设置全局的 kvm_x86_ops

arch/x86/kvm/x86.c 中,定义了名为 kvm_x86_ops静态变量,通过export_symbol 宏全局范围!!! 内导出。

1
2
3
// arch/x86/kvm/x86.c
struct kvm_x86_ops *kvm_x86_ops __read_mostly;
EXPORT_SYMBOL_GPL(kvm_x86_ops);

给全局kvm_x86_ops赋值

1
kvm_x86_ops = ops;

3.1.6. 设置 MMU 的shadow PTE masks

1
2
// 设置MMU的shadow PTE masks
kvm_mmu_set_mask_ptes();

3.1.7. timer初始化

1
kvm_timer_init();

3.1.8. lapic初始化

1
kvm_lapic_init();

3.2. workqueue?

1
kvm_irqfd_init();

3.3. 架构相关的硬件设置

1
kvm_arch_hardware_setup();

3.3.1. vmx/svm 的硬件配置 hardware_setup()

实际上调用的是

1
kvm_x86_ops->hardware_setup();
3.3.1.1. 设置全局 vmcs_config 和 vmx_capability
1
2
3
4
5
6
7
// arch/x86/kvm/vmx/capabilities.h
extern struct vmcs_config vmcs_config;
extern struct vmx_capability vmx_capability;

// arch/x86/kvm/vmx/vmx.c
if (setup_vmcs_config(&vmcs_config, &vmx_capability) < 0)
    return -EIO;

这里的这两个全局变量不是指针, 而是真正值, 所以就直接分配了内存

3.3.1.2. 一堆检查
3.3.1.3. posted 中断的 wakeup 处理函数设置
1
kvm_set_posted_intr_wakeup_handler(wakeup_handler);
3.3.1.4. percpu的VMXON region
1
alloc_kvm_area();

给每个cpu分配struct vmcs, 用于VMXON region

1
2
3
4
5
6
7
8
9
10
11
12
struct vmcs_hdr {
        u32 revision_id:31;
        u32 shadow_vmcs:1;
};

struct vmcs {
        struct vmcs_hdr hdr;
        u32 abort;
        char data[0];
};

DECLARE_PER_CPU(struct vmcs *, current_vmcs);

3.3.2. msr保存到全局变量msrs_to_save[]数组

1
kvm_init_msr_list();

通过rdmr_safe()把msr保存到全局变量msrs_to_save[]数组.

3.4. 检查每个处理器对vmx的兼容性

1
2
3
for_each_online_cpu(cpu) {
        smp_call_function_single(cpu, check_processor_compat, &r, 1);
}

对每一个online的cpu调用smp_call_function_single, 这个函数是让第一个参数(cpu)运行第二个参数(check_processor_compat), 最终调用kvm_x86_opscheck_processor_compatibility

在前面的kvm_arch_init中已经初始化了kvm_x86_ops, 这里以vmx为例

最终调用vmx_check_processor_compat, 检查处理器的兼容性

3.5. 注册CPU状态变化的通知函数

1
2
r = cpuhp_setup_state_nocalls(CPUHP_AP_KVM_STARTING, "kvm/cpu:starting",
                    kvm_starting_cpu, kvm_dying_cpu);

当cpu状态变成starting会调用回调函数kvm_starting_cpukvm_dying_cpu, 这里面会使能禁止cpu虚拟化特性

  • hardware_enable
    • hardware_enable_nolock()
      • kvm_arch_hardware_enable() // 使能虚拟化特性
  • hardware_disable
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
static int kvm_starting_cpu(unsigned int cpu)
{
    raw_spin_lock(&kvm_count_lock);
    if (kvm_usage_count)
        hardware_enable_nolock(NULL);
    raw_spin_unlock(&kvm_count_lock);
    return 0;
}

static void hardware_enable_nolock(void *junk)
{
    int cpu = raw_smp_processor_id();
    int r;

    if (cpumask_test_cpu(cpu, cpus_hardware_enabled))
        return;

    cpumask_set_cpu(cpu, cpus_hardware_enabled);

    r = kvm_arch_hardware_enable();

    if (r) {
        cpumask_clear_cpu(cpu, cpus_hardware_enabled);
        atomic_inc(&hardware_enable_failed);
        pr_info("kvm: enabling virtualization on CPU%d failed\n", cpu);
    }
}

static int kvm_dying_cpu(unsigned int cpu)
{
    raw_spin_lock(&kvm_count_lock);
    if (kvm_usage_count)
        hardware_disable_nolock(NULL);
    raw_spin_unlock(&kvm_count_lock);
    return 0;
}

static void hardware_disable_nolock(void *junk)
{
    int cpu = raw_smp_processor_id();

    if (!cpumask_test_cpu(cpu, cpus_hardware_enabled))
        return;
    cpumask_clear_cpu(cpu, cpus_hardware_enabled);
    kvm_arch_hardware_disable();
}

3.6. 注册reboot时候的通知函数

原理同上, 重启的时候会调用这个函数

1
register_reboot_notifier(&kvm_reboot_notifier);

3.7. 给kvm_vcpu分配cache

根据注释, 主要为了满足fx_save的对齐要求

1
2
3
4
5
6
7
8
9
/* A kmem cache lets us meet the alignment requirements of fx_save. */
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),
                    sizeof_field(struct kvm_vcpu, arch),
                    NULL);

后面会给创建vcpu时候使用, 该cache的arch部分可被拷贝到用户态使用. 这里的vcpu_sizesizeof(struct vcpu_vmx)

3.8. 赋值file_operations的模块名

把早先传给kvm_init()的参数THIS_MODULE, 也就是vmx的模块名分别赋值给三个file operation结构体变量:

1
2
3
kvm_chardev_ops.owner = module;
kvm_vm_fops.owner = module;
kvm_vcpu_fops.owner = module;

这三个变量都是file_operation结构体被部分初始化全局变量, 分别用于处理对不同介质的设备读写(一切皆是文件).

被部分初始化的函数入口地址分别指向kvm_dev_ioctl, kvm_vm_release,noop_llseek等函数(kvm_main.c)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// virt/kvm/kvm_main.c
static struct file_operations kvm_vcpu_fops = {
    .release        = kvm_vcpu_release,
    .unlocked_ioctl = kvm_vcpu_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl   = kvm_vcpu_compat_ioctl,
#endif
    .mmap           = kvm_vcpu_mmap,
    .llseek     = noop_llseek,
};

static struct file_operations kvm_vm_fops = {
    .release        = kvm_vm_release,
    .unlocked_ioctl = kvm_vm_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl   = kvm_vm_compat_ioctl,
#endif
    .llseek     = noop_llseek,
};

static struct file_operations kvm_chardev_ops = {
    .unlocked_ioctl = kvm_dev_ioctl,
    .compat_ioctl   = kvm_dev_ioctl,
    .llseek     = noop_llseek,
};

3.9. 注册设备文件/dev/kvm

1
misc_register(&kvm_dev);

misc_register函数是linux内核的一个通用接口,主要作用是为了注册设备文件,kvm模块借用该接口创建了/dev/kvm设备文件, 具体查看参看的文章(KVM实战: 原理、进阶与性能调优的)

下面是设备文件的结构体

1
2
3
4
5
6
7
8
9
10
11
// virt/kvm/kvm_main.c
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,
};

3.10. 动作注册

1
2
3
4
register_syscore_ops(&kvm_syscore_ops);

kvm_preempt_ops.sched_in = kvm_sched_in;
kvm_preempt_ops.sched_out = kvm_sched_out;

从命名看, kvm_syscore_ops是系统核心函数, 包含suspendresume:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// virt/kvm/kvm_main.c
static struct syscore_ops kvm_syscore_ops = {
    .suspend = kvm_suspend,
    .resume = kvm_resume,
};

// drivers/base/syscore.c
static LIST_HEAD(syscore_ops_list);

void register_syscore_ops(struct syscore_ops *ops)
{
    mutex_lock(&syscore_ops_lock);
    list_add_tail(&ops->node, &syscore_ops_list);
    mutex_unlock(&syscore_ops_lock);
}
EXPORT_SYMBOL_GPL(register_syscore_ops);

kvm_preempt_ops是结构体preempt_ops:

1
static __read_mostly struct preempt_ops kvm_preempt_ops;

从命名看, sched_insched_out可以申请调度器对任务的换入换出.

3.11. debugfs初始化

1
kvm_init_debug();

kvm模块的加载依赖debugfs,所以在加载之前要手动挂载.

这个函数建立了debugfs目录下的kvm目录,然后建立了一系列复杂的tracepoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// virt/kvm/kvm_main.c
static int kvm_init_debug(void)
{
    int r = -EEXIST;
    struct kvm_stats_debugfs_item *p;

    kvm_debugfs_dir = debugfs_create_dir("kvm", NULL);

    kvm_debugfs_num_entries = 0;
    for (p = debugfs_entries; p->name; ++p, kvm_debugfs_num_entries++) {
        if (!debugfs_create_file(p->name, 0444, kvm_debugfs_dir,
                     (void *)(long)p->offset,
                     stat_fops[p->kind]))
            goto out_dir;
    }
}

目前支持的tracepoint有:

1
2
// arch/x86/kvm/x86.c
struct kvm_stats_debugfs_item debugfs_entries[] = {

3.12. vfio操作初始化

1
kvm_vfio_ops_init();