1. 为什么我会盯上内核模块里的CMA分配先交代一下背景。我手上有个模拟项目X需要在某嵌入式平台上跑一个视频采集模块传感器过来的原始帧数据量不小一帧1080p的RAW图差不多8MB左右而且采集频率是30fps。最初图省事直接在驱动里用kmalloc加dma_alloc_coherent来申请缓冲区结果跑到高负荷场景时系统卡顿明显偶尔还出现分配失败。查了一圈问题基本出在内存碎片化长时间运行后物理内存被拆得七零八落大块连续内存越来越难拿而DMA和摄像头控制器偏偏就吃这一套——它们要的是物理连续的内存不是虚拟地址连续就行的。后来调研下来发现Linux内核早就提供了一个专门对付这类场景的机制CMAContiguous Memory Allocator连续内存分配器。它跟普通伙伴系统最大的不同在于它平时把内存“借”给其他子系统用一旦有设备申请大块连续内存它会通过迁移页面把这些内存收回来再以连续区域的形式交给申请者。相当于你在公司里有一块专属停车区白天大家都能临时停但你的车要回来时保安会清场把位置腾给你。这个设计理念很优雅实际调起来却有不少门道尤其是和内核模块结合使用时踩坑的地方比想象中多。这篇文章想聊的就是我在内核模块里实际使用CMA内存分配的经验怎么配置、怎么在模块里声明和使用、DMA映射怎么做、调试时怎么确认内存真的来自CMA池、以及那些文档里不会写清楚的坑。适合正在做驱动开发、尤其是涉及视频采集、网络收包、GPU显存这类需要大块连续内存场景的人参考。已经熟悉CMA基础概念的可以直接跳到第4节看实战踩坑。2. CMA的分配原理与内核模块的接入方式2.1 CMA在内核里到底是怎么工作的要讲清楚CMA在模块里怎么用得先明白它的运作机制。CMA区域在内核启动阶段就会从物理内存中划出来比如在设备树里配置一个reserved-memory节点指定reusable属性或者在内核命令行里用cma参数指定大小。这个区域对伙伴系统而言是一块“可回收”的保留区普通页面分配器在内存紧张时可以到这里拿页用但这些页面随时可能被CMA机制回收。当某个驱动调用cma_alloc时内核会做三件事在CMA区域内找到一块足够大的空闲连续区间。如果区间内某些页面正被其他进程或子系统占用就通过页面迁移migrate把它们挪走——具体来说是把这些页的内容复制到普通内存区域的新页上再更新页表映射。迁移完成后把这块连续区间标记为“专有”交给驱动使用。这个迁移操作代价不低尤其是页面属于匿名映射或文件映射时处理方式还不一样。所以CMA分配不保证实时性中断上下文里是没法直接调用的这也是为什么很多驱动在probe阶段或者工作队列里做初始化分配。2.2 内核模块如何接入CMA池对内核模块来说接入CMA池有两种常见路径。一种是直接调cma_alloc这需要先拿到CMA区域的struct cma *对象一般通过cma_get_base或设备树API来获取。另一种是更推荐的做法——通过DMA子系统走通用API比如dma_alloc_coherent或dma_alloc_from_contiguous只要设备设备的DMA掩码和CMA区域配置正确DMA子系统会自动从CMA池里分配连续内存。我实际用的就是第二种方式。设备树里配置好了CMA区域驱动里用device_managed_dma_alloc或者直接dma_alloc_coherent申请缓冲区底层如果CMA区域可用且内存紧张时分配器会自动落到CMA池里。但这里有个容易被忽略的前提不是所有dma_alloc_coherent都保证走CMA。如果系统CMA区域没配置或者大小不够DMA子系统会退回伙伴系统的普通连续分配效果就打折扣了。2.3 设备树里的CMA配置示例先看一个典型的设备树配置reserved-memory { #address-cells 2; #size-cells 2; ranges; cma_buffer: cma_buffera0000000 { compatible shared-dma-pool; reusable; size 0x0 0x10000000; /* 256MB */ alignment 0x0 0x200000; linux,cma-default; }; }; video_cap: video-cap... { /* ... */ memory-region cma_buffer; };需要注意reusable属性是必须的表示这块保留内存可以临时借给伙伴系统用。没有这个属性这块区域就变成纯保留区白白占着256MB没有任何灵活性系统可用内存会肉眼可见地减少。linux,cma-default表示把它设为系统默认CMA池这样dma_alloc_coherent这类API可以自动使用它。在驱动侧可以用of_reserved_mem_device_init_by_idx或of_reserved_mem_device_init把设备和这块CMA区域关联起来。之后dma_alloc_coherent就会优先从这个区域分配。3. 在模块里申请与使用CMA内存的完整流程3.1 驱动代码里的初始化顺序我在模拟项目X里新增的采集驱动初始化流程大概是这样的static int video_cap_probe(struct platform_device *pdev) { struct video_cap_dev *dev platform_get_drvdata(pdev); struct device *dev pdev-dev; int ret; /* 把设备和CMA region关联起来 */ ret of_reserved_mem_device_init_by_idx(dev, dev-of_node, 0); if (ret) { dev_err(dev, failed to init reserved mem\n); return ret; } /* 申请DMA缓冲区这里会尝试从CMA池分配 */ dev-buf_size 8 * 1024 * 1024; /* 8MB一帧RAW图 */ dev-buf_virt dma_alloc_coherent(dev, dev-buf_size, dev-buf_phys, GFP_KERNEL); if (!dev-buf_virt) { dev_err(dev, failed to alloc coherent buffer\n); return -ENOMEM; } /* 后续再申请描述符表、状态缓冲区等 */ /* ... */ }这里有个细节of_reserved_mem_device_init_by_idx必须在任何DMA分配之前调用因为它负责建立设备到CMA区域的映射关系。如果漏了这步后续dma_alloc_coherent拿到的内存可能来自普通伙伴系统分配而不是CMA池。3.2 释放与清理模块卸载时对应释放static void video_cap_remove(struct platform_device *pdev) { struct video_cap_dev *dev platform_get_drvdata(pdev); if (dev-buf_virt) { dma_free_coherent(pdev-dev, dev-buf_size, dev-buf_virt, dev-buf_phys); dev-buf_virt NULL; } of_reserved_mem_device_release(pdev-dev); }of_reserved_mem_device_release负责解除设备和CMA区域的关联这一步容易被新手忽略。不调用也不会立刻报错但会导致设备残留对CMA区域的引用后续如果重新绑定驱动或热插拔可能出现内存泄漏或分配异常。3.3 为什么用dma_alloc_coherent而不是直接cma_alloc我见过一些人图省事直接cma_alloc一个struct cma *上手。这确实能拿到CMA内存但有个问题DMA API和CMA API维护了两套不同的抽象。直接用cma_alloc你拿到的是物理地址还得自己想办法映射到虚拟地址、处理DMA掩码、做cache一致性维护。而dma_alloc_coherent把这些全包了——它同时返回虚拟地址和物理地址并且保证cache一致性也就是所谓的“coherent”映射对大多数DMA场景正好够用。所以我的建议是能用DMA API就别裸调CMA API除非你在做CMA子系统本身的调试或扩展。内核里暴露的CMA API更多是给底层子系统用的不是给普通驱动作者用的。4. 调试时如何确认内存真的来自CMA池4.1 从设备树到/proc的双重验证配置做完后怎么知道自己申请的内存到底是不是从CMA池来的我一般用两个手段第一个是看启动日志。内核启动时如果CMA配置成功会有类似这样的输出cma: Reserved 256 MiB at 0x00000000a0000000如果看到这行说明CMA区域已经reserve成功了。第二个是运行时查看/proc/meminfo和/proc/buddyinfoCMA区域的内存会体现在特定zone里。更直接的方式是看/sys/kernel/debug/cma/下的调试节点分配情况会记录在count和used等字段里。我常用的一个快速检查命令cat /sys/kernel/debug/cma/cma-0/used如果驱动已经成功从CMA池分配了内存这里的used值会增加。没有挂载debugfs的话先挂载mount -t debugfs none /sys/kernel/debug4.2 通过物理地址判断来确认还有一个更硬核的验证方法打印分配到的物理地址看它是否落在CMA区域范围内。设备树里配置的cma_buffera0000000物理起始地址是0xa0000000大小256MB所以只要分配的物理地址落在0xa0000000到0xb0000000之间基本可以确定来自CMA池。我在驱动里临时加了一行调试日志dev_info(dev, buf phys: 0x%llx size: 0x%zx\n, (unsigned long long)dev-buf_phys, dev-buf_size);实测下来当系统内存压力较大时分配到的地址会落在CMA区域内系统空闲时反而可能拿到普通内存。这个现象让我意识到CMA分配不是“必须从CMA池出”而是“内存紧张时优先从CMA池保底”。对很多场景来说这正是我们想要的平时不挤占普通内存关键时刻保证有连续大块可用。4.3 DMA方向与一致性处理申请到内存只是第一步。DMA传输真正跑起来之前还必须正确设置DMA方向和同步操作。以视频采集为例摄像头传感器通过DMA把数据写入我们申请的缓冲区CPU再读取处理。这个方向是从设备到内存对应DMA方向应为DMA_FROM_DEVICE。在启动每次采集前需要调用dma_map_single(dev, buf_virt, buf_size, DMA_FROM_DEVICE);或直接使用coherent映射时配合dma_sync_single_for_device/dma_sync_single_for_cpu做同步。我之前犯过一个错误忘了在读取前调用dma_sync_single_for_cpu导致CPU读到的是cache里旧的脏数据画面出现随机花屏。后来改用coherent映射加屏障问题才稳定解决。5. 实战中容易踩的坑与排查链路5.1 CMA区域配置了但驱动分配仍然失败现象设备树配置了256MB CMA区域启动日志也显示reserve成功但驱动里dma_alloc_coherent仍然返回NULL。排查链路如下第一反应看分配大小和区域大小是否匹配。视频帧是8MB一帧如果单次分配超过CMA区域剩余可分配大小就会失败。用/sys/kernel/debug/cma/cma-0/used查看当前占用如果剩余空间低于申请大小那就是典型的容量配置不足。第二件事查设备DMA掩码。dma_set_mask_and_coherent设置不对DMA子系统可能认为设备无法访问CMA区域的高地址直接拒绝分配。我遇到过一次设备树里CMA区域放在0xa0000000而驱动里只设置了32位DMA掩码结果地址超过4GB范围被拒绝表现就是分配失败。解决办法是把CMA区域放在设备可寻址范围内或者提高DMA掩码位数。第三种可能比较隐蔽of_reserved_mem_device_init_by_idx返回成功但设备其实没有绑定到正确的CMA区域。尤其是当系统里有多个CMA区域时索引或者memory-region属性指向错误驱动会默默从默认CMA池或普通内存分配看起来“成功”但实际不符合预期。核对设备树配置和实际运行时行为最直接的方式就是看分配的物理地址区段。5.2 CMA分配导致系统卡顿的根因在高负载场景下我遇到过一个更棘手的问题CMA分配偶尔会伴随明显的系统卡顿持续时间几百毫秒到一秒不等。最开始我以为是DMA传输本身的问题抓了sched调度器日志后发现卡顿恰好发生在cma_alloc调用期间。根因在页面迁移。当CMA区域内已有页面被匿名映射或文件页占用时cma_alloc要把这些页面迁移出去迁移过程中要写回数据、更新页表、处理TLB失效这些操作在内存压力大时会变得非常慢。更麻烦的是如果进程正忙hot地访问这些页面迁移过程还可能触发反复的缺页异常。缓解方案有几个方向cma_alloc调用加上GFP_NOWAIT标志避免在原子上下文或要求低延迟的场景中触发耗时迁移。在驱动初始化时一次性分配好所有缓冲区避免运行中频繁调用cma_alloc。如果迁移确实无法避免把CMA区域中的页面提前用migrate_pages预迁移或者通过mlock等方式减少可移动页的驻留。我在模拟项目X里最终采用的是方案2初始化时分配好三个帧缓冲轮转使用运行中不再做任何CMA分配。卡顿问题基本消失代价是多占了两倍内存但换来稳定性和实时性这个取舍在视频采集场景里是划算的。5.3 CMA内存被DMA之外的其他模块占用还有一个坑需要特别提醒CMA区域内的内存对伙伴系统开放意味着普通进程的页面也可能被放到CMA区域里。如果某个进程占用着CMA区域里的一块页面且处于不可迁移状态比如锁定的匿名页或某些内核对象而你恰好需要分配那块区域系统可能会失败或者触发慢速迁移路径。排查这个问题的思路是检查页面的迁移类型。内核里/proc/pagetypeinfo能看到各迁移类型的页分布如果CMA区域里的不可移动页偏多说明配置上存在问题。更常见的做法是在设备树CMA区域里指定alloc-ranges限制CMA池只覆盖某段合适的内存减少被普通进程占用的概率。我之前在一个项目里把CMA区域配置在了DMA设备访问不到的地址范围同时又离开了ZONE_NORMAL区域导致dma_alloc_coherent总是回退到普通内存分配。后来通过调整CMA区域到设备可寻址的范围并限制分配类型问题才解决。6. CMA与伙伴系统交互时的行为边界6.1 为什么CMA区域不是“越早分配越好”初学CMA时我很自然地认为CMA区域是“专属内存池”应该尽快占为己有。实际内核的做法恰恰相反CMA区域在没有人使用时会尽量借给伙伴系统。借用机制让CMA区域参与整体内存管理系统内存不足时能临时使用这些页面这样既保证了连续内存供应又不浪费内存。但这带来一个隐藏问题如果你的驱动加载比较晚CMA区域里的页面可能已经被其他子系统占满了。此时你想一次性分配几十MB就可能因为迁移开销太大而失败或超时。解决方案除了提前初始化外还可以通过cma_alloc时指定GFP_KERNEL搭配较高的优先级让分配器更积极地迁移页面。6.2GFP_KERNEL/GFP_ATOMIC分配上下文的影响cma_alloc支持的标志位会影响分配行为。GFP_KERNEL允许睡眠和页面迁移适合在进程上下文里使用GFP_ATOMIC不允许睡眠若CMA区域内页面需要迁移就会直接失败。我的习惯是驱动初始化阶段用GFP_KERNEL做完整分配运行时的突发分配改为GFP_ATOMIC加降级策略宁可拿不到也不要卡住中断路径。6.3 CMA区域的大小与碎片化配置CMA区域时大小不是越大越好。CMA区域过大会导致可用内存减少毕竟它的大部分页面平时虽然“借”给伙伴系统但所有权仍归属于CMA管理。区域过小则在高分辨率场景下容易分配失败。推荐的做法是先估算需要几块缓冲区每块多大。比如视频采集需要三块8MB帧缓冲那就是24MB。再留一定余量应对迁移损耗和异步分配一般乘以1.5到2倍配置为48MB左右。用/proc/buddyinfo和/sys/kernel/debug/cma/观察实际使用峰值逐步调整。在我这个模拟项目X里最初配了128MB后来发现利用率极低平白浪费了内存。降到48MB后功能不变可用内存明显多了。这里也引出一个经验不要迷信“大CMA”按需配置才是合理的。7. 性能影响与实测数据7.1 分配耗时对比我专门做了个简单测试对比kmalloc、dma_alloc_coherent无CMA压力时和CMA紧张时的分配耗时分配方式场景平均耗时kmalloc8MB系统空闲约50微秒dma_alloc_coherent8MB系统空闲CMA区域有货约120微秒dma_alloc_coherent8MB系统高负载需要页面迁移约8毫秒极端时30毫秒可以看到页面迁移成本极其昂贵。单纯从API角度你根本看不出问题只有压力测试才能暴露。这也是我强调“初始化时一次性分配”的原因。7.2 运行期零分配的设计针对视频采集模块我最终采用的方案是固定池化缓冲#define FRAME_NUM 3 #define FRAME_SIZE (8 * 1024 * 1024) static struct frame_buf { void *virt; dma_addr_t phys; bool in_use; } frame_bufs[FRAME_NUM];采集驱动启动时把所有帧缓冲全部从CMA池分配好运行时循环切换。每个缓冲在DMA传输完成后归还到空闲链表不再调用任何分配函数。这种设计让系统稳定性和中断延迟都恢复到正常水平同时也规避了CMA迁移带来的抖动。7.3 监测DMA性能时注意的指标用CMA内存做DMA传输除了分配延迟外还要关注传输带宽和Cache一致性开销。在ARM平台上coherent映射和non-coherent映射性能差异明显尤其是CPU频繁访问DMA写入的数据时。如果不需要CPU高频访问dma_alloc_noncoherent配合手动同步可能比dma_alloc_coherent更高效。视频采集场景里CPU通常只需要做少量帧头解析用coherent映射反而简单直接。8. 最后再分享两条实操经验第一次调CMA时我栽过一个大跟头为了验证分配是否来自CMA池我在驱动里加了大量临时代码结果其中一个错误修改让dma_alloc_coherent白白多分配了一倍的内存系统可用内存骤降。排查了好久才发现是调试代码里多申请了一块缓冲区。所以无论是调CMA还是做其他驱动开发临时调试代码一定要做好标记和记录用完后立即删除不然坑的是自己。另一条经验是关于文档阅读的内核源码树里Documentation/devicetree/bindings/reserved-memory/reserved-memory.yaml和cma相关代码注释是最权威的参考网上很多教程和博客信息已经过时尤其是低版本内核和高版本内核在CMA行为上有不少差异。我手里用的这个平台内核版本较新CMA默认行为已经和几年前的资料描述不一致了。遇到问题优先看内核文档和源码而不是直接照搬网上的帖子。CMA这玩意用好了是解决大块连续内存分配的利器用不好就是一场性能灾难。希望这篇基于我自己实际调试经验的分享能帮你少走几条弯路。下次如果你的驱动也遇到大块连续内存分配失败不妨先看看CMA日志再查查设备树配置多半答案就在那里。