把管道两端和缓冲区一起交给命令对象
virCommandSetSendBuffer 统一接管数据和管道,让虚拟 TPM 调用方只负责取得口令、传入缓冲区并组装参数。
工程问题#
启动 swtpm 前要把口令经管道送给子进程。旧调用方既创建管道,又传递写端和缓冲区,还要在多处失败分支关闭两端、清空已转移指针,容易把资源归属判断分散到不同层。
实际实现#
接收双指针后立即偷取缓冲区并清空调用方指针,本地 g_autofree 接住失败早退;成功后再把缓冲区和管道两端转交命令对象管理。错误记录到 cmd->has_error,统一由命令执行路径报告。
处理流程#
- 入口消费缓冲区,使调用方指针立刻变为 NULL
- 创建管道并准备非阻塞写端,失败时本地释放并登记错误
- 成功后转交 sendBuffers 和 pass-fd 列表,由命令对象负责后续 I/O 与清理
源码片段#
固定版本 · 原文摘录1716virCommandSetSendBuffer(virCommand *cmd,1717 unsigned char **buffer,1718 size_t buflen)1719{1720 g_autofree unsigned char *localbuf = g_steal_pointer(buffer);1721 int pipefd[2] = { -1, -1 };1722 size_t i;17231724 if (virCommandHasError(cmd))1725 return -1;17261727 if (virPipeQuiet(pipefd) < 0) {1728 cmd->has_error = errno;1729 return -1;1730 }17311732 if (fcntl(pipefd[1], F_SETFL, O_NONBLOCK) < 0) {1733 cmd->has_error = errno;1734 VIR_FORCE_CLOSE(pipefd[0]);1735 VIR_FORCE_CLOSE(pipefd[1]);1736 return -1;1737 }入口通过双指针消费调用方缓冲区,把原指针置空;本地 g_autofree 在早退时负责释放。pipe 或 fcntl 失败写入命令对象的错误状态,fcntl 失败还显式关闭两端 FD。
1739 i = virCommandGetNumSendBuffers(cmd);1740 VIR_REALLOC_N(cmd->sendBuffers, i + 1);17411742 cmd->sendBuffers[i].fd = pipefd[1];1743 cmd->sendBuffers[i].buffer = g_steal_pointer(&localbuf);1744 cmd->sendBuffers[i].buflen = buflen;1745 cmd->sendBuffers[i].offset = 0;17461747 cmd->numSendBuffers++;17481749 virCommandPassFD(cmd, pipefd[0], VIR_COMMAND_PASS_FD_CLOSE_PARENT);17501751 return pipefd[0];1752}成功后缓冲区与写端转给 sendBuffers,读端通过 virCommandPassFD 登记。第二次 g_steal_pointer 取消本地自动释放,返回的 FD 仅用于组装参数,调用者不再负责关闭它。
259 conn = virGetConnectSecret();260 if (!conn)261 return -1;262263 memcpy(seclookupdef.u.uuid, secretuuid, sizeof(seclookupdef.u.uuid));264 if (virSecretGetSecretString(conn, &seclookupdef,265 VIR_SECRET_USAGE_TYPE_VTPM,266 &secret, &secret_len) < 0)267 return -1;268269 *fd = virCommandSetSendBuffer(cmd, &secret, secret_len);270 return 0;271}虚拟 TPM 准备取得口令后,把 &secret 直接交给命令对象。函数前部的 secret 声明使用 g_autofree;转移后被置空。SetSendBuffer 内部错误延迟到 virCommand 执行阶段报告,因此此处返回 0 只表示准备流程继续,并不代表管道已成功写完。
两个片段展示同一函数的失败和成功分支,另附 swtpm 准备阶段的实际调用;没有省略号拼接。
收益与取舍#
工程分析带来的收益
- 调用方在成功与失败路径遵守同一种缓冲区所有权规则
- 创建、传递和回收管道收敛到同一个抽象,TPM 调用方不再自行拼接这些步骤
付出的代价
- 消费式接口不能在失败后保留原始缓冲区重试;调用者必须知道传入后指针会清空
- 准备步骤把错误留在命令对象中,必须继续检查 virCommandRun 等最终执行结果;g_autofree 也不等于安全擦除
可比较的选择
调用方创建 pipe 并分步转交libvirt 2021 年前的真实实现;每个调用方要协调缓冲区、读端和写端的不同释放时机。
只在成功时接管缓冲区这是可行的 API 约定,但调用方需要根据返回结果切换资源归属;此实现选择无条件消费以减少这种分支。
版本与适用范围#
固定于 libvirt v10.10.0 的非 Windows 实现及 QEMU 虚拟 TPM 调用路径。这是所有权 API 与错误处理写法,不把 GLib 自动清理机制说成 libvirt 的发明。
参考来源#
源码、官方文档与原始论文src/util/vircommand.c · virCommandSetSendBuffer · ownership transfer
相关实现与比较#
3 个相关案例nginx 将小额分配变成池内游标推进,并以池的生命周期统一释放;较大的分配另建记录管理。
QEMU 重建 FlatView 后发布新指针,已有读者继续使用旧视图,直到引用归还且 RCU 宽限期结束才销毁。
libvirt 把 FD 和定时器接到 GLib 主循环,将停止监听与释放对象分开,并提高连接清理任务的优先级,避免资源用尽时清理一直排不上。