跳转至

增加零拷贝和新后端以后,怎样保持接口可演进

SDK 首版采用输入复制。新的设备吞吐更高,团队希望增加借用 buffer、更多 payload 和另一个后端。如果只是扩结构体、改宏、加几个平台分支,旧调用者可能仍然编译通过,行为与布局却已改变。

复杂度与演进已讨论变化传播。本章将它落到 API、ABI、协议、所有权和发布组合的具体决定。下文 v2 是演进方案,公开源码仍是版本 1。

先写出一次变化触碰了什么

当前公开头文件包含固定容量宏、config、caps、notice、key 和 callback。wire 使用显式序列化,内核软件设备使用独立 UAPI。

新需求 变化范围 需要保留或重新约定
控制 payload 更大 wire、core 槽、入口副本、预算 上限、拒绝、截断与资源
借用输入 API、buffer 归还、所有错误分支 最后使用点、取消与停止
新设备后端 能力、发送、完成、恢复 terminal 义务与身份
异步 callback 交付 交付队列、消费者与销毁 输出借用寿命、顺序、满时行为
新配置字段 源码、ABI、默认行为 旧调用者传入了多少字节

这张表先约束改动范围,再比较方案。如果一次容量变化需要业务应用理解多个 backend 特例,就应检查边界是不是泄漏了实现。

API、ABI 与 wire 分开承诺

源码 API 兼容回答旧源代码能否重编并保留语义;ABI 兼容回答已编译程序能否继续调用新库;协议兼容回答不同版本端点能否正确交互。三个承诺不能靠一个版本号同时覆盖。

例如给 config 尾部加字段:旧源码重新编译后可能自动得到零初始化;但旧二进制只分配旧尺寸,新库直接读新字段会越界。返回值为结构体的接口还受调用约定和布局影响。

当前 sdk_caps 按值返回,不能在保持 ABI 的口号下直接增加返回结构。是否支持预编译库替换应明确列出;本项目目前提供源码重编的交付方式,不承诺任意旧二进制透明兼容。

扩展结构时,把长度作为协议的一部分

一种未来候选是引入独立 v2 入口,以大小与版本限定配置读取。例如概念接口:

/* 设计示意,当前 sdk.h 尚未提供。 */
int sdk_create_v2(struct sdk **out,
                  const void *config, size_t config_size,
                  sdk_callback callback, void *context);

实现先验证最小前缀与版本,按已知尺寸读取字段,未提供字段采用文档化默认值。未来字段、未知版本、保留位和非法长度怎样处理,都要明确规定并测试。

另一种候选是保留旧入口,通过新的 setter / builder 配置可选能力。它减少一次性结构扩张,却引入配置阶段与运行阶段的状态限制;setter 是立即生效还是下一次 start 生效,也要写清。

方案 适合的变化 主要代价
独立 v2 配置入口 一组需要同时成立的新约束 两代入口、转换与支持矩阵
setter / builder 可组合的可选配置 状态机、失败回滚与中间状态
修改旧结构并要求全量重编 可控制全部消费者的内部交付 放弃旧 ABI,发布与回滚必须同步

opaque handle 隐藏内部布局,但 public config / notice 仍属于接口。不能因为句柄是 opaque 就忽略其它结构的兼容性。

能力协商比按版本猜功能更准确

版本表示编码与行为规则,能力表示当前构建、设备和会话支持什么。较新的库可能裁掉数据流,旧设备也可能实现某个可选能力。

当前能力包含 payload 上限、请求额度与 remote_cancel / pause_data。公开实现明确报告后两项不支持。未来协商应在接受业务之前完成,得到完整上限和可选能力;不支持时明确拒绝或采用已约定的降级。

不能收到超长消息就截断后交给上层,也不能把不识别的版本字段跳过去当作有效请求。兼容转换层要验证长度、枚举、保留位、整数范围和身份,并把语义变化写入版本规则。

零拷贝先定义归还,再计算收益

当前输入复制让 submit 返回后调用者可复用原 buffer。若新增借用接口,应保留旧接口语义,通过单独入口或显式模式定义新契约。

至少需要一份归还表:

路径 buffer 谁持有 归还事件
参数或 admission 同步失败 调用者 调用返回,不产生异步归还
入队但被 REJECT SDK 在约定区间内借用 唯一归还通知
正常完成 SDK / backend / 设备 最后使用结束后归还
超时 / cancel 可能仍有远端访问 业务 terminal 与安全归还是否分离
reset / stop 清算在途访问 硬件与软件活动静止后归还

对真实 DMA,业务超时不等于设备停止访问。可能需要把请求 terminal 与 buffer-release 分成两个事件;否则调用者收到超时后归池,设备晚到写入就破坏了另一个对象。

“零拷贝”也应列出究竟消除了哪一次复制。入口、协议封装、socket、设备 buffer 和 callback 之间仍可能复制;借用引入的 pin、映射、缓存同步、对齐和停止成本都进入资源账。已有复制版本是比较基线,新方案的收益必须用同负载与相同正确性条件测量。

行为兼容往往比布局兼容更容易遗漏

把同步 callback 改成异步交付,函数签名可能不变,但顺序、重入、payload 有效期和 stop 的完成定义已经变化。改变默认 deadline、满时丢弃策略或取消获胜规则,同样可能破坏调用者假设。

每次发布列出哪些行为保持、哪些改变、如何迁移,以及旧调用者会怎样失败。需要废弃旧语义时,设置支持期限和可观察提示,避免长期维护所有历史行为组合。

支持矩阵要有代价上限

未来假设有 2 个 runtime、3 个 backend、2 种输入所有权模式、2 种数据功能配置。理论组合已是 24 种,再加两个编译目标就是 48 种。

不必承诺所有笛卡尔积。先列出实际支持的组合,区分共同 core 向量、runtime 行为、后端协议和硬件验证。共享测试覆盖共同规则,不能代替某个组合独有的退出、DMA 或配置路径。

验证层 主要覆盖 不能替代
core 向量 身份、deadline、终态与守恒 线程 / 任务调度
runtime 回归 入队、唤醒、启动、停止 真实设备行为
public consumer 头文件、符号、链接与能力 长期 ABI 承诺
协议混版对照 新旧消息与拒绝 掉电一致性
目标硬件 DMA、IRQ、资源与时延 所有其它配置组合

现有 public-test、功能裁剪与两端 runtime 回归已经提供一部分基线;v2、借用模式和协议混版尚未实现。

演进拆成可回退的提交

先增加可观测指标并建立基线;再增加独立新入口与 capability,保留旧行为;之后实现一个受支持组合,跑完整归还与故障矩阵;最后迁移调用者并收紧旧接口支持。

回滚前还要确认新版本有没有写入旧版本无法理解的持久状态,设备固件能否退回,以及正在借用的 buffer 是否已经归还。回退代码版本不自动回退外部状态。

本次案例的合理起点是保留复制接口,先测复制是否为显著瓶颈,再决定是否承担借用和新版本成本。架构的进阶判断体现在变化理由、完整责任与撤回条件上,而不是抽象层或接口数量。