1690 字
8 分钟
x86和arm架构实现-逐渐以小块方式启用脏日志

x86和arm64架构实现-逐渐以小块方式启用脏日志#

背景#

在虚拟机进行热迁移时,需要追踪哪些内存页被虚拟机修改过,在先前,当一个正在运行的大内存虚拟机(首次开启脏页日志时,KVM 需要一次性遍历虚拟机的所有内存页并设置为写保护。这个操作非常耗时,会导致虚拟机出现一次明显的、长达数百毫秒的卡顿。

所以在 x86 上,先不一次性遍历所有内存设置写保护,而是将这一步操作放到后面,以小块的方式一块一块的启动写保护进行脏页跟踪。但是这里遇到一个问题,如果不启用写保护,那虚拟机对内存的修改没有反应到脏页位图里面,就会丢失这个脏页修改过了的信息。所以,在开始的时候就将脏页位图全部设置为 1——即全脏,在后面逐渐启用写保护的时候才会将对应的脏页位图修改为 0,此时已经开启了写保护,就会跟踪这个内存的情况了。

X86 实现#

在 x86 中首先实现该功能,分为两部分补丁:

KVM: x86: enable dirty log gradually in small chunks

[PATCH v3 0/2] KVM: x86: Enable dirty logging lazily for huge pages

第一个补丁先实现对于小块内存页启用延迟写保护,对于大页还是直接进行写保护。

第二个补丁发现大页也可以和第一个补丁中对于小块内存页一样的操作,于是对于大页也启用延迟写保护。

好的,我们来详细讲解一下这个 Linux 内核补丁。

具体实现#

主要分为两部分:

  • KVM 核心通用逻辑修改:添加启用 KVM_DIRTY_LOG_INITIALLY_SET 脏页为 1 的逻辑
  • x86 架构相关:添加启用 KVM_DIRTY_LOG_INITIALLY_SET 时不进行写保护的逻辑。

下面逐个分析每个文件的修改内容:

  1. include/uapi/linux/kvm.h (用户空间API头文件)
  • 作用: 定义 KVM 与用户空间程序(如 QEMU)交互的接口。

  • 改动:

    • 增加了两个新的宏定义:

      • #define KVM_DIRTY_LOG_MANUAL_PROTECT_ENABLE (1 << 0)

      • #define KVM_DIRTY_LOG_INITIALLY_SET (1 << 1)

    • 解读: 这定义了两个新的功能标志位。KVM_DIRTY_LOG_INITIALLY_SET 就是开启我们上面提到的“初始全脏”优化功能的开关。用户空间程序可以通过 KVM_ENABLE_CAP 这个 ioctl 命令来设置这些标志,从而启用新特性。

  1. Documentation/virt/kvm/api.rst (API 文档)
  • 作用: KVM API 的官方文档。

  • 改动:

    • 更新了 KVM_CAP_MANUAL_DIRTY_LOG_PROTECT2 能力的说明。

    • 解释了新增的 KVM_DIRTY_LOG_INITIALLY_SET 标志的作用:创建脏页位图时,所有位会被初始化为1,并且它依赖于 KVM_DIRTY_LOG_MANUAL_PROTECT_ENABLE 必须同时被设置。

    • 解读: 确保内核开发者和用户空间开发者能够理解这个新功能的用法和限制。

  1. virt/kvm/kvm_main.c (KVM 核心通用逻辑)
  • 作用: 包含 KVM 的核心、与体系结构无关的代码。

  • 改动:

    • __kvm_set_memory_region 函数中,当创建脏页位图(dirty_bitmap)后,增加了一个判断:如果 KVM_DIRTY_LOG_INITIALLY_SET 功能被启用,就调用 bitmap_set() 将整个位图的所有位都设置为 1

    • kvm_vm_ioctl_check_extension_generic 函数中,声明 KVM 支持新的能力组合。

    • kvm_vm_ioctl_enable_cap_generic 函数中,添加了处理 KVM_CAP_MANUAL_DIRTY_LOG_PROTECT2 的逻辑,允许用户空间设置新的标志位,并校验了标志位的合法性(例如 INITIALLY_SET 不能单独设置)。

    • 解读: 这是实现“步骤1:立即假装所有页都是脏的”的核心代码。同时,这里也完成了新功能的注册和启用逻辑。

  1. include/linux/kvm_host.h (KVM Host 内核头文件)
  • 作用: 定义 KVM 内核部分使用的数据结构和内联函数。

  • 改动:

    • kvm 结构体中的 manual_dirty_log_protect 成员从 bool 类型改为 u64 类型。因为之前它只是一个开关,现在需要存储多个标志位。

    • 增加了一个内联辅助函数 kvm_dirty_log_manual_protect_and_init_set(),用来方便地检查新的优化功能是否被启用。

    • 解读: 适应新功能需要,修改了数据结构并提供了便捷的检查函数,使代码更清晰。

  1. arch/x86/include/asm/kvm_host.h (x86 架构相关的头文件)
  • 作用: 定义 x86 架构特有的 KVM 数据结构和函数原型。

  • 改动:

    • 修改了 kvm_mmu_slot_remove_write_access 函数的声明,增加了一个 start_level 参数。

    • 解读: 这是个关键改动。这个函数的作用是移除内存区域的写权限(即写保护)。通过增加 start_level 参数,调用者可以指定从哪个页表级别(比如巨页级别或小页级别)开始进行写保护,而不是像以前一样总是处理所有级别的页。

  1. arch/x86/kvm/mmu/mmu.c (x86 内存管理单元)
  • 作用: 实现 x86 架构的影子页表等内存虚拟化核心逻辑。

  • 改动:

    • 相应地修改了 kvm_mmu_slot_remove_write_access 函数的实现,使其能够根据传入的 start_level 参数,只对特定级别及以上的页表进行写保护操作。

    • 解读: 这是对上一条头文件改动的具体实现,让写保护操作变得更加灵活可控。

  1. arch/x86/kvm/x86.c (x86 架构通用逻辑)
  • 作用: x86 KVM 的主要实现文件。

  • 改动:

    • kvm_mmu_slot_apply_flags 函数中,当需要启用脏页日志时,它会检查新功能是否开启。

    • 如果新功能开启 (kvm_dirty_log_manual_protect_and_init_set 返回真),它将调用 kvm_mmu_slot_remove_write_access 并传入 PT_DIRECTORY_LEVEL,这表示只写保护巨页。

    • 如果新功能未开启(旧逻辑),它传入 PT_PAGE_TABLE_LEVEL,表示写保护所有页,包括4KB的小页。

    • 解读: 这是实现“步骤2:只写保护巨页”的核心调度代码。它根据功能开关,决定了写保护的粒度。

  1. arch/x86/kvm/vmx/vmx.c (Intel VMX 特定代码)
  • 作用: 包含 Intel 虚拟化技术(VMX)相关的特定实现。

  • 改动:

    • vmx_slot_enable_log_dirty 函数中,对 kvm_mmu_slot_leaf_clear_dirty 的调用增加了一个条件判断。只有在新功能未开启时,才执行这个操作。

    • 解读: 这主要是为了兼容 Intel 的 PML(Page-Modification Logging)脏页日志机制。确保在使用 PML 时,如果启用了“初始全脏”模式,不要去清除叶子页表项的脏位,从而保持逻辑一致性。

arm64#

[PATCH v2] KVM/arm64: Support enabling dirty log gradually in small chunks

由于 x86 中已经实现”KVM 核心通用逻辑修改:添加启用 KVM_DIRTY_LOG_INITIALLY_SET 脏页为 1 的逻辑”,所以不需要太多的修改。只需要在 arm64 初始化脏页位图的时候添加若开启 KVM_DIRTY_LOG_INITIALLY_SET 则初始化为 1 的判断即可。

总结#

若要在 riscv 中实现,也只需要在 riscv 初始化脏页位图的时候添加若开启 KVM_DIRTY_LOG_INITIALLY_SET 则初始化为 1 的判断即可。