RK3588边缘AI视觉中零拷贝跨进程通信的DMA-BUF实践
2026/9/5 17:04:11 网站建设 项目流程

RK3588这个芯片,做边缘AI视觉的人应该都很熟了:4颗A76加4颗A55,内置6TOPS算力的NPU,还有RGA、VPU、Mali-G610 GPU,接口上MIPI CSI、HDMI、PCIe一应俱全。但芯片规格只是第一步,真正让项目跑起来并且跑得稳,数据通路怎么设计才是见功夫的地方。这个话题我打算接着前几期,专门聊零拷贝跨进程通信。

为什么单独拿出来说?因为我见过太多项目,模型跑得飞快,帧率却上不去,调度器一查,CPU全耗在memcpy上了。RK3588上部署yolov8或者做多路RTSP实时视频监控,数据从摄像头到NPU再到编码器,如果每个环节都拷贝一遍,1080p@30fps一路视频光拷贝的带宽开销就有近100MB/s,四路就是400MB/s。这个数字在板子上会直接体现为CPU占用飙升、掉帧、延迟抖动。这篇文章把零拷贝这件事从原理到落地讲透,适合正在用RK3588做边缘AI视觉、被多进程架构下的数据交互效率困扰的开发者。

1. 为什么边缘AI视觉里,跨进程数据通路会成为性能瓶颈

1.1 一份帧数据在RK3588内部要"旅行"多远

先不急着上代码,我们先看清楚一个问题:一份图像数据从摄像头进来,到最终显示或编码输出,在RK3588内部到底要经过多少个环节。

典型的边缘AI视觉链路是这样的:

摄像头sensor通过MIPI CSI接口输出RAW或YUV数据,进入ISP做坏点校正、降噪、3A等处理,ISP输出的数据落到DDR内存里。接着,内存里的帧要送给NPU做推理(比如yolov8目标检测),NPU算完输出检测框坐标、类别、置信度,这些结果通常交给CPU做后处理(NMS、过滤、画框),画完框的帧又要送给显示控制器(DRM/KMS)或者硬编码器(VPU)出RTSP流。

在这条链路上,数据在DDR里被多个硬件单元读写了多次。如果把系统设计成多进程架构——采集进程只管采集,AI进程只管推理,推流进程只管编码——那么每一帧图像还要跨越进程边界。跨进程意味着什么?在传统实现里,就是memcpy:发送方把数据从自己的用户态缓冲区拷到内核,接收方再从内核拷到自己的用户态缓冲区。

这里有个很多人容易忽略的细节:RK3588这类SoC和普通PC不同,它的NPU、VPU、RGA对内存有各自的偏好和要求。NPU希望输入是物理连续的,VPU和RGA对地址对齐有要求,GPU则需要考虑缓存一致性。这些硬件单元的"内存视图"和进程的"虚拟内存视图"之间,本身就隔着一层。如果你只是简单用memcpy做跨进程数据交换,哪怕不考虑拷贝本身的CPU开销,也绕不开"还要再映射给硬件用"这层麻烦。

1.2 拷贝的代价:带宽、延迟、CPU占用三本账

我们来算一笔实在的账。

一路1080p分辨率、NV12格式(YUV420半平面)的帧,大小是 1920 × 1080 × 1.5 ≈ 3.1MB。

如果30fps,一秒钟就是约93MB/s。

如果系统里有4路摄像头同时做AI分析,就是约373MB/s。

这还只是"源数据"的体量。实际上,在普通跨进程拷贝方案中,数据往往不止拷一次。例如:

  • 采集进程从V4L2 buffer读出(一次read或mmap后的用户态访问)
  • 发送给AI进程时通过共享内存/消息队列拷贝一次
  • AI进程把数据整理成NPU输入格式又拷贝一次
  • AI进程把结果图像发给显示/编码进程再拷贝一次

这么算下来,一份数据在整个生命周期里可能被拷贝3到4次。4路1080p@30fps的场景,光拷贝吞吐量就超过1GB/s,这还是在RK3588双通道LPDDR4/LPDDR5内存带宽还算充裕的前提下。问题的关键不是"带宽不够用",而是每次拷贝都意味着:

  • CPU缓存被频繁刷掉,影响其他任务的执行效率;
  • 帧延迟增加,对实时性要求高的场景(比如工业质检、运动检测联动)很致命;
  • 额外占用DDR带宽,而NPU、GPU、VPU这些硬件单元也在抢同一份带宽;
  • 内存占用偏高,因为每一份数据在多个进程里有多个副本。

我做过的实际压测里,4路1080p@30fps、模型是yolov8s的情况下,如果全链路都用memcpy方式跨进程传帧,CPU占用率大概多出20%到35%,端到端延迟增加3到8毫秒。这个损耗在demo里看不出什么,但在长时间运行的工业现场,累计的掉帧数和延迟抖动会直接影响系统的可用性。

1.3 明确边界:哪些场景真的需要零拷贝

说了这么多,我也得泼盆冷水:不是所有项目都需要零拷贝。

如果只是单路视频、非实时后处理、CPU负载余量很大,那用传统的共享内存加拷贝完全没问题,实现简单,调试也容易。零拷贝方案会引入fd管理、生命周期同步、缓存一致性等一堆复杂度,小项目里可能得不偿失。

真正需要零拷贝的场景有这几个特征:

  • 多路视频并发,数据吞吐量大(两三路1080p以上);
  • 帧率要求高,或者端到端延迟敏感;
  • 多进程架构,模块之间职责分离;
  • 长时间稳定运行,CPU占用和散热压力需要控制。

如果你正卡在这几个特征里,那下面的内容值得认真看。

2. 零拷贝的底层地基:DMA-BUF与dma_heap的工作方式

2.1 DMA-BUF不是某个驱动,而是一套共享协议

聊零拷贝,绕不开DMA-BUF。很多人第一次接触这个词是在安卓的SurfaceFlinger里,觉得它是某个图形相关的驱动。其实DMA-BUF是Linux内核里一套通用的缓冲区共享机制,核心思路是:一块物理内存由某个分配者创建,关联一个文件描述符(fd),其他进程或设备拿到这个fd后,通过mmap把它映射到自己的地址空间,或者直接通过这个fd把内存交给硬件DMA使用。整个过程不拷贝数据,只传递"句柄"。

可以这样理解:传统拷贝是复印一份文件给对方,零拷贝是把柜子的钥匙递给对方,对方自己打开柜子取用那份文件。

在RK3588的开发板上,你大概率会看到这些节点:

  • /dev/dma_heap/system
  • /dev/dma_heap/system-uncached
  • /dev/dma_heap/cma(有些固件里叫linux,cma)
  • 以及Rockchip自家的 /dev/ion(老固件常见)

dma_heap是内核5.6之后主推的分配接口,ION逐渐被淘汰。在RK3588的现代固件(内核5.10及以上)里,推荐直接使用dma_heap。

2.2 RK3588上能直接拿到的内存类型

实际使用中,边缘AI视觉项目主要关心两类内存:

  1. 连续物理内存(CMA):NPU、VPU、ISP这些硬件单元做DMA时,很多要求物理地址连续。CMA区域就是预留给这类用途的。dma_heap的cma heap分配出来的就是这类内存。

  2. 非连续但可DMA的内存(system heap):硬件支持通过IOMMU/SMMU做分散聚合,所以不需要物理连续。RK3588的NPU、RGA、VPU都有SMMU支持,所以很多场景用system heap就够了,分配成功率更高,也不容易把CMA区域耗尽。

此外还有uncached和cached的区别。cached内存对CPU访问友好(走Cache),但硬件DMA操作后需要做缓存同步;uncached内存CPU访问慢一些,但不存在缓存一致性问题,硬件写完CPU直接读就是最新数据。

这块内存怎么分配?在用户态可以直接打开/dev/dma_heap/system-uncached,发一个DMA_HEAP_IOCTL_ALLOC ioctl,拿到fd。代码大致长这样:

#include <linux/dma-heap.h> #include <sys/ioctl.h> #include <fcntl.h> #include <unistd.h> int alloc_dma_buf(size_t size) { int heap_fd = open("/dev/dma_heap/system-uncached", O_RDWR); if (heap_fd < 0) { perror("open dma_heap"); return -1; } struct dma_heap_allocation_data data = { .len = size, .fd_flags = O_CLOEXEC | O_RDWR, .heap_flags = 0, }; if (ioctl(heap_fd, DMA_HEAP_IOCTL_ALLOC, &data) < 0) { perror("DMA_HEAP_IOCTL_ALLOC"); close(heap_fd); return -1; } close(heap_fd); return data.fd; // 这就是dma-buf的fd }

注意,分配到的这个fd属于某个进程。如果另一个进程也要用它,要么通过fork继承(特权场景常用),要么走跨进程fd传递机制(下面会专门讲)。

2.3 CPU访问DMA-BUF时那个最容易漏掉的SYNC操作

拿到dma-buf的fd之后,进程可以用mmap把它映射进自己的用户态地址空间:

void *map_dma_buf(int dma_fd, size_t size) { void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, dma_fd, 0); if (addr == MAP_FAILED) { perror("mmap dma_buf"); return NULL; } return addr; }

这里有一个非常多见的坑:对于cached类型的内存,CPU访问之前和硬件访问之后,必须做缓存同步。如果你直接读写mmap出来的地址,而这块内存刚被NPU或者摄像头DMA改写过,你读到的很可能是Cache里的旧数据。反过来,CPU改完数据要交给硬件,如果不主动刷Cache,硬件DMA可能读到旧数据。

内核提供的接口是DMA_BUF_IOCTL_SYNC:

struct dma_buf_sync sync = { .flags = DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ, }; ioctl(dma_fd, DMA_BUF_IOCTL_SYNC, &sync); // 在这里安全地读取数据 sync.flags = DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ; ioctl(dma_fd, DMA_BUF_IOCTL_SYNC, &sync);

如果不想纠结同步,可以干脆用uncached内存,代价是CPU读写性能稍差。但注意:用uncached内存做CPU后处理(比如NMS循环、画框)时,大量随机访问会明显慢于cached内存。我自己实测,画框这种频繁小写操作,uncached比cached慢15%到30%。所以更推荐的做法是:NPU输入用uncached保证一致性,画框、叠加这种CPU操作放到一个cached的dma-buf上,配合SYNC ioctl来管理。

3. 在RK3588上打通一路零拷贝视觉管线

3.1 端到端链路设计与内存分配策略

聊完底层机制,我们来看一个完整的设计。假设要这样做一路AI视觉任务:

  • 进程A:摄像头采集 + ISP输出
  • 进程B:RKNN NPU推理 + 后处理
  • 进程C:RGA缩放/画框 + VPU编码RTSP推流

三个进程各司其职。传统方案里A把每帧拷贝给B,B处理完拷贝给C。零拷贝方案的设计目标,是让同一块dma-buf在这三个进程之间流转,只在必要的时候才新分配。

一个经验做法是建立一个"帧缓冲池":

  1. 进程A启动后,从dma_heap分配一定数量的dma-buf(按帧池容量,比如5到8帧);
  2. 每帧图片数据直接由ISP/RGA写入这些dma-buf;
  3. 进程A通过IPC把dma-buf的fd发给进程B;
  4. 进程B在fd上mmap或交给RKNN,推理完成后,把fd再转给进程C;
  5. 进程C用RGA叠加结果、缩放,然后送入VPU编码。

内存分配只发生在启动阶段,运行阶段只有fd在不同进程间流转,数据本身不移动。

这里有个细节值得强调:提前分配帧池,不要在每帧处理时反复分配/释放dma-buf。我在项目里一开始偷懒,每帧都现分配,结果多路并发时内存碎片很快出现,跑几个小时就分配失败。帧池化之后系统稳定运行时间从几小时提升到一周以上。

3.2 跨进程转发DMA-BUF fd的两种可靠姿势

那么问题来了:fd怎么从一个进程传给另一个进程?fd本质上是进程级的文件描述符表项,不能直接通过普通的int传值给对方——同一个整数值在不同进程里可能指向完全不同的文件。

两种可靠的转发方式:

方式一:Unix域socket + SCM_RIGHTS(Linux通用)

用sendmsg/recvmsg发送辅助数据,让内核把fd从一个进程的fd表复制到另一个进程的fd表。这是纯用户态最通用的方案,不依赖Android系统,纯Linux环境(比如Debian 11、Ubuntu)都能用。

// 发送方:把dma_fd通过socket传给接收方 void send_fd(int sock, int fd) { struct msghdr msg = {0}; char buf[CMSG_SPACE(sizeof(int))] = {0}; struct iovec iov = { .iov_base = "F", .iov_len = 1 }; msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_control = buf; msg.msg_controllen = sizeof(buf); struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); cmsg->cmsg_level = SOL_SOCKET; cmsg->cmsg_type = SCM_RIGHTS; cmsg->cmsg_len = CMSG_LEN(sizeof(int)); memcpy(CMSG_DATA(cmsg), &fd, sizeof(int)); sendmsg(sock, &msg, 0); } // 接收方:收到的是一个新的、可用的fd int recv_fd(int sock) { struct msghdr msg = {0}; char buf[CMSG_SPACE(sizeof(int))] = {0}; struct iovec iov = { .iov_base = "F", .iov_len = 1 }; msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_control = buf; msg.msg_controllen = sizeof(buf); recvmsg(sock, &msg, 0); struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); if (!cmsg || cmsg->cmsg_type != SCM_RIGHTS) { return -1; } int fd = 0; memcpy(&fd, CMSG_DATA(cmsg), sizeof(int)); return fd; }

这里的开销是:一次sendmsg + 一次内核级的fd表复制。相比整帧数据拷贝,这个开销几乎可以忽略,实测一次fd传递大约在微秒到几十微秒级别。

方式二:Binder(Android系统方案)

如果你的RK3588跑的是Android系统,Binder是更熟悉的方案。Android的SurfaceBuffer跨进程共享本质上就是把dma-buf fd通过Binder传给对方,接收方再用gralloc映射。但这个方案和Android图形栈绑得比较紧,做纯Linux边缘计算设备时基本用不上。这篇文章按纯Linux环境来讲,用的就是SCM_RIGHTS。

还有一个"土办法"是让两个进程fork自同一个父进程,fd天然被继承,不需要传递。但这要求主进程先分配好所有dma-buf并派生子进程,进程结构上不够灵活,我一般只在快速原型时用。

3.3 实测:零拷贝与普通拷贝的量化对比

这里放一组我在RK3588开发板(8GB内存版本,Debian 11系统)上做的对比实测,单路1080p@30fps NV12帧,从"采集进程到AI进程"这一段。

方案A:普通共享内存 + memcpy。每帧约3.1MB,拷贝用时平均约1.2毫秒,发送方和接收方各占一次memcpy,总共约2.4毫秒,期间CPU占用约为单核的25%左右。

方案B:dma-buf + SCM_RIGHTS传fd。每帧不拷贝数据,只有一次fd传递,用时平均约40微秒,CPU占用几乎可以忽略。

单从这一段看,每帧省了2.3毫秒。整条链路如果省掉3次拷贝,每帧就是7毫秒左右的收益。对于30fps(帧间隔33毫秒)的场景,这7毫秒就是稳定不掉帧和频繁掉帧的分水岭。

这个数据在不同固件版本、不同内存配置下会有波动,但量级是稳定的:零拷贝在延迟上快一到两个数量级,CPU开销接近零。

4. 和RKNN NPU对接时的对齐、生命周期与同步细节

4.1 RKNN输入输出对内存的"隐性要求"

在RK3588上做AI视觉,推理引擎几乎都是RKNN Runtime。rknn_input结构体里有一个成员叫buf_type,我见过不少人在这一步忽略了零拷贝入口:

rknn_input inputs[1]; inputs[0].index = 0; inputs[0].type = RKNN_TENSOR_UINT8; inputs[0].size = frame_size; inputs[0].fmt = RKNN_TENSOR_NHWC; inputs[0].buf = mmap_addr; // 指向dma-buf mmap出来的地址 inputs[0].pass_through = 0; inputs[0].buf_type = RKNN_INPUT_TYPE_NORMAL; // 注意这里

如果buf_type设为RKNN_INPUT_TYPE_NORMAL,驱动容易走拷贝路径。要真正利用dma-buf的零拷贝能力,需要把buf字段换成dma-buf fd,并让RKNN走对应的零拷贝输入通道。具体API在不同版本的rknn-toolkit2里略有差异,但思路一致:希望NPU直接拿到的是物理/IOMMU可寻址的内存,而不是用户态一个普通malloc地址。

这里有个很隐蔽的坑:RKNN对输入内存往往要求对齐。比如yolov8在RK3588上部署时,输入分辨率如果是640×640,那还好;但如果你的图像尺寸比较特殊,推理前通过RGA做缩放时,要确保输出内存的宽、高、stride满足NPU的对齐要求。常见的做法是分配dma-buf时就把宽对齐到16或64字节,否则模型加载不报错,推理结果却会偶尔出现偏移或错位。

4.2 fd生命周期:谁分配、谁释放、怎么防泄漏

dma-buf的核心是一个文件对象,引用计数由内核维护。一个fd被SCM_RIGHTS传给另一个进程后,内核会为接收进程创建新的fd,同时引用计数加一。释放的规则是:每个进程关闭自己的fd,当所有fd都关闭时,dma-buf内存才会被真正回收。

这带来两个问题:

问题一:重复close。如果接收方用完后close了fd,但发送方以为对方还在用,又close一次——不会崩溃,但可能提前释放内存。所以需要在项目里明确:一个帧dma-buf从池中取出后,由哪个进程负责关闭。我习惯的做法是"最后一个使用者负责关闭",把fd的归属权随帧一起传递,后处理完成后由接收方close并归还给帧池。

问题二:fd泄漏。如果接收方进程收到fd后异常退出,或发送方发送后忘记close,fd会一直占着。长时间运行会出现"分配失败"的诡异问题。我的排查经验是:定期通过 /proc/pid/fd 计数观察fd数量是否线性增长。多路视频跑一晚上,fd数如果不回落,基本就是哪里泄漏了。代码规范上,所有SCM_RIGHTS接收路径建议加上MSG_CMSG_CLOEXEC,避免子进程意外继承。

4.3 fence同步:不让NPU和CPU互相踩脚

零拷贝的隐藏复杂度在同步,不在传输。当多个硬件设备共享一块内存时,必须保证"谁写完谁才能读"。比如NPU正在读dma-buf推理,CPU此时不能去改写这份数据。

现代内核里,这个职责由sync_file(fence)机制承担。dma-buf可以关联一个或一组fence,设备的DMA操作完成时signal fence,等待方通过poll等待fence触发。Rockchip的NPU驱动和V4L2驱动都实现了fence机制,使用的时候要在应用层把等待逻辑处理好。

举例:进程A的采集驱动给帧1挂上"写完成"fence,进程B把帧1交给NPU时会等待这个fence;同样,NPU推理完成后会signal"读完成"fence,进程C才能对这帧做画框或编码。

如果忽略这一步,常见症状是:偶发性花屏、推理结果偶尔错乱、画面撕裂。这类问题在开发机上很难复现,一到现场就频繁出现,排查成本极高。所以架构设计时一定要把"谁等待谁"的fence链条画清楚。

5. 我在实际项目中踩过的四个坑

5.1 RGA输出内存64字节对齐的隐性问题

RK3588的RGA2/RGA3硬件对输出内存地址有对齐要求,不同固件要求不完全一样,但64字节对齐是基本盘。如果你用RGA把YUV缩放或者转成RGB送给NPU,输出dma-buf的地址和stride都要对齐。

具体踩坑是这样:我用RGA把1080p NV12缩放成640×640 RGB,输出buffer用dma_heap分配,当时觉得dma_heap分配的内存天然对齐,没做检查。结果偶发出现输出图像右移几个像素的情况。排查到后面才发现,dma_heap的物理地址对齐没问题,但RGA计算stride时要求buffer宽度按64字节对齐,640×3(RGB三个通道)=1920,1920对64刚好整除,没问题;换成某些非标分辨率时就会出问题。建议所有RGA相关buffer宽度统一按64字节对齐计算stride,再乘以高度。

5.2 进程异常退出导致fd泄漏

这是多进程零拷贝方案里最麻烦的问题:发送方把fd发给接收方,接收方处理到一半崩溃了,fd没人关。如果之后发送方又发新fd,最终所有分配器都因为fd耗尽或内存不释放而罢工。

我在一个9路相机的项目里遇到过一次诡异的"运行12小时后画面逐渐卡死"。定位过程很痛苦,最后是靠在每路进程里加心跳上报、崩溃自动重启,并且在重启时全量close旧fd、重建帧池解决。这个方案的代价是重启瞬间会有几帧丢失,但系统整体可用性大幅提升。

更彻底的做法是引入一个"资源管理器"进程,统一管理dma-buf的分配和回收,其他进程只通过IPC请求fd,用完归还。坏处是绕了一层,性能有一点损耗;好处是任何进程崩溃都不会把内存池搞乱。如果你做的是长期运行的工业设备,我推荐花这个代价。

5.3 缓存一致性在RK3588上的特殊表现

缓存一致性的坑大多数时候在"看起来没问题"的场景里潜伏。比如我一度全部用uncached内存做帧缓冲,CPU画框确实慢一些,但功能正常,也就没深究。后来把帧缓冲改成cached内存以提升CPU访问性能,结果画面出现间歇性花屏,测试一天才出现两三次,非常难排查。

最后定位到是CPU画框后没有做DMA_BUF_IOCTL_SYNC的START|WRITE刷Cache,VPU读的时候读到了部分旧数据。这里提醒一下:使用cached dma-buf时,CPU写前后都必须做SYNC;不能只做一次。写入前用START|WRITE,写入后用END|WRITE。这一对操作缺一个,长期运行都会出问题。

5.4 多路并发下的内存碎片难题

多路视频并发时,每一路都要维护自己的一堆帧缓冲。如果所有路共用一个大池子,虽然内存利用率高,但分配/释放频率也高,时间一长CMA区域会被切成碎片,后续大块分配失败。

我的做法是:把关键路径上的大块dma-buf全部在启动阶段预分配好,运行期间不新增、不释放;如果需要动态调整,也只在系统空闲时做。预分配的数量按"最大路数 × 每路帧数 × 余量"计算,比如8路每路5帧,再留2路余量,就是50帧左右。一套这样的固定池方案配合不加锁的环形队列流转,实测跑7×24小时没有出现分配失败。

6. 零拷贝方案选型建议:什么场景该用、什么场景别碰

6.1 先想清楚:多进程还是单进程多线程

零拷贝跨进程通信解决的是"多进程架构下的高效数据共享"问题。但反过来想,如果模块之间信任度高、代码库统一,单进程多线程可能是更简单的方案:线程天然共享地址空间,一个指针就能传数据,不需要fd,不需要SCM_RIGHTS,不需要管生命周期归属。

什么情况下值得多进程?

  • 模块由不同团队维护,或者要单独升级、单独重启;
  • 某模块(比如采集驱动封装)稳定性不够,崩了不能带崩整个系统;
  • 有安全隔离需求,不同进程的权限不同。

如果这些你都不需要,我的建议是优先单进程多线程,把线程绑定到不同CPU核心(RK3588有8个核心,A76和A55各司其职),数据用线程间无锁队列传递,性能足够,开发效率高很多。

6.2 什么时候不要用零拷贝

零拷贝不是银弹。遇到下面情况,我会选择退回传统拷贝方案:

  • 数据量小、频率低:比如只是传一些结构化的小数据(检测框坐标、温度值),一次拷贝才几百字节,没必要付出维护fence和生命周期的复杂度;
  • 数据形态需要转换:比如RAW图转RGB、NV12转BGR,转换本身就要读写全部数据,这时候"拷贝+转换"合在一起反而更高效;
  • 硬件单元不支持直接访问:有些第三方IP核或外设没有SMMU,要求物理连续内存,而零拷贝池不好满足,不如拷贝一次省心。

6.3 我的最终架构建议

综合这些经验,我现在在RK3588上做边缘AI视觉的标准架构是这样的:

  • 采集进程:V4L2/ISP采集,启动时从dma_heap分配固定帧池,帧数据由ISP直接写入dma-buf;
  • 推理进程:通过SCM_RIGHTS接收dma-buf fd,交给RKNN零拷贝推理,后处理(NMS、逻辑判断)在cached dma-buf上做;
  • 输出进程:RGA叠加结果、VPU编码RTSP推流,同样读写dma-buf fd;
  • 帧池和fd生命周期由"最后使用者"负责,每个进程崩溃时都有守护进程拉起,重启后重建帧池。

这套方案在9路1080p@25fps + yolov8s检测的工业场景里跑过,CPU占用稳定控制在20%左右,端到端延迟平均80毫秒,最长连续运行两周没有出现内存或fd泄漏。相比最初全拷贝的版本,CPU占用降了三分之一,掉帧数归零。

最后再分享一个小技巧:调试阶段可以在每个进程里定期打印 /proc/pid/status 里的VmLck、/proc/pid/fdinfo 里的dma-buf信息,把fd数量和内存占用打点记录下来,跑一晚上看曲线。所有不能收敛的曲线都是问题,定位起来比瞎猜快得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询