news 2026/10/22 3:26:10

V4L2与DMA-BUF联动解析:相机内核内存映射与泄漏排查实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
V4L2与DMA-BUF联动解析:相机内核内存映射与泄漏排查实战

如果你是在应用层或者 HAL 层调相机,遇到“申请内存失败”、“预览莫名其妙花屏”、“相机开一段时间就再也打不开”这类问题,通常第一反应是去翻上层逻辑。但根据我维护这套 QCOM_CAMERA 内核驱动的经验,九个问题里有八个最后都戳到内核内存管理这里。今天这篇系列的第一篇,先把方向掰过来:从硬件数据通路开始,把 V4L2 videobuf2、DMA-BUF、SMMU/IOMMU 这三者在相机内核内存里各自扮演什么角色讲清楚,然后沿着“一块 buffer 从用户态落到硬件 IOVA”的完整路径走一遍,最后用两个真实排查案例告诉你,内存泄漏不一定是引用计数崩了,也可能是映射没有成对释放。这篇适合 BSP/驱动工程师,以及长期被相机问题折磨的 HAL 工程师,看完能直接对着 QCOM_CAMERA 的代码路径去定位问题。

1. 为什么相机内存问题跑到最后都要回内核

1.1 先算一笔账:相机的内存压力是怎样产生的

很多人不把相机当 IO 设备,觉得它和读个文件、写个 socket 差不多。但相机的数据通路完全不是这个概念。以最常见的 1080P@60fps 预览为例,一帧 NV12 原始数据大约是 1920 × 1080 × 1.5 字节,大概 3 MB,60fps 就是 180 MB/s 的持续写入带宽。如果同时跑着预览、录像、拍照,或者上了 4K、高帧率,那每秒的数据量轻松翻到 0.8 GB/s 甚至更高。

这个量级下,CPU 不可能像 memcpy 一样把 sensor 数据搬到内存。整条链路靠硬件 DMA 直接把数据从 MIPI 接口写到 DDR 里的指定地址。这个“指定地址”就是内核提前分配好的 buffer。问题就在这:应用层看到的只是一个个 buffer fd,但 fd 背后连接的是物理内存页、IOMMU 映射、cache 一致性状态,这些全部由内核侧驱动在维护。上层那些“buffers a lot but cannot get one”的怪象,大概率都能映射到内核侧某个引用计数或者映射关系没配对。

1.2 HAL 侧查不出来的问题往往藏在这里

我之前处理过一个案例:相机连续开关 50 次之后,应用层mmap直接返回 ENOMEM,预览根本起不来。HAL 同事加了满屏 log,发现每次关闭相机时REQUEST_BUFS都能释放,但系统整体的 buffer 数量却在缓慢上升,谁都说不清是哪里多出来的。

我把目光转到内核层,直接看iova_map_count,发现每次 open/close 一个来回,SMMU 映射数没有回到基线,而是稳定多出 2 个。这就是典型的上层看着没事、下层悄悄泄漏。这个经历基本定下了我处理相机问题的原则:遇到内存相关异常,永远先怀疑内核侧的映射和引用计数,再看上层。

1.3 内核内存参与者全景:CAMSS、VB2、DMA-BUF、SMMU

搞清楚谁是谁,比上来就看代码重要得多。在 QCOM_CAMERA 这套驱动里,内存相关的角色主要有四个:

参与者职责类比
CAMSS/CAM 驱动负责 sensor/ISP 的配置、stream on/off 控制,向 V4L2 注册 video 设备相机的“总调度室”
videobuf2 (VB2)V4L2 标准的 buffer 队列框架,维护 buffer 的状态机和生命周期相机内存的“仓库管理员”
DMA-BUF承载 buffer 的通用内核对象,通过 fd 在进程间共享,负责底层页引用包裹和快递单
SMMU/IOMMU完成物理地址到 IOVA 的映射,给硬件提供连续地址空间地址翻译官

它们咬合的顺序是:用户态发起 buffer 请求 → VB2 调用驱动回调分配 DMA-BUF → 驱动 attach/map → SMMU 出 IOVA → 硬件拿着 IOVA 做 DMA。后面的章节就按这条链路一步步拆。

2. Buffer 落地链路:从 fd 到 IOVA 到底发生了什么

2.1 起点:REQBUFS 和 queue_setup 的协商逻辑

用户态通过VIDIOC_REQBUFS申请 buffer,最终会进入内核 V4L2 core 的vb2_reqbufs,进而触发驱动注册在vb2_ops里的queue_setup回调。这个回调是“协商点”:上层告诉驱动想要多少 buffer、什么格式,驱动可以在这里校验、修正,甚至拒绝。

我维护的这套 QCOM_CAMERA 驱动里,queue_setup一般长这样:

static int cam_queue_setup(struct vb2_queue *vq, unsigned int *num_planes, unsigned int sizes[], struct device *alloc_devs[]) { struct cam_device *cam = vb2_get_drv_priv(vq); unsigned int w = cam->fmt.width; unsigned int h = cam->fmt.height; unsigned int stride, size; /* 以 NV12 为例,一个连续 buffer 包含 Y 和 UV */ stride = ALIGN(w * 2, 128); size = ALIGN(stride * h * 3 / 2, 4096); *num_planes = 1; sizes[0] = size; if (alloc_devs) alloc_devs[0] = cam->dev; return 0; }

这里有几个非常容易踩的坑。第一个是修改了sizes[0]却没有同步考虑num_planes,导致后续 mmap 时按旧大小映射,用户态拿到尺寸不匹配的 buffer。第二个是queue_setup里不要做任何耗时操作,它可能被并发调用,我在早期版本试过在这里等待硬件状态,结果和 stream off 的锁互相纠缠,直接 watchdog。

2.2 DMA-BUF 的三层关系:attach、map、IOVA

QCOM_CAMERA 这类 BSP 驱动里,buffer 本体通常由 ION 或 DMA-BUF heap 分配。用户态拿到一个 fd,通过QBUF回给驱动,驱动在buf_prepare阶段开始真正“接住”这块内存。这里必须区分三层概念:

  • dma_buf_attach():表示“我这个设备想使用这个 DMA-BUF”,建立起设备与 buffer 的关联;
  • dma_buf_map_attachment():表示“请为我这个设备准备好可用的地址列表”,通常返回sg_table;
  • IOMMU 映射:把sg_table里的物理页映射成硬件可访问的 IOVA,IOVA 连续但物理页完全可以是分散的。

很多泄漏问题就出在这三层不成对。我见过一份代码,每次 open 时 attach + map,close 时只dma_buf_put,把 detach 和 unmap 全漏了。dma_buf_put 只是把一个引用减掉,它并不会自动帮你清理设备侧已经建立的映射关系。

实际编码里我会把“建立映射”和“释放映射”封装成两个对称函数,保证异常分支也走同一个出口:

static int cam_map_buffer(struct cam_buffer *buf) { buf->attach = dma_buf_attach(buf->dmabuf, cam_dev); if (IS_ERR(buf->attach)) return PTR_ERR(buf->attach); buf->sgt = dma_buf_map_attachment(buf->attach, DMA_BIDIRECTIONAL); if (IS_ERR(buf->sgt)) { dma_buf_detach(buf->dmabuf, buf->attach); buf->attach = NULL; return -ENOMEM; } /* 然后通过 IOMMU API 生成 IOVA,供硬件 DMA 使用 */ return 0; } static void cam_release_mapping(struct cam_buffer *buf) { if (buf->sgt) { dma_buf_unmap_attachment(buf->attach, buf->sgt, DMA_BIDIRECTIONAL); buf->sgt = NULL; } if (buf->attach) { dma_buf_detach(buf->dmabuf, buf->attach); buf->attach = NULL; } }

2.3 cache 属性:相机 buffer 为什么常常不走 WriteBack

提到相机内存,避不开 cache 一致性问题。sensor/ISP 是 DMA 写内存的一方,CPU/GPU 是读内存的一方。如果 buffer 属性是 WriteBack,那么硬件 DMA 写完之后,CPU cache 里可能还留着旧数据,直接读就会读出花屏、条纹、颜色异常。

传统做法是在每个 buffer 的关键转折点手动做 cache clean/invalidate。QCOM_CAMERA 里很多 preview/scaler 路径干脆把 region 属性设成不带 cache 或者 write-combine,用 CPU 访问速度换一致性简单。但这不是免费的:如果 CPU 要频繁读写这块 buffer,uncached 的性能会非常难看,所以有的 ISP 统计 buffer 又会走 WriteBack +dma_sync_single_for_device。

我的原则是:按访问频率和访问方来定属性。硬件只写、CPU 只读的预览 buffer,无 cache 是靠谱的;CPU 频繁读写的控制类 buffer,用 WriteBack 加同步接口。实测下来,宁可在 map 和 unmap 时多写两个 sync 调用,也别在出问题时对着 PTE 属性一头雾水。

2.4 对齐:一个“反直觉”的硬约束

应用层看到的 1080P,到了内核侧还要再做一轮 stride、宽高、地址对齐。硬件 DMA 引擎往往要求线宽 64/128 对齐,帧高 16/32 对齐,buffer 首地址页对齐。这些不是形式主义,一旦对不齐,轻则在 queue_setup 里返回 -EINVAL,重则特定分辨率下出现边缘噪点和撕裂。

最简单可靠的思路就是分配尺寸统一按 4KB 对齐,再按硬件限制放大 stride。宁可多占几十 KB,也别把对齐系数写成一个“看起来够用”的魔数。

3. 硬件在消耗哪块内存:Ping-Pong 与 VB2 状态机

3.1 VB2 的五个状态:谁在什么时候推进状态

很多人看 VB2 代码一头雾水,是因为没把状态机先记住。一个 buffer 在 VB2 队列里有几个关键状态:用户手里持有(DEQUEUED)、通过 QBUF 还给内核(QUEUED)、streaming 过程中被硬件实际使用(ACTIVE/DONE)、硬件写完回到完成队列等待用户 DQ。

驱动的buf_queue回调把 buffer 放进驱动自己的待处理队列,start_streaming之后驱动可以把它交给硬件,硬件完成后在中断里调用vb2_buffer_done,VB2 core 会把这个 buffer 放回 done 队列,用户态 DQBUF 就能拿回这一帧。状态由 VB2 core 推进,驱动不要自己去改状态,否则很容易出现“buffer 还在 dequeued 状态却被硬件写”的错误。

3.2 Ping-Pong 背后的双缓冲/三缓冲逻辑

刚接手 QCOM_CAMERA 那阵,我看到“ping-pong”这个词还以为是什么特殊硬件机制。后来想通了,它就是经典的乒乓缓存:硬件在 ping buffer 上写当前帧,用户态在 pong buffer 上读上一帧,帧边界一到,两边交换。如果只有一个 buffer,硬件和用户会互相踩同一块内存,图像必挂。

多放一个 buffer 变成三缓冲,硬件总能提前 reserve 下一块,用户态读完上一块时硬件已经开始写另一块,容忍抖动能力强很多。代价是内存占用上升。QCOM_CAMERA 的 stream on 配置里有指明初始排队 buffer 数量的地方,我的经验是至少保证 4~6 个,低于这个数,高帧率场景很容易出现 queue underflow,表现就是帧率抖动。

3.3 中断里只做一件事:标记完成

硬件帧中断到来时,驱动要做的是把 index、timestamp 等信息填好,然后调用vb2_buffer_done,但这并不意味着你可以在中断上下文里为所欲为。vb2_buffer_done如果触发用户态唤醒,内部会有锁和调度逻辑,直接在原子上下文里调用有死锁风险。

正确姿势是中断里只置位一个 pending 标志,唤醒一个 workqueue 或 kthread,在线程上下文完成真正的 done 处理。QCOM_CAMERA 这类驱动在压力测试下很容易触发这个问题,看起来是偶发卡死,实际是中断上下文和一个持锁路径互相等待。

4. 释放链路里的暗礁:引用计数失衡与残留映射

4.1 stream off 时正确的释放顺序

释放比分配更容易出错,因为分配是正着搭积木,释放是倒着拆,而且拆的时候硬件可能还在用。我踩过一个坑:stream off 时先把所有 buffer 的 IOVA 映射给 unmap 了,再去等硬件停止。硬件如果刚好还在等最后一次 DMA 写,就会写到一个已经被 SMMU 移除映射的地址,直接触发 IOMMU fault。

我现在固定了一套顺序:先向硬件下发 abort/stream off 命令并等待确认;然后等待所有正在进行的 DMA 传输进入 idle;之后才把 queue 里的 buffer 状态回收,最后再走 unmap/detach。这个顺序可以总结成一句:先让水管工停水,再拆管道。

static int cam_stop_streaming(struct vb2_queue *q) { struct cam_device *cam = vb2_get_drv_priv(q); /* 1. 停硬件,等 DMA 静止 */ cam_send_hw_abort(cam); cam_wait_for_idle(cam); /* 2. 回收队列中的 buffer 状态 */ /* ... 把挂在驱动队列里的 buffer 全部归还给 VB2 core ... */ /* 3. 最后才释放映射 */ cam_release_all_mappings(cam); return 0; }

4.2 三类典型的泄漏路径

我总结了在 QCOM_CAMERA 里最常见的三类泄漏路径,排查的时候先对照这张表:

泄漏类型现象根因
fd 泄漏系统 fd 数持续上升,相机进程掉 buffer用户态或 HAL 层没有 close 所有的 buffer fd
attach 泄漏DMA-BUF 引用数不归零,buffer 永远释放不掉驱动 attach 后没有对应的 detach
map/IOVA 泄漏SMMU 映射数缓慢增长,最终 iova 耗尽map_attachment 或 IOMMU map 没有成对 unmap

第三种最隐蔽,因为dma_buf_put不会暴露异常,除非你单独数 iova map 数量。我通常在驱动里维护一个原子计数器,每次iommu_map成功加一,iommu_unmap后减一,stream off 后检查是否回到基线。哪一次没回基线,那一帧调用栈就是直接线索。

4.3 我在代码里加的内存水位哨兵

基于上面的经验,我给 QCOM_CAMERA 驱动加了一个 debugfs 节点,每次开关流之后把系统关键内存指标打印出来:

$ cat /sys/kernel/debug/qcom_camera/meminfo active_buffers : 5 queued_buffers : 8 dma_buf_attach : 13 iova_maps : 13 cache_sync_pend : 0

另加了一个周期性的 watchdog,如果 iova_maps 数量超过配置阈值就主动打告警。第一次上线时误报了好几次,原因是摄像头应用在快速开关预览时,旧 stream 的释放和新 stream 的建立有重叠,阈值设太紧。

最终处理是加了一小段 grace period,只统计“空闲状态下”的 map 数量。这个小哨兵帮我快速定位过不少问题,强烈建议你也给自己维护的驱动加一个。

5. 两个真实排查案例:IOVA 泄漏与缓存同步引起的条纹

5.1 案例一:连续开关相机 50 次后无法分配内存

现象很直接:整机跑一段时间后相机打不开,mmap返回 ENOMEM,重启后恢复。HAL 侧 log 看不出问题,每次 close 都按正常流程走了。我开始怀疑内核侧有残留映射,于是在驱动里加了 map 计数,并写了一个自动化开关脚本循环跑。

日志里的关键变化是这样的:

[QCM_MEM] stream_on: iova_maps 16 -> 18 [QCM_MEM] stream_off: iova_maps 18 -> 17 [QCM_MEM] WARNING: baseline not restored, now 17

每次开关流,map 数量不是回到 16,而是停在 17。跑 30 轮以后 IOMMU 地址空间就被耗光。顺着新增的 map 调用栈查,发现一个 feature 请求的特殊路径里,dma_buf_map_attachment成功后如果后续某个寄存器配置失败,代码直接走错误返回,只 put 了 dma_buf 引用,没有调用dma_buf_unmap_attachment。

修复方法也很直接:把该错误出口改成调用前面写的cam_release_mapping()统一收尾。修完后再跑 100 轮开关脚本,map 计数始终回到 16。这个案例让我意识到,驱动代码里最危险的不是正常路径,而是异常路径里的“提前 return”。

5.2 案例二:预览偶发横向条纹和绿色噪点

另一个问题更隐蔽:预览正常,但特定光照和特定分辨率下,画面偶发几条横向错位条纹,频率不高,一天出现一两次。最初所有人怀疑 ISP tuning,改了好几个版本参数,毫无进展。我坚持把方向拉回内存,开始怀疑 cache 同步。

排查时先把问题定位到“该花的时候才花”的规律上。我发现出现条纹的那一帧,恰好是在 CPU 侧把前一帧数据拷贝走、同一块 buffer 马上重新给硬件用的时刻。检查buf_prepare回调,发现这个路径上只做了地址对齐和 size 校验,没有做dma_sync_single_for_device。硬件要读的 buffer 里,可能还有 cache line 是旧数据,于是出现局部数据错乱。

修法不是简单加一个 sync 就完事,还要确认方向。硬件读之前要用dma_sync_single_for_device让 CPU cache 里的脏数据写回;硬件写完之后、CPU 读之前要用dma_sync_single_for_cpuinvalidate cache。方向搞反,问题只会更糟。这个案例最终改名成一条经验:对“同一块 buffer 被 CPU 和硬件轮番访问”的场景,光靠属性配置是不够的,必须把同步点列成表格逐个检查。

5.3 排查内存问题的常用武器清单

现象内核工具/节点看什么
分配失败dmesgalloc failed、ENOMEM的调用栈,先定位是哪个驱动在申请
IOVA 泄漏debugfs 自定义节点 /iommudebugfsmap 数量是否回到基线,配合调用栈统计
DMA-BUF 引用异常/sys/kernel/debug/dma_buf/bufinfo每个 buf 的 refcount、attach 列表
花屏/条纹PTE 属性、dma_sync_*调用点cache 属性是否匹配、同步方向是否正确
帧率抖动VB2 队列状态、驱动日志是否出现 queue underflow、buffer 排队不足

这套组合拳不是一次想出来的,是前几个项目里的血泪积累。遇到问题时先跑一遍清单,能把 80% 的“玄学”变成“某个具体调用少了”。

6. 内核篇(一)的边界和下一篇该看什么

这次先聊到这里。整个内核内存管理的拼图还有几块必须补上:用户态 mmap 那一层映射是如何与内核 DMA-BUF 对接的,cache 属性在不同 camera 路径下的完整选择策略,以及 CMA/DMA-BUF heap 的预留配置对相机内存压力的影响。这些我打算在下一篇里按同样细的粒度展开。

如果你刚接手 QCOM_CAMERA 或者其他移动 SoC 的相机驱动,我建议第一周不要急着改代码,先把“谁拿引用、谁释放映射”画成一张表格,对着queue_setup、buf_prepare、start_streaming、stop_streaming、release这几个回调挨个对一遍。真正动手时,给 map/attach 加上计数器,给关键出口打上日志,让内存路径变成可观测的,再玄学的 bug 都会露出马脚。内存问题的报错大多不在它发生的地方,顺着引用和映射这两条绳摸,总能摸到线头。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 3:24:45

Docker入门指南:从环境一致性到容器编排实战

1. 先搞清楚 Docker 到底解决了什么问题1.1 从“环境不一致”这个老大难说起你肯定遇到过这种情况:代码在自己电脑上跑得好好的,一到同事的机器或者测试服务器上就报错。排查半天,发现是环境变量没配上、依赖库版本对不上,甚至操作…

作者头像 李华
网站建设 2026/10/22 3:23:00

网络参考模型详解:从OSI到TCP/IP,掌握数据封装与排障思路

2. 网络参考模型想搞懂华为数通方向的HCIA,网络参考模型是绝对绕不开的第一道门槛。不管你以后是去调路由器、配交换机,还是跟人家排查一个“网络不通”的问题,最后都会回到这张图上:数据到底是怎么从一台设备跑到另一台设备&…

作者头像 李华
网站建设 2026/10/22 3:21:43

Redis主从复制“三次握手”源码拆解:状态机与同步机制

用INFO replication观察过一次主从同步的人&#xff0c;应该都有过类似的经历&#xff1a;从库刚配好replicaof&#xff0c;日志里刷出一堆“MASTER <-> REPLICA sync started”&#xff0c;主库那边同步进度也跳了好几个状态&#xff0c;最后两边都指向online&#xff0…

作者头像 李华
网站建设 2026/10/22 3:20:55

MATLAB实战:深度置信网络(DBN)在股票时间序列预测中的应用

最近在折腾时间序列预测的时候&#xff0c;发现深度置信网络&#xff08;DBN&#xff09;这个老牌选手居然在股票数据上表现挺有意思。今天就拿MATLAB实操一把&#xff0c;咱们边写代码边聊门道。先说清楚我为什么回头看DBN。现在一提深度学习做预测&#xff0c;大家条件反射就…

作者头像 李华
网站建设 2026/10/22 3:20:51

Visual Studio 2017 Installer 闪退打不开?三步排查修复指南

各位搞 Visual Studio 2017 的朋友&#xff0c;不知道你们有没有遇到过这种情况&#xff1a;某天打开电脑&#xff0c;想继续写点代码&#xff0c;双击 VS2017 的快捷方式&#xff0c;图标转了两圈&#xff0c;然后就没有然后了。要么直接闪退&#xff0c;要么弹个提示说找不到…

作者头像 李华
网站建设 2026/10/22 3:20:00

均数估计与t检验:SAS、R、Stata、SPSS实战指南

下午刚把第三章的作业看完&#xff0c;十个里面至少有五个问我同一个问题&#xff1a;均数估计和假设检验的公式明明会手算&#xff0c;为什么换成SAS、R、Stata、SPSS就不知道点哪里、写什么命令。这个感觉我太懂了。学校课程往往只带一遍某一款软件&#xff0c;真到了自己手里…

作者头像 李华