QEMU 热升级:CPR 的资源交接与状态恢复
一、QEMU 进程替换中的资源与状态保留
更新 QEMU 二进制后,已经运行的虚拟机仍由旧进程维护。要让新版本接管,需要保留客户机内存,交接 CPU 和设备状态,并重新连接相关宿主资源。QEMU 的 CheckPoint and Restart(CPR)为同一宿主机上的进程替换提供了几种迁移模式。
本文重点分析 cpr-transfer:旧、新 QEMU 进程短暂并存,内存在原共享后端中保留,文件描述符通过 Unix socket 传递,CPU 与设备状态通过主迁移通道恢复。客户机在交接期间暂停,随后从原执行进度继续;共享 RAM 减少了停机窗口中的内存搬运。
CPR 处理 QEMU 进程替换。Libvirt 守护进程更新主要通过重连已有 QEMU 恢复管理,vhost-user 后端和宿主内核的更新则还需各自的状态交接与恢复协议。
源码版本:QEMU 10.2.0。
二、共享 RAM,重建对象并恢复执行状态
跨主机热迁移需要把内存内容送到另一台机器。CPR-transfer 的两个进程位于同一宿主,可以映射同一个共享内存后端。迁移前后的 GPA 布局保持兼容,新进程的 HVA 可以改变;QEMU 的用户态对象和 KVM 虚拟机则重新建立,再加载原有的执行状态。
Mermaid 源码
flowchart TB
accTitle: CPR-transfer 的资源与状态交接
accDescr: 旧进程和新进程分别创建 QEMU 对象、KVM VM 和 vCPU。CPR 通道把 name 与 id 对应的 fd 交给新进程,两个不同的 fd 和 HVA 可以引用同一共享 RAM。主迁移通道恢复 CPU 和设备状态,客户机执行不能同时在两端进行。
subgraph OLD["旧 QEMU 进程"]
O["原 QEMU 对象与 KVM VM / vCPU"]
OF["ram0:fd 17<br/>HVA 0x7f1000000000"]
OS["CPU 寄存器与设备状态"]
O --> OF
O --> OS
end
subgraph NEW["新 QEMU 进程"]
N["重建 QEMU 对象与 KVM VM / vCPU"]
NF["ram0:fd 23<br/>HVA 0x7e2000000000"]
NS["加载原执行点的状态"]
N --> NF
NS --> N
end
R["同一宿主的共享 RAM 后端<br/>内存内容原地保留"]
OF -->|MAP_SHARED| R
NF -->|MAP_SHARED| R
OF -. "CPR 通道:name + id + SCM_RIGHTS" .-> NF
OS -->|main 通道:VMState| NS
W["资源引用可重叠<br/>客户机执行权只能属于一端"]
O -.-> W
N -.-> W
classDef process fill:#eaf0f6,stroke:#8198ac,color:#263c4c;
classDef shared fill:#e4f1ec,stroke:#438575,color:#173e34;
classDef guard fill:#fff4db,stroke:#b89a55,color:#59491e;
class O,OF,OS,N,NF,NS process;
class R shared;
class W guard;
| 内容 | 处理方式 | 需要保持的关系 |
|---|---|---|
| 客户机 RAM | 新进程重新映射共享后端;跳过普通 RAM 页数据搬运 | 内容、大小、后端标识和客户机地址布局对应 |
| CPU 与用户态设备状态 | 通过主迁移流保存、加载 | 寄存器、队列位置、定时器等恢复为同一执行点 |
| QEMU 对象与 KVM VM / vCPU | 目标进程按兼容配置重新创建 | 新对象恢复原有配置和执行状态,使用目标进程自己的指针与内核 fd |
| 已接入 CPR 的宿主资源 | 传递打开的 fd,目标取回对应内核对象 | 按 name 与 id 匹配;具体设备仍有专门的交接步骤 |
| 磁盘内容 | 继续使用原有块设备或镜像,协调访问权 | 交接期间不能被另一方独立修改,也不能同时由两端客户机写入 |
文件描述符是进程内的索引。通过 SCM_RIGHTS 发送 fd 后,接收进程获得一个指向同一打开文件对象的描述符,数值无需相同。这个动作新增引用,不会自动关闭发送方的 fd,也不会自动把客户机执行权转移过去。
以一台 8 GiB 虚拟机为例,新进程取得共享 RAM 后端后,不必先收取一份 8 GiB 的普通页流;它仍需创建机器与 KVM 对象,并从旧进程接收 CPU、设备和队列状态。假设暂停后的下一条指令读取 GPA 0x2000,接管后该地址必须读到原来的内容,指令位置、队列索引与待处理中断也要对应同一个执行点。
接管顺序因此是“取得可共享资源 → 准备新对象 → 停住旧执行流 → 装载状态 → 放行新执行流”。先看两条通道与停机时序,再展开描述符清单和可操作示例。
三、先传 CPR 资源,再连接主状态通道
目标 QEMU 通过两项 -incoming 配置接收 main 与 cpr 通道。CPR 通道需要支持 SCM_RIGHTS,通常是 Unix domain socket;普通 TCP 连接只能传字节,无法代替它传递宿主 fd。main 通道继续使用迁移状态格式,并不承担共享 RAM 的完整复制。
| 通道 | 内容 | 接收时机 |
|---|---|---|
| cpr | magic 0x51435052(QCPR)、版本 1、CprState 与 fd 附带数据 | 目标创建机器对象后、创建设备和打开 monitor 之前 |
| main | 普通迁移框架中的 CPU、设备及相关状态 | 目标完成必要的资源初始化并开始监听后 |
传递 fd 时,qemu_file_put_fd() 先刷新已有数据,再随一个占位字节发送附带描述符;接收侧读出该字节及对应 fd。把 fd 的整数值直接写进普通网络流,接收端只能得到一个没有意义的本地编号。
目标需要先取得 fd,才能用它创建内存后端与相关设备,因此 cpr_state_load() 放在普通设备创建之前。它可能阻塞等待源端发起迁移,此时目标 monitor 尚未就绪。管理程序若坚持先等待新 QEMU 的 QMP 握手、再通知旧 QEMU 迁移,会卡在启动依赖上。
fd 清单的传递发生在客户机停机之前。源端发出 CPR 状态后等待对端关闭该通道;目标设置好主迁移监听后关闭 CPR 通道,源端收到 HUP 再连接 main。HUP 在成功路径中表示可以继续连接,目标异常退出也会产生 HUP,因此源端仍须检查后续连接结果。
四、停止旧 vCPU,再让新进程接管
Mermaid 源码
sequenceDiagram
accTitle: CPR-transfer 的准备、停机与恢复
accDescr: 新 QEMU 在创建设备和打开 monitor 前等待 CPR 通道。旧 QEMU 先发送描述符清单,目标准备资源并监听主通道后关闭 CPR 通道。旧端收到 HUP 才连接主通道,再停止客户机、发送状态。目标恢复完成后接管执行,旧端保持 postmigrate。
participant M as 管理程序
participant S as 旧 QEMU
participant D as 新 QEMU
participant R as 共享 RAM 后端
M->>D: 启动,指定 cpr 与 main incoming
Note over D: cpr_state_load 等待连接<br/>尚未打开 monitor
M->>S: mode = cpr-transfer,migrate 两个通道
S->>D: cpr:QCPR 头部、清单、SCM_RIGHTS
Note over S,D: 源客户机尚可运行<br/>目标持有资源引用,但不执行客户机
D->>R: 用收到的 fd 映射共享 RAM
D->>D: 初始化兼容机器、设备与新 KVM VM
D->>D: 设置 main 监听
D-->>S: 关闭 cpr 端,源端收到 HUP
S->>D: 连接 main 通道
Note over S,D: 若目标失败,HUP 也可能出现<br/>还需检查 main 连接结果
S->>S: migration_stop_vm:finish-migrate
Note over S,D: 客户机停机窗口开始
S->>D: main:CPU、设备及其他迁移状态
Note over R: RAM 内容保留,不进行普通页数据搬运
D->>D: 加载状态、激活资源与中断路由
S->>S: 保持 postmigrate
D->>D: 按原运行状态与 autostart 恢复客户机
Note over S,D: 仅新端执行,停机窗口结束
M->>D: 确认恢复结果
M->>S: 清理已完成交接的旧进程
main 连接就绪后,migrate_fd_connect() 对 CPR 模式先调用 migration_stop_vm(),将源虚拟机置于 finish-migrate,再创建迁移线程。此后保存的 CPU 和设备状态对应停止后的执行点。CPR 在保存内存状态前停止客户机,不依赖运行期间的脏页迭代。
migrate_ram_is_ignored() 对 cpr-transfer 返回 true,普通 RAM 数据不再按预拷贝方式发送。CPU 与设备状态仍需经过 VMState 保存和恢复;目标为新建的 KVM vCPU 写回寄存器等状态,恢复中断路由与后端关系。主迁移流的分段和设备状态编码见QEMU 迁移流格式与状态恢复。
完成交接时,源端保持 postmigrate,目标按照原运行状态与 autostart 设置决定是否执行。只有目标恢复完成并允许启动后,客户机才由新 QEMU 继续运行。源进程短暂保留用于管理和清理,不意味着可以重新执行同一份共享内存里的客户机。
五、描述符清单与共享内存
CprState 保存 fds 链表和 VFIO 设备交接信息。每个 CprFd 用一组稳定标识说明该 fd 属于什么资源;后端初始化时调用 cpr_find_fd(name, id),找到已经接收的 fd 就复用,未找到时才走常规创建路径。
| 字段 | 含义 | 序列化方式 |
|---|---|---|
name | 资源名称,例如内存后端的名称 | 由 namelen 指定长度的字节串 |
namelen | 名称长度,包含结尾 NUL | 32 位整数 |
id | 同名资源下的编号,例如某个中断向量 | 32 位有符号整数 |
fd | 当前进程中的打开文件描述符 | transfer 模式使用 VMSTATE_FD,经 SCM_RIGHTS 传递 |
next | 本进程链表连接 | 由 VMState 链表加载逻辑重建,不传原指针 |
以名称为 ram0 的 memfd 内存后端为例,下面的数字只是一次交接模型。两个 HVA 都能访问同一 RAM 内容,GPA 到 HVA 的新映射由目标进程重新登记:
资源标识:name = "ram0", namelen = 5, id = 0
旧 QEMU:fd = 17,RAM 的 HVA 基址 = 0x7f1000000000
│ SCM_RIGHTS
新 QEMU:fd = 23,RAM 的 HVA 基址 = 0x7e2000000000
fd 17 与 fd 23 → 同一个共享 memfd 后端
相同 RAM 偏移 → 相同内容;用户态虚拟地址无需相同
memfd_backend_memory_alloc() 先查清单,复用 fd 后调用 memory_region_init_ram_from_fd() 建立 MemoryRegion。附属 RAM 也要考虑:VGA、ROM 等隐式 RAMBlock 没有显式的 memory-backend 对象,aux-ram-share=on 使这些区域通过共享 fd 分配。只共享客户机主内存、遗漏附属 RAM,会破坏整机状态的连续性。
CPR-transfer 要求内存后端设置 share=on,且虚拟机启动时就采用可交接的后端和附属 RAM 配置。RAM 注册为可迁移内存时,会为不兼容的 RAMBlock 设置 blocker,发起迁移时统一检查。资源名也要与后端的实际命名一致,采用兼容配置时可能使用完整对象路径。运行中的私有匿名内存不能仅靠临时改变迁移 mode 就变成可共享资源。
六、双进程示例:观察资源交接与客户机连续执行
下面按 QEMU 10.2.0 的 CPR 示例组织一次最小交接:同一版本的两个 QEMU、512 MiB memfd 内存、一个 vCPU、两条 Unix 通道。客户机使用测试用内核与 initramfs,不挂载业务磁盘、不配置网络。initramfs 启动后在串口周期输出一个递增计数器,便于辨认恢复后的执行进度。命令与状态是教学示例,未在本文中执行;其中的内核、initramfs 和 QEMU 路径需要换成自己的测试文件。
先在一个 shell 中创建独立运行目录,准备两个进程共用的机器参数。内核与 initramfs 必须相同;ram0、内存大小、CPU 型号和设备拓扑也保持一致。这里的共享内存来自 memfd,无需复用其他虚拟机的文件路径。
QEMU=/path/to/qemu-10.2.0/bin/qemu-system-x86_64
KERNEL=/path/to/test/bzImage
INITRD=/path/to/test/initramfs.cpio.gz
LAB=$(mktemp -d /tmp/qemu-cpr.XXXXXX)
chmod 700 "$LAB"
printf '运行目录:%s\n' "$LAB"
COMMON=(
-accel kvm -cpu host -smp 1 -m 512M
-machine pc-q35-10.2,memory-backend=ram0,aux-ram-share=on
-object memory-backend-memfd,id=ram0,size=512M,share=on
-nodefaults -display none -monitor none
-kernel "$KERNEL" -initrd "$INITRD" -append console=ttyS0
)
"$QEMU" "${COMMON[@]}" \
-qmp unix:"$LAB/source.qmp",server=on,wait=off \
-serial file:"$LAB/source.console" &
SOURCE_PID=$!
"$QEMU" "${COMMON[@]}" \
-qmp unix:"$LAB/target.qmp",server=on,wait=off \
-serial file:"$LAB/target.console" \
-incoming unix:"$LAB/main.sock" \
-incoming '{"channel-type":"cpr","addr":{"transport":"socket","type":"unix","path":"'"$LAB"'/cpr.sock"}}' &
TARGET_PID=$!
两个进程放在后台,是因为目标会先阻塞在 CPR 资源接收处。此时旧端串口计数应继续增长,新端 QMP 尚未就绪属于正常顺序。使用 socat -,echo=0 UNIX-CONNECT:"$LAB/source.qmp" 连接旧端,收到 QMP greeting 后逐条输入以下请求。JSON 中的 /tmp/qemu-cpr.EXAMPLE 要替换为刚才打印的运行目录:
{"execute":"qmp_capabilities"}
{"execute":"query-status"}
{"execute":"migrate-set-parameters","arguments":{"mode":"cpr-transfer"}}
{"execute":"migrate","arguments":{"channels":[
{"channel-type":"main","addr":{"transport":"socket","type":"unix","path":"/tmp/qemu-cpr.EXAMPLE/main.sock"}},
{"channel-type":"cpr","addr":{"transport":"socket","type":"unix","path":"/tmp/qemu-cpr.EXAMPLE/cpr.sock"}}
]}}
{"execute":"query-migrate"}
{"execute":"query-status"}
migrate 返回成功只表示请求被接受,继续查询直到状态收束。目标 QMP 就绪后,用另一个连接访问 target.qmp,完成 qmp_capabilities 协商,再执行 query-status。成功路径应同时具备下面的证据:
| 观察位置 | 预期结果 | 说明 |
|---|---|---|
旧端 query-migrate | status: completed | 源侧迁移任务完成;仍需核对目标与客户机 |
旧端 query-status | status: postmigrate、running: false | 旧进程还在,客户机已停止执行 |
新端 query-status | status: running、running: true | 目标已经放行客户机 CPU |
| 两个串口日志 | 旧端停止输出,新端继续原计数,未重新出现启动流程 | 计数只用于辨认连续性;串口缓冲可能影响末尾少量输出,不能据此精确测量停机时间 |
计数器可在客户机测试程序中每秒递增并打印,同时保存一次启动标识。新端若从初始值重来或启动标识变化,应检查是否发生了客户机重启。要测量停顿,另由宿主记录每条输出到达的单调时间;周期为一秒的输出只能发现较大间断,无法给出毫秒级 downtime。
确认目标正常后,给旧端发送 {"execute":"quit"} 清理旧进程。结束测试时再退出新端,确认两个 PID 均已退出后删除本次运行目录。若迁移失败,先按下一节核对双端状态与资源归属,保留日志;不要把旧端 cont 当作统一恢复步骤。使用不同 QEMU 版本前,还需要单独验证机器类型及设备迁移兼容性。
七、兼容性、停机时间与失败边界
同机更新仍使用迁移兼容约定。新进程需要创建对应的机器、CPU 和设备配置,能够读取源端状态版本;资源名称和设备拓扑也要匹配。共享 RAM 解决了内存搬运问题,无法替不兼容的 VMState 或不同设备布局自动转换状态。
前面的最小示例没有直通设备。扩展到 VFIO / IOMMUFD 时,还要交接设备 fd、已固定页、中断 eventfd 和 DMA 关联。该版本的相应 CPR 路径会保留设备资源,调整旧 KVM 的中断连接,再在目标恢复路由;已进入旧 LAPIC 的待处理中断随 CPU 状态恢复。因此除了共享 RAM 与 VMState,还需逐项检查物理设备的资源和通知路径。地址与 DMA 对应关系见VFIO 设备直通的 DMA 地址转换与隔离。
停机窗口从源端停止执行延续到目标客户机恢复,包括状态保存与加载、资源激活、寄存器同步和必要的线程调度。fd 预交接和一部分目标初始化在停机前完成。即使 RAM 数据没有复制,目标页表建立、KVM 映射恢复和后端重连仍可能带来成本,应分别测量准备时间、客户机停顿和恢复后的尾延迟。
| 失败阶段 | 需要判断的事实 | 处理边界 |
|---|---|---|
| 资源预交接或 main 连接之前 | 旧客户机可能仍在运行;目标可能仅持有 fd 引用 | 检查实际运行状态与目标清理结果,不把连接失败当作客户机必然已停机 |
| 源端已停机、目标未确认接管 | 状态是否完整、块设备与中断连接处于哪一侧 | 依赖迁移错误处理和后端恢复结果;不能承诺任意失败都可无损自动回滚 |
| 目标已执行客户机 | 共享内存和磁盘内容已由新执行流推进 | 旧进程不能直接 cont;否则会出现双端执行与并发写入 |
本版本的 CPR-transfer 不与 postcopy、background-snapshot 或 COLO 组合,也不支持 memory-backend-epc。文档把更多 vhost、字符设备 fd 的连续保留列为后续方向,不能据此推断所有后端已能无中断交接。是否可用应检查具体设备的 CPR 路径及迁移 blocker。
八、从保存重启到进程间交接
普通保存恢复也能保持客户机执行位置,但暂停后写出全部 RAM、再读入新进程时,停顿会随内存复制量增长。同机升级利用共享后端保留 RAM,把这部分工作从交接路径中移除;剩余的对象创建、设备恢复与兼容性检查仍需完成。
2023 年的 cpr-reboot 实现 提供保存状态、退出旧 QEMU、再启动新进程的路径。2025 年的 cpr-transfer 实现 增加并存进程间的 fd 交接,让共享资源可以在旧进程退出前交给新进程。cpr-exec 则使用 exec 替换当前进程映像。
| 模式 | 进程与资源交接 | 主要取舍 |
|---|---|---|
| cpr-reboot | 保存状态后退出,另行启动;RAM 可共享保留或写入状态文件 | 步骤清晰,但新进程准备通常落入停机窗口;跨宿主重启保留 RAM 需要持久化内存后端等条件 |
| cpr-transfer | 两个进程短暂并存,通过 Unix socket 传 fd 和主通道交状态 | 可提前准备目标资源,需要协调监听顺序与唯一执行权 |
| cpr-exec | 保留 PID,通过 exec 启动新程序;CPR 清单写入 memfd 并跨 exec 保留 fd | 省去两个并存进程的 CPR socket,状态主通道需要支持先写后读;exec 后的恢复失败不能靠旧进程继续运行 |
这些机制把“更新运行虚拟机的软件”拆成可检查的资源保留、状态恢复和执行切换。选择方案时,首先确定哪些资源能够保留、哪些必须重建,再评估状态兼容和失败恢复,最后才比较停机时长。