Libvirt RPC 的编码与调用分发
一、Libvirt RPC 与跨进程状态查询
应用程序调用 Libvirt API 时,连接 URI 决定请求交给哪个驱动。对于常见的 qemu:///system 连接,客户端通过 RPC 访问管理守护进程,再由服务端 QEMU 驱动处理虚拟机对象。应用拿到的 virDomainPtr 是本进程中的句柄,跨进程传输的是对象标识与参数。
本文跟踪一次 virDomainGetInfo():查询已有虚拟机的状态、内存、vCPU 数和 CPU 时间。连接建立与认证已完成;传输采用本机 UNIX socket,服务端可以是 libvirtd 或模块化的 virtqemud。
以虚拟机 demo 为例,客户端把 UUID 等标识编码进 CALL;服务端按过程号选择查询函数,用 UUID 找到对象,再把结果放入 REPLY。客户端靠调用序号找到原等待者。编码解决跨进程表示,分发决定执行哪项操作,序号解决多个调用并发时的响应归属,下面依次展开这三段。
源码版本:Libvirt 12.0.0。
二、从驱动接口到 RPC 请求
virDomainGetInfo() 校验参数后调用 conn->driver->domainGetInfo。客户端连接绑定 remote 驱动时,这个函数指针指向 remoteDomainGetInfo();服务端的本地 QEMU 连接则指向 qemuDomainGetInfo()。相同的公开 API 可以在两端出现,实际执行者由连接对象上的驱动表决定。
过程号、参数结构和返回结构在 remote_protocol.x 中定义,构建时生成 XDR 编解码与两端样板代码。查询操作的过程号是 REMOTE_PROC_DOMAIN_GET_INFO = 16;通用 remote program 为 0x20008086,其 RPC 协议版本为 1,可由不同 Libvirt 软件版本共同使用。程序与版本号、过程定义
| 对象 | 所在位置 | 保存的内容 |
|---|---|---|
virDomainPtr | 调用者进程 | 所属连接、名称、UUID 和运行时 ID;不包含 QEMU 进程的完整状态 |
remote_nonnull_domain | RPC 参数 | name、16 字节 uuid、32 位 id,不传指针 |
virNetMessage | 两端 RPC 层 | 解码后的头、字节缓冲区、长度与当前位置,负责消息读写 |
virDomainObj | 服务端 QEMU 驱动 | 定义、运行状态、进程 PID、作业与驱动私有数据;由驱动按 UUID 查找 |
客户端 callFull() 递增连接上的计数器,假设为 demo 的本次查询取得 serial 42,再由 virNetClientProgramCall() 填充头、编码参数并等待响应。等待期间释放 remote 驱动锁,使事件与流数据的处理不被这把锁阻塞。调用记录与消息仍保留,后续收到 serial 42 的响应才能继续完成这次 API 调用。调用入口
三、示例:56 字节请求与 60 字节响应
UNIX stream socket 和 TCP 都提供字节流,单次读取可能只得到半条消息,也可能带来多条消息的数据。Libvirt 先读取 4 字节长度,再按长度接收其余部分。长度包含自身,后接固定 24 字节头与过程专用载荷;接收端检查长度边界后才扩展缓冲区。线格式、长度解码
| 字节偏移 | 字段 | 本次 CALL 的值 | 含义 |
|---|---|---|---|
| 0–3 | length | 56 | 完整消息长度,含前面的 4 字节 |
| 4–7 | prog | 0x20008086 | 选择 RPC 程序 |
| 8–11 | vers | 1 | 程序的协议版本 |
| 12–15 | proc | 16 | 查询虚拟机信息 |
| 16–19 | type | 0,CALL | 区分调用、响应、事件和流数据 |
| 20–23 | serial | 42 | 本次调用的序号,响应必须保留 |
| 24–27 | status | 0,OK | CALL 固定为 OK;响应可携带错误状态 |
XDR 按大端序编码整数,并以 4 字节为基本对齐单位。C 结构中的 char 和 short 在 XDR 中也占 4 字节,hyper 占 8 字节;字符串由长度和内容构成,必要时补齐到 4 字节边界,不传结尾 NUL。因此报文不能用 C 结构的 sizeof 或本机内存布局解释。
公共长度界定整条消息的字节范围,载荷中的字符串或数组长度用于解析具体结构,两者分别校验。协议对消息、字符串和各类数组设置上限,解码器按具体过程的结构处理。这样可避免对端声明一个巨大长度后,接收进程直接按该值无限分配内存;结构解码成功也不等于业务参数有效,驱动仍要验证实际操作。
Mermaid 源码
flowchart TB
accTitle: Libvirt RPC 的报文组成
accDescr: 一条 RPC 消息先有四字节总长度,再有二十四字节公共头,然后按消息类型和状态携带调用参数、返回值、错误或事件。GetInfo 示例请求五十六字节,响应六十字节。
L["长度字段 · 4 B<br/>包含长度字段自身"] --> H["公共头 · 24 B<br/>prog / vers / proc / type / serial / status<br/>六个 XDR 字段,每个 4 B"]
H --> C["CALL / OK<br/>GetInfo 参数 · 28 B<br/>name + UUID + id"]
H --> R["REPLY / OK<br/>GetInfo 结果 · 32 B<br/>状态、内存、vCPU 数、CPU 时间"]
H --> E["REPLY / ERROR<br/>virNetMessageError<br/>错误码、错误域、文本等"]
H --> A["MESSAGE / OK<br/>异步事件载荷<br/>按事件过程号解码"]
C --> CT["示例请求 · 56 B<br/>proc = 16,serial = 42"]
R --> RT["示例响应 · 60 B<br/>proc = 16,serial = 42"]
classDef frame fill:#e4f2ef,stroke:#5e8f87,color:#203e3b;
classDef payload fill:#edf3f8,stroke:#7591ac,color:#233b50;
classDef error fill:#fbf0e6,stroke:#b58a60,color:#5f432b;
class L,H frame;
class C,R,A,CT,RT payload;
class E error;
以名称 demo 为例,请求载荷为 name 长度 4 B + 名称 4 B + UUID 16 B + id 4 B = 28 B,加公共部分共 56 B。若名称是 vm1,3 字节内容后补 1 字节,长度仍是 56 B。UUID 是固定长度 opaque,不额外增加长度字段。对象标识
请求:length=56,CALL / OK,serial=42
payload = name("demo") | UUID[16] | id
响应:length=60,REPLY / OK,serial=42
payload = state[4] | maxMem[8] | memory[8]
| nrVirtCpu[4] | cpuTime[8]
示例:state=RUNNING,maxMem=8388608 KiB,memory=8388608 KiB
nrVirtCpu=4,cpuTime=2500000000 ns
返回载荷共 32 字节,完整响应是 60 字节。maxMem 与 memory 的单位是 KiB,cpuTime 的单位是纳秒。实际值由驱动查询得出,上例只展示编码关系。返回结构
四、服务端分发与对象查询
Mermaid 源码
sequenceDiagram
accTitle: 一次 virDomainGetInfo 调用的往返
accDescr: 客户端公开 API 经 remote 驱动编码请求;服务端 RPC 分发到 QEMU 驱动,按 UUID 查找对象并读取信息;结果携带相同 serial 返回给对应等待调用。
participant A as 应用与客户端 API
participant C as remote 驱动与 RPC 客户端
participant S as 守护进程 RPC 分发
participant D as 服务端 QEMU 驱动
A->>C: virDomainGetInfo(domain, info)
C->>C: 编码 domain 标识,分配 serial=42
C->>S: CALL / proc=16 / serial=42
S->>S: 定位程序与过程,认证,XDR 解码
S->>D: 服务端 virDomainGetInfo → qemuDomainGetInfo
D->>D: 按 UUID 查找并锁住 virDomainObj
D->>D: 对象 ACL;读状态、内存、vCPU、进程 CPU 时间
D-->>S: 填充 virDomainInfo,释放对象引用与锁
S-->>C: REPLY / OK / serial=42 + XDR 返回值
C->>C: prog、vers、serial 匹配等待者,再检查 proc
C-->>A: 解码为本地 info,返回 0
Note over C,S: 异步 MESSAGE 经另一条分发路径处理,不占用普通 API 返回值
服务端按 program/version 找程序,按 proc 找 virNetServerProgramProc。该表项包含处理函数、参数与返回结构大小、XDR 过滤函数和认证要求。通用分发器检查认证,解码参数,设置当前客户端身份,再调用过程处理函数;QEMU 驱动随后执行对象级 ACL 检查。认证通过只说明客户端身份可被接受,仍需判断它能否读取目标虚拟机。通用分发
生成的 remoteDispatchDomainGetInfo() 从客户端上下文取得服务端连接,把 name/uuid/id 转成本地句柄,再调用公开 API。QEMU 驱动通过 UUID 查找并锁住 virDomainObj。这段代码位于 Libvirt 守护进程中,从管理对象取得状态、内存和 vCPU 信息;对活跃实例,再按其 QEMU PID 读取宿主进程 CPU 时间。RPC 的实际工作因而包含对象查询与宿主统计读取,而不要求每项字段都另发 QMP。对象查找、GetInfo 实现
名称便于展示,运行时 ID 随实例启动而分配,UUID 则用于这里的对象定位。客户端持有旧句柄时,服务端对象可能已经删除或状态改变,因此发送一个曾经有效的 ID 不能跳过查找与权限检查。获得句柄和执行查询属于不同时间点,对象锁只保护本次访问的一致性。
返回值填入 remote_domain_get_info_ret,RPC 层复用请求头,只把 type 改为 REPLY、status 改为 OK,再编码载荷并加入发送队列。本例由此生成前节的 60 字节响应,保留 serial 42。服务端查询已结束,客户端仍需收齐消息、找到原等待者并解码结果,才能完成这次 API 调用。两端样板代码由协议生成器维护。
五、响应匹配与错误分类
多个线程可以使用同一连接发出调用。例如 serial=42 的请求先发出,serial=43 的请求却先执行完;客户端用 prog + vers + serial 将响应交给各自等待记录。上层继续检查 proc、type 和 serial,再按该过程的返回过滤函数解码。这个关联允许调用交错完成,同时保留各自的返回值与错误。等待队列匹配、响应检查
serial 42 的成功响应解码后,客户端把 state、memory 等字段填入调用者传入的 virDomainInfo,virDomainGetInfo() 返回 0。调用者此时得到本次查询结果;demo 随后仍可继续执行或改变状态,下次查询会产生新的调用序号与结果。
生命周期等异步事件使用 MESSAGE 类型,经事件分发路径处理,不会当作普通 API 返回值。协议还定义了 STREAM、带文件描述符的调用和响应等类型;流控制与 FD 传递有各自规则,不能直接套用本例的固定载荷布局。
目标 UUID 不存在、ACL 拒绝或驱动查询失败时,服务端返回 REPLY / ERROR,载荷改为 virNetMessageError,包含错误域、错误码、级别及文本等信息。客户端恢复本地错误对象,API 返回 -1。连接断开和解码失败则可能直接终止等待,连一条完整的错误响应都收不到。错误结构
serial 负责本连接内的响应关联,不是服务端的持久化去重键。尤其对于启动、修改配置等写操作,断线时请求可能已经执行,只是响应尚未抵达;应用应先查询实际状态,再决定是否重试。启动过程见Libvirt 虚拟机启动的进程与状态管理。
六、协议复用、接口扩展与兼容成本
RPC 起初服务于客户端库与 libvirtd 的通信,后来公共消息、连接和分发代码独立为可复用的 RPC 基础设施。program/version 选择一组协议,procedure 选择其中的操作,具体参数则由各自的 XDR 定义。新服务因而可以复用消息收发和认证,而不必把所有业务都塞进同一张过程表。RPC 架构文档
这种分离也有维护约束:过程号和已经发布的字段布局需要保持兼容,公共编解码成功后仍要做业务权限与参数检查。客户端和服务端由同一协议定义生成样板,减少手写两份编码的偏差,但不能替代对接口语义的设计。后来的守护进程拆分改变了消息最终到达的进程,上层 API 和 RPC 边界得以保留。
对本例,可以依次核对四个结果:完整请求已按 length 接收;proc=16 已分派;UUID 查询取得对象;serial=42 的响应已交回调用者。请求延迟可能出现在连接、RPC 排队或驱动执行中,应分段定位;一次网络往返时间不能代表整次 API 调用的全部耗时。