virt-report虚拟化社区动态

KVM 脏页日志:位图与 Dirty Ring

本文由 OpenAI Codex (GPT-6-Astra) 辅助编写。

一、脏页记录与并发写入跟踪

预拷贝迁移需要反复收集客户机修改过的内存页。KVM 负责记录客户机执行期间产生的脏页,QEMU 将这些记录汇入迁移位图,随后读取和传输相应页面。记录的交接顺序决定了并发写入能否在下一轮被发现。

本文分析 KVM 位图接口和 Dirty Ring 的记录、收割与重新保护过程,始终跟踪 P3 这一页:客户机把内容从 v1 改为 v2,迁移器必须发出 v2,或者留下再次发送 P3 的记录。硬件缓冲以 x86 PML 为例,接口演进部分再说明跨架构内存顺序与补充位图。

源码版本:QEMU 10.2.0、Linux 6.18。

内存迭代和停机切换见《QEMU 预拷贝迁移的内存迭代与停机控制》。

二、记录、重新保护与读页的顺序

写脏跟踪围绕客户机物理页进行。KVM 内存槽(memslot)定义一段客户机物理内存及其宿主映射,注册与建图过程见内存映射与 EPT 缺页处理。启用 KVM_MEM_LOG_DIRTY_PAGES 后,KVM 为该内存区域记录写入。一个脏页标记只表达“此页需要重新读取”,不携带页内容,也不记录客户机写过哪些字节。

一个页面可以被连续修改许多次。采用写保护跟踪时,首次写入触发记录并放行后续写入;采用硬件脏位或 PML 时,记录的产生还受硬件脏状态影响。迁移关心当前页内容,因此重复记录可以合并。重新保护或重置硬件脏状态后,下一次写入需要再次进入脏页日志。

  1. 客户机写入KVM 记录内存槽和页偏移,客户机页内容发生变化。
  2. QEMU 收割收取地址记录,暂存在对应 KVMSlot 的位图中。
  3. 重新保护KVM 完成重置,使后续写入能够再次被记录。
  4. 迁移读取合并到迁移待发送集合,读取页内容并传输。

重新保护必须先于迁移读取。这样,读页之前发生的写入可以被本次读取覆盖;读页之后发生的写入则会留下新记录,供后续迭代处理。运行中的客户机仍可能在读页过程中修改内存,最终一致性由后续迭代和停机阶段共同完成。

三、位图接口:按内存槽收集脏页

KVM_GET_DIRTY_LOG 按 memslot 返回位图,每个比特对应一个页偏移。例如槽中 P1、P3、P6 被修改,位图按 P0 至 P7 展示为 0 1 0 1 0 0 1 0。它可以直接合并同页的重复写入,也方便按内存地址扫描。

位图长度由内存槽大小决定。以 1 TiB 内存、4 KiB 页为例,一份位图为 32 MiB;即使本次只修改少数页面,传统接口仍需交接对应槽的位图。内存规模大、写入较稀疏时,处理整张位图的开销会更加明显。这只是单份位图的大小,QEMU 与内核还可能维护其他用途的位图。

反过来,如果同一批页面被密集重复修改,位图天然把重复事件合并成一位;环形日志仍要消费已发布的地址记录。选择接口时应比较内存规模、记录密度与收割成本,不能把“环”本身看作所有负载都更快的保证。

收取与清除的两种方式

传统 KVM_GET_DIRTY_LOG 路径在返回前清除所收取的脏位,并完成相应的跟踪重置。启用 KVM_CAP_MANUAL_DIRTY_LOG_PROTECT2 后,收取和重新保护分开:用户态先获得位图,再通过 KVM_CLEAR_DIRTY_LOG 处理选定范围。QEMU 可以据此安排清除与读页,减少过早重新保护造成的重复记录。手动保护接口

两种方式都需要维护“重新保护在前、读取页面在后”的顺序。Dirty Ring 改变了内核向用户态交付地址记录的形式,这条顺序仍然适用。

四、Dirty Ring:按 vCPU 交付地址记录

Dirty Ring 为每个 vCPU 分配一个可映射到宿主用户态的环形数组,由宿主内核中的 KVM 写入,QEMU 收割线程或 vCPU 线程消费;客户机不访问这个环。条目由 flags、slot 和 offset 组成:slot 标识地址空间及内存槽,offset 是槽内页偏移。

kvm_dirty_gfn 字段宽度内容与更新者
flags32 位KVM 发布 DIRTY,QEMU 收割后写 RESET,KVM 回收为 0
slot32 位KVM 写入地址空间 ID 与 memslot ID;它与环数组下标无关
offset64 位KVM 写入槽内页偏移,单位是页,不是字节

例如环位 0 中的 {flags: DIRTY, slot: 0, offset: 3}表示内存槽 0 的第 3 页。若槽起始 GPA 为 0、页大小为 4 KiB,对应 GPA 为 0x3000。条目仅记录地址,页面内容由迁移线程之后读取。共享条目定义

空闲 · 00KVM 可以填入 slot 和 offset
DIRTY · 01地址记录已发布,等待收割
RESET · 1XQEMU 已收割,等待内核重置
空闲 · 00条目回收;读页仍须等重置调用返回

QEMU 的 kvm_fetch_index 跟踪消费位置,KVM 的 dirty_index 与 reset_index 分别跟踪生产和回收位置。下标递增后按环容量取模,物理槽位可以循环使用。QEMU 必须连续收割,不能跳过前面的 DIRTY 条目;置为 RESET 只表示用户态已处理该地址,尚未完成页的重新保护。

下面先观察单页写入,再逐步执行收割、重置和迁移读取。可以在任意两步之间再次写页,检查新内容由本次读取覆盖,还是进入后续脏页记录。

脏页记录与重新保护

两个 vCPU 共用 8 页内存,第一轮复制已完成。写入后依次收割、重置、发布并读取页面,观察源端与目的端内容版本。

  1. vCPU 0 写 P3,KVM 向它的环中发布 slot = 0, offset = 3, DIRTY。
  2. QEMU 收割该记录,设置 KVMSlot 位图中的 P3,并将环条目标记为 RESET。
  3. KVM_RESET_DIRTY_RINGS 完成重新保护,条目可回收。
  4. QEMU 合并迁移待发送位图,随后读取并传送 P3。
  5. 重新保护后的再次写入会产生新记录,后续同步仍能发现该页。

启用 JavaScript 后可交错执行客户机写入与收割过程,也可按“示例下一步”观察完整顺序。

示例每页使用一个共享映射,已放行写入在重新保护前不再产生新条目。重置调用按完成的一步展示,发布操作合并 QEMU 全局位图与 RAMBlock 位图同步。硬件缓存及其预留槽位未展开,2 / 4 / 8 个槽位仅用于观察环的回绕和等待,不是实际配置值。页内容版本表示写入次数,逻辑步骤不代表耗时。

环满时的处理

环容量有限,未回收的条目不能被覆盖。Linux 使用 soft limit 提前触发 KVM_EXIT_DIRTY_RING_FULL,预留部分槽位接收硬件缓冲中的剩余记录。对应 vCPU 返回用户态,QEMU 收割并重置后才能继续运行。增大环容量可以减少满环退出,但会增加每个 vCPU 的共享内存占用,也需要及时处理累积记录。写入与满环退出

QEMU 还有周期性收割线程,并在全局脏页同步时主动收割。后台收割、满环处理和迁移同步可能从不同上下文进入,因此它们需要共同遵守锁和内存顺序约束。

五、收割、重置与并发写入

1. 先重置保护,再交给迁移线程

假设 P3 已被记录并允许继续写入。QEMU 收割 P3 后,如果立即让迁移线程读到内容 v1,然后客户机把它改成 v2,直到最后才重新保护,那么 v2 发生在尚未重新保护的间隙,可能不会留下新记录。本轮已发送 v1,下一轮又没有 P3,更新就会遗漏。

Dirty Ring 条目与脏页的交接顺序
flags 归零表示环位可回收;重新保护完成由 RESET ioctl 返回确定。QEMU 在这条边界之后才把页交给迁移读取路径。
flags 归零表示环位可回收;重新保护完成由 RESET ioctl 返回确定。QEMU 在这条边界之后才把页交给迁移读取路径。 打开原图
Mermaid 源码
sequenceDiagram
    accTitle: Dirty Ring 条目与脏页的交接顺序
    accDescr: KVM 先写入 slot 和 offset,再发布 DIRTY。QEMU 持有 slots 锁收割地址、设置槽位图和 RESET。内核回收环位并完成页跟踪重置,ioctl 返回后 QEMU 才允许迁移路径读取所收割的页面。
    participant K as KVM
    participant Q as QEMU 收割路径
    participant M as 迁移读取路径
    Note over K,Q: flags、slot、offset 通过共享环交接
    K->>K: 空闲环位 flags = 0<br/>写 slot、offset
    K->>K: 发布 flags = DIRTY
    Q->>Q: 获取 slots 锁
    Q->>K: acquire 读取 DIRTY,取得地址
    Q->>Q: 设置 KVMSlot.dirty_bmap
    Q->>K: release 写 flags = RESET
    Note over Q,M: 锁内暂存记录;尚不允许读页
    Q->>K: KVM_RESET_DIRTY_RINGS
    K->>K: acquire 读取 RESET<br/>回收环位 flags = 0
    Note over K: 环位归零时,整批重置可能仍未结束
    K->>K: 完成整批页跟踪重置<br/>及相应 MMU 处理
    K-->>Q: ioctl 返回
    Q->>Q: 释放 slots 锁
    Q-->>M: 后续同步汇入迁移待发送位图
    M->>M: 读取页内容并发送
    Note over K,M: 重置后再写该页会产生新记录<br/>本次读取未覆盖的写入由后续迭代处理

kvm_dirty_ring_reap() 持有 slots 锁,先将记录合并到 KVMSlot.dirty_bmap,再调用 KVM_RESET_DIRTY_RINGS。锁保护了这段临时状态,防止其他线程在重置完成前通过位图读取页面。实际边界是重置 ioctl 返回;观察到某个环槽的 flags 已归零,还不足以判断整批重新保护完成。QEMU 收割与重置

2. 条目发布的内存顺序

KVM 先填入 slot、offset,再用内存屏障保证它们先于 DIRTY 标记可见。QEMU 以 acquire 语义读取 flags,确认条目有效后才读取地址;收割完成后以 release 语义发布 RESET。内核处理重置时再以 acquire 语义读取该标记。这个交接避免了“看见新状态,却读到旧地址”的重排问题。QEMU acquire / release、KVM 发布记录

3. 硬件缓冲与全局同步

环已经读空时,硬件中仍可能留有尚未提交的记录,例如 x86 PML 缓冲。因此,QEMU 的 kvm_dirty_ring_flush() 先同步 kick 所有 vCPU,保证它们至少退出到用户态一次,让硬件记录进入环,然后再收割。单纯扫描一次用户态可见的环,无法替代这次完整同步。同步退出与 flush

全局同步也没有冻结客户机未来的写入。同步之后产生的新脏页继续进入日志;停机阶段则在 vCPU 和相关设备停止产生修改后完成最后的状态汇总。

六、源码:从环条目到迁移位图

环节入口与作用
记录脏页mark_page_dirty_in_slot():根据运行中的 vCPU 和能力选择环或补充位图。
发布环条目kvm_dirty_ring_push():写入地址、发布 DIRTY、推进生产位置并检查 soft limit。
收割记录kvm_dirty_ring_reap_one():按序读地址、设置槽位图、标记 RESET。
重新保护kvm_dirty_ring_reset():按页范围合并重置操作,调用架构相关 MMU 处理。
汇入 QEMU 位图kvm_log_sync_global():flush 后同步槽位图,按阶段补收 backup bitmap。
合并迁移集合migration_bitmap_sync():同步脏页来源,再合并进 RAMBlock 待发送位图。

启用 Dirty Ring 后,QEMU 仍使用位图。KVMSlot.dirty_bmap 用来合并环中的地址记录,QEMU 全局脏页位图承接各来源的修改,RAMBlock.bmap 则维护迁移待发送集合。同一页在不同收割周期再次出现时,位图可以去重;它们分别承担内核交接、统一汇总和迁移调度的职责。

设备模拟、vhost 和直通设备也可能修改客户机内存,需要各自的脏页记录与同步路径。仅收取 vCPU 的 Dirty Ring,不能覆盖整台虚拟机的全部写入。

回到 P3 的例子,完整条件是:其地址已经交给 QEMU、KVM 已重新允许跟踪下一次修改、迁移线程随后读取页面。只做到“把环读空”还缺少重新保护;只做到“把页读完”也不能保证之后的修改进入日志。Dirty Ring 改善地址记录的交付效率,正确性仍由这一整段交接顺序保证。

七、接口演进与代价

1. 从整槽位图到地址记录

位图适合按页索引和集合运算;Dirty Ring 让内核与用户态可以按新增地址记录交接,并将生产位置分散到各 vCPU。QEMU 在 2021 年加入 Dirty Ring 支持。对于大内存、稀疏写入负载,这种接口减少了反复交接整张内核位图的需求;它同时引入满环退出、周期收割和重置同步。QEMU 的迁移扫描及内部位图处理仍然存在,不能据此推断整个迁移过程都只与本轮脏页数成正比。Dirty Ring 支持提交

2. 内存顺序与 ARM 支持

QEMU 在 2022 年补充 flags 读取的内存屏障,保证地址字段不会先于有效标记读取。2023 年的 ARM 支持使用 KVM_CAP_DIRTY_LOG_RING_ACQ_REL,并处理 KVM_CAP_DIRTY_LOG_RING_WITH_BITMAP。后者允许在缺少 vCPU 环上下文时仍通过内核位图记录修改,例如保存 GIC / ITS 状态时写入客户机内存。读取屏障修正、ARM 支持提交

补充位图要在最后相关状态写入完成后收取。QEMU 通过全局同步的 last_stage 参数安排这一步,避免在收取之后又产生未覆盖的修改。是否启用这些能力需要在运行时探测,不能只依据客户机架构或版本号判断。

3. 脏页跟踪开销的观测

kvm_dirty_ring_full、kvm_dirty_ring_reap 和 kvm_dirty_ring_flush 是宿主 QEMU 的 trace 事件。可在目标 QEMU 的 HMP monitor 中用 info trace-events 查看事件与开关,再按所选 trace backend 收集输出;它们不属于 Linux 内核的 kvm:* 事件列表。reap 给出本次收割条目数和耗时,flush 标识全局同步的开始与结束。事件定义、QEMU tracing 接口

结合这些记录观察满环退出频率、收割量及同步耗时,再与客户机延迟、迁移脏页速率和总时长比较。同一页可以在多轮收割中重复出现,因此累计条目数不等于独立脏页数。更大的环占用更多共享内存,更频繁的收割消耗更多处理时间;写脏速率超过传输能力时,还需要带宽或写脏控制来帮助迁移收敛。

八、参考资料