libvirt/qemu内存快照的实现原理分析记录
libvirt 内存快照实现原理深度分析
1. 概述
当你执行virsh snapshot-create-as <domain> --memspec savefile或virsh snapshot-create <domain> snapshot.xml(其中 XML 指定了<memory snapshot='external'/>),libvirt 会创建一个包含虚拟机内存状态的快照。这看似简单的一条命令,背后却涉及 libvirt 多层架构的精密协作——从公共 API 到 QEMU 驱动,再到 QEMU Monitor 命令。本文将逐层拆解。
2. 快照类型与内存快照定位
libvirt 定义了三种内存快照位置(snapshot_conf.h):
typedefenum{VIR_DOMAIN_SNAPSHOT_NOSTATE=VIR_DOMAIN_NOSTATE,VIR_DOMAIN_SNAPSHOT_RUNNING=VIR_DOMAIN_RUNNING,VIR_DOMAIN_SNAPSHOT_PAUSED=VIR_DOMAIN_PAUSED,VIR_DOMAIN_SNAPSHOT_SHUTOFF=VIR_DOMAIN_SHUTOFF,// ...VIR_DOMAIN_SNAPSHOT_DISK_SNAPSHOT,// 磁盘快照专用状态}virDomainSnapshotState;快照定义的核心数据结构(snapshot_conf.h):
struct_virDomainSnapshotDef{virDomainMomentDef parent;intstate;// 快照时的域状态virDomainSnapshotLocation memory;// 内存快照类型:internal/external/nochar*memorysnapshotfile;// 外部内存状态文件路径size_tndisks;virDomainSnapshotDiskDef*disks;// ...};关键字段解读:
memory:决定内存快照是internal(嵌入 qcow2 磁盘文件)、external(独立文件)还是no(不保存内存)memorysnapshotfile:当 memory 为 external 时,保存内存状态的文件路径state:快照时刻 VM 的状态,回滚时用于判断是否需要恢复内存
--memspec本质就是设置memory = VIR_DOMAIN_SNAPSHOT_LOCATION_EXTERNAL,并指定memorysnapshotfile。
3. API 入口与调用链
整体调用链如下:
virDomainSnapshotCreateXML() // 公共 API: src/libvirt-domain-snapshot.c └→ qemuDomainSnapshotCreateXML() // QEMU 驱动实现: src/qemu/qemu_driver.c └→ qemuSnapshotCreateXML() // 快照核心逻辑: src/qemu/qemu_snapshot.c4. 准备阶段:qemuSnapshotPrepare
在真正执行快照前,qemuSnapshotPrepare()(qemu_snapshot.c)负责校验合法性。关键逻辑:
staticintqemuSnapshotPrepare(virDomainObj*vm,virDomainSnapshotDef*def,bool*has_manual,unsignedint*flags){bool active=virDomainObjIsActive(vm);// ...for(i=0;i<def->ndisks;i++){switch(disk->snapshot){caseVIR_DOMAIN_SNAPSHOT_LOCATION_INTERNAL:found_internal=true;// 活跃域不能使用内部磁盘快照 + 外部内存快照的组合break;caseVIR_DOMAIN_SNAPSHOT_LOCATION_EXTERNAL:external++;break;// ...}}// 关键约束:内部内存快照要求所有磁盘都参与内部快照if((def->memory==VIR_DOMAIN_SNAPSHOT_LOCATION_INTERNAL&&!found_internal)||(found_internal&&forbid_internal)){// 报错:内部快照要求所有磁盘都参与}// 不允许混合内部和外部if((def->memory==VIR_DOMAIN_SNAPSHOT_LOCATION_EXTERNAL&&found_internal)){// 报错:混合内部和外部目标不支持}}核心约束:
- 外部内存快照(
--memspec)+ 外部磁盘快照:✅ 合法组合 - 外部内存快照 + 内部磁盘快照:❌ 不允许
- 内部内存快照 + 外部磁盘快照:❌ 不允许
- 内部内存快照要求所有磁盘都参与内部快照
5. 核心执行:qemuSnapshotCreateActiveExternal
这是内存快照的真正核心入口(qemu_snapshot.c):
staticintqemuSnapshotCreateActiveExternal(virQEMUDriver*driver,virDomainObj*vm,virDomainMomentObj*snap,virQEMUDriverConfig*cfg,bool has_manual,unsignedintflags){bool memory=snapdef->memory==VIR_DOMAIN_SNAPSHOT_LOCATION_EXTERNAL;bool thaw=false;bool resume=false;// ...整个流程可以分解为以下阶段:
5.1 文件系统冻结(Quiesce)
if(flags&VIR_DOMAIN_SNAPSHOT_CREATE_QUIESCE){frozen=qemuSnapshotFSFreeze(vm,NULL,0);if(frozen>0)thaw=true;// 标记后续需要解冻}如果用户指定了--quiesce,libvirt 通过 QEMU Guest Agent 调用fsfreeze冻结客户机文件系统,确保磁盘数据一致性。
5.2 暂停虚拟机
if(virDomainObjGetState(vm,NULL)==VIR_DOMAIN_RUNNING){if(memory&&!has_manual)resume=true;// 标记后续需要恢复运行if(((memory||has_manual)&&!(flags&VIR_DOMAIN_SNAPSHOT_CREATE_LIVE))){if(qemuProcessStopCPUs(driver,vm,VIR_DOMAIN_PAUSED_SNAPSHOT,VIR_ASYNC_JOB_SNAPSHOT)<0)gotocleanup;}}关键点:
- 内存快照必须暂停 VM(非 Live 模式下),因为需要获取一个一致的内存状态
resume标记记录了 VM 原本是运行状态,快照完成后需要恢复VIR_DOMAIN_SNAPSHOT_CREATE_LIVE标志允许不暂停 VM(但这是渐进式,更复杂)
5.3 创建外部磁盘快照
if(qemuSnapshotCreateActiveExternalDisks(vm,snap,blockNamedNodeData,flags,asyncJob)<0)gotocleanup;外部磁盘快照通过 QMPtransaction命令原子性地完成:
- 为每个磁盘创建 qcow2 overlay 文件
- 使用
blockdev-snapshot-sync或blockdev-snapshot命令 - 原始磁盘变为只读 backing file,新 overlay 成为活跃层
5.4 保存内存状态
这是内存快照最关键的步骤:
if(memory){// 将 VM 状态保存到 memorysnapshotfile 指定的文件if(qemuSaveImageCreate(driver,vm,snapdef->memorysnapshotfile,compressor,asyncJob)<0)gotocleanup;memory_unlink=true;}qemuSaveImageCreate内部调用 QEMU Monitor 的migrate命令,将内存页转储到文件:
QMP: {"execute": "migrate", "arguments": {"uri": "exec:cat > /path/to/savefile"}}本质上,这个过程和virsh save一样——QEMU 的 live migration 机制被复用来将内存页写入文件。保存的数据包含:
- libvirt save header:包含 XML 域定义、cookie 等
- QEMU vmstate:设备状态
- 内存页数据:客户机物理内存的完整转储
5.5 恢复虚拟机运行
cleanup:if(thaw)qemuSnapshotFSThaw(vm,false);// 解冻文件系统if(resume&&virDomainObjIsActive(vm))qemuProcessStartCPUs(driver,vm,VIR_DOMAIN_RUNNING_UNPAUSED,VIR_ASYNC_JOB_SNAPSHOT);// 恢复 CPU 运行5.6 错误回滚
if(ret<0&&memory_unlink&&!memory_existing)unlink(snapdef->memorysnapshotfile);// 删除未完成的内存转储文件如果过程中任何步骤失败,已创建的内存文件和磁盘 overlay 都会被回滚清理。
6. 内部内存快照路径
对于内部内存快照(memory = VIR_DOMAIN_SNAPSHOT_LOCATION_INTERNAL),走的是完全不同的路径(qemu_snapshot.c):
staticintqemuSnapshotCreateActiveInternal(virQEMUDriver*driver,virDomainObj*vm,virDomainMomentObj*snap,unsignedintflags){// 1. 暂停 VMif(virDomainObjGetState(vm,NULL)==VIR_DOMAIN_RUNNING){qemuProcessStopCPUs(driver,vm,VIR_DOMAIN_PAUSED_SAVE,...);resume=true;}// 2. 通过 QMP 发送 snapshot-save 命令job=qemuSnapshotCreateActiveInternalStart(vm,snapdef);// 内部调用: qemuMonitorSnapshotSave()// 3. 等待异步 job 完成while((rv=qemuSnapshotCreateActiveInternalDone(vm,job))!=1){qemuDomainObjWait(vm);}}内部快照使用 QMP 命令snapshot-save:
rc=qemuMonitorSnapshotSave(qemuDomainGetMonitor(vm),jobname,snapdef->parent.name,devices[0],(constchar**)devices);这个命令将 vmstate(内存 + 设备状态)嵌入到 qcow2 磁盘映像文件中。QEMU 的 qcow2 格式本身支持这种 vmstate 存储机制。
外部 vs 内部内存快照对比:
| 维度 | 外部(--memspec) | 内部 |
|---|---|---|
| 内存存储位置 | 独立文件 | qcow2 磁盘文件内部 |
| 磁盘快照类型 | 必须 external | 必须 internal |
| QEMU 命令 | migrate到文件 | snapshot-save |
| 磁盘格式要求 | 无特殊 | 必须 qcow2 |
| 文件系统影响 | 额外产生一个文件 | qcow2 文件膨胀 |
7. 回滚(Revert)到内存快照
当执行virsh snapshot-revert回滚到内存快照时,流程如下(在qemuSnapshotRevertActive中):
如果目标快照包含内存状态(
memory != NO且state == PAUSED/RUNNING):- 如果 VM 正在运行,先停止 VM
- 使用
qemuProcessStart()重新启动 VM,并传入内存快照文件 qemuProcessStart内部通过 QEMU 的-incoming参数和migrate incoming恢复内存状态- 如果快照状态是
PAUSED,恢复后保持暂停
如果目标快照没有内存状态(
state == SHUTOFF):- 停止 VM
- 切换磁盘到快照对应的状态
- VM 保持关闭
8. QEMU 层面:内存保存的底层机制
当 libvirt 调用qemuSaveImageCreate时,最终的 QEMU 执行路径:
libvirt: qemuSaveImageCreate() └→ qemuMonitorMigrateToFile() └→ QMP: {"execute": "migrate", "arguments": {"uri": "exec:..."}} └→ QEMU: migration/thread → RAM save → vmstate_save → 写入文件QEMU 的 migration 框架被复用于内存转储:
- RAM save:遍历客户机物理内存页,逐页写入目标(通常经过压缩)
- vmstate save:保存所有虚拟设备的状态(CPU 寄存器、设备配置等)
- 数据流经过 libvirt 的 compressor 管道(可选压缩),最终写入
memorysnapshotfile
9. 完整流程图
virsh snapshot-create-as --memspec savefile │ ▼ virDomainSnapshotCreateXML() ← 公共 API │ ▼ qemuDomainSnapshotCreateXML() ← QEMU 驱动 │ ▼ qemuSnapshotCreateXML() ← 解析 XML、创建 snapdef │ ▼ qemuSnapshotPrepare() ← 校验磁盘/内存快照组合合法性 │ ▼ qemuSnapshotCreateActiveExternal() │ ├── 1. FSFreeze (quiesce) ← QEMU Guest Agent ├── 2. StopCPUs ← 暂停 VM ├── 3. External Disk Snapshot ← QMP transaction (blockdev-snapshot) ├── 4. Save Memory State ← QMP migrate → memorysnapshotfile ├── 5. FSThaw ← 解冻文件系统 └── 6. StartCPUs ← 恢复 VM 运行10. 总结
libvirt 的内存快照实现是一个精密的多层协作过程:
- 配置层(
snapshot_conf):定义快照元数据,区分memory字段的 internal/external/no - 校验层(
qemuSnapshotPrepare):确保磁盘和内存快照类型的兼容性 - 执行层(
qemuSnapshotCreateActiveExternal):编排暂停、磁盘快照、内存转储、恢复的完整序列 - QEMU 层:通过 QMP
transaction完成磁盘快照,通过migrate完成内存转储
外部内存快照(--memspec)的设计哲学是解耦——内存状态和磁盘状态分别存储在独立文件中,不依赖 qcow2 的内部快照能力,这为存储后端的灵活性提供了基础。
