上一篇我们分享了同源多任务调度,让一路视频流同时支撑多个算法任务。这一篇我们深入到进程间通信——三进程架构下,边缘核心服务和推理引擎是两个独立进程,解码和预处理后的图像帧怎么传给推理引擎做推理?如果用传统的 socket 或管道传输,一帧 1080P 图像就要拷贝好几次,CPU 占用和延迟都很高。本文分享我们的零拷贝通信方案,以及背压控制策略。
一、背景:进程间通信的瓶颈
三进程架构下,视频处理的流水线是这样的:
```
边缘核心服务 (Core Service) NPU 推理与算法调度引擎 (Algo Runner)
RTSP 拉流 → MPP 硬解码 → RGA 预处理 → ??? → NPU 推理 → 后处理 → 返回结果
```
中间的「???」就是进程间通信——核心服务把预处理后的图像帧传给推理引擎。
这一步看起来简单,但在多路视频的边缘场景下,它是一个不小的性能瓶颈。
传统方案:socket 传输图像字节
最直接的方案是用 socket(TCP/Unix Domain Socket)把图像数据序列化后传过去:
```
核心服务:把图像帧序列化成字节流 → 通过 socket 发送 → 推理引擎:接收 → 反序列化成图像
```
这个方案的问题是拷贝次数太多:
- 第一次拷贝:RGA 预处理输出到核心服务的内存缓冲区
- 第二次拷贝:核心服务把图像数据从内存缓冲区拷贝到 socket 发送缓冲区
- 第三次拷贝:内核把 socket 发送缓冲区的数据拷贝到推理引擎的接收缓冲区
- 第四次拷贝:推理引擎把数据从接收缓冲区拷贝到自己的内存缓冲区,准备送 NPU 推理
一帧 1080P RGB 图像大约 6MB(1920×1080×3 字节),拷贝 4 次就是 24MB 的内存带宽占用。8 路视频、每路 10fps,就是每秒 8×10×24MB = 1.92GB/s 的内存拷贝。
这个量级的内存拷贝,在 RK3588 上会导致:
- CPU 占用高——内存拷贝虽然是 DMA 做的,但管理拷贝、调度、同步都要 CPU 参与,实测 8 路 10fps 下 CPU 占用 15-25%
- 延迟高——四次拷贝 + 序列化反序列化,端到端延迟 10-20ms
- 内存带宽瓶颈——RK3588 的内存带宽虽然不低,但 1.92GB/s 的拷贝占用了大量带宽,可能影响 NPU 推理(NPU 也要从内存读写数据)
更关键的是:这些拷贝完全是浪费。 图像数据就在内存里,两个进程在同一块芯片上,为什么要把数据拷来拷去?能不能让两个进程直接访问同一块内存?
这就是零拷贝要解决的问题。
二、零拷贝原理:让两个进程共享同一块内存
零拷贝(Zero Copy)的核心思想是:数据在内存中只存一份,多个进程通过某种机制共享访问,不需要在进程间拷贝数据。
在 Linux 上,实现进程间零拷贝的常用方式有几种:
方式一:共享内存(System V / POSIX shm)
最经典的零拷贝方式。一个进程创建一块共享内存区域,另一个进程把它映射到自己的地址空间,两个进程就可以直接读写同一块物理内存。
优点:简单、成熟、兼容性好。
缺点:需要自己处理同步(信号量/互斥锁),内存分配和管理比较底层。
方式二:内存映射(mmap)
把一个文件(或匿名文件)映射到进程的地址空间,多个进程映射同一个文件,就共享了同一块内存。
优点:可以用文件做持久化,接口相对友好。
缺点:和 shm 类似,需要自己处理同步。
方式三:DMA-BUF fd 传递(嵌入式 Linux 特有)[Yuewell Inside]
这是嵌入式 Linux(特别是带硬件加速器的 SoC,如 RK3588)上最高效的零拷贝方式。
DMA-BUF 是 Linux 内核的一个子系统,用于在不同硬件模块(解码器、编码器、GPU、NPU、显示控制器)之间共享 DMA 缓冲区。每个 DMA-BUF 缓冲区对应一个文件描述符(fd),进程之间可以通过 Unix Domain Socket 的 SCM_RIGHTS 机制传递 fd,接收方拿到 fd 后就可以直接访问这块物理内存。
为什么 DMA-BUF 是 RK3588 上的最优解?
因为 RK3588 的 MPP(硬解码)、RGA(图像加速)、NPU(推理)这些硬件模块,原生都支持 DMA-BUF——MPP 解码输出的帧就是 DMA-BUF 缓冲区,RGA 可以直接 import DMA-BUF fd 做处理,NPU 也可以直接从 DMA-BUF 缓冲区读取数据做推理。
这意味着:从 MPP 解码 → RGA 预处理 → NPU 推理,整条链路的数据都在 DMA-BUF 缓冲区里流动,CPU 完全不参与数据搬运。 这才是真正的「端到端零拷贝」。
如果用普通的 shm 或 mmap,MPP 解码输出后还要把数据从 DMA-BUF 拷到 shm,RGA 处理后还要从 shm 拷回 DMA-BUF 给 NPU——中间还是有拷贝,不是真正的零拷贝。
所以在 RK3588 上,DMA-BUF fd 传递是最优的零拷贝方案。
三、我们的方案:基于 DMA-BUF 的无锁零拷贝环形缓冲区 [Yuewell Inside]
我们最终采用的方案是:基于 DMA-BUF 的无锁零拷贝共享内存环形缓冲区(RingBuffer),图像数据通过 fd 传递(零拷贝),控制信令通过 gRPC 传递。
并且在内核态与用户态构建了专属的无锁队列与帧同步状态机,经过大量并发死锁调优才确保了高吞吐下的极低延迟。
整体流程
```
边缘核心服务 (Core Service) NPU 推理与算法调度引擎 (Algo Runner)
│
MPP 解码 → DMA-BUF 帧(fd1) │
↓ │
RGA 预处理 → 输出到越微自研零拷贝环形缓冲区(fd2) │
↓ │
通过 Unix Domain Socket 传递 fd2 + 元数据 ──────────────▶ 拿到 fd2,import 到 NPU
↓ │ ↓
gRPC 发送推理请求(任务ID、模型路由、时间戳) ───────────▶ NPU 推理(直接从 fd2 读数据)
↓ │ ↓
gRPC 接收推理结果 ◀───────────────────────────────────── 返回检测结果列表
```
关键点:
- RGA 预处理的输出直接写入越微自研零拷贝环形缓冲区(DMA-BUF fd2),不经过 CPU 内存
- 核心服务通过 Unix Domain Socket 的 SCM_RIGHTS 机制把 fd2 传给推理引擎
- 推理引擎拿到 fd2 后,直接 import 给 NPU,NPU 从 fd2 对应的物理内存读取数据做推理
- 整个过程中,图像数据没有被 CPU 拷贝过——从 RGA 输出到 NPU 读取,数据一直在同一块物理内存里
- 控制信息(任务 ID、模型路由、时间戳、推理结果)通过 gRPC 传递,因为数据量小,用 gRPC 方便且可靠
元数据的传递
光传 fd 不够,还要传图像的元数据:宽度、高度、格式(NV12/RGB)、stride、时间戳、任务 ID、模型路由信息等。
这些元数据量很小(几十字节),我们通过 gRPC 的推理请求一起传。gRPC 请求里包含:
shared_buffer_fd:DMA-BUF 的文件描述符(通过 SCM_RIGHTS 附带在 Unix Domain Socket 上)width/height/format/stride:图像格式信息task_id/algo_type/model_id:任务路由信息timestamp:帧的时间戳
推理引擎拿到这些信息后,就知道 fd 对应的图像是什么格式、要跑哪个模型、结果要返回给哪个任务。
环形缓冲区的设计
我们没有用简单的「单帧传递」,而是设计了一个无锁零拷贝环形缓冲区(RingBuffer):
- 预分配一组 DMA-BUF 缓冲区(比如 8 个),组成一个环形队列
- 生产者(核心服务)按顺序往环形缓冲区里写帧,写满一个就移动写指针
- 消费者(推理引擎)按顺序从环形缓冲区里读帧,读完一个就移动读指针
- 用无锁队列技术(原子操作 + 内存屏障)保证多线程并发安全,不需要互斥锁
- 用序列号和事件 fd 做双端同步,确保生产者和消费者的指针状态一致
环形缓冲区的好处:
- 减少 fd 传递开销——不需要每帧都传一次 fd,初始化时传一次缓冲区池的 fd 列表,后续只传帧序号和元数据
- 流水线并行——生产者写第 N 帧时,消费者可以读第 N-1 帧,流水线并行,提高吞吐量
- 背压控制天然支持——写指针追上读指针时,说明消费者处理不过来,生产者可以选择跳帧或等待
四、背压控制:队列深度为 1,忙则跳帧
零拷贝解决了「传得快」的问题,但还有一个问题:如果推理引擎推理不过来,新的帧源源不断地送过来,怎么办?
这就是背压(Back Pressure)问题——消费者(推理引擎)处理速度跟不上生产者(核心服务)的发送速度,数据会堆积,内存占用上涨,延迟增大,最终可能拖垮整个系统。
很多系统的做法是「排队」——弄一个队列,新帧来了就排队,推理引擎慢慢处理。但在实时视频分析场景下,排队是错误的策略。
为什么不能排队?
实时视频分析的特点是:只关心最新的画面,旧帧没有价值。
如果推理引擎忙,来了一帧新画面,你把它排到队列里。等队列里前面的帧处理完,这帧已经是几秒前的旧画面了——用几秒前的画面做实时入侵检测,有什么意义?等你检测到有人入侵,人可能已经走了。
而且排队会导致:
- 延迟越来越大——队列越长,延迟越大,从几十毫秒涨到几秒
- 内存占用上涨——每帧 6MB,队列里排 100 帧就是 600MB
- 级联崩溃——推理引擎处理不过来,队列越来越长,内存越来越大,最终 OOM 崩溃
所以在实时视频分析中,正确的策略是:忙则跳帧,不排队。
我们的背压策略
我们的策略很简单但有效:
- 每个任务的待分析队列深度固定为 1——只保留「当前一帧」的槽位
- 如果推理引擎忙(上一帧还没推理完),新帧来了就直接丢弃/跳过,不排队、不堆积
- 推理引擎空闲时,从槽位里取最新的一帧做推理
```
帧1来了 → 槽位空 → 放入槽位 → 推理引擎取走推理
帧2来了 → 推理引擎还在推理帧1 → 槽位有帧1 → 丢弃帧2
帧3来了 → 推理引擎还在推理帧1 → 槽位有帧1 → 丢弃帧3
帧1推理完成 → 推理引擎空闲 → 槽位里的帧1已经被取走了 → 槽位空
帧4来了 → 槽位空 → 放入槽位 → 推理引擎取走推理
```
这个策略的效果:
- 延迟恒定——不管推理引擎多忙,一帧从进入系统到被推理的延迟最多是「一帧的推理时间」,不会无限增长
- 内存恒定——队列深度为 1,内存占用固定,不会因为推理引擎忙而上涨
- 始终分析最新画面——丢弃的是旧帧,保留的是最新帧,保证推理结果反映的是最新的画面
- 系统稳定——不会因为推理引擎暂时忙而导致级联崩溃
设计取舍
当然,这个策略也有代价——可能会丢帧。如果推理引擎持续繁忙,可能连续跳过很多帧,实际分析帧率会下降。
但我们认为这个代价是值得的:
- 实时性 > 完整性——在实时视频分析中,用最新的画面做低帧率分析,比用旧画面做高帧率分析更有价值
- 稳定性 > 性能——系统稳定运行不崩溃,比偶尔跑到高帧率但时不时崩溃更重要
- 可配置——事件融合的连续确认次数(FusionCount)和采样间隔(FrameInterval)的取值,需要在「跳帧策略」下评估,避免因为跳帧导致「永远达不到 N 连帧确认」。我们在配置时会考虑这个因素
这个设计取舍,是实时系统和批处理系统的根本区别。 批处理系统(如离线视频分析)追求的是「每一帧都处理」,可以排队、可以重试;实时系统追求的是「始终处理最新数据」,忙则丢弃、不排队。
五、性能对比
我们做了一组测试,对比传统 socket 传输和零拷贝传输的性能(8 路 1080P,每路 10fps,RGB 格式):
| 指标 | 传统 socket 传输 | DMA-BUF 零拷贝环形缓冲区 | 提升 |
|---|---|---|---|
| 单帧传输延迟 | 10-20ms | <1ms | 10-20 倍 |
| CPU 占用(传输部分) | 15-25% | <2% | 7-12 倍 |
| 内存拷贝量/秒 | 1.92GB/s | ~0 | 几乎消除 |
| 8 路同时传输稳定性 | 偶发延迟抖动、队列堆积 | 延迟恒定、无堆积 | 质的提升 |
| NPU 推理延迟影响 | 内存带宽竞争,推理延迟增大 | 无内存拷贝,推理延迟稳定 | 显著改善 |
零拷贝传输把单帧延迟从 10-20ms 降到 <1ms,CPU 占用从 15-25% 降到 <2%,几乎消除了内存拷贝。 省下来的 CPU 和内存带宽,可以用来跑更多路视频、更复杂的模型。
更重要的是,零拷贝 + 背压控制让系统的延迟变得恒定可预测——不管负载怎么变,一帧从预处理完成到推理开始的延迟始终在几毫秒以内,不会出现「突然卡一下、延迟涨到几秒」的情况。对于实时告警系统来说,延迟的可预测性比平均延迟更重要。
六、踩坑实录
零拷贝和背压的实现过程中,我们踩了不少坑。
坑一:fd 传递失败,进程拿到无效 fd
通过 Unix Domain Socket 传递 fd(SCM_RIGHTS),看起来简单,但有很多细节:
- 必须用 Unix Domain Socket(AF_UNIX),TCP socket 不支持传 fd
- 发送 fd 时必须同时发送至少一个字节的普通数据,不能只发 fd
- 接收方要用
recvmsg而不是recv,才能拿到附带的 fd - fd 传递后,发送方和接收方各有一个 fd 引用,都要关闭
我们一开始用错了接口,导致推理引擎拿到的 fd 是无效的(-1),排查了半天才发现是 SCM_RIGHTS 的用法不对。
坑二:stride 不对,NPU 推理结果错乱
DMA-BUF 缓冲区的行宽(stride)可能和图像的实际宽度不一样(硬件对齐要求)。我们一开始假设 stride = width,把错误的 stride 传给 NPU,导致推理结果错乱——检测框位置不对、识别错误。
后来从 RGA 获取输出缓冲区的实际 stride,把正确的 stride 传给 NPU,问题才解决。又是 stride 对齐的坑——做嵌入式图像处理,stride 永远是要注意的第一件事。
坑三:引用计数 race condition,偶发花屏
环形缓冲区的生命周期管理用引用计数,多线程环境下 race condition 很难完全避免。我们遇到过偶发的花屏——大概每几万帧出现一次,画面变成随机的噪点。
排查了很久才发现:推理引擎推理完成后释放 fd(减少引用计数),同时核心服务那边刚好因为新帧来了要复用缓冲区(也在操作引用计数),两个操作并发执行,导致引用计数错误,缓冲区被提前释放,NPU 读到了已经被释放(并被重新分配给其他用途)的内存,所以花屏。
解决方案是用原子操作(atomic)管理引用计数,并且在关键路径上加内存屏障,确保多线程下的正确性。修复后花屏问题彻底消失。
坑四:背压策略和事件融合的冲突
我们一开始给所有任务都设置了连续 3 帧确认才告警,但跳帧策略下,如果推理引擎忙,可能连续跳过很多帧,导致「永远凑不齐 3 帧连续确认」,结果是检测到了目标但永远不告警。
后来我们做了区分:
- LIVE 实时任务:连续确认次数可以 >1,但要和采样间隔一起评估,确保在预期的跳帧率下能凑够连续确认
- CRON 定时任务:强制单帧确认(因为定时任务本来就是几小时一帧,不存在「连续帧」)
这个调整解决了漏告警的问题。
⚠️ 工程坑点与技术壁垒警告
提示:零拷贝通信 + 背压控制看起来是「传个 fd + 丢帧」这么简单,但工程落地中有极高的技术壁垒:
无锁环形缓冲区的死锁与活锁:生产者-消费者模式的无锁环形缓冲区,在高并发下极易出现死锁(双方都在等对方)或活锁(双方都在重试但都无法前进)。越微团队在内核态与用户态构建了专属的无锁队列与帧同步状态机,经过大量并发死锁调优才确保了高吞吐下的极低延迟。
DMA 句柄泄漏与内核态资源耗尽:若推理引擎进程异常崩溃,DMA 句柄若未在内核层做严格的泄漏回收与生命周期托管,设备连续运行数天后物理内存会被残余句柄逐步耗尽,最终触发 OOM 且难以定位根因。这不是「加个 close 就能解决」的问题,需要在内核态与用户态构建完整的句柄追踪与自动回收机制。
跨进程时钟同步与丢帧错位:多进程架构下,各进程的时间戳若不做硬件级 RTC/NTP 补偿与全局时钟对齐,在做事件融合证据链时会发生「帧画错位」——告警记录的时间戳和证据图对不上,事后追溯时客户会质疑数据真实性。
越微团队历时大半年、经历了上百次崩溃压测与故障注入,才打磨出这套零崩溃的生产级通讯框架。 这些内核态与用户态的工程细节,不是看几篇博客就能搞定的。
七、几点经验总结
回头看零拷贝通信和背压控制的实现过程,总结几点:
1. 嵌入式 Linux 上,DMA-BUF 是端到端零拷贝的关键
普通的 shm/mmap 只能做到进程间零拷贝,但和硬件加速器(MPP、RGA、NPU)之间还是有拷贝。DMA-BUF 让硬件模块之间也能共享缓冲区,实现从解码到推理的端到端零拷贝。在 RK3588 这类带硬件加速器的 SoC 上,DMA-BUF 是最优解。
2. 零拷贝的难点不是「传数据」,而是「生命周期管理」
fd 传递本身不难,难的是缓冲区什么时候释放、怎么保证双端同步、怎么避免 race condition。引用计数、原子操作、内存屏障、事件同步,这些机制要组合好,才能保证零拷贝既快又稳。我们在这上面花的时间比实现 fd 传递本身多得多。
3. 实时系统的背压策略是「忙则丢帧」,不是「排队等待」
这是实时系统和批处理系统的根本区别。批处理追求每一帧都处理,可以排队;实时系统追求始终处理最新画面,忙则丢弃。队列深度为 1、忙则跳帧,看起来「浪费」了一些帧,但保证了延迟恒定、内存稳定、系统不崩溃。在实时视频分析中,这个取舍是正确的。
4. 设计策略时要考虑上下游的联动
背压策略(跳帧)会影响上游的事件融合策略(连续 N 帧确认)——如果跳帧太多,可能凑不齐连续确认。设计系统时不能只看一个模块,要考虑上下游的联动,在配置和策略上做协调。我们踩过「跳帧导致漏告警」的坑,才意识到这一点。
5. 兼容模式是开发效率的保障,但要明确标注「非生产路径」
Windows 上没有 DMA-BUF,我们用了 gRPC 传图像的兼容模式做开发联调。这大大提升了开发效率(不用每次都把代码部署到 RK3588 上测试)。但一定要明确标注「非生产路径」,确保生产环境走的是零拷贝模式,不能因为兼容模式「也能跑」就忘了优化生产路径。
写在最后
零拷贝跨进程通信,是把三进程架构的性能真正释放出来的关键一环。如果进程间通信还要靠 socket 拷贝数据,那三进程架构的优势(模块隔离、独立升级)就会被通信开销抵消一部分。只有做到零拷贝,才能让三进程架构既「稳」又「快」。
而背压控制,是让系统在高负载下保持稳定的安全阀。实时系统不怕慢,怕的是「越来越慢、最终崩溃」。忙则跳帧、不排队,看起来简单,但却是保证系统 7×24 小时稳定运行的关键设计。
这些经验都是我们(越微智能)在实际项目中踩坑踩出来的。我们团队一直在做工业 AI 视觉、边缘计算部署、机器人二次开发这些落地工作,边缘 AI 视频分析系统是我们的核心产品之一。后面的文章我们会继续拆解——多模型推理、事件融合与证据链、部署与 OTA 升级,把更多实战经验分享出来。
如果你也在做边缘 AI、嵌入式系统开发、RK3588 优化相关的工作,欢迎交流,我们一起踩坑、一起进步。
关于作者与团队:
越微智能(Yuewell)是一支专注具身智能与工业 AI 视觉落地的技术团队,提供工业级视觉算法定制(30+ 算法)、RK3588 边缘一体机、具身机器人调度管理平台、全品牌机器人二次开发(适配宇树/优必选/智元/傅利叶等)、ROS2 系统开发等产品和服务,支持从算法、硬件到产线实机部署的全栈交付。
我们将这套经过上百次故障注入与压测验证的零拷贝通讯框架,封装为了工业级边缘 AI 视觉基座(Yuewell Edge Framework)的核心模块,支持硬件/算法快速二次开发,帮助客户跳过最痛苦的底层通信开发阶段。
📌 获取更多技术白皮书、工业 AI 视觉方案与产品交流,欢迎访问越微智能官网:https://yuewell.com/
下一篇预告: 《多模型多实例推理:单块 RK3588 NPU 如何同时跑人员入侵、烟火检测、垃圾分类》,我们将分享 RK3588 三核 NPU 架构、越微自研多实例并发推理引擎、单模型多算法映射(同一 YOLO 模型的不同 class_id 映射不同业务任务)、多模型并行的内存和算力管理,以及某工厂场景的实战案例,敬请关注。