1. 问题现场:既不是IMX335的锅,也不全是libcamera的错
先描述一下我遇到问题的环境。板子是STM32MP257F,Arm Cortex-A35双核,带Mali-G310 GPU,跑的是Yocto构建的Linux系统。摄像头模组是IMX335,500万像素CMOS sensor,通过CSI-2接口接入。软件栈是libcamera + libcamera-apps,内核用的是5.15主线内核加上ST官方补丁。
第一次跑起来的时候,主流的配置完全没有问题:2592x1944,10-bit RAW,30fps,出图正常,颜色正常,曝光控制也正常。但是当我在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或720p |
| Viewfinder | 预览 | 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。对于每个buffer,libcamera还会再调用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 先认识CMA(Contiguous Memory Allocator)
CMA是Linux内核中用于分配连续物理内存的机制。它的核心思路是:平时把物理内存页面分配给可移动的用户态页面使用,当设备驱动需要连续内存时,通过migratepages把这些页面迁移走,空出一块连续区域。
这个设计看起来巧妙,但在实际运行时有一个致命问题:当系统内存碎片化严重时,即使总内存还有富余,也可能无法凑出足够大的连续区域。
STM32MP257F的Linux内核通常配置了CONFIG_DMA_CMA=y,默认CMA大小可以通过内核命令行参数cma=256M或者设备树里的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个buffer | 6个buffer |
|---|---|---|---|---|
| 主流(2592x1944) | 9.6 MB | 10 MB | 40 MB | 60 MB |
| 辅流(640x640) | 0.78 MB | 2 MB | 8 MB | 12 MB |
| 合计 | 48 MB | 72 MB |
如果加上ISP的另外几个内部缓冲(比如统计信息buffer、3A buffer),总需求轻松突破100MB。
如果板子的CMA大小是默认的cma=64M或者cma=96M,那么当主流占用了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 pipeline(unicam)对buffer数量的管理更保守,默认只分配4个buffer。
STM32MP2的官方Yocto BSP,为了兼顾ISP和GPU等模块,默认CMA设置并不是很大,特别是在256MB内存的低配板型上,CMA通常只分到64MB。这就导致同样的libcamera应用,在树莓派上跑得好好的,在STM32MP2上却出现内存分配失败。
这提醒我们:从树莓派迁移到嵌入式MPU平台时,CMA配置是必查项。
4. 修复与规避:从配置到代码的四种实操方案
4.1 方案一:调整内核CMA大小(最快最直接)
如果你对CMA机制还不够熟悉,首选方案就是调大内核的CMA区域。这里有两种方式。
方式一:修改内核命令行参数
在Bootloader(U-Boot)的bootargs中添加:
cma=128M比如原来的bootargs是:
console=ttySTM0,115200 root=/dev/mmcblk1p2 rootwait改成:
console=ttySTM0,115200 root=/dev/mmcblk1p2 rootwait cma=128M方式二:修改设备树
在设备树的根节点下找到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_CMA=y,那默认的CMA大小是由CONFIG_CMA_SIZE_MBYTES决定的,通常为0,然后通过内核命令行参数指定。
在STM32MP257F的官方BSP里,设备树中通常已经定义了这个节点,所以直接改设备树中的size是更规范的做法。
设置多大比较合适?我的建议是:先预估你的所有视频流需求,把每个流的单帧大小乘以buffer数量求和,再乘以1.5的余量系数。比如上面的例子,主流就算10MBx6=60MB,辅流2MBx8=16MB,3Abuffer20MB,合计约96MB,那么128MB是合理起点。如果板子内存是1GB,设置256MB也没问题。
4.2 方案二:调整libcamera的buffer数量配置
如果不想动内核,或者CMA大小已经因为其他原因固定,那可以在应用层减少buffer数量。
libcamera的stream配置中有一个bufferCount字段。在生成CameraConfiguration之后,你可以直接修改:
std::unique_ptr<CameraConfiguration> 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::vector<std::unique_ptr<FrameBuffer>> &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 buffer | 30fps | 30fps | 8MB |
| 改4 buffer | 30fps | 30fps | 30MB |
| 改2 buffer | 25fps | 30fps | 55MB |
在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开发者最宝贵的资产。