Skip to content

跨进程 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 → FREE
  • FREE:可交给设备写入;
  • CAPTURING:设备正在生产当前帧;
  • READY:帧完成,等待分发;
  • LEASED:一个或多个消费者仍在使用;
  • 所有 lease 释放后才能回到 FREE

跨进程场景通常需要显式 ACK:生产者发送 {sequence, fd, metadata},消费者完成后返回 {sequence, done}。只有匹配当前 generation 的 ACK 才能释放 lease。

背压策略必须提前决定

当消费者变慢时,系统只能在几种策略里选:

  1. 阻塞生产者;
  2. 扩大 buffer pool;
  3. 丢弃最旧未消费帧;
  4. 断开慢消费者;
  5. 对不同消费者设置不同的 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 在任何时刻都能回答谁拥有、谁在读、何时回收。