QEMU 与 KVM 协作

本页目录

增加 IOThread 不保证所有设备操作并行,预拷贝多跑几轮也不保证脏页减少。QEMU 与 KVM 协作需要同时管理执行上下文、设备状态和迁移一致性⁠。完整后端还可能在其他进程中。

查阅目标阅读位置
机器与执行组织线程、创建、执行循环
通知与数据分工eventfd
收敛、切换与故障定位迁移相关章节

线程不是设备的固定一对一映射

vCPU 线程进入 KVM_RUN,主事件循环处理管理与相应设备事件;配置了 IOThread 的设备可以使用独立事件循环。并非每个 virtio 设备自动得到一个线程,线程池、后端和设备实现也会参与 I/O。IOThreads

多线程允许并行,也带来状态同步和执行上下文约束。QEMU 的全局锁、设备锁及 AioContext 等机制负责特定状态,不能仅通过“加 IOThread”推断所有设备操作都并行或无锁。

从创建到可执行机器

阶段用户空间负责什么KVM 提供什么
接口探测查询 API 与所需能力system fd、扩展查询
机器创建选择机器布局与 backingVM 对象与内存注册接口
CPU 初始化设置架构所需寄存器、特性与启动状态vCPU 对象、架构 ioctl
设备建立配置设备模型与后端可选内核设备、irqfd/ioeventfd 等
运行与退出处理处理需用户空间完成的操作guest 执行和可在内核完成的退出

仅调用 CREATE_VM、注册 RAM 和 CREATE_VCPU 不代表能启动操作系统:还缺 CPU 模式、启动代码或固件、中断控制器及设备等配置。教程中的简化流程必须与“完整启动器”区分。

mmap 创建 backing 后,物理页可能按需分配;MAP_NORESERVE 不保证之后内存永远足够。memslot 表达 guest 地址映射,VMM 仍要维护 backing 生命周期,变更或解除映射需与 KVM 协调。KVM API

执行循环的两个返回层次

硬件退出可以由 KVM 处理后直接重入 guest;KVM_RUN 返回才进入 VMM 的退出处理。返回错误时先处理 errno,成功返回再读取 run->exit_reason;不能把每次 ioctl 返回都解释成一个正常设备请求。

对于需要完成的 MMIO/PIO 读写,VMM 填回数据后再次进入 KVM,才完成相应 guest 操作。迁移前如果只保存寄存器而忽略尚未完成的退出,可能丢失状态边界。QEMU 已有架构与设备协作逻辑,不宜拿几行缺少错误处理的伪 C 函数当作其真实源码。

eventfd 减少通知绕路,不承载全部数据

ioeventfd 可以把匹配的 guest I/O 写入转换成 eventfd 通知;irqfd 可以让相应 eventfd 信号关联到 guest 中断注入。请求数据仍在 virtqueue 或设备协议描述的缓冲中,eventfd 不是每条完整请求内容。

内核 vhost 后端可据此减少 QEMU 用户空间处理数据请求的往返,但初始化、特性协商、设备状态和迁移控制仍需要 VMM。用户空间后端则有另一组共享内存和进程间协议。

预拷贝迁移与收敛

预拷贝迁移:保持源端运行,最后统一切换 准备目标与兼容状态 机器类型、CPU 特性、设备和存储路径 1 复制内存并跟踪修改 后续轮次发送仍被写脏的页 2 停止源端,完成最终状态传输 剩余脏页、CPU、设备与在途操作协调 3 目标恢复执行 确认切换结果,避免两个实例同时对外写入 4 脏页速率不保证逐轮下降;停机时间取决于工作负载与迁移策略。

源 VM 运行时继续修改内存,迁移器需要跟踪本轮之后仍变脏的页。写保护触发跟踪是一种方式,硬件脏位、dirty bitmap/ring 等也可能参与;不能把所有机制描述成“一开始把全部 RAM 设为只读”。

若持续写脏的速率接近或超过传输能处理的速率,反复预拷贝可能不收敛。新一轮脏页量不保证比上一轮少;带宽、压缩、工作负载节流、切换策略等共同影响结果。迁移框架

教学条件速率
有效复制能力1 GiB/s
工作负载持续重新写脏大范围页面约 1.5 GiB/s

此时不能等待“多拷几轮自然清零”。真实收敛还受重复写同一页、页粒度和窗口影响,不能把两个速率直接当精确预测公式。

从 8 GiB 到最终停机:逐轮算一次

设需要复制的内存 M=8 GiB,有效发送速率 B=2 GiB/s,每轮新产生、下一轮需要发送的不同脏页按 D=0.5 GiB/s 增长。假设速率固定、无压缩,暂不考虑页粒度取整与额外传输开销。这是按轮次聚合的模型,D 表示不同脏页折算的数据量,不是应用执行 store 的原始字节数。

本轮发送量为 R 时,时间是 R/B;这段时间产生的新一轮工作量是 min(M, D×R/B)。在未触及 M 上限时,收缩因子是 D/B=1/4。

运行中的轮次本轮发送 GiB本轮耗时 s下一轮待发 GiB
1842
2210.5
30.50.250.125
40.1250.06250.03125

若固定的 CPU/设备切换开销取 40 ms,第三轮后立即停止源端,需要 0.125/2 s + 40 ms = 102.5 ms。再跑第四轮后,剩余 0.03125 GiB,即 32 MiB,预计停机为 15.625 + 40 = 55.625 ms,才落入本例的 60 ms 预算。

from fractions import Fraction as F

def precopy(memory, bandwidth, dirty_rate, rounds):
    assert memory > 0 and bandwidth > 0 and dirty_rate >= 0 and rounds >= 0
    remaining = F(memory)
    rows = []
    for _ in range(rounds):
        seconds = remaining / bandwidth
        new_dirty = min(F(memory), dirty_rate * seconds)
        rows.append((remaining, seconds, new_dirty))
        remaining = new_dirty
    return rows, remaining

rows, remaining = precopy(F(8), F(2), F(1, 2), 4)
assert [r[2] for r in rows] == [F(2), F(1, 2), F(1, 8), F(1, 32)]
fixed_stop = F(40, 1000)
assert rows[2][2] / 2 + fixed_stop == F(1025, 10000)
assert remaining / 2 + fixed_stop == F(55625, 1000000) <= F(60, 1000)
for dirty in (F(2), F(3)):
    unstable, left = precopy(F(8), F(2), dirty, 4)
    assert left == 8 and all(row[2] == 8 for row in unstable)
assert precopy(F(8), F(2), F(0), 1)[1] == 0

当 D≥B,本模型每轮都重新得到全部 8 GiB,继续增加轮数没有缩短最终停机。实际负载的热页集合可能很小:同一页被写一万次,某个统计窗口里仍可能只需要发送一次。因而应结合不同脏页量、窗口和实际带宽判断;不能拿内存写带宽计数直接代入 D。QEMU 的 dirty-limit 等机制通过限制写脏负载帮助收敛,但代价包括源端工作负载受影响。QEMU Dirty limit

复制之后再清脏位,可能擦掉刚发生的新写入

考虑一页当前值为 0,脏位已置上。迁移器把 0 复制到目标后,guest 把源页改成 1,脏位仍为 1。若迁移器此时为了表示“这页复制完了”而无条件清脏位,就把新写入的证据一起擦掉;下一轮既看不到脏页,也不会补传值 1。

错误的清除时机:目标仍是 0,新写入却不再标脏 复制源页 0 目标得到 0 guest 写入 1 源页脏位仍为 1 迁移器清脏位 新写入证据被擦除 下轮跳过此页 目标遗漏值 1 需要先建立下一轮跟踪,再读取和发送本轮页面 图示单页的逻辑竞态;真实实现还要协调重新保护、脏位收集和 DMA。

正确协议需要建立明确的跟踪边界,让本轮所处理的旧脏状态与之后的新修改分开。下面用原子交换脏集合表达这个边界:取出本轮集合并开启下一轮跟踪,然后复制;复制期间发生的修改进入新的集合。它没有实现真实的 KVM 脏位、写保护恢复、内存屏障或 DMA 跟踪。

class Memory:
    def __init__(self):
        self.pages = {0: 0}
        self.dirty = {0}

    def write(self, page, value):
        self.pages[page] = value
        self.dirty.add(page)

    def take_epoch(self):
        old, self.dirty = self.dirty, set()
        return old  # 模型中此操作不可被写入打断

# 错误顺序:复制后的清除吞掉了新修改。
bad = Memory()
target = {0: bad.pages[0]}
bad.write(0, 1)
bad.dirty.clear()
assert not bad.dirty and target != bad.pages

# 正确边界:本轮复制之后的新修改留给下一轮。
source = Memory()
target = {}
for page in source.take_epoch():
    target[page] = source.pages[page]
source.write(0, 1)
assert source.dirty == {0} and target[0] == 0
for page in source.take_epoch():
    target[page] = source.pages[page]
assert target == source.pages and not source.dirty
for value in range(2, 102):
    source.write(0, value)
assert len(source.dirty) == 1  # 一百次写入,仍只有一个不同脏页

真实系统有 KVM、QEMU RAM 记账与迁移发送等多层跟踪,不能把模型里的 take_epoch() 直接替换成任意一个 clear ioctl。内核重新保护页面、硬件脏位收集和发送顺序需要共同保证“读取之后的修改不会丢失”。复制过程中页内容还可能变化;最终停止源端后重新同步剩余脏状态,才能形成最后的一致切换边界。

下面改变有效复制带宽 B 与脏页产生速率 D,再选择在哪轮结束预拷贝。比较剩余集合与停机复制时间,观察 D/B 接近 1 时为何难以继续缩小。

M 是总内存,M_n 是第 n 轮需复制的数据。模型假设恒定带宽与不同页面的有效脏化速率,并用总内存封顶;重复写同一页不能按每次写入字节无限累计。

正在呈现知识画面
再多复制几轮,剩余脏数据一定减少吗?

本轮复制需要 Mᵢ/B 时间,其间新脏数据近似 D×Mᵢ/B。D/B<1 时递减;接近或超过 1 时,继续预拷贝不保证收敛。

最后一刻要一致的不只是 RAM

切换时的状态为什么要一并协调
vCPU 寄存器、虚拟中断与定时器恢复正确执行位置与待处理事件
设备队列、后端与剩余 I/O保留尚未完成操作的边界
CPU 特性、机器类型、设备版本目标必须能恢复源状态
存储与网络访问权完成系统级切换,避免双端写入

因此迁移一致性不能只用“RAM 已复制完”判断。

postcopy 可在目标恢复后按需获取尚缺页面,改变了停机与缺页等待的权衡,也让切换后源/目标故障恢复更复杂。它不等于预拷贝的“最后一轮开快一点”,不应只比较单一 downtime 数字。

迁移成功前后必须明确哪个实例拥有对外写入权,避免分裂运行。直通设备能否迁移取决于设备状态与 DMA 跟踪能力,见 内存与设备虚拟化。

观察问题时先找所在层

CPU 高占用时区分 guest 有效执行、频繁退出与宿主调度等待;I/O 延迟时区分 guest 排队、通知、QEMU/后端执行和实际存储网络;迁移停滞时看脏页速率、有效带宽与设备阻塞。

/dev/kvm 可用性探测见 KVM 架构。进一步源码可从 QEMU accel/kvm/kvm-all.c、Linux virt/kvm 和对应 arch 目录进入,版本相关路径应以实际 checkout 为准。