QEMU 迁移流格式与状态恢复
一、迁移流的组成与状态恢复
QEMU 迁移流承载 RAM、CPU 和设备的运行状态。目的端先创建兼容的机器与设备模型,再根据流中的记录恢复源端状态:识别流格式和机器配置,解析 RAM 布局并更新页面,装载源端暂停后保存的 CPU 与设备状态,最后完成恢复执行前的检查。
这条路径由两层格式配合完成:外层 section 选择处理器并校验边界,内层载荷描述页面或设备字段。以下用同一组四个内存页贯穿完整流示例,再逐层展开头部、RAM 编码、VMState 和接收流程。
源码版本:QEMU 10.2.0。
以下以 x86 / KVM、普通 RAM 的单流预拷贝为例,机器与设备已在目的端创建,存储另行准备。内存重传和停机切换条件见《QEMU 预拷贝迁移的内存迭代与停机控制》。
二、完整示例:从流头到状态恢复
先把整条流缩成一个四页例子:前三页的初始内容为 A1、B1、C1,第四页全零;运行期间前三页先后变为 A2、B2、C2。其中 A2 在迭代批次重传,B2、C2 留到最终停机阶段发送。接收端始终顺序读主流,并把载荷交给相应处理器:
MAGIC + VERSION → CONFIGURATION
→ RAM START:块名与长度,建立内存布局关联
→ RAM PART:A1、B1、C1、零页
→ RAM PART:A2 覆盖旧 A1
→ 切换通知 → RAM END:B2、C2 覆盖旧 B1、C1
→ CPU / 设备 FULL:恢复寄存器与设备内部状态
→ EOF → 可选诊断描述 → 装载收尾与恢复执行
每个 RAM section 内:页记录… → RAM EOS → 外层 footer
QEMUFile提供顺序读写接口;迁移框架在流中标识状态处理器,RAM、CPU 和设备各自负责载荷。左侧按发送顺序排列整条迁移流,右侧展开相应记录的字段。RAM 载荷内部的 EOS、section 尾部的 footer 和整条状态流的 EOF 属于不同层次。
下面把 pc.ram 的四页例子逐条展开。先跟随左侧记录识别当前阶段,再看字段条如何编码。A2 重传时,目的端通过块名和偏移覆盖原位置;流中的 section ID 则用于找到处理这些页的 RAM 处理器。
迁移流的发送与装载
跟踪同一个 pc.ram 中的四页,观察页内容重传、最终同步和 CPU / 设备状态装载。点击“下一步”,记录沿通道到达后才更新目的端。
pc.ram · 16 MiB
vCPU 正在执行
PIC · IRR 01 / IMR 00 / ISR 00
有序迁移流 →
序列化 → 解析
pc.ram · 16 MiB
vCPU 状态未装载
设备状态未装载
尚未读取流头
A1 / A2 表示同一页的不同内容;绿色表示当前副本一致,橙色表示尚未同步,虚线表示目的端缺页或旧副本。页大小为 4 KiB,画面只跟踪前四页,其余页省略。
系统已启用减少动态效果;手动选择步骤会直接显示解析后的状态,不播放移动动画。
01 · 流头8 B · 无 section ID
MAGICBE32 · 4 BVERSION = 3BE32 · 4 B51 45 56 4d | 00 00 00 03
QEVM | 流格式版本 3
源端 qemu_savevm_state_header() 写出;目的端 qemu_loadvm_state_header() 校验。版本 3 标识流格式,独立于 QEMU 发行版本。
02 · 配置摘要pc-q35-10.2 · 名称 11 B
CONFIG = 071 BN = 11BE32 · 4 Bpc-q35-10.211 B07 | 00 00 00 0b | 70 63 2d 71 33 35 2d 31 30 2e 32
type | N = 11 | p c - q 3 5 - 1 0 . 2
本例配置未使用条件子段,也没有 section ID 或 section footer;若页大小、能力或 UUID 校验需要额外信息,会追加相应 VMState 子段。
源端序列化 vmstate_configuration;目的端核对已创建机器的类型与兼容性,机器与设备对象已在迁入前准备。
03 · RAM STARTsection ID = 1 · ram / instance 0 / version 4
START = 011 BID = 1BE32ram1 B 长度 + 3 B0 / 4实例 / 版本 · 各 BE32pc.ram 布局总量、块名、块长度RAM EOSBE64 · 0x10FOOTER + ID7e + BE32(1)01 | 00 00 00 01 | 03 72 61 6d | 00 00 00 00 | 00 00 00 04
section ID 1 "ram" instance 0 version 4
00 00 00 00 01 00 00 04 # 16 MiB | MEM_SIZE(0x004)
06 70 63 2e 72 61 6d # 名称长度 6 + "pc.ram"
00 00 00 00 01 00 00 00 # 该块 used_length = 16 MiB
00 00 00 00 00 00 00 10 # RAM EOS
7e 00 00 00 01 # section footer,ID 与头部一致
源端 ram_save_setup() 发送布局;目的端匹配 ram 处理器并核对 RAMBlock。读到 RAM EOS 才返回外层校验 footer;此时没有装载页面内容。
04 · 首次页传输PART · A1 / B1 / C1 / 零页
PART + ID02 + BE32(1)A1offset / flags / 块名 / 4 KiBB1 / C1offset / flags / 各 4 KiB零页offset / flags / 00RAM EOSBE64 · 0x10FOOTER + ID7e + BE32(1)02 00 00 00 01 # PART,section ID 1
00 00 00 00 00 00 00 08 # A1: offset 0x0000 | PAGE(0x008)
06 70 63 2e 72 61 6d # 首次页记录带 "pc.ram"
a1 a1 … [共 4096 B] # A1 页内容,重复字节是本例数据
00 00 00 00 00 00 10 28 # B1: 0x1000 | PAGE | CONTINUE
b1 b1 … [共 4096 B]
00 00 00 00 00 00 20 28 # C1: 0x2000 | PAGE | CONTINUE
c1 c1 … [共 4096 B]
00 00 00 00 00 00 30 22 00 # 零页: 0x3000 | ZERO | CONTINUE,填充值 00
… [其他 RAM 页记录省略]
00 00 00 00 00 00 00 10 # RAM EOS
7e 00 00 00 01 # section footer
CONTINUE = 0x020 只省略重复的块名,不省略偏移或内容;零页用 1 B 填充值代表 4 KiB。目的端收到这批后已有 A1 / B1 / C1 / 0;源端继续运行,本例随后将 A、C 写成 A2、C2。
ram_save_iterate() 编码页记录;目的端 ram_load() 按偏移写入。一次 PART 是一次发送批次,不等于完整的一轮预拷贝。
05 · 重传 A2相同块、相同偏移,覆盖旧副本
PART + ID02 + BE32(1)A2 页头offset 0 | 0x028A2 页内容4096 BRAM EOSBE64 · 0x10FOOTER + ID7e + BE32(1)02 00 00 00 01 # 仍是 PART / section ID 1
00 00 00 00 00 00 00 28 # offset 0 | PAGE | CONTINUE
a2 a2 … [共 4096 B] # 新内容 A2 覆盖目的端 A1
00 00 00 00 00 00 00 10 # RAM EOS
7e 00 00 00 01 # section footer
块名沿用上一批的 pc.ram。目的端把偏移 0 的 A1 替换为 A2,C2 仍未发送;源端随后又将 B1 写成 B2。流中没有单独的“第几轮”字段,也不会因为收到 PART 就运行客户机。
06 · 暂停源端客户机本地操作 · 没有对应的网络字节
migration_stop_vm() 停止源端执行。从这里开始计算停机阶段;源端不再产生本例中的 B、C 新版本,但目的端仍只有 B1 / C1,需要接收最终同步。
暂停在源端本地完成,迁移流中没有对应的 STOP section;这一步只改变执行状态。
07 · 切换阶段通知COMMAND · 不带 section footer
COMMAND = 081 BSWITCHOVER_START = 11BE16 · 2 B长度 = 0BE16 · 2 B08 | 00 0b | 00 00
type | 命令 11 | 载荷长度 0
send-switchover-start 在此版本默认开启。接收端分派切换回调,随后继续装载最终内存与设备状态;启动检查在装载收尾阶段进行。
08 · 最终内存同步END · B2 / C2
END + ID03 + BE32(1)B2offset / flags / 4096 BC2offset / flags / 4096 BRAM EOSBE64 · 0x10FOOTER + ID7e + BE32(1)03 00 00 00 01 # END 是外层记录开头,不在载荷末尾
00 00 00 00 00 00 10 28 # B2: offset 0x1000 | PAGE | CONTINUE
b2 b2 … [共 4096 B]
00 00 00 00 00 00 20 28 # C2: offset 0x2000 | PAGE | CONTINUE
c2 c2 … [共 4096 B]
00 00 00 00 00 00 00 10 # RAM EOS 结束本次 RAM 载荷
7e 00 00 00 01 # footer 校验 section ID
ram_save_complete() 在最后一次脏页同步后发送余页。目的端现在得到 A2 / B2 / C2 / 0,继续等待 CPU 与设备状态装载。
09 · 同步 vCPU 寄存器源端本地操作 · 非迁移流记录
cpu_synchronize_all_states() 将暂停后的 KVM vCPU 状态同步到 QEMU CPU 对象。本例选取 RAX = 0x2a、RIP = 0x1000;这些寄存器值随后随 CPU FULL 记录传输。
10 · CPU FULLsection ID = 2 · cpu / instance 0 / version 12
FULL = 041 BID = 2BE32cpu1 B 长度 + 3 B0 / 12实例 / 版本 · 各 BE32寄存器、段、FPU…VMState 载荷FOOTER + ID7e + BE32(2)04 | 00 00 00 02 | 03 63 70 75 | 00 00 00 00 | 00 00 00 0c
section ID 2 "cpu" instance 0 version 12
00 00 00 00 00 00 00 2a # env.regs[0] / RAX,x86-64 的 BE64
… [其余 15 个通用寄存器,各 8 B]
00 00 00 00 00 00 10 00 # env.eip / RIP = 0x1000
… [eflags、FPU、段寄存器等剩余字段和条件子段]
7e 00 00 00 02 # section footer
vmstate_x86_cpu 规定字段顺序和长度,流中只保存相应编码值。目的端按支持的状态版本与字段表装载 CPU 对象,此时仍不执行 vCPU。
11 · i8259 FULLsection ID = 3 · 主 PIC instance 0x20 / version 1
FULL = 041 BID = 3BE32i82591 B 长度 + 5 B0x20 / 1实例 / 版本 · 各 BE32PIC 字段16 × UINT8FOOTER + ID7e + BE32(3)04 | 00 00 00 03 | 05 69 38 32 35 39 | 00 00 00 20 | 00 00 00 01
section ID 3 "i8259" instance 0x20 version 1
00 01 00 00 # last_irr / irr / imr / isr
00 20 00 00 # priority_add / irq_base / read_reg_select / poll
00 00 00 00 # special_mask / init_state / auto_eoi / rotate_on_auto_eoi
00 00 00 00 # special_fully_nested_mode / init4 / single_mode / elcr
7e 00 00 00 03 # section footer(ltim 子段在本例不出现)
字段依据 vmstate_pic_common 排列,各占 1 B。IRR = 01 表示存在待处理中断,目的端通过该状态继续处理尚未完成的中断请求。主 PIC 的 legacy instance ID 取 I/O 基址 0x20。
本例只展开一个 PIC。实际机器还会保存其他设备 FULL,CPU 与设备记录的次序取决于处理器注册顺序。
12 · 主状态流结束EOF · 单字节 00
EOF = 001 B00 # 退出外层状态记录循环
qemu_loadvm_state_main() 读到 EOF 后退出分发循环,目的端继续完成装载收尾、运行状态与存储检查。连接关闭和客户机启动由各自的后续流程处理。
13 · VM Description可选 · EOF 后的诊断描述
VMDESC = 061 BJSON 长度 NBE32 · 4 BJSONN B06 | [N 的 4 B 大端编码] | 7b … 7d
type | JSON 字节数 | { … }
内容形如 {"page_size":4096,"devices":[…]}
实际 N 由完整设备字段描述的 JSON 字节数决定,上述省略文本只表示结构。接收端按长度读完诊断描述;CPU 与设备恢复使用前面已经装载的二进制状态。
14 · 目的端恢复执行装载成功且允许自动启动的分支
恢复的 CPU 对象同步到 KVM,设备装载与存储激活检查通过,保存的运行状态和 autostart 允许执行后,目的端才调用 vm_start()。此时目的端使用 A2 / B2 / C2 / 0、RAX / RIP 及 PIC 状态继续运行。
源端保持停止并进入迁移完成状态,不能与目的端同时执行同一虚拟机。若禁用自动启动或保存的是暂停状态,成功迁入也可以保持暂停。
完整迁移流的字段示例
下列各行依次相接;竖线与注释只用于分组,不属于传输字节。省略号所在处有实际载荷,不能跳过;暂停与 CPU 同步是本地操作,不插入额外标记。
51 45 56 4d 00 00 00 03 # 流头
07 00 00 00 0b 70 63 2d 71 33 35 2d 31 30 2e 32 # CONFIG
01 00000001 03 72616d 00000000 00000004 # START: ID=1,ram,0,4
0000000001000004 06 70632e72616d 0000000001000000 # 16 MiB / pc.ram
0000000000000010 | 7e 00000001 # RAM EOS / footer
02 00000001 | 0000000000000008 06 70632e72616d [a1 × 4096] # PART: A1
0000000000001028 [b1 × 4096] # B1 / CONTINUE
0000000000002028 [c1 × 4096] # C1 / CONTINUE
0000000000003022 00 # 零页
… [其余页记录] …
0000000000000010 | 7e 00000001 # RAM EOS / footer
02 00000001 | 0000000000000028 [a2 × 4096] # PART: 重传 A2
0000000000000010 | 7e 00000001
# 源端暂停(无流字节)
08 000b 0000 # SWITCHOVER_START
03 00000001 | 0000000000001028 [b2 × 4096] # END: 最终 B2
0000000000002028 [c2 × 4096] # 最终 C2
0000000000000010 | 7e 00000001
# KVM → QEMU 同步寄存器
04 00000002 03 637075 00000000 0000000c # FULL: cpu,0,12
000000000000002a … [其他通用寄存器] … 0000000000001000 # RAX / … / RIP
… [其余 CPU 字段与条件子段] … | 7e 00000002
04 00000003 05 6938323539 00000020 00000001 # FULL: i8259,0x20,1
00 01 00 00 00 20 00 00 00 00 00 00 00 00 00 00 | 7e 00000003
… [其他设备 FULL 与各自 footer] …
00 # EOF
06 [N:BE32] [N 字节 JSON] # 可选 VM Description
# 目的端检查与恢复(无流字节)
按 QEMU 10.2.0 格式构造的字段示例,不是抓包;省略了其余 RAM 页、设备和部分 CPU 字段,不能直接作为可导入的快照。section ID 1 / 2 / 3 为本例分配,多字节整数按大端表示;使用普通预拷贝单主流与默认 footer,不启用 MultiFD、后拷贝或压缩。
在源端运行期间,框架写入流头、配置摘要、RAM 初始化和多个迭代批次。暂停后,最终 RAM 保存先于非迭代状态保存;后者先同步 CPU 状态,再遍历设备处理器。目的端从流开始就解析和装载,而非收到 EOF 后才统一拷贝。最终保存顺序
三、流头、分段与边界
1. 流头与配置摘要
流头是两个大端 32 位整数:QEMU_VM_FILE_MAGIC = 0x5145564d与QEMU_VM_FILE_VERSION = 3,完整字节为 51 45 56 4d 00 00 00 03。前者是 magic,后者是流格式版本。各 CPU、RAM 或设备处理器另有独立的状态版本,用于管理各自字段的兼容性。格式常量
默认随后写入 CONFIGURATION 0x07:机器名称长度为 BE32,名称本身按字节写入,不带终止 NUL;条件子段补充页大小、能力等兼容性信息。目的端用这些字段检查已创建的机器,完整机器与设备配置仍需在迁入前准备。配置字段、发送入口
2. 两种 section 头
外层类型常量以 QEMU_VM_SECTION_为前缀:START、PART、END、FULL分别为 0x01至 0x04;QEMU_VM_SECTION_FOOTER为 0x7e。图表使用简称,源码可按完整宏名查找。
| 记录 | 流中的字段,按顺序 |
|---|---|
| START / FULL | 类型 1 B → section ID 4 B → 名称长度 1 B → 名称 N B → 实例 ID 4 B → 状态版本 4 B |
| PART / END | 类型 1 B → section ID 4 B |
| Section Footer | 0x7e 1 B → section ID 4 B |
这些 4 字节整数均为大端。START建立可迭代处理器的关联,PART/END只引用 section ID;FULL一次传输一个处理器的完整状态。名称和实例用于匹配目的端已注册的对象,section ID 的引用范围限定在本次迁移流中。头与 footer 的实现
例如假设 RAM 的 section ID 为 1,其 START 头为:
01 # SECTION_START
00 00 00 01 # section ID(本例取 1)
03 72 61 6d # 名称长度 3;"ram"
00 00 00 00 # instance ID = 0
00 00 00 04 # RAM 状态版本 = 4
目的端先按 ram / instance 0 找到本机 RAM 处理器,把源端 section ID 1 记到该处理器的 load_section_id。后来包含 A2 的 PART 只携带 ID 1,也能回到同一个处理器。该关联在 START 时建立,延续到后续 PART 和 END,内存页面的块名与偏移则由 RAM 处理器继续解析。
默认每次 START、PART、END、FULL 的载荷之后都有 5 字节 footer,用于核对处理器是否准确消费载荷。接收端按处理器格式读完载荷后,再校验 footer 的标记和 ID。由于页面内容本身也可能出现 0x7e,扫描该字节无法确定载荷边界。默认设置、footer 校验
四、内存记录的编码
1. 布局先于页面
ram_save_setup()先写 BE64 的“总 RAM 字节数 OR MEM_SIZE 0x004”,随后逐块写名称长度(1 B)、名称、块长度(BE64)。目的端用块名寻找本机 RAMBlock,并按长度累计扣减总量,读完布局后遇到 RAM EOS。后续页记录通过“块+块内偏移”定位,使用目的端对应 RAMBlock 的本地映射。布局发送、布局接收
RAMBlock 是各自 QEMU 进程里的对象,其 host 指向本机映射。假设源端 pc.ram 映射在 0x10000000,目的端映射在 0x50000000,四页例子中的第三页会这样定位:
源 QEMU 读取:0x10000000 + 0x2000
流中表达: RAMBlock "pc.ram" + 块内偏移 0x2000 + 页面内容
目的 QEMU 写入:0x50000000 + 0x2000
因此两端可以采用不同的宿主虚拟地址。块名、偏移和页面编码建立跨进程的对应关系,源端 C 指针只用于本地读取。
2. 偏移与标志共用八字节
页记录的首个 BE64 保存块内页偏移,低位存放标志。普通页用 PAGE 0x008,零页用 ZERO 0x002。首次发送某块或切换 RAMBlock 时,还要发送名称长度和名称;同一块的后续记录可加 CONTINUE 0x020,复用上次块名。
普通页(4 KiB 页、同一 RAMBlock,偏移 0x2000):
00 00 00 00 00 00 20 28 # 0x2000 | PAGE | CONTINUE
[4096 字节页面内容]
零页(同一 RAMBlock,偏移 0x3000):
00 00 00 00 00 00 30 22 # 0x3000 | ZERO | CONTINUE
00 # 零填充值,不传整页
本批 RAM 载荷结束:
00 00 00 00 00 00 00 10 # RAM_SAVE_FLAG_EOS
7e [4 字节 section ID] # 外层 footer,不属于 RAM 载荷
普通页长度由目标页大小决定,并无逐页长度字段。零页在此版本只允许零填充,接收端会拒绝非零填充值。CONTINUE只控制块名复用;每条页记录仍携带自己的偏移、编码标志和相应载荷。标志定义、页头、零页与普通页
| 边界 | 具体含义 |
|---|---|
RAM EOS · BE64 0x10 | RAM 处理器结束本批载荷,返回外层解析器。 |
SECTION_END · 类型 0x03 | 本条外层记录是可迭代处理器的最终阶段,载荷仍在其后。 |
Section Footer · 0x7e+ID | 校验刚消费完的 section;每批都可以有,不等于整个 RAM 完成。 |
EOF · 0x00 | 结束主状态记录循环,不等于网络连接立即关闭。 |
ram_save_iterate()每次调用可以只传输部分待传页,首次扫描本身就可能跨多个 PART。停机后 ram_save_complete()做最终脏页同步,再用相同的页编码发送余页。流里没有统一的轮次序号;分析抓取数据时,不宜把 PART 个数当作预拷贝轮数。
五、暂停后保存 CPU 与设备状态
源端暂停后,默认还会发送切换通知:08 00 0b 00 00,分别是 COMMAND、SWITCHOVER_START = 11和长度 0。接收端据此分派 switchover_start回调,后续仍需完成内存与设备状态装载,才能进入运行状态检查。COMMAND 的长度属于这种记录自身的格式,普通设备 section 仍没有统一载荷长度。命令编码、发送时机
完成 RAM 后,cpu_synchronize_all_states()把 KVM 维护的寄存器状态同步进 QEMU 对象,再保存非迭代状态。CPU、设备及其他处理器按注册顺序写 FULL,不保证“全部 CPU 在前、全部设备在后”。标记 early_setup的状态还可以在初始化阶段提前保存。
VMStateDescription规定要保存的字段、类型、版本条件以及保存前和装载后回调。它与状态处理器和字段定义共同完成序列化:
| 本地结构 | 关键字段 | 如何参与编码与装载 |
|---|---|---|
SaveStateEntry | idstr / instance_id / version_id / section_id / load_section_id | 关联状态处理器;接收 START / FULL 后记录源端 section ID,后续 PART / END 可据此找回处理器 |
VMStateDescription | name / version_id / minimum_version_id / fields / subsections | 定义可接受版本、字段列表及条件子段,组织保存与装载回调 |
VMStateField | offset / size / num / info / flags / version_id | 在本地对象中定位字段,由类型的 put / get 函数写入或读取线格式 |
这些结构描述如何访问对象。offset是本地 C 对象中的字段偏移,不直接作为迁移流中的字段地址发送;接收端根据自己的状态描述找到对应成员。SaveStateEntry、VMState 字段与描述
x86 CPU 的状态表以这些字段开始:
VMSTATE_UINTTL_ARRAY(env.regs, X86CPU, CPU_NB_REGS),
VMSTATE_UINTTL(env.eip, X86CPU),
VMSTATE_UINTTL(env.eflags, X86CPU),
在 x86-64 目标中,它们使用 64 位目标字宽;后面的 FPU、段寄存器、控制寄存器等继续按类型保存。VMState 按声明逐个序列化字段,编码独立于 C 对象中的指针和填充布局。接收端按对应状态版本及字段顺序装载。CPU 状态表、字段遍历
i8259 的 irr、imr、isr分别用 UINT8 保存中断请求、屏蔽和服务中状态。virtio 除客户机 RAM 中的队列描述符,还需要交接设备状态、协商特性、last_avail_idx等设备侧处理位置。否则 RAM 虽然一致,设备仍可能从错误位置继续工作。i8259、virtio
条件子段用 SUBSECTION 0x05+名称长度(1 B)+名称+版本(BE32)+子段载荷表达。它嵌在相应 VMState 内部,没有统一长度或独立的 section footer。父处理器读取完字段与子段之后,返回框架校验外层 footer。子段保存与装载
六、接收解析与恢复执行
qemu_loadvm_state_main()先取一个外层类型字节。START/FULL 读取名称和实例,找到目的端处理器并检查版本;PART/END 复用之前建立的 section ID 关联。然后进入 RAM、VMState 或旧式设备的专用装载函数。对象缺失或状态版本不受支持时,装载返回错误。
Mermaid 源码
flowchart TD
accTitle: 迁移流的处理器分发与恢复边界
accDescr: 接收端按外层记录匹配处理器,由处理器消费载荷,再校验 section footer。EOF 结束主记录循环,装载收尾与运行条件检查之后才允许客户机执行。
header["校验 Magic、版本与配置"] --> type{"读取外层类型"}
type -->|START / FULL| first["按名称和实例匹配处理器\n检查版本,记录 load_section_id"]
type -->|PART / END| again["按 load_section_id 查找处理器"]
first --> load["vmstate_load\nRAM、设备回调或声明式字段装载"]
again --> load
load --> footer["check_section_footer\n校验尾标记与 section ID"]
footer --> type
type -->|COMMAND| command["按命令号处理\n正常继续时读取下一记录"]
command --> type
type -->|EOF| finish["离开主记录循环\n读取可选描述,完成装载收尾"]
finish --> ready["同步 CPU 状态\n检查运行状态、autostart 与存储"]
ready --> run["满足条件后 vm_start"]
EOF 后可能跟随 VMDESCRIPTION 0x06+JSON 字节长度(BE32)+JSON,供调试工具分析状态字段;实际恢复使用前面已装载的二进制状态。普通预拷贝接收端按条件读完诊断描述,再完成装载收尾与 CPU 状态同步。处理器匹配、装载收尾
cpu_synchronize_all_post_init()使恢复的 CPU 对象状态同步到 KVM vCPU;真正开始执行还需要后续运行状态检查。process_incoming_migration_bh()考虑原运行状态、autostart 与存储激活,满足条件才调用 vm_start()。因此 EOF、装载完成和客户机恢复执行是三个不同的时刻。目的端运行决策
回到四页示例,装载完成时目的端应持有 A2、B2、C2 和零页,CPU 与设备则处于源端停机时保存的状态。随后是否开始执行,再由运行条件决定。排查失败可沿同一路径定位:magic 或格式版本错误发生在流入口;名称、实例或版本不匹配发生在处理器选择;RAM 范围和设备字段错误发生在载荷装载;数据全部读完后仍未运行,再检查 autostart、存储激活与原运行状态。
七、格式的演进与兼容
早期设备分别手写保存与装载函数,字段顺序与条件容易在两端产生偏差。2009 年引入的声明式 VMState集中字段类型与读写逻辑,降低重复维护的成本,但旧式处理器仍然存在。已经发布的线格式也不会因此自动兼容:字段顺序、版本号与条件子段都必须谨慎设计。
二进制流本身没有为每个字段携带名称;离线分析通常需要对应设备代码。后来增加的VM Description在 EOF 后附加诊断 JSON,便于工具还原字段含义,并保留已有二进制编码。跨版本迁移仍受机器类型、设备字段及双方能力约束。
MultiFD 将部分内存数据移到并行通道,后拷贝调整执行权交接与页面传输的先后顺序,设备并行传输也会引入独立通道。这些路径需要额外的同步协议,保证目的端恢复时各部分状态已经就绪。外层框架继续负责识别状态处理器,具体处理器负责解析载荷和检查版本兼容性。