增加 IOThread 不保证所有设备操作并行,预拷贝多跑几轮也不保证脏页减少。QEMU 与 KVM 协作需要同时管理执行上下文、设备状态和迁移一致性。完整后端还可能在其他进程中。
| 查阅目标 | 阅读位置 |
| 机器与执行组织 | 线程、创建、执行循环 |
| 通知与数据分工 | eventfd |
| 收敛、切换与故障定位 | 迁移相关章节 |
线程不是设备的固定一对一映射
vCPU 线程进入 KVM_RUN,主事件循环处理管理与相应设备事件;配置了 IOThread 的设备可以使用独立事件循环。并非每个 virtio 设备自动得到一个线程,线程池、后端和设备实现也会参与 I/O。IOThreads
多线程允许并行,也带来状态同步和执行上下文约束。QEMU 的全局锁、设备锁及 AioContext 等机制负责特定状态,不能仅通过“加 IOThread”推断所有设备操作都并行或无锁。
从创建到可执行机器
| 阶段 | 用户空间负责什么 | KVM 提供什么 |
| 接口探测 | 查询 API 与所需能力 | system fd、扩展查询 |
| 机器创建 | 选择机器布局与 backing | VM 对象与内存注册接口 |
| 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。用户空间后端则有另一组共享内存和进程间协议。
预拷贝迁移与收敛
源 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 |
| 1 | 8 | 4 | 2 |
| 2 | 2 | 1 | 0.5 |
| 3 | 0.5 | 0.25 | 0.125 |
| 4 | 0.125 | 0.0625 | 0.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。
正确协议需要建立明确的跟踪边界,让本轮所处理的旧脏状态与之后的新修改分开。下面用原子交换脏集合表达这个边界:取出本轮集合并开启下一轮跟踪,然后复制;复制期间发生的修改进入新的集合。它没有实现真实的 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 时为何难以继续缩小。
Mn+1=min(M,BDMn),Tstop≈BMn+1+Tfixed
M 是总内存,M_n 是第 n 轮需复制的数据。模型假设恒定带宽与不同页面的有效脏化速率,并用总内存封顶;重复写同一页不能按每次写入字节无限累计。
正在呈现知识画面
再多复制几轮,剩余脏数据一定减少吗?本轮复制需要 Mᵢ/B 时间,其间新脏数据近似 D×Mᵢ/B。D/B<1 时递减;接近或超过 1 时,继续预拷贝不保证收敛。
{"id":"migration-dirty-rate","title":"再多复制几轮,剩余脏数据一定减少吗?","summary":"本轮复制需要 Mᵢ/B 时间,其间新脏数据近似 D×Mᵢ/B。D/B<1 时递减;接近或超过 1 时,继续预拷贝不保证收敛。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"bandwidth\" data-i18n=\"bandwidth\"></label><output for=\"bandwidth\" id=\"bandwidth-value\"></output><input id=\"bandwidth\" type=\"range\" min=\"100\" max=\"1000\" value=\"500\" step=\"10\"></div><div class=\"control\"><label for=\"dirty\" data-i18n=\"dirty\"></label><output for=\"dirty\" id=\"dirty-value\"></output><input id=\"dirty\" type=\"range\" min=\"0\" max=\"1200\" value=\"250\" step=\"10\"></div><p class=\"caption\" data-i18n=\"rounds\"></p><div class=\"rounds\"><button data-round=\"0\"><span>1</span><div><i id=\"round-0\"></i></div><strong id=\"size-0\"></strong></button><button data-round=\"1\"><span>2</span><div><i id=\"round-1\"></i></div><strong id=\"size-1\"></strong></button><button data-round=\"2\"><span>3</span><div><i id=\"round-2\"></i></div><strong id=\"size-2\"></strong></button><button data-round=\"3\"><span>4</span><div><i id=\"round-3\"></i></div><strong id=\"size-3\"></strong></button><button data-round=\"4\"><span>5</span><div><i id=\"round-4\"></i></div><strong id=\"size-4\"></strong></button><button data-round=\"5\"><span>6</span><div><i id=\"round-5\"></i></div><strong id=\"size-5\"></strong></button><button data-round=\"6\"><span>7</span><div><i id=\"round-6\"></i></div><strong id=\"size-6\"></strong></button><button data-round=\"7\"><span>8</span><div><i id=\"round-7\"></i></div><strong id=\"size-7\"></strong></button></div><div class=\"metrics\"><div><span data-i18n=\"remaining\"></span><strong id=\"remaining\"></strong></div><div><span data-i18n=\"downtime\"></span><strong id=\"downtime\"></strong></div></div><div class=\"formula\" id=\"formula\"></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"shrinking\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"stalled\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.rounds button{display:grid;grid-template-columns:20px minmax(0,1fr) 62px;align-items:center;gap:8px;width:100%;padding:10px 8px;margin:5px 0;font-size:12px}.rounds button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.rounds button>div{height:18px;background:var(--surface)}.rounds i{display:block;height:100%;background:var(--accent);transition:width .2s}.rounds strong{font:11px var(--viz-font-ui);text-align:right}.formula{min-height:6em;white-space:pre-line}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let chosen=2;function draw(){const b=+$('#bandwidth').value,d=+$('#dirty').value,ratio=d/b,sizes=[512];for(let i=0;i<8;i++)sizes.push(Math.min(512,ratio*sizes[i]));set('bandwidth-value',b+' MB/s');set('dirty-value',d+' MB/s');for(let i=0;i<8;i++){$('#round-'+i).style.width=sizes[i]/512*100+'%';set('size-'+i,viz.number(sizes[i],1)+' MB');document.querySelector('[data-round=\"'+i+'\"]').setAttribute('aria-current',String(i===chosen));}set('remaining',viz.number(sizes[chosen+1],2));set('downtime',viz.number(sizes[chosen+1]/b*1000+20,1));set('formula','D/B = '+viz.number(ratio,2)+'\\nM'+(chosen+2)+' = min(512, '+d+' × '+viz.number(sizes[chosen],2)+' / '+b+')\\nstop = '+viz.number(sizes[chosen+1],2)+' / '+b+' s + 20 ms');set('explanation',t(ratio<1?'shrinking':'stalled'));}$('#bandwidth').oninput=$('#dirty').oninput=draw;document.querySelectorAll('[data-round]').forEach(e=>e.onclick=()=>{chosen=+e.dataset.round;draw();});draw();","audio":false,"strings":{"bandwidth":"有效复制 B / MB/s","dirty":"写脏 D / MB/s","downtime":"估计停机 / ms","heading":"再多复制几轮,剩余脏数据一定减少吗?","intro":"调节有效带宽 B 与写脏速率 D,比较八轮的待复制量。直接点击某一轮,查看在该轮结束后停止 VM 所需的最终复制时间。","ratio":"D/B","remaining":"所选轮结束后剩余 / MB","reset":"重置","rounds":"点击一轮:条长是该轮开始待复制量","scope":"内存 M=512 MB(十进制),有效带宽已包含复制开销。按均匀唯一脏字节近似,Mᵢ₊₁=min(M,D×Mᵢ/B)。选择轮次表示该轮完成后停机;停机=剩余/B+固定设备收尾 20 ms。省略压缩、重复脏页、真实追踪成本与其他迁移阶段,不预测真实 VM 停机。","scopeLabel":"模型范围与约定","shrinking":"每轮可消去一部分剩余量,但越接近 1,收敛越慢。","stalled":"写脏跟上了复制;条带不会逐轮缩短。此模型下不能靠无限多轮得到很小的最终复制量。"}}
最后一刻要一致的不只是 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 为准。