virt-report虚拟化社区动态

QEMU 热升级:CPR 的资源交接与状态恢复

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

一、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 虚拟机则重新建立,再加载原有的执行状态。

CPR-transfer 的资源与状态交接
两个进程映射同一共享 RAM 后端,分别拥有自己的 QEMU 对象和 KVM 虚拟机。CPR 通道传送已登记资源的描述符,主通道传送执行状态;客户机执行权只交给一端。
两个进程映射同一共享 RAM 后端,分别拥有自己的 QEMU 对象和 KVM 虚拟机。CPR 通道传送已登记资源的描述符,主通道传送执行状态;客户机执行权只交给一端。 打开原图
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 的完整复制。

通道内容接收时机
cprmagic 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,再让新进程接管

CPR-transfer 的准备、停机与恢复
源端先发送 fd 清单,目标据此准备资源;目标监听 main 后关闭 CPR 通道。源端随后建立 main 并停止客户机,发送执行状态,目标恢复完成后才运行客户机。
源端先发送 fd 清单,目标据此准备资源;目标监听 main 后关闭 CPR 通道。源端随后建立 main 并停止客户机,发送执行状态,目标恢复完成后才运行客户机。 打开原图
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名称长度,包含结尾 NUL32 位整数
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-migratestatus: completed源侧迁移任务完成;仍需核对目标与客户机
旧端 query-statusstatus: postmigrate、running: false旧进程还在,客户机已停止执行
新端 query-statusstatus: 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 后的恢复失败不能靠旧进程继续运行

这些机制把“更新运行虚拟机的软件”拆成可检查的资源保留、状态恢复和执行切换。选择方案时,首先确定哪些资源能够保留、哪些必须重建,再评估状态兼容和失败恢复,最后才比较停机时长。

九、源码与资料:资源清单和状态交接