增加零拷贝和新后端以后,怎样保持接口可演进¶
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 是否已经归还。回退代码版本不自动回退外部状态。
本次案例的合理起点是保留复制接口,先测复制是否为显著瓶颈,再决定是否承担借用和新版本成本。架构的进阶判断体现在变化理由、完整责任与撤回条件上,而不是抽象层或接口数量。