CMA工程实践 驱动缓冲优化与内存压力测试方案 - 第1节
1. CMA概述
CMA(Contiguous Memory Allocator,连续内存分配器)是 Linux 内核中用于解决大块连续物理内存分配问题的内存管理子系统。它在内核 3.5 版本中合入主线,旨在为 DMA(直接内存访问)、多媒体编解码、GPU 显存等需要物理连续缓冲区的设备驱动提供可靠的内存分配机制,同时尽可能降低对系统整体内存利用率的负面影响。
核心定位:CMA 不是替代伙伴系统(Buddy System)或 slab 分配器,而是作为它们之上的专用连续内存管理框架,在内存碎片化严重的系统中保障大块连续物理页的可用性。
1.1.1 为什么需要 CMA?
在长期运行的嵌入式 Linux 系统中,内存碎片化是一个普遍问题。伙伴系统虽然能高效管理 2 的幂次阶(order)页面,但随着频繁的分配与释放,物理内存会被分割成大量不连续的小块,导致无法满足高阶(如 order-4、order-8)连续页面的分配请求。传统解决方案包括预留大块内存(浪费严重)或使用 IOMMU/SMMU 做地址映射(增加硬件复杂度和延迟)。CMA 提供了一种更优雅的折中方案。
| 方案 | 优点 | 缺点 |
|---|---|---|
| 预留专用内存池 | 分配确定性高,无碎片问题 | 内存利用率低,预留区域空闲时也无法被其他模块使用 |
| IOMMU/SMMU 映射 | 允许使用离散物理页,缓解连续需求 | 硬件依赖、TLB 开销、延迟增加 |
| CMA | 空闲时内存可被其他模块使用,分配时迁移页面保证连续 | 分配时可能触发页面迁移,引入延迟 |
1.2 CMA 解决内存碎片化的原理
CMA 的核心思想是“预留但不独占”。它在系统启动时从伙伴系统中预留一块物理连续区域(CMA 区域),但该区域在未被驱动使用时,可以像普通内存一样被匿名页、页缓存等可移动页面占用。当驱动需要连续内存时,CMA 分配器会将该区域内的可移动页面迁移到其他位置,从而“腾出”连续的物理空间。
关键机制:页面迁移(Page Migration)是 CMA 实现动态连续性的基础。只有被标记为
MIGRATE_MOVABLE 的页面才能被 CMA 迁移。因此,驱动在 CMA 区域中分配的页面必须符合可迁移属性。
1.2.1 工作流程
- 预留阶段:内核启动时,通过设备树(Device Tree)或内核参数
cma=指定 CMA 区域的大小和起始地址。该区域被标记为MIGRATE_CMA类型,加入伙伴系统的可迁移页面列表。 - 空闲复用:在无 CMA 分配请求时,CMA 区域内的物理页被当作普通可移动页面,供用户空间进程、文件缓存等使用。
- 分配触发:驱动调用
dma_alloc_coherent()或cma_alloc()请求连续内存时,CMA 分配器开始工作。 - 页面迁移:如果目标区域已被占用,分配器遍历该区域内的页面,将可移动页面逐页迁移到其他空闲区域,并更新页表映射。
- 分配完成:迁移完成后,CMA 区域内的页面全部空闲且连续,分配器将其分配给驱动使用。
1.3 CMA 在 Linux 内核中的作用
CMA 在内核中扮演着“连续内存的保障者”角色,其作用贯穿多个子系统:
- DMA 引擎:为 DMA 描述符环、数据缓冲区提供物理连续内存,避免分散-聚集(SG)模式带来的性能开销。
- 多媒体框架:V4L2(视频采集)、ALSA(音频)、DRM(显示)等驱动通常要求帧缓冲区物理连续,CMA 是首选分配器。
- GPU 与加速器:部分嵌入式 GPU 没有 IOMMU,必须使用物理连续内存作为命令缓冲区或纹理内存。
- 加密引擎:硬件加密加速器通常需要连续内存存放密钥和 IV 向量。
注意:CMA 分配可能触发直接回收(direct reclaim)和页面迁移,导致分配延迟达到毫秒级。对于实时性要求高的场景,建议在系统初始化阶段预分配 CMA 缓冲区,或使用
GFP_NOWAIT 标志避免阻塞。
1.3.1 CMA 与伙伴系统的关系
CMA 区域在伙伴系统中被标记为 MIGRATE_CMA 迁移类型。伙伴系统的页面迁移类型包括:
| 迁移类型 | 说明 | 是否可被 CMA 迁移 |
|---|---|---|
MIGRATE_UNMOVABLE |
内核核心代码、页表、不可移动驱动内存 | 否 |
MIGRATE_MOVABLE |
用户空间匿名页、页缓存、可移动驱动内存 | 是 |
MIGRATE_RECLAIMABLE |
可回收的缓存(如 dentry、inode 缓存) | 否(需先回收) |
MIGRATE_CMA |
CMA 区域专用类型 | — |
MIGRATE_ISOLATE |
临时隔离,用于内存热插拔或 CMA 迁移 | — |
1.4 CMA 配置与数据结构
CMA 的配置通常在设备树中完成,也可以通过内核参数动态调整。以下是典型的设备树配置示例:
// 设备树中定义 CMA 区域
reserved-memory {
#address-cells = <2>;
#size-cells = <2>;
ranges;
linux,cma {
compatible = "shared-dma-pool";
reusable;
size = <0x0 0x10000000>; // 256MB
alignment = <0x0 0x200000>; // 2MB 对齐
linux,cma-default;
};
};
内核中 CMA 的核心数据结构是 struct cma,定义在 include/linux/cma.h:
struct cma {
unsigned long base_pfn; // CMA 区域起始物理页帧号
unsigned long count; // 总页数
unsigned long *bitmap; // 位图,记录页面分配状态
unsigned int order_per_bit; // 每个位代表的阶数
struct mutex lock;
const char *name;
struct page *pages; // 指向 page 数组的指针
};
关键字段说明:
base_pfn 和 count 定义了 CMA 区域的物理范围。bitmap 以位图方式管理页面分配,order_per_bit 决定位图的粒度(通常为 0,即每页一位)。pages 数组用于在迁移时快速定位页面结构。
1.5 CMA 的优缺点总结
| 维度 | 说明 |
|---|---|
| 优点 | 内存利用率高(空闲时被复用)、无需硬件 IOMMU、内核主线支持、配置灵活 |
| 缺点 | 分配延迟不确定(页面迁移耗时)、需要驱动配合使用可移动内存、CMA 区域大小需提前规划 |
| 适用场景 | 嵌入式 Linux、Android 设备、多媒体终端、工业控制、车载信息娱乐系统 |
| 不适用场景 | 硬实时系统(需确定性延迟)、已有 IOMMU 的服务器平台、内存极小的 MCU 系统 |
工程建议:在项目初期,通过
/proc/meminfo 中的 CmaTotal 和 CmaFree 字段监控 CMA 使用情况。如果 CmaFree 长期接近 0,说明 CMA 区域过小或驱动分配过于频繁,需要调整 CMA 大小或优化驱动分配策略。