虚拟化:传统热迁移
热迁移(Live Migration)的本质是:在虚拟机运行过程中,将 VM 状态从源主机传输到目标主机,且中断时间可控。要做到这一点,必须先知道 VM 的哪些内存页在迁移周期内被修改过——这就是 脏页跟踪(Dirty Page Tracking)。
本文梳理两条独立的脏页跟踪路径:CPU 侧(KVM MMU)和 设备侧(VFIO/SMMU),以及它们如何共同跳过 MMIO BAR。
一、CPU 物理地址空间的构成
1 | +----------------------+ |
物理地址空间大致分为两类: - Memory(DRAM):可缓存、有 writeback 语义、可参与 dirty tracking - Device(MMIO、ACPI、GIC):不可缓存、不可speculative、无 dirty 语义
二、传统热迁移的脏页跟踪机制
2.1 跟踪对象:Guest RAM Pages
热迁移中,跟踪的是 guest RAM pages 的变化。
Dirty tracking 依赖 CPU 页表的硬件语义: - Writable PTE:CPU 可写 - Dirty Bit:CPU 写操作后硬件自动置位 - Writeback Cache:写入后最终落盘
满足这些条件的内存才能被 dirty logging / writable fault 跟踪。
2.2 为什么 MMIO 不参与跟踪
MMIO 在 ARM64 对应 DEVICE_nGnRE,在 x86 对应 UC/WC。其页表属性决定了:
| 属性 | MMIO 的表现 | 对 dirty tracking 的影响 |
|---|---|---|
| 不可缓存 | 写操作直接到设备 | 无 writeback,无法感知 "脏" |
| 不可 speculative | CPU 不会推测性访问 | 无 dirty bit 语义 |
| 无 side effect | 每次访问可能触发硬件行为 | 不能随意重复访问 |
| 不 coherent | 不参与缓存一致性协议 | 无法与其他 agent 协调 |
结论:MMIO BAR 的本质不是 "内存",而是 "设备行为接口"(Device Transaction Interface)。它是 CPU 与 PCIe 设备之间的控制/数据交互窗口,不能当普通 RAM 使用。
三、VFIO 设备的热迁移
VFIO 直通设备涉及三类概念:
| 概念 | 由谁管理 | 在热迁移中的角色 |
|---|---|---|
| BAR mmap | MMU(stage-2) | 不能跟踪(页表属性决定) |
| DMA Buffer | SMMU/IOMMU | 跟踪 device DMA 写入 |
| PCI Config Space | PCI 总线 | 不跟踪 |
vfio bar mmap区域
vfio bar mmap区域不应该热迁移中进行脏页跟踪,因为前一后换了一套设备,要重新建立mmap映射。
不跟踪的防护手段:qemu向kvm注册内存区域时,会通过KVM_SET_USER_MEMORY_REGION,里面有一个KVM_MEM_LOG_DIRTY_PAGES,普通ram会带上这个,从而阻止热迁移的脏页跟踪。而qemu根本不给这些页面加这个标签
1 | 1. VFIO BAR mmap 子区域创建 |
总的来说,由于qemu对vfio bar进行mmap的时候,flags 不带 RAM_MIGRATABLE,导致热迁移中不会为它设置KVM_MEM_LOG_DIRTY_PAGES的flags,从而内核不会对它进行跟踪。
ACC 加速器设备完整的 VFIO BAR mmap流程如下:
1 | 完整调用链 (从 QEMU 启动 ACC VFIO 设备开始) |
VFIO设备的内部状态
VFIO设备的内部状态,存在设备自己的硬件寄存器或内部RAM中,QEMU必须通过特殊的VFIO通道将其”抽取”出来,序列化后作为一段特殊的数据流传输。所以这些也不会被迁移。
VFIO在热迁移中,靠有限状态机来跟踪迁移状态
include/uapi/linux/vfio.h:1238-1248 定义了 8
种设备状态:
| 状态 | 含义 |
|---|---|
| ERROR | 错误状态 |
| STOP | 设备停止,不工作也不传输数据 |
| RUNNING | 设备正常运行 |
| STOP_COPY | 设备停止,正在传输最终状态数据 |
| RESUMING | 目标端正在恢复设备状态 |
| RUNNING_P2P | 运行中 + P2P(peer-to-peer) |
| PRE_COPY | 预拷贝阶段,设备运行中,可增量传输数据 |
| PRE_COPY_P2P | 预拷贝 + P2P |
内核维护一个静态的二维状态转换表 vfio_from_fsm_table[8][8],定义每个 (当前状态,目标状态) 对的下一步骤。根据设备支持的 migration_flags(设备迁移能力位掩码:STOP_COPY / P2P / PRE_COPY),会自动跳过不支持的状态节点。
例如:STOP → RUNNING 会先经过 RUNNING_P2P(如果设备支持 P2P),让 QEMU 每次 ioctl 时逐步推进。
完整的迁移状态转换流程:
其中,内核侧的驱动的 mig_state 结构体以及迁移数据文件和kzalloc出来的数据,本质上是host侧的内核堆内存,属于 host kernel RAM,不会被“KVM guest dirty page tracking” 跟踪。他们的迁移是靠 VFIO 中 vendor migration callback进行打包,比如通过 vfio_pci_save_state()。
而QEMU侧整个VFIOMigration 整个结构体由 glib 堆分配,是本地缓存,也不会被跟踪。
总而言之,QEMU 用 memory_region_init_ram() 注册的才是 guest RAM,才可能被迁移和跟踪。不在 ram_list 链表中,KVM dirty log 根本看不到它们。
被设备驱动”钉住”(pinned)的内存页
cpu真实会访问到的页除了mmap的外,还有:被设备驱动“钉住”(pinned)的内存页。它们是设备与虚拟机内存进行数据交换的“窗口”和“契约”,其内容在迁移时必须被完整传输,以保证设备恢复后能找到正确的数据。比如描述符环(Descriptor Ring)
pre-copy-dirty-page-tracking=off 就是 QEMU 在预拷贝阶段对设备脏页的彻底封杀。表示预拷贝阶段完全不跟踪,只有停机后再一次性把这些东西迁移过去。
它的防护手段是,在qemu发起内存页进行ro防护时,qemu会自动过滤掉这些页,根本不会发给kvm。
1 | flowchart TD |
3.1 VFIO 的 dirty tracking 路径
VFIO 跟踪的是 device DMA 写入了哪些 guest RAM pages,这些由 SMMU 管理,而非 MMU。
1 | CPU Device (DMA) |
两条路径互相独立,各自覆盖各自的场景。
3.2 MMIO BAR 为什么不能被跟踪
MMIO BAR 的 VMA 类型是 VM_PFNMAP,其物理地址在 memblock
中是 "reserved",不在 "memory" 中。这导致:
- QEMU 注册时不带
KVM_MEM_LOG_DIRTY_PAGES标志 → dirty_bitmap 为 NULL kvm_is_device_pfn()返回 true → 即使触发 fault,也被标记为 device memory
四、KVM MMU 侧:MMIO BAR 如何跳过脏页跟踪
4.1 核心入口:user_mem_abort()
当 guest 第一次访问 MMIO BAR 触发 stage-2 fault 时进入(arch/arm64/kvm/mmu.c:1476)。
4.2 两层判断决定是否跟踪
第一层:logging_active — 脏页日志是否对该 memslot 启用
1 | bool logging_active = memslot_is_logging(memslot); |
1 | static bool memslot_is_logging(struct kvm_memory_slot *memslot) |
关键事实:QEMU 注册 MMIO BAR 为 KVM memslot
时,从来不带 KVM_MEM_LOG_DIRTY_PAGES 标志。因此:
- MMIO BAR 所在 memslot 的
dirty_bitmap == NULL memslot_is_logging()返回falselogging_active = false
代码注释(mmu.c:1544-1547)明确写了: 1
2
3
4/*
* logging_active is guaranteed to never be true for VM_PFNMAP
* memslots.
*/
结论:脏页日志开启时,只有 RAM memslot 的 stage-2 PTE 会被 write-protect。MMIO BAR 的 stage-2 PTE 保持 writable,guest 写 MMIO 不会触发 permission fault。
第二层:kvm_is_device_pfn() — PFN 是不是设备内存
即使 guest 第一次访问 MMIO BAR(初始 mapping fault),也会进入
user_mem_abort():
1 | pfn = __gfn_to_pfn_memslot(memslot, gfn, false, false, NULL, |
1 | static bool kvm_is_device_pfn(unsigned long pfn) |
pfn_is_map_memory() 问的是:这个物理地址在 memblock
里是不是 "memory"(普通 RAM)?MMIO BAR 的物理地址在 memblock 中是
"reserved",不在 "memory" 里,所以返回 false →
kvm_is_device_pfn() 返回 true →
device = true。
4.3 device = true 触发的五种特殊处理
| 处理 | 代码位置 | 说明 |
|---|---|---|
| 不设 DBM | mmu.c:1692 | 传统迁移和 HDBSS 都不会对 MMIO 做硬件脏页跟踪 |
| 设 Device 属性 | mmu.c:1698-1699 | MAIR 设为 Device nGnRE(非 Gathering、非 Reorder、Early Write Ack) |
| 禁止 THP | mmu.c:1657 | 永远按 base page 粒度映射 |
| 禁止执行 | mmu.c:1633 | MMIO 不可执行 |
| 禁止 MTE | mmu.c:1672 | 内存标签扩展不适用于设备内存 |
1. 不设 DBM(Dirty Bit Management)
1 | if (!device && writable && kvm->arch.enable_hdbss && logging_active) |
!device 为 false → DBM 永远不会加到 MMIO 的
stage-2 PTE 上。
debug 检查: 1
2
3
4if (device && writable && kvm->arch.enable_hdbss && logging_active) {
pr_info_ratelimited("KVM_HDBSS_DBG: BUG detected! "
"DBM would be added to MMIO ...");
}
2. 设置 Device 内存属性
1 | if (device) |
Stage-2 PTE 的 MAIR 字段被设为 Device nGnRE,而不是 Normal Cacheable。设备寄存器不能被 speculative access、不能合并、不能缓存。
3. 禁止 THP
1 | if (vma_pagesize == PAGE_SIZE && !(force_pte || device)) { |
device memory 不参与 transparent hugepage 调整,永远按 base page 粒度映射。
4. 禁止执行
1 | if (exec_fault && device) |
5. 禁止 MTE
1 | if (fault_status != ESR_ELx_FSC_PERM && !device && kvm_has_mte(kvm)) { |
4.4 mark_page_dirty_in_slot() 是 no-op
代码在 mmu.c:1725-1728 无条件调用了: 1
2
3
4if (writable && !ret) {
kvm_set_pfn_dirty(pfn);
mark_page_dirty_in_slot(kvm, memslot, gfn);
}
但进入 mark_page_dirty_in_slot()(kvm_main.c:3450):
1
2
3if (memslot && kvm_slot_dirty_track_enabled(memslot)) {
// set_bit_le
}
kvm_slot_dirty_track_enabled(): 1
2
3
4static inline bool kvm_slot_dirty_track_enabled(const struct kvm_memory_slot *slot)
{
return slot->flags & KVM_MEM_LOG_DIRTY_PAGES;
}
MMIO BAR 的 memslot 没有 KVM_MEM_LOG_DIRTY_PAGES
标志 → 直接返回,set_bit_le 永远不会被执行。
这是一个双重保险:即使前面的
logging_active 和 device 检查都漏了,最终的
bitmap 写入也会被挡住。
五、完整代码链总结
1 | Guest 第一次访问 MMIO BAR |
六、哪些页会被标成 DEVICE
PCIe MMIO BAR: - ACC accelerator BAR - GPU BAR - NIC BAR - NVMe BAR
来源: 1
2pci_iomap()
remap_pfn_range()
PCI ECAM / Config Space
ECAM window,例如 0x40000000+
GIC/ITS 页
1 | GICD |
七、对比:SMMU 路径 vs KVM MMU 路径
| SMMU/IOMMU 路径 | KVM MMU 路径 | |
|---|---|---|
| 跟踪对象 | Device DMA 写 IOVA | CPU 写 GPA |
| 谁写 | 设备 | Guest CPU |
| 跟踪什么 | DMA buffer(guest RAM) | 普通 RAM |
| 核心函数 | vfio_sync_dirty_bitmap() | user_mem_abort() |
| MMIO BAR 如何跳过 | vfio_listener_skipped_section() | kvm_is_device_pfn() + logging_active=false |
| 关键标志 | !(is_iommuMapped && is_ram) | memslot->dirty_bitmap == NULL |
八、MMIO BAR 的本质总结
| 类别 | BAR中内容 | 典型用途 | 是否高频访问 | 是否适合 migration dirty tracking |
|---|---|---|---|---|
| 控制寄存器 | control/status register | 启停设备、reset、enable DMA | 低频 | 否 |
| Doorbell寄存器 | queue doorbell | 通知硬件有新任务 | 极高频 | 否 |
| Queue/Descriptor Window | SQ/CQ寄存器 | 提交/完成队列管理 | 高频 | 否 |
| ... | ... | ... | ... | ... |
MMIO BAR 不具有 normal memory semantics: - 不能当普通 RAM 使用 - 不能 cache/writeback - 可能有 side effect - 可能触发硬件行为
在 user_mem_abort 中会被标志为
device,从而跳过所有脏页跟踪机制。