QEMU QMP 协议实战:从握手到热插拔的完整指南
发布时间:2026/10/9 17:20:09 锦皓数字建站

1. 从一个被忽视的调试入口说起很多人第一次接触 QEMU都是从命令行参数开始的。敲一行qemu-system-aarch64 -M virt -cpu cortex-a57 ...虚拟机就跑起来了。用久了会发现一个尴尬的事虚拟机跑起来之后想动态改点东西——比如热插拔一块盘、临时挂起、查一下当前块设备的实际状态——命令行参数是静态的改不了。这时候就得靠 QMP也就是QEMU Machine Protocol。QMP 是 QEMU 对外暴露的一套基于 JSON 的控制协议。它跑在一个 socket 或者标准输入输出上客户端发 JSON 命令QEMU 回 JSON 响应中间还可能穿插异步事件。你可以把它理解成 QEMU 的遥控器接口命令行是开机前设定好的剧本QMP 是运行中随时能按的按钮。libvirt、virt-manager 这些上层管理工具底层跟 QEMU 对话靠的就是它。热词里那一堆qemu模拟arm64、termux安装qemu、windows使用qemu安装openeular arm虚拟机只要涉及运行中动态操作最后都会落到 QMP 上。这篇东西写给两类人一类是已经在用 QEMU 跑虚拟机、但只会改命令行参数的另一类是想自己写个脚本或小工具去管理 QEMU 实例的。我会把 QMP 的协议结构、握手流程、命令分类、事件机制、实操脚本、以及踩过的坑按我自己的理解顺序讲一遍。看完你应该能自己写一个能连上 QEMU、查状态、做热插拔的小工具。2. QMP 到底是什么为什么不是 QAPI2.1 QMP 和 QAPI 的关系别搞混热词里同时出现了 QMP 和 QAPI这俩经常被混着说但其实是两层东西。QAPI 是 QEMU 内部的 schema 定义框架它用一套自定义的 DSL 描述所有命令、事件、类型然后通过代码生成器生成 C 结构体、JSON 序列化/反序列化代码、以及文档。QMP 是这套 schema 对外暴露的运行时协议也就是 QAPI 定义出来的东西通过 JSON 报文在 socket 上跑起来的那一层。打个比方QAPI 是接口定义文件类似.proto或者 OpenAPI specQMP 是真正跑在网线上的 HTTP/JSON 流量。你写客户端的时候面对的是 QMP但你想知道某个命令有哪些参数、返回什么结构得去翻 QAPI 生成的文档也就是qemu-qmp-ref那份东西。这个区分很重要因为很多人搜QMP 命令列表搜不到其实应该搜 QAPI 的 reference。QEMU 源码里qapi/目录下那一堆.json文件就是所有命令的源头定义。2.2 为什么用 JSON 而不是自定义二进制协议QEMU 早期其实有过别的监控接口比如 HMPHuman Monitor Protocol就是你在 QEMU 窗口里敲info block、quit那种。HMP 是给人看的输出是格式化文本解析起来极其痛苦——不同版本输出格式还不一样写脚本的人被坑得死去活来。QMP 换成 JSON 有几个直接好处。第一结构化字段名和类型明确不用正则去抠文本。第二跨语言任何语言都有 JSON 库Python、Go、Rust 都能几行代码接上。第三可扩展加字段不影响老客户端客户端忽略不认识的字段就行。第四请求响应和事件能统一在一个通道里异步事件和同步响应用同一套 JSON 对象表达靠字段区分。代价是 QMP 比 HMP 啰嗦人肉敲命令不现实。但 QMP 本来就不是给人敲的是给程序用的。真要人肉调试QEMU 还留了 HMP 作为 QMP 的一个子命令human-monitor-command可以两全。2.3 QMP 能干什么一张能力清单不夸张地说QEMU 运行时的几乎所有可动态调整的东西都在 QMP 里。我按用途分几类方便你对号入座类别典型命令用途生命周期stop、cont、system_reset、quit暂停、恢复、重启、退出块设备blockdev-add、device_add、blockdev-snapshot热插拔盘、打快照网络netdev_add、netdev_del动态加网卡后端设备device_add、device_del、device-list-properties热插拔设备、查属性迁移migrate、migrate-incoming、query-migrate在线迁移查询query-status、query-block、query-cpus-fast查各种运行时状态内存query-memory-size-summary、balloon内存气球、容量查询事件SHUTDOWN、RESET、STOP、BLOCK_IO_ERROR异步通知这张表不是全部QAPI 里命令有几百个但日常用得最多的就是上面这些。你如果只是想让脚本能查状态、能优雅关机、能热插一块盘掌握这四类就够了。3. 协议结构握手、命令、事件三件套3.1 连接建立后的第一件事greetingQMP 不是连上就能发命令的有个握手过程。客户端连上 socket 之后QEMU 会先主动推一条greeting消息过来长这样{ QMP: { version: { qemu: {major: 8, minor: 2, micro: 0}, package: }, capabilities: [oob] } }这条消息告诉你三件事QEMU 版本号、支持的 capabilities 列表、以及协议版本。capabilities里如果有oob说明支持 out-of-band 命令也就是某些命令可以插队执行不被主循环阻塞——这个后面讲。收到 greeting 之后客户端必须发一条qmp_capabilities命令才能进入正常命令模式{execute: qmp_capabilities}QEMU 回{return: {}}到这一步握手完成。没发qmp_capabilities之前除了少数几个命令其他一律报错。这是新手最常踩的坑之一连上了、发了命令、收到CommandNotFound或者GenericError八成是忘了握手。3.2 命令报文的两种形态QMP 命令报文有两种写法取决于有没有参数。无参数命令直接给execute字段{execute: query-status}有参数命令参数放在arguments里{ execute: device_add, arguments: { driver: virtio-blk-pci, drive: disk1, id: mydisk } }响应统一是{return: ...}或者{error: ...}。成功时return里是命令的返回值可能是空对象、字符串、数组、复杂结构。失败时error里是{ error: { class: GenericError, desc: Device mydisk is already in use } }class是错误分类desc是给人看的描述。写脚本的时候判断成功失败要看有没有error字段而不是看return是不是空——有些命令成功也返回空对象。3.3 事件异步消息怎么混进来QMP 是单通道的同步响应和异步事件都从同一个 socket 出来。客户端怎么区分看字段。有event字段的是事件有return或error字段的是命令响应。一个典型事件{ event: RESET, data: {guest: true}, timestamp: {seconds: 1700000000, microseconds: 123456} }timestamp是 QEMU 加上去的方便你记录事件发生时间。data是事件携带的负载不同事件结构不同有的没有data。这里有个实操上的大坑如果你的客户端是发一条命令、读一条响应的简单模型遇到事件就会错位。比如你发了query-status结果先收到一个RESET事件你的代码把事件当成响应解析直接崩。正确做法是循环读直到读到带return/error的那条把中间的事件缓存或分发出去。这个逻辑后面给代码。3.4 out-of-band 命令为什么需要插队默认情况下QMP 命令是在 QEMU 主循环里串行执行的。如果某条命令卡住了比如查询一个响应很慢的块设备后面的命令全得排队。oobcapability 就是解决这个的开启后标记为 oob 的命令可以在单独的线程里执行不被主循环阻塞。开启方式是在握手时带上{execute: qmp_capabilities, arguments: {enable: [oob]}}然后发 oob 命令时加个标记{execute: query-status, id: req1, control: {run-oob: true}}注意id字段这是客户端自己生成的请求 ID响应里会原样带回用来匹配请求和响应。在 oob 场景下id几乎是必须的因为响应顺序不再保证和请求顺序一致。普通场景下id可选但建议都带上方便调试。4. 动手从零写一个 QMP 客户端4.1 启动一个带 QMP 的 QEMU 实例先得有个 QEMU 在跑并且开了 QMP。最简方式是用-qmp参数qemu-system-aarch64 \ -M virt -cpu cortex-a57 -m 1024 \ -qmp unix:/tmp/qmp.sock,serveron,waitoff \ -nographic-qmp后面跟的是 QMP 的监听地址。unix:/tmp/qmp.sock表示用 Unix domain socketserveron表示 QEMU 作为服务端监听waitoff表示不阻塞等待客户端连接。也可以用 TCP-qmp tcp:127.0.0.1:4444,serveron,waitoff注意QMP 默认没有任何认证。Unix socket 靠文件权限保护TCP 则完全裸奔。千万不要把 QMP 监听到公网地址上任何能连上的人都能控制你的虚拟机包括执行quit直接关掉它。生产环境要么用 Unix socket要么套一层访问控制。如果你已经有 QEMU 在跑但没开 QMP那就得重启加参数。QMP 没法在运行时动态开启这是设计上的取舍——控制通道必须在启动时确定。4.2 用 Python 写一个最小可用客户端Python 标准库自带socket和json不用装任何东西。下面这个类我用了很久够稳import socket import json class QMPClient: def __init__(self, path): self.sock socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.sock.connect(path) self.buf b self.events [] # 读 greeting self._read_message() # 握手 self.execute(qmp_capabilities) def _read_message(self): while True: # 先看缓冲区里有没有完整的一行 if b\n in self.buf: line, self.buf self.buf.split(b\n, 1) if line.strip(): return json.loads(line) chunk self.sock.recv(4096) if not chunk: raise ConnectionError(QMP connection closed) self.buf chunk def execute(self, cmd, **args): msg {execute: cmd} if args: msg[arguments] args self.sock.sendall((json.dumps(msg) \n).encode()) # 循环读跳过事件 while True: resp self._read_message() if event in resp: self.events.append(resp) continue if error in resp: raise RuntimeError(resp[error][desc]) return resp.get(return) def close(self): self.sock.close()用起来q QMPClient(/tmp/qmp.sock) print(q.execute(query-status)) print(q.execute(query-cpus-fast)) q.close()这段代码的关键点在于execute里的while True循环它把事件和响应分开处理事件存起来响应才返回。这就是 3.3 节说的那个坑的解法。很多人图省事写recv一次就json.loads遇到事件必崩。4.3 一个容易忽略的细节消息边界QMP 用换行符\n分隔消息但一次recv不保证读到完整消息。TCP 是字节流Unix socket 也是一条 JSON 可能被拆成多个包也可能多条消息挤在一个包里。所以必须自己维护缓冲区按\n切分。上面代码里self.buf就是干这个的。每次recv追加到缓冲区然后检查有没有\n有就切出一条完整消息。这个模式是所有基于行分隔协议的客户端都要写的不写迟早出问题——本地测试可能一直正常一上负载就偶发解析失败。实操心得调试 QMP 的时候我习惯在_read_message里加一行print(line)把原始报文打出来。JSON 解析失败时能立刻看到到底收到了什么比猜快得多。5. 常用命令实战查询、热插拔、迁移5.1 查询类命令先看清楚现状再动手动手改之前先查这是铁律。几个最常用的查询命令query-status返回虚拟机运行状态{return: {status: running, singlestep: false, running: true}}status可能是running、paused、shutdown、prelaunch等。写脚本做优雅关机时先查状态如果是paused得先cont再发关机。query-block返回所有块设备信息包括插入的介质、读写统计、是否可写{ return: [ { device: disk1, inserted: { file: /var/lib/vm/disk1.qcow2, drv: qcow2, ro: false }, stats: {rd_bytes: 12345, wr_bytes: 67890} } ] }query-cpus-fast比老的query-cpus快因为它不触发 CPU 状态同步返回每个 vCPU 的线程 ID、是否在线{ return: [ {cpu-index: 0, thread-id: 12345, props: {core-id: 0}} ] }thread-id很有用你可以拿它去taskset绑核或者用perf单独分析某个 vCPU 的性能。5.2 热插拔块设备blockdev-add 加 device_add 两步走热插一块盘是 QMP 最经典的用法但很多人第一次做会失败因为它需要两步而且顺序不能反。第一步把后端存储加进来用blockdev-add{ execute: blockdev-add, arguments: { driver: qcow2, node-name: disk2, file: { driver: file, filename: /var/lib/vm/disk2.qcow2 } } }这里node-name是给这个块节点起的名字后面device_add要引用它。driver是格式层qcow2、rawfile是协议层file、nbd、rbd 等。这种分层结构是 QEMU 块设备模型的核心理解了这个各种存储后端就都能拼出来。第二步把前端设备挂上去用device_add{ execute: device_add, arguments: { driver: virtio-blk-pci, drive: disk2, id: disk2-dev } }drive字段引用第一步的node-nameid是这个设备的唯一标识卸载时要用。卸载反过来先device_del删前端再blockdev-del删后端。{execute: device_del, arguments: {id: disk2-dev}} {execute: blockdev-del, arguments: {node-name: disk2}}注意device_del是异步的命令返回不代表设备已经删干净。设备真正移除后会发一个DEVICE_DELETED事件。如果你紧接着就blockdev-del可能报节点仍在使用。稳妥做法是等DEVICE_DELETED事件到了再删后端。这个坑我踩过不止一次尤其在脚本里连续操作时。5.3 在线迁移migrate 命令族迁移是 QMP 里比较重的一块命令有好几个。最简的用法{ execute: migrate, arguments: { uri: tcp:192.168.1.100:4444, channels: [] } }发出去之后迁移在后台跑你得用query-migrate轮询进度{ return: { status: active, ram: {total: 1073741824, transferred: 536870912, remaining: 536870912}, total-time: 5000 } }status从setup到active到completed中间可能进postcopy-active。ram里的remaining是还没传的内存字节数可以用来估算剩余时间。迁移过程中如果源端内存变化太快remaining一直不降说明脏页产生速度超过了传输速度会进入dirty-limit或者干脆卡住。这时候要么降负载要么开auto-converge{execute: migrate-set-parameters, arguments: {auto-converge: true}}auto-converge会自动给 vCPU 降频牺牲一点性能换迁移能收敛。这是生产环境迁移大内存虚拟机的常用手段。6. 事件机制与错误处理让脚本真正可靠6.1 值得关注的事件清单QMP 事件有几十种日常需要处理的主要是这几类事件触发时机典型用途SHUTDOWN客户机发起关机感知虚拟机要关了RESET虚拟机复位记录重启次数STOP/RESUME暂停/恢复同步状态DEVICE_DELETED设备移除完成确认热拔完成BLOCK_IO_ERROR块设备 IO 出错告警、切盘MIGRATION迁移状态变化跟踪迁移进度GUEST_PANICKED客户机内核 panic自动重启或告警SHUTDOWN事件带一个guest字段true表示是客户机主动发起的比如powerofffalse表示是 QEMU 层面触发的。这个区分在自动化运维里很有用客户机主动关机可以放心回收资源QEMU 层面关机可能是异常得告警。6.2 事件驱动的脚本骨架一个健壮的 QMP 客户端应该是事件驱动的而不是纯轮询。骨架大概这样def run_forever(qmp): while True: msg qmp._read_message() if event in msg: handle_event(msg) elif return in msg or error in msg: handle_response(msg) def handle_event(msg): ev msg[event] if ev SHUTDOWN: print(guest shutdown, guest-initiated:, msg[data][guest]) elif ev BLOCK_IO_ERROR: print(IO error on, msg[data][device]) elif ev DEVICE_DELETED: print(device removed:, msg[data][device])这个模型下命令响应和事件在同一个循环里处理不会错位。如果你用异步框架asyncio、gevent思路一样只是把阻塞读换成异步读。6.3 错误分类与重试策略QMP 的error.class有几个常见值处理策略不同GenericError通用错误通常是参数不对或状态不允许重试没用得改逻辑。CommandNotFound命令不存在可能是 QEMU 版本太老或者拼错了。DeviceNotFound设备不存在检查id或node-name。GenericError里带 is already in use资源冲突先清理再重试。重试策略上我的经验是只有明确是瞬态错误的才重试比如迁移过程中的临时失败。参数错误、状态错误重试一万次也没用只会刷屏。判断瞬态还是永久看desc里的关键词或者干脆维护一个白名单。实操心得写自动化脚本时我给每个 QMP 命令都包一层重试装饰器但重试条件写得很严——只对GenericError且desc包含 try again 或 busy 的重试最多三次间隔指数退避。这样既覆盖了瞬态问题又不会在真错误上死循环。7. 常见问题排查速查表实际用 QMP 遇到的问题八成集中在下面这几类。我整理成表方便对照现象可能原因排查方向连上后发命令报CommandNotFound没发qmp_capabilities先握手响应和预期对不上把事件当响应解析了循环读到return/errorJSON 解析偶发失败没处理消息分片维护缓冲区按\n切device_del后blockdev-del报占用设备还没删完等DEVICE_DELETED事件迁移卡在active不收敛脏页产生太快开auto-converge或降负载TCP 连不上监听地址或防火墙检查-qmp参数和端口命令执行很慢主循环被阻塞考虑开oobcapability收到一堆BLOCK_IO_ERROR底层存储出问题查宿主机磁盘和网络存储再补几个不那么常见但很坑的QEMU 版本差异。QMP 命令在不同 QEMU 版本间有增删改。比如query-cpus在新版本里被标记为 deprecated推荐用query-cpus-fast。写跨版本脚本时先query-qmp-schema拿到当前 QEMU 支持的完整 schema再决定用哪些命令。这个命令返回一大坨 JSON描述了所有命令、事件、类型的定义是自省的利器。id字段冲突。device_add的id必须全局唯一重复会报错。脚本里最好用带前缀的命名比如disk-uuid避免和 QEMU 内部生成的 id 撞车。热插拔 CPU 的限制。不是所有架构和机器类型都支持 CPU 热插拔。x86 上一般可以arm64 的virt机器类型支持有限。动手前先query-hotpluggable-cpus查一下当前配置支持哪些 CPU 槽位别盲目device_add。内存气球和 KSM 的交互。用balloon命令回收内存时如果宿主机开了 KSM回收效果可能和预期不符因为 KSM 已经把相同页合并了。排查内存问题时要把这层考虑进去。8. 我踩过的几个真实坑说几个文档里不会写、但实际会遇到的。第一个是握手时机。有次写脚本连上 socket 后立刻发query-status结果报错。原因是 greeting 还没读完就发了命令QEMU 那边状态机还没准备好。正确做法是连上后先阻塞读一条消息就是 greeting再发qmp_capabilities再发业务命令。顺序错一步都不行。第二个是事件积压。长时间运行的客户端如果不及时读 socketQEMU 那边的事件会堆在缓冲区里堆满了可能阻塞 QEMU 主循环。我见过一个监控脚本只在需要查状态时才读 socket结果跑了一天把 QEMU 卡住了。QMP 连接必须持续读哪怕你暂时不关心事件也得读出来丢掉不能让它积压。第三个是优雅关机的正确姿势。直接发quit是硬关相当于拔电源客户机文件系统可能损坏。优雅关机应该发system_powerdown这会模拟按电源键让客户机自己走关机流程。然后监听SHUTDOWN事件等到了再quit。这个流程在自动化里特别重要我见过太多脚本直接quit导致磁盘镜像损坏的案例。第四个是迁移后的连接处理。迁移完成后源端的 QEMU 进程还在QMP 连接也还在但虚拟机已经跑到目标端了。这时候你在源端发命令行为可能很怪。正确做法是迁移完成后主动断开源端连接去连目标端的 QMP。目标端的 QMP 地址在启动时用-qmp指定或者迁移时通过migrate的uri参数约定。第五个是schema 自省的价值。刚开始我总去网上搜命令用法后来发现query-qmp-schema才是权威。它返回的 schema 里每个命令的参数类型、是否可选、返回结构都写得清清楚楚。写脚本前先拉一份 schema 存下来比翻文档快得多。而且 schema 是运行时拿的和当前 QEMU 版本完全匹配不会有版本对不上的问题。9. 把 QMP 用起来的几个方向QMP 本身是个协议价值在于它能撑起什么。我见过和做过的几个方向自定义监控面板。用 QMP 定期query-block、query-cpus-fast、query-memory-size-summary把数据推到自己的监控系统。比走 libvirt 轻量适合嵌入式或者资源受限的场景。自动化测试编排。测试用例里需要动态插盘、拔盘、模拟 IO 错误QMP 的blockdev-add、device_del、block_set_io_throttle正好覆盖。配合事件监听能写出很精细的测试流程。故障注入。QMP 有一些命令专门用来模拟异常比如blockdev-snapshot配合blockdev-del可以模拟存储故障stop/cont可以模拟卡顿。做高可用测试时很有用。轻量管理工具。不想引入 libvirt 那一整套依赖时直接用 QMP 写个几十行的管理脚本能启动、查询、关机、迁移够用。热词里那些termux安装qemu、windows使用qemu安装openeular arm虚拟机的场景往往就是这种轻量需求QMP 脚本比装 libvirt 合适得多。和上层工具集成。很多编排系统需要和 QEMU 对话QMP 是标准入口。理解 QMP 之后看 libvirt 的源码或者调试 libvirt 和 QEMU 之间的问题会清晰很多——你知道 libvirt 发的每条命令到底在干什么。最后分享一个小技巧调试 QMP 时可以用socat把 Unix socket 转成 TCP然后用nc或者浏览器插件手动发 JSON比写代码快。命令是socat UNIX-CONNECT:/tmp/qmp.sock TCP-LISTEN:5555,reuseaddr然后nc localhost 5555就能手敲命令了。不过记得手敲时每条 JSON 后面要跟换行否则 QEMU 不认。这个方式适合快速验证某个命令的参数格式验证完再写进脚本。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。