做Camera的工程师大多会有这样的体验:上层HAL里各种buffer配置、request queue写得再漂亮,跑一轮长时间预览加录像之后,系统可用内存还是肉眼可见地往下降,甚至出现黑屏、花屏、设备无法出流。问题往往不在算法,也不在业务逻辑,而是在QCOM_CAMERA内核侧的内存管理上。
QCOM_CAMERA,简单说就是高通平台相机子系统在内核驱动里的那套东西,从早期的msm camera驱动到后来的cam_xxx系列模块,外围叫法变过很多次,但核心要回答的问题始终是几个:buffer从哪来、怎么给到硬件、CPU和硬件之间的cache怎么保持一致、用完了怎么回收、出了问题从哪查。这一层不梳理清楚,上层做得再花哨都是白搭。
这篇文章专门聊kernel侧的内存管理。它不涉及HAL里复杂的算法策略,也不聊某个具体设备树的写法,而是从一块buffer在内核里的完整生命周期讲起,把分配、映射、同步、回收、调试这条路走一遍。适合正在做Camera驱动、多媒体中间件、系统稳定性或者内存调优的同学参考,也适合刚入行的新手在开始改代码之前,先建立一张“内存管理地图”。
1. 开篇:为什么QCOM_CAMERA的kernel内存管理值得单独聊一聊
1.1 一次预览花屏带出来的血泪教训
有一次我在排查一个项目里的预览花屏问题,现象非常稳定:开预览前几秒画面正常,一旦从拍照切回预览,或者连续切换几次前后摄,图像就开始出现条纹和错位色块。上层翻来覆去查了HAL的pipe配置、ISP参数、format协商,都没有问题。
最后查下来,问题出在内核侧对同一个dma_buf做了CPU和硬件侧的交叉访问,中间漏了一次cache同步。也就是说,CPU往buffer里写入了参数或者metadata之后,没有在合适的时间点把cache内容刷到内存,硬件直接拿到的是一段脏数据。
类似的问题在Camera领域太典型了。大多数和Camera相关的稳定性、画质问题,追到最后都会落到内存管理的某个环节:buffer申请失败、映射关系没建立好、cache同步顺序错乱、引用计数没管理住导致泄漏。所以我说,内核侧的内存管理不是“一个模块”,而是所有Camera问题最终汇聚的那一堵墙。
1.2 这篇文章覆盖的范围和边界
我会按一条主线来讲:从分配器选型开始,到fd如何变成dma_buf,再到如何映射给硬件、如何做cache同步、如何释放回收,最后给出定位问题的调试路径。这些都是我在实际项目中反复用到、也反复踩过坑的环节。
HAL层面能做的内存优化不在本文范围内,比如buffer复用策略、request调度的QoS参数,这些我认为属于另一层逻辑。但上层怎么调,底层能不能接得住,底线仍然由kernel侧内存管理决定。举个例子,HAL决定了要申请10路buffer做双摄虚化,内核就得考虑这10路buffer的物理内存是否够、CMA区域是否会产生碎片、SMMU映射表是否够用。所以这篇文章最终还是要回答:底层怎么做,才能让上层玩得起来。
2. 先说整体链路:一块buffer在内核侧的完整旅程
2.1 一次拍照背后,buffer经历了什么
很多人看Camera驱动,喜欢直接盯ISP的寄存器配置或者sensor出图时序,但一块buffer在内核里的旅程,实际上是另一套完整逻辑。
一次典型的拍照流程大致是这样的:
- HAL通过v4l2或自定义的设备节点下发一个request,里面包含buffer的fd、stream id、settings信息。
- 内核驱动通过fd拿到对应的dma_buf句柄。
- 驱动把dma_buf attach到camera设备上,建立设备和buffer之间的绑定关系。
- 通过SMMU映射,把dma_buf关联的物理内存映射到设备可访问的地址空间。
- sensor硬件把曝光出来的数据写到这个地址,或者ISP从这里读取数据进行处理。
- 处理完成后,驱动解除映射、detach,把buffer还给上层。
这一套流程里,最关键的是第2步到第4步,也就是fd到dma_buf再到设备映射的过程。很多“分配失败”“映射错误”的log,问题就出在这几行的代码路径上。
2.2 连续内存、SMMU和物理内存的三角关系
早期的Camera硬件接口比较简单,sensor输出数据给到内存控制器,要求必须是一段物理连续内存,否则DMA无法找到下一块数据。这也是为什么过去大家一谈到Camera内存,第一反应就是CMA。CMA专门用来分配物理连续的大块内存,满足这类硬件的需求。
但是QCOM_CAMERA发展到后来,硬件侧普遍集成了SMMU。SMMU相当于给硬件设备也做了一层页表映射,设备访问的地址可以不再等于物理地址。这意味着,物理上不连续的内存片段可以通过SMMU页表拼成一个设备侧虚拟连续地址空间。带上SMMU之后,Camera驱动不一定非要申请物理连续内存,dma-buf heaps里的system heap也能用,前提是驱动在attach之后正确走一遍IOMMU映射。
这个设计提高了内存利用率,但也引入了新问题:SMMU映射本身需要分配页表内存,映射表项数量是有限的,不同设备之间还要做隔离。所以现在查Camera内存问题,除了看物理内存够不够,还要看SMMU映射是否失败、页表是否泄漏。
2.3 map和sync,很多人混为一谈
在这条链路里,新手最容易混淆的两个概念是map和sync。
map指的是建立地址映射,让某个访问者能够看到这段内存。比如用户态mmap拿到虚拟地址,内核态通过dma_buf_vmap拿到内核虚拟地址,硬件通过SMMU映射拿到设备地址。这三者本质上是把同一块物理内存关联到不同的地址空间。
sync指的是数据同步。当CPU和硬件DMA同时访问同一块内存时,由于CPU有cache,硬件读到的数据和CPU写入的数据可能不一致。DMA cache同步问题不是新概念,但每次出问题都让人头疼,因为现象往往是花屏、错数据这些很难一句话说清楚的问题。
我的经验是:遇到问题先分清当前访问者是CPU还是硬件,再决定该做for_device还是for_cpu方向的同步。顺序反了,或者漏掉了,后面全白搭。这部分后面我会展开讲。
3. Buffer从哪来:分配器选型与heap配置要点
3.1 ION和dma-buf heaps,老平台与新平台的取舍
内核侧的内存分配,以前在Android生态里几乎是ION一家独大。ION把不同类型的内存分配需求拆成不同的heap:system heap给常规非连续内存,carveout heap给固定预留内存,CMA heap给物理连续内存。QCOM_CAMERA早期的驱动基本都是基于ION的。
后来内核社区推动用dma-buf heaps替代ION,因为ION长期没有进入上游主线,维护分散、厂商定制化严重。新一代内核里,dma-buf heaps是主流方向,QCOM平台也逐渐迁移到这套框架。系统里常见的是 system heap 和 cma heap,通过设备节点 /dev/dma_heap/system 和 /dev/dma_heap/cma 暴露给用户态。
老平台过渡期,可能两边并存。判断你的平台上用的是哪一套,直接看Camera驱动代码里使用的是 ion_alloc 还是 dma_heap_buffer_alloc,或者看用户态HAL引用了哪些头文件。如果代码里混着两套逻辑,兼容层就要特别小心,因为两套框架的fd封装方式不同,拿到的buffer语义也不同。
3.2 不同硬件场景下,heap该怎么选
QCOM_CAMERA内部其实不止一种buffer需求。sensor raw数据buffer,通常要求物理连续或者至少设备侧地址连续,首选CMA;给GPU做贴图或者给显示用的buffer,非连续内存就够;涉及安全特性的buffer,比如secure camera场景,只能用专用的安全heap。
实际配置的时候,我习惯按硬件模块的使用方式倒推,不盲目追求“全部用CMA”或者“全部用system heap”。
举个例子,之前有个项目,HAL里一口气申请了20个4K分辨率的raw buffer,全部走CMA,结果系统跑一会儿之后,内核报出“cma_alloc failed”之类的错误。原因并不是内存总量不够,而是CMA区域被反复分配释放之后产生了碎片,大块连续内存申请不出来。后来把一部分不要求物理连续的buffer切到system heap,再用SMMU映射,问题就缓解了。
所以选heap的逻辑不是“哪个性能好”,而是“硬件哪个环节真的需要物理连续”。判断依据就是这条buffer最终给哪个硬件模块使用、那个模块是否接了SMMU、硬件是否支持scatter-gather。
3.3 CMA区域大小和调整的注意点
CMA区域大小通常在设备树或者内核配置里预留,比如:
linux,cma { compatible = "shared-dma-pool"; reusable; size = <0x0 0x20000000>; /* 512MB */ linux,cma-default; };这块区域看起来是“预留”出来的,但它本身是reusable的,用户可以回收给普通页面使用。也就是说CMA内存并不是完全被Camera占死,而是当Camera需要大块连续内存时,系统要把这些页面迁移回来并整理成连续块。迁移过程有延迟,可能触发直接内存回收,产生卡顿。
调整CMA大小要分场景评估。预览场景一般每帧raw buffer在十几MB以内,4K拍照可能到几十MB。如果平台还有GPU、Display、Video等模块也想用CMA,就需要统筹。总的感觉是:CAMERA独占的连续内存需求不算大,但突发需求明显,比如连拍、HDR多帧合成,一次性会申请多个大buffer。这种情况宁可预留稍大一些的CMA,也不要让它在运行中反复触发page migration。
3.4 buffer数量和大小设计的一点参考
我通常会在项目初期做一个简单的经验估算。以一个13M像素、10bit raw、双stream为例,单帧数据量大约是13M乘以2字节,约26MB。HAL的buffer池通常是4到8路,也就是一次占住200MB上下。如果还要做多帧合成,比如夜景模式的7帧,那内存需求直接翻几倍。
这种数量级下,buffer开得太小会阻塞出流,开得太大很容易把系统内存打爆。内核侧要做的是把分配请求拦下来,在分配前做一次预算检查。如果HAL一下子申请了超过阈值的内存,驱动最好直接返回失败,而不是让系统在内存高压里挣扎到最后被LMK杀掉。
4. 分配与映射链路:从fd到硬件地址
4.1 从文件描述符到dma_buf
上层通过buffer fd传递内存,内核侧的真正入口是fd对应的struct file,以及绑定在这个file上的dma_buf。拿到fd之后,驱动一般要走这样的路径:
struct dma_buf *dmabuf = dma_buf_get(fd); if (IS_ERR(dmabuf)) return PTR_ERR(dmabuf); /* 建立设备与buffer的绑定 */ struct dma_buf_attachment *attach; attach = dma_buf_attach(dmabuf, dev); if (IS_ERR(attach)) { dma_buf_put(dmabuf); return PTR_ERR(attach); } /* 获取sg_table,并建立IOMMU映射 */ struct sg_table *sgt; sgt = dma_buf_map_attachment(attach, DMA_BIDIRECTIONAL); if (IS_ERR(sgt)) { dma_buf_detach(dmabuf, attach); dma_buf_put(dmabuf); return PTR_ERR(sgt); }这段代码是非常标准的dma_buf使用姿势,但有几个细节值得反复检查。
第一,dma_buf_get和dma_buf_put必须成对。get会把引用计数加一,如果内核路径上提前return了,很容易把put漏掉,导致buffer永远无法释放。在内核日志里表现为fd已经关了,但dma_buf还在。
第二,dma_buf_attach的返回值必须检查。一个buffer不一定能attach到所有设备上,尤其是不同heap的buffer,对设备访问权限和时间戳要求不同。老代码里偶尔会有人忽略这个返回值,后面访问sgt时直接空指针崩溃。
第三,dma_buf_map_attachment返回的sgt,最终要在request完成之后unmap。unmap的时候要确保硬件确实不再访问这段内存了,否则可能出现硬件还在写数据,驱动已经解除映射,下一帧buffer被其他模块复用的竞态问题。
4.2 用户态mmap和内核态vmap的路径差异
HAL层拿到fd之后,通常要mmap到用户空间做数据读写。dma_buf自身实现了mmap接口:
static int my_dmabuf_mmap(struct dma_buf *dmabuf, struct vm_area_struct *vma) { return dma_buf_mmap(dmabuf, vma, 0); }这里有一个容易踩的坑:用户态mmap出来的地址,缓存属性并不一定和硬件DMA访问一致。有的平台上,用户态映射默认是write-back的,CPU写入之后要等cache回写,硬件才能看到数据。到了内核侧,如果你想直接在驱动里访问buffer内容做调试,比如打印raw数据的前几个字节,推荐用dma_buf_vmap,拿到内核虚拟地址再memcpy,而不是自己去翻物理页面。
void *vaddr = dma_buf_vmap(dmabuf); if (vaddr) { /* 读取或者修改buffer内容,注意加cache同步 */ dma_buf_end_cpu_access(dmabuf, DMA_TO_DEVICE); }记住,vmap拿到的是CPU视角的地址,它和SMMU给硬件侧映射出来的设备地址不是同一个东西。在讨论地址时,永远先问一句:谁在访问,CPU还是硬件?这在协同调试时能省下大量擦屁股的时间。
4.3 SMMU映射和硬件设备attach的常见坑
带SMMU的高通平台上,设备侧地址空间是独立管理的。驱动使用dma_buf_map_attachment之后,底层IOMMU驱动会分配IOVA,并把sg_table里的每一段物理页面填到页表里。这个过程中的典型问题是:
IOVA空间碎片化,映射大buffer时找不到足够大的连续IOVA区域。这和物理内存碎片看着很像,但日志上不一样,IOMMU报的是iova alloc失败。解决思路通常是调整IOVA空间大小,或者开启IOMMU的地址压缩/低地址优先策略。
page table深度不足,当设备需要映射大量buffer时,页表内存占用会显著上升。你要是在调试中看到内核内存不断上涨但实际分配出去的buffer并不多,可以查一下SMMU的pgtable是不是在泄漏。打开iommu debugfs看domain的page table计数最直观。
不同设备之间的SMMU隔离问题。Camera子系统内部包含了IFE、IPE、ICP等多个硬件模块,每个模块都有独立的SMMU或独立domain。如果buffer错误地attach到了错误的设备上,硬件访问时可能拿到一个被隔离无法访问的地址,表现出来就是黑屏或者写数据没有效果。
4.4 cache一致性:最容易翻车的环节
前面反复提到cache,这里集中说透。
CPU通过cache访问内存,硬件DMA直接访问内存地址,两者看到的可能不是同一份数据。解决方式就是使用dma_buf的cache操作接口:
/* CPU开始访问buffer之前 */ dma_buf_begin_cpu_access(dmabuf, DMA_FROM_DEVICE); /* 在这里读取硬件写入的数据 */ /* CPU访问结束后,把数据刷给硬件 */ dma_buf_end_cpu_access(dmabuf, DMA_FROM_DEVICE);这里面有两个方向。
DMA_FROM_DEVICE是硬件往内存写,CPU要读。begin_cpu_access要把cache invalidate掉,防止CPU读到旧的cache内容。DMA_TO_DEVICE是CPU写数据给硬件读,end_cpu_access要做clean,也就是把cache内容刷回内存。
实际操作中,我遇到最多的问题是方向传错。比如CPU往buffer里填metadata然后给ISP去读,有人误传了DMA_FROM_DEVICE,结果clean操作没生效,硬件读到的还是内存里的旧值。这种问题不是每次都复现,和cache line状态、CPU核数、大小核调度都有关系,排查起来特别费劲。
避免这个问题的唯一有效方式,是在驱动代码里把每次同步的语义写清楚,注释里写明是谁写、谁读、哪个方向,不允许含糊。
5. 释放、回收与生命周期管理
5.1 引用计数和fd关闭的关系
dma_buf使用引用计数管理生命周期,主要计数有两层。
第一层是fd对应的file structure,fd关闭时file的release回调触发,dma_buf_put会减一次引用。第二层是attach引用。每个设备attach到dma_buf上,都会增加引用计数。dma_buf只有在所有fd关闭并且所有设备都detach之后,才能真正释放物理内存。
这带来一个常见问题:fd已经关闭了,但某个驱动模块忘记detach,或者dma_buf_get之后没有dma_buf_put,引用计数一直不为零。表现就是上层已经释放了buffer,但系统内存没有降下来。
调试这类问题时,我首先会看dma_buf的引用计数记录。内核开启CONFIG_DMABUF_DEBUG后,会在debugfs里把dma_buf文件、引用进程、相关的设备列表暴露出来。
5.2 内核侧如何判断buffer泄漏
最直接的方法是周期性采样/proc/meminfo和DMA-BUF的debug节点,对比一段时间内dma_buf的总量是否持续上涨。
以现代内核为例,直接看这个节点:
cat /sys/kernel/debug/dma_buf/bufinfo输出里能看到每个dma_buf的size、attached设备、exp_name和当前引用它的进程。如果某些buffer名字频繁出现,size又特别大,比如几十MB,而且引用它的进程反复变来变去,那基本可以锁定是谁在申请buffer却没有释放。
老平台如果还走ION,可以看:
cat /sys/kernel/debug/ion/ion_mm_heap里面会列出heap的总大小、已分配、空闲以及各类buffer的个数。如果“alloc bytes”只升不降,说明有路径泄漏。
5.3 主动回收和被动回收的边界
内核在内存压力大的时候会启动回收机制,但这套机制对dma_buf并不总是有效。拿CMA举例,CMA区域内存可以移动,但如果buffer已经被硬件attach并且dma_buf_map_attachment成功了,页面就被固定住了,短时间内无法迁移回收。
所以做QCOM_CAMERA内核驱动的时候,要特别注意buffer生命周期里存在一段“不可回收期”。这段周期越短,系统内存压力越小。有的项目里,HAL会一次性把8路buffer都attach到硬件上待命,看似省了request下发时间,实际上让内核白白占住大量不可回收内存。
我的建议是:内核侧只保留真正在硬件管线上的buffer映射,空闲buffer尽量让上层归还回buffer池,不要长期处于attach状态。这个边界需要在HAL和kernel之间明确约定,否则两边的优化方向会互相打架。
6. 问题排查方法与实战记录
6.1 遇到Camera内存问题,先看这几个节点
不要一上来就怀疑某个驱动函数写错了,先看证据。
排查的第一个节点永远是DMA-BUF统计:
cat /sys/kernel/debug/dma_buf/bufinfo第二节点看物理内存分布:
cat /proc/buddyinfobuddyinfo会把内存按阶数列出来,如果高阶空闲块很少,说明物理内存碎片严重。CMA分配失败基本都是这个原因。
第三节点击SMMU/IOMMU状态。高通平台一般有对应的iommu debug节点,看映射次数和页表总数。
第四个节点是打开trace event,用tracefs抓dma_buf的关键路径:
echo 1 > /sys/kernel/tracing/events/dma_buf/enable抓一段视频录像的时间窗口,能清晰看到哪些buffer被map了、被unmap了,分布是否合理。
我之前遇到过一个问题:视频预览持续两小时后内存缓慢增长。从dumpsys meminfo和dma_buf bufinfo里看到有几个size较大的buffer引用计数始终不减,而且每次重启预览都会新增一批,最后定位到是HAL在特定场景下重复申请了buffer,内核侧没有对同一份buffer做复用判断。
6.2 花屏问题:优先怀疑cache和同步
花屏的分支很多,但QCOM_CAMERA场景里,cache同步出问题的概率极高。
一个典型的定位方法:在硬件访问buffer之前,故意用CPU读一遍buffer内容,比如memset整个buffer为零。如果下一帧花屏位置改变了,或者花屏消失了一两帧,基本可以断定是cache问题。这个操作很简单,但非常有效。
再配合tracepoint,看CPU访问和硬件访问的时间顺序。如果CPU写完之后没有发生对应方向的end_cpu_access,或者硬件访问完没有begin_cpu_access就交给CPU去读,那问题就锁定了。
还有一类花屏是SMMU映射故障,现象和cache问题很接近,但是有周期性,比如只在切成4K分辨率后出现。这种要看SG表遍历结果,确认硬件拿到的每个地址和实际物理页面一一对应。不要觉得映射一定是对的,调试时我见过不止一次,因为某个标志位没设置,IOMMU把最后一段页面映射到了错误地址。
6.3 CMA分配失败与碎片问题处理
CMA分配失败的常见日志特征是“cma_alloc failed”,或者看到驱动自己打的“buffer allocation error”。这类问题多发生在长时间录像后切换拍照,或者多路stream同时启动的场景。
处理手段按优先级排:
- 检查当前request数量是否超过了硬件能力。HAL里把burst request数量降下来,能立竿见影。
- 加大CMA区域。但是要评估page migration带来的卡顿风险。
- 把不要求物理连续的buffer切到system heap。大部分预览buffer是可以这么做的。
- 图像压缩或者格式调整,减少单帧buffer大小。
在项目里,我通常先做第3步,因为它不改变硬件能力,只改变内存来源。等稳定了再评估是否需要动CMA大小。
6.4 常见问题速查表
| 现象 | 可能根因 | 优先排查手段 | 解决方向 |
|---|---|---|---|
| 预览花屏 | CPU和硬件访问cache未同步 | trace文件确认同步方向 | 补漏sync或修正DMA方向 |
| 长时间运行后内存上涨 | buffer泄漏或引用计数未释放 | 对比不同时间点dma_buf bufinfo | 检查get/put和attach/detach配对 |
| 高分辨率启动失败 | CMA连续内存不足 | 查看/proc/buddyinfo | 换system heap或调整CMA |
| 黑屏但无报错 | SMMU映射域错误或IOVA分配失败 | iommu debugfs查页表 | 检查设备attach目标 |
| 切场景时卡顿 | 大块内存反复迁移回收 | 看DMA-BUF trace map/unmap | 减少map/unmap次数 |
| 系统被杀进程 | 单帧buffer过大,总内存超限 | 统计buffer池总量 | 限制并发request数量 |
这张表是我平时给团队做培训时用的底稿。实际项目里,十次有七八次都会落到表格里的前两行。你看,这页表格里的问题,本质上都是生命周期和同步方向的问题,而不是内存带宽或者硬件算力的问题。
7. 写到最后的一些体会
文章写到这里,其实并没有把所有细节讲完。比如buffer在Camera硬件内部的分区管理、fence与buffer解耦、以及和GPU/Display共享buffer的同步机制,这些单独拿出来都是大话题。
说回我自己的感受。我在QCOM_CAMERA内存管理上踩过的坑,绝大多数不是“不懂某个API”,而是“对一个环节想当然”。比如觉得dma_buf_map_attachment之后就不用管cache了,觉得fd关了buffer就肯定释放了,觉得SMMU映射不会失败。想当然的每一次,最后都会变成夜里的一通电话。
如果你正在排查一个反复出现的内存问题,我的建议是:先把所有buffer的完整生命周期画出来,标注清楚每个环节的访问者是谁、引用计数加在哪、减在哪、cache同步方向是什么。这张图画对了,问题基本就解决了一半。下一篇我会继续聊Camera buffer和fence的联动,以及多模块共享buffer时如何避免同步竞态。