跨进程 DMA-BUF 的真正难点:所有权与回收合同
DMA-BUF 让多个设备和进程共享同一块图像内存,Unix Socket 的 SCM_RIGHTS 又能把 fd 发送给另一个进程。很多实现做到这里就认为“零拷贝完成了”。
其实,fd 只是访问能力。更难的问题是:生产者什么时候可以再次写这块 buffer?
两种生命周期不能混淆
传递 fd 后,内核会让接收方获得自己的文件描述符引用,因此发送方关闭本地 fd,不会立刻销毁底层对象。这解决的是对象是否还存在。
但相机 buffer pool 往往会循环复用同一块内存:
text
frame 100 写入 buffer A
↓
fd(A) 发送给消费者
↓
生产者把 A 放回采集队列
↓
frame 104 再次写入 buffer A如果消费者此时才开始处理“frame 100”,它读到的可能已经是 frame 104。fd 仍然有效,数据语义却已经失效。
一个最小 ownership 状态机
text
FREE → CAPTURING → READY → LEASED → FREEFREE:可交给设备写入;CAPTURING:设备正在生产当前帧;READY:帧完成,等待分发;LEASED:一个或多个消费者仍在使用;- 所有 lease 释放后才能回到
FREE。
跨进程场景通常需要显式 ACK:生产者发送 {sequence, fd, metadata},消费者完成后返回 {sequence, done}。只有匹配当前 generation 的 ACK 才能释放 lease。
背压策略必须提前决定
当消费者变慢时,系统只能在几种策略里选:
- 阻塞生产者;
- 扩大 buffer pool;
- 丢弃最旧未消费帧;
- 断开慢消费者;
- 对不同消费者设置不同的 latest-only / every-frame 策略。
没有策略并不代表没有背压,只是背压最终会表现成 queue 堆积、随机复用或内存增长。
接收端必须验证真实 fd
packet 里的 size 只是发送方声明,不能证明 fd 对应对象真的足够大。mmap 前至少要检查:
text
required = offset + stride × height
fd_capacity >= required否则,metadata 与 fd 不匹配时可能直接触发 SIGBUS。同样,width、height、stride 的乘法要做 overflow 检查。
Unix Socket 路径也属于安全边界
服务启动时不要对可配置路径无条件 unlink。应先确认:
- 路径位于受控目录;
- 现有对象确实是 socket;
- owner 与权限符合预期;
- bind 前后没有被替换;
- socket 权限只允许需要的用户或组访问。
建议的协议字段
除几何信息外,建议每个消息至少包含:
- magic 与 version;
- stream id;
- sequence 与 generation;
- timestamp 与 time domain;
- plane count、offset、stride、size;
- pixel format;
- ACK policy 或 lease id。
如何验证不是“偶尔可用”
- 人为让消费者 sleep,观察是否出现旧 sequence 对应新像素;
- 把 socket queue 压满,确认背压策略符合设计;
- 反复断开、重连消费者;
- 用错误 fd、短 fd、超大 stride 做负向测试;
- 在重启期间验证 generation,拒绝上一代 ACK;
- 长时间运行并监控 in-flight lease 数是否回落。
零拷贝系统的完成线不是“没有 memcpy”,而是:每一块 buffer 在任何时刻都能回答谁拥有、谁在读、何时回收。