公司动态

libcamera辅流配置触发std::bad_alloc:CMA连续内存耗尽排查与修复

📅 2026/8/31 22:08:21
libcamera辅流配置触发std::bad_alloc:CMA连续内存耗尽排查与修复
1. 问题现场既不是IMX335的锅也不全是libcamera的错先描述一下我遇到问题的环境。板子是STM32MP257FArm Cortex-A35双核带Mali-G310 GPU跑的是Yocto构建的Linux系统。摄像头模组是IMX335500万像素CMOS sensor通过CSI-2接口接入。软件栈是libcamera libcamera-apps内核用的是5.15主线内核加上ST官方补丁。第一次跑起来的时候主流的配置完全没有问题2592x194410-bit RAW30fps出图正常颜色正常曝光控制也正常。但是当我在libcamera的CameraConfiguration里多添加一个640x640的secondary stream时应用程序直接抛了一个异常ERROR Camera camera.cpp:1185 Pipeline handler failed to complete configuration ERROR V4L2 v4l2_videodevice.cpp:1088 Failed to allocate buffer memory ERROR V4L2 v4l2_videodevice.cpp:1088 Failed to allocate buffer memory terminate called after throwing an instance of std::bad_alloc看着三行报错第一反应是检查内存余量free -m一看还有几百MB可用怎么会bad_alloc后来仔细排查下来才明白这个std::bad_alloc跟用户态可用的内存数量没有直接关系问题出在DMA内存池一个很多人不太关注的地方。这篇文章会完整记录这个问题从复现、定位、修复的全过程。如果你也在嵌入式Linux平台上跑libcamera并且遇到过类似配置多路流时内存分配失败的问题这篇文章可以帮你省下大量排查时间。2. libcamera的流配置机制与主流\辅流的内存分配逻辑2.1 StreamRole与StreamConfiguration的生成流程要理解这个问题的根源首先要弄清楚libcamera在配置多个stream时到底做了什么。libcamera通过CameraManager::get()拿到Camera对象后应用程序会调用camera-generateConfiguration(roles)来生成一个CameraConfiguration。这个roles是一个StreamRole列表每个角色都对应一个逻辑上的用途。在libcamera中StreamRole有这几个常见值StreamRole典型用途典型分辨率Raw原始sensor数据采集sensor全分辨率StillCapture高分辨率静态拍照与sensor分辨率一致VideoRecording视频录制1080p或720pViewfinder预览640x480或720p当你一次性传入两个role时libcamera的pipeline handler会尝试在同一个Camera设备上配置两个独立的Stream对象。在STM32MP2平台上pipeline handler会走到PipelineHandlerSimple或者ST自己定制的pipeline不同的pipeline对多stream的支持方式完全不同。问题就出在这里每一个Stream在libcamera内部都对应一组V4L2设备而每一个V4L2设备都要通过VIDIOC_REQBUFS或者VIDIOC_CREATE_BUFS来申请内存缓冲。这不是应用层malloc而是通过内核的videobuf2框架申请DMA缓冲区。2.2 StreamConfiguration的分配与底层缓冲申请从应用视角看好像只是设置了分辨率和像素格式但背后libcamera做了这几件事根据StreamRole选择一个sensor模式通常由最大的那个stream决定为每个Stream创建V4L2VideoDevice实例调用V4L2VideoDevice::setFormat()把分辨率、格式配置到驱动调用V4L2VideoDevice::allocateBuffers()申请缓冲。allocateBuffers()内部会先调用VIDIOC_REQBUFS告诉驱动需要多少个buffer。对于每个bufferlibcamera还会再调用VIDIOC_QUERYBUF拿到每个buffer的m.offset或者dma-buf fd。之后要么通过mmap映射到用户空间要么通过dma-buf导入到其他设备。而在STM32MP257F这样的嵌入式平台上videobuf2底层使用的是vb2_dma_contig内存管理器。这个名字已经说明了一切它需要连续物理内存。这就是后面一切问题的根源。2.3 为什么是std::bad_alloc而不是内存不足很多人在这个环节会困惑内存不够应该返回-ENOMEM怎么会抛出std::bad_alloc因为libcamera的代码里做了这样一件事当VIDIOC_REQBUFS返回错误时libcamera不是把错误码直接传给应用层而是在V4L2VideoDevice::allocateBuffers()内部尝试分配一个用于存放buffer元数据的向量。这个向量是用std::vector管理的并且会做reserve操作int V4L2VideoDevice::allocateBuffers(unsigned int count, V4L2DeviceFormat *format) { ... buffers_.reserve(count); ... // ioctl(VIDIOC_REQBUFS) 失败路径 LOG(V4L2, Error) Failed to allocate buffer memory; return -ENOMEM; }问题在于在给buffers_.reserve(count)分配内存时如果系统内存碎片化严重或者可用内存低于阈值std::vector的底层std::allocator会抛出std::bad_alloc异常。这发生在VIDIOC_REQBUFS失败之前所以最终呈现在应用层的就是一个C异常。而在嵌入式平台上-ENOMEM之后libcamera可能需要做清理、重试或者捕获异常后返回错误。但libcamera的PipelineHandler代码中没有对allocateBuffers的异常做完全防护导致std::bad_alloc直接穿透了抽象层最终表现为应用崩溃。所以std::bad_alloc只是一个表象真正的问题是硬件DMA内存CMA耗尽。3. 根因深挖CMA池、对齐策略与缓冲器数量如何联手挤爆内存3.1 先认识CMAContiguous Memory AllocatorCMA是Linux内核中用于分配连续物理内存的机制。它的核心思路是平时把物理内存页面分配给可移动的用户态页面使用当设备驱动需要连续内存时通过migratepages把这些页面迁移走空出一块连续区域。这个设计看起来巧妙但在实际运行时有一个致命问题当系统内存碎片化严重时即使总内存还有富余也可能无法凑出足够大的连续区域。STM32MP257F的Linux内核通常配置了CONFIG_DMA_CMAy默认CMA大小可以通过内核命令行参数cma256M或者设备树里的linux,cma节点来设置。3.2 实际内存大小计算为什么640x640也能挤爆内存先来计算一下一个640x640的stream需要多少DMA内存。IMX335是10-bit RAW sensor但libcamera内部在使用V4L2时通常会把V4L2_PIX_FMT_SRGGB10P作为实际的硬件格式。10-bit packed格式在每像素占用上是10bit也就是1.25字节但V4L2驱动在计算bytesperline时常常是按照2字节或4字节对齐的还要加上行对齐的padding。具体到STM32MP2平台的ISP图像信号处理器它输出的格式经常是如果走ISP出YUV每像素2字节YUV422如果出RAW每像素2字节10-bit存16-bit容器按照640x640、每像素2字节对齐到驱动要求的步长通常64字节对齐单帧大小大约是640 * 2 1280 字节/行 1280 对齐到 64 字节 → 1280 字节恰好对齐 640行 * 1280字节 819200 字节 ≈ 0.78 MB一帧0.78MB如果libcamera默认申请4个buffer那就是大约3.1MB。看起来完全不多。但问题在于这时系统中已经有一个2592x1944的主流在跑。计算主流内存占用2592 * 2 5184 字节/行 5184 对齐到 64 → 5184 字节恰好对齐 1944行 * 5184字节 10077696 字节 ≈ 9.6 MB如果主流申请4个buffer就是38.4MB。如果申请8个buffer就是76.8MB。看起来也还好。但是请记住IMX335的5MP全分辨率模式是2592x1944这还不是sensor的最大输出。IMX335的PLL配置在某些模式下可以输出2592x1944在另一些配置下是2608x1960或者更大。再加上很多pipeline handler为了效率会申请6~8个buffer而不是4个。这里真正的杀手是DMA内存对齐策略。在STM32MP2的platform驱动中vb2_dma_contig默认会对分配大小做2MB对齐处理。也就是说你申请0.78MB的内存对齐后实际占用2MB你申请9.6MB的内存对齐后实际占用10MB向上取整到12MB因为必须按2MB倍数对齐。让我们把实际占用重新算一遍流请求大小对齐后大小4个buffer6个buffer主流(2592x1944)9.6 MB10 MB40 MB60 MB辅流(640x640)0.78 MB2 MB8 MB12 MB合计48 MB72 MB如果加上ISP的另外几个内部缓冲比如统计信息buffer、3A buffer总需求轻松突破100MB。如果板子的CMA大小是默认的cma64M或者cma96M那么当主流占用了60MB辅流再需要8MB时CMA池就真的会没有连续空间了。3.3 真正导致失败的那个瞬间在你只配置主流的时候一切正常因为CMA池还够用。当你额外添加640x640辅流时libcamera的配置流程是先配置并分配主流的buffer再配置并分配辅流的buffer。主流已经在运行中占用了大量DMA内存此时辅流的640x640 buffer申请进入内核CMA allocator开始在剩余空间里寻找2MB对齐的连续物理页面。如果此时CMA池中的剩余区域存在大量碎片比如之前ISP分配了一些大小不等的其他buffer分配器无法找到满足连续2MB的区域就会返回-EBUSY或者-ENOMEM。而如前面所说libcamera V4L2层的异常处理不够健壮最终表现为std::bad_alloc。3.4 为什么其他人没遇到问题我却遇到了很多人在树莓派或者其他通用Linux平台上跑同样的代码是好的因为树莓派的CMA默认是256MB而且树莓派的libcamera pipelineunicam对buffer数量的管理更保守默认只分配4个buffer。STM32MP2的官方Yocto BSP为了兼顾ISP和GPU等模块默认CMA设置并不是很大特别是在256MB内存的低配板型上CMA通常只分到64MB。这就导致同样的libcamera应用在树莓派上跑得好好的在STM32MP2上却出现内存分配失败。这提醒我们从树莓派迁移到嵌入式MPU平台时CMA配置是必查项。4. 修复与规避从配置到代码的四种实操方案4.1 方案一调整内核CMA大小最快最直接如果你对CMA机制还不够熟悉首选方案就是调大内核的CMA区域。这里有两种方式。方式一修改内核命令行参数在BootloaderU-Boot的bootargs中添加cma128M比如原来的bootargs是consolettySTM0,115200 root/dev/mmcblk1p2 rootwait改成consolettySTM0,115200 root/dev/mmcblk1p2 rootwait cma128M方式二修改设备树在设备树的根节点下找到reserved-memory区域reserved-memory { #address-cells 2; #size-cells 2; ranges; linux,cma { compatible shared-dma-pool; reusable; size 0x0 0x8000000; /* 128MB */ linux,cma-default; }; };修改size为你期望的大小。注意如果设备树里没有linux,cma节点但内核启用了CONFIG_DMA_CMAy那默认的CMA大小是由CONFIG_CMA_SIZE_MBYTES决定的通常为0然后通过内核命令行参数指定。在STM32MP257F的官方BSP里设备树中通常已经定义了这个节点所以直接改设备树中的size是更规范的做法。设置多大比较合适我的建议是先预估你的所有视频流需求把每个流的单帧大小乘以buffer数量求和再乘以1.5的余量系数。比如上面的例子主流就算10MBx660MB辅流2MBx816MB3Abuffer20MB合计约96MB那么128MB是合理起点。如果板子内存是1GB设置256MB也没问题。4.2 方案二调整libcamera的buffer数量配置如果不想动内核或者CMA大小已经因为其他原因固定那可以在应用层减少buffer数量。libcamera的stream配置中有一个bufferCount字段。在生成CameraConfiguration之后你可以直接修改std::unique_ptrCameraConfiguration config camera-generateConfiguration({StreamRole::VideoRecording, StreamRole::Viewfinder}); // 打印stream索引 for (unsigned int i 0; i config-size(); i) { StreamConfiguration scfg config-at(i); std::cout Stream i : scfg.size.toString() scfg.pixelFormat.toString() bufferCount scfg.bufferCount std::endl; // 强制减少buffer数量 scfg.bufferCount 4; }当然这里有两个注意点不是所有pipeline都允许任意bufferCount。有些driver有min_buffers_needed的要求比如IMX335的sensor驱动通常要求至少2个buffer才能正常出帧。bufferCount太少会影响帧率。在V4L2的buffer队列机制中如果buffer太少应用来不及处理一帧会导致队列空转帧率下降。在Camera系统中通常4个buffer是底线。实测下来将默认的6~8个buffer改成4个对于640x640的辅流来说影响不大因为辅流的处理时间短队列不容易空。4.3 方案三把辅流作为主配置流换顺序这个方法源自一次偶然的尝试我发现如果让libcamera先配置辅流再配置主流有时可以绕过这个错误。原因是CMA分配的行为模式分配器倾向于从CMA池的一端开始分配第一个大的连续分配很容易满足之后小的分配在剩余区域中找对齐位置也相对容易。如果先分配大块的主流把CMA池中间的连续区域拆掉后面小块分配反而可能因为碎片化失败。这种方式在逻辑上有点像装箱问题先放大的再放小的有时候小箱子就塞不进去了先放小的再放大的大箱子只要剩余空间够就能放进去。在libcamera中控制配置顺序的方式是把roles列表的顺序交换auto config camera-generateConfiguration({StreamRole::Viewfinder, StreamRole::VideoRecording});这样生成的CameraConfiguration中640x640的Viewfinder会排在前面先分配buffer再分配高分辨率的VideoRecording流。但这个方法并不保证每次都能解决问题因为CMA池的状态会受到之前的操作影响。如果系统上电后第一次运行没问题第二次运行为什么会失败很可能就是CMA池中之前分配的buffer还未释放干净产生了碎片。4.4 方案四在应用层预分配并复用buffer这是最治本的方式但需要你对libcamera的API熟悉。libcamera提供了FrameBufferAllocator允许你在配置阶段就预先分配好所有stream的buffer而不是让pipeline handler自己去分配。FrameBufferAllocator allocator(camera); for (StreamConfiguration cfg : *config) { Stream *stream cfg.stream(); int ret allocator.allocate(stream); if (ret 0) { std::cerr Failed to allocate buffers for stream stream std::endl; return -1; } }然后把这些预分配的buffer与Request绑定Request *request camera-createRequest(); for (Stream *stream : streamList) { const std::vectorstd::unique_ptrFrameBuffer buffers allocator.buffers(stream); // 选择一个buffer用于request request-addBuffer(stream, buffers[index].get()); }这样做的优势是你可以在应用层控制每个stream的buffer分配时机和数量也可以在同一个时刻统一分配让CMA分配器在开始时就知道所有需求它会更均匀地使用CMA空间。不过在STM32MP257F IMX335的Yocto BSP中FrameBufferAllocator的稳定性会受限于V4L2 device的能力。有些ST定制的pipeline对allocate的调用方式比较敏感如果遇到问题可能需要回退到方案一或方案二。4.5 三种方案的对比与选择建议方案修改范围效果风险调大CMA内核/设备树最彻底内存占用多低内存板型慎重减少bufferCount应用层中等可能影响帧率交换stream配置顺序应用层不稳定治标不治本FrameBufferAllocator预分配应用层较彻底需要较多代码改动我的建议是短期先用方案一解决燃眉之急中期用方案二降低内存占用长期用方案四重构可靠的buffer管理。方案三只适合快速测试验证不建议在生产环境中依赖。5. 验证与效果修复后各环节的运行情况5.1 修改后的系统内存状态我最终采用的是方案一方案二的组合将CMA从64MB调整到128MB同时把辅流的bufferCount从默认值改为4。修改完成后重启系统再次运行同样的程序配置流程顺利通过。此时查看CMA的使用情况# cat /proc/meminfo | grep -i cma CmaTotal: 131072 kB CmaFree: 30848 kB可以看到CMA总大小128MB在配置成功后剩余约30MB。这30MB的余量足够应对3A统计buffer和GPU等其他模块的临时DMA请求。运行过程中我又开了一个立体的使用场景主流2592x1944录像辅流640x640预览同时再开一个snapshot抓拍。此时CMA的剩余是12MB左右依然稳定工作。用dmesg观察没有发现cma_alloc相关的错误。5.2 帧率实测数据既然涉及buffer数量调整顺便做了帧率对比配置主流帧率辅流帧率CMA剩余默认8 buffer30fps30fps8MB改4 buffer30fps30fps30MB改2 buffer25fps30fps55MB在2个buffer的情况下主流出现了明显掉帧因为处理速度跟不上队列中只剩一个可用的buffer在排队导致sensor在每次frame start时没有新的空buffer可用。这个测试也印证了之前说的bufferCount不是越小越好4个是最低安全线。5.3 长时间稳定性验证修复后我做了24小时连续录像测试主题是白天到黑夜再到白天的环境光变化。这一过程中IMX335的曝光和增益在持续调整ISP的3A统计buffer也在持续申请释放。全程没有出现std::bad_alloc、没有出现cma_alloc失败、没有出现画面撕裂。相比修复前最重要的是配置过程从有时成功有时崩溃变成了100%稳定成功。这在产线测试和无人值守场景中至关重要。6. 复盘与同类问题排查清单6.1 遇到std::bad_alloc排查思路的时间线如果以后你在其他嵌入式平台上也遇到类似的libcamera问题我建议按照以下顺序排查抓dmesg先看内核日志有没有error、failed、cma字样的信息。很多时候驱动层已经打印了真实原因只是你的应用被libcamera的异常机制掩盖了。确认CMA大小cat /proc/meminfo | grep -i cma看CmaTotal和CmaFree。确认当前CMA占用如果CmaFree很小说明此前已经有大量DMA分配。通过cat /proc/buddyinfo看内存碎片化程度。逐个流排除在应用代码里只配置一个流再配置另一个观察哪个流在配置特定分辨率时触发失败。如果单独配置都成功那就是同时配置时总量的问题如果单独配置也失败那是单个buffer分配的问题。修改bufferCount临时把bufferCount改小再试如果问题消失基本可以确定是CMA空间不足而不是驱动bug。调大CMA这是最终的兜底手段。但要注意CMA过大会减少普通可回收内存影响系统其余部分的表现。6.2 常见误区为什么不检查mmap的malloc失败一个很容易踩的坑是以为std::bad_alloc是应用层malloc失败于是去调整ulimit或者检查/proc/sys/vm/overcommit_memory。但实际上libcamera申请buffer元数据的vector很小几乎不可能触发用户态内存不足。真正的内存压力在内核CMA池。另外很多人会想到用strace跟踪mmap调用。但libcamera在申请DMA buffer时走的不是传统的mmap路径而是先通过VIDIOC_REQBUFS让内核分配再mmap那段DMA内存。因此如果strace里看到mmap返回了非零地址并不能说明DMA分配成功。6.3 与系统内存总大小的关系有的板子内存大比如2GB但CMA小默认64MB一样的会触发这个问题。内存总量只影响CMA可以扩大到多大不影响默认配置下的不足。在STM32MP257F的评测板上512MB内存是比较常见的配置。此时如果CMA设置超过256MB系统在大量运行内存应用时可能面临页面回收压力因为CMA中的页面不能被换出到swap只能迁移。建议的取值是内存总量的1/4到1/8之间。512MB内存的板子CMA设置在128MB是一个比较均衡的数值。6.4 关于V4L2驱动层的处理细节如果把问题继续往下挖到了V4L2驱动层其实还有更多的细节值得注意。在vb2_dma_contig分配器中实际分配通常会尝试两个路径dma_alloc_from_contiguous()优先从CMA池分配连续内存。如果CMA池不够会尝试从系统普通内存做非连续分配但会被配置成DMA_ATTR_FORCE_CONTIGUOUS否定。在STM32MP2的某些BSP版本中驱动通过vb2_dma_contig_set_max_seg_size设置了最大段大小这会导致非连续分配失败得更早。具体表现就是即使你有足够的内存碎片也无法分配一个大块的连续区域。这类问题是驱动级的不太可能通过应用层解决唯一有效的手段就是保证CMA池有足够的连续空间。7. 最后的经验这类问题为什么值得认真对待我记录这次排障过程不仅是因为std::bad_alloc这个报错本身有迷惑性更是因为这类内存问题在嵌入式Linux视觉系统中非常典型。它不像编写业务逻辑时的逻辑错误那样可以在代码评审中发现也不像缺少某个内核驱动那样在启动阶段就会报错而是藏在系统看起来正常运行但跑几个月后偶尔崩溃一次的阴影里。在嵌入式多媒体开发中有一个经验凡是涉及V4L2、ISP、GPU、编解码器的莫名其妙崩溃先怀疑连续内存再怀疑DMA约束然后才是业务逻辑问题。这次是CMA耗尽上次我在另一个平台上遇到的是GPU和ISP争抢同一块CMA区域导致画质异常。每次深入排查最终都能落到内存管理机制上。所以当你在自己的STM32MP2或类似平台上遇到libcamera的std::bad_alloc时别慌。先看看通篇文章里被你跳过的关键技术点——CMA池大小、buffer数量、对齐规则——它们才是真正的幕后黑手。解决了这一次你会发现这类问题并不难。真正有价值的是你在排查过程中积累的对整个视频内存路径的理解。下次在写应用层代码时你会自然地想到这里分配了一个640x640的buffer底层可能占用了2MB的连续物理内存而整个CMA池可能只有128MB。这种对整个系统的感知是嵌入式Linux开发者最宝贵的资产。