CMA工程实践:区域配置与驱动缓冲性能优化方法 - 第1节

1.1 CMA 的定义与核心定位

CMA(Contiguous Memory Allocator,连续内存分配器)是 Linux 内核中用于解决大块连续物理内存分配难题的子系统。它通过预留一块物理内存区域,在系统运行时动态管理该区域,既允许常规页面分配使用该区域,又能在驱动需要大块连续内存时快速回收并分配。

维度 说明
全称 Contiguous Memory Allocator
引入版本 Linux 3.5(2012年)
内核位置 mm/cma.c + include/linux/cma.h
核心目标 为 DMA、GPU、多媒体编解码等需要大块连续物理内存的驱动提供可靠分配
设计哲学 预留 + 动态迁移 + 按需回收
工程视角:CMA 不是传统意义上的专用内存池,而是一种"弹性预留"机制——平时允许可移动页面占用 CMA 区域,仅在驱动申请时通过页面迁移腾出连续空间。

1.2 CMA 在 Linux 内核中的作用

CMA 解决了内核中长期存在的"大块连续物理内存分配难"问题,其作用贯穿多个子系统:

  • DMA 引擎:大多数 DMA 控制器要求缓冲区物理地址连续,CMA 提供可靠的大块连续内存,避免分散的 SG(Scatter-Gather)列表带来的性能开销。
  • GPU 与显示子系统:帧缓冲、图形纹理、显示控制器需要连续物理内存,CMA 保证这些场景不因内存碎片而分配失败。
  • 多媒体编解码:视频解码器、ISP(图像信号处理器)通常需要大块连续内存存放原始帧数据,CMA 是这些驱动的主要内存来源。
  • 网络设备:部分网卡硬件要求接收/发送描述符和缓冲区物理连续,CMA 提供底层支持。
  • 减少内存碎片化影响:通过将大块连续分配需求从伙伴系统中剥离,CMA 降低了伙伴系统因长期保留大块区域而产生的碎片化程度。
关键数据:在嵌入式设备(如 4K 视频解码场景)中,一次需要分配 8~16 MB 连续物理内存。没有 CMA 时,系统运行 2~3 小时后因碎片化导致分配失败的概率超过 60%;启用 CMA 后,分配成功率稳定在 99.9% 以上。

1.3 CMA 解决内存碎片化问题的基本原理

CMA 的核心原理是 预留 + 迁移 + 回收,通过三个阶段的协同工作来对抗内存碎片化:

  1. 预留阶段:系统启动时从物理内存中划分一块连续区域作为 CMA 区域(通过内核参数 cma= 或设备树 reserved-memory 节点配置)。
  2. 共享阶段:CMA 区域内的物理页面被加入伙伴系统,允许可移动页面(MOVABLE 类型)占用这些页面,提高内存利用率。
  3. 回收阶段:当驱动调用 dma_alloc_coherent() 或 cma_alloc() 时,CMA 将占用该区域的页面通过 migrate_pages() 迁移到其他区域,回收出连续物理空间。
注意:CMA 依赖页面迁移机制,因此被占用的页面必须是可移动的(MIGRATE_MOVABLE)。如果 CMA 区域被不可移动页面(如内核栈、slab 对象)长期锁定,CMA 分配可能失败或产生较大延迟。
CMA 解决内存碎片化原理流程图 CMA 解决内存碎片化原理流程图 阶段1:预留 启动时划分连续物理区域 (cma= 或 reserved-memory) 阶段2:共享 页面加入伙伴系统 (可移动页面占用) 阶段3:回收 页面迁移 (腾出连续空间) 触发条件 dma_alloc_coherent / cma_alloc 无 CMA 时的碎片化 大块连续区域被小页面分割 分配大块内存时需遍历/合并 高概率失败 分配结果 连续物理内存 分配成功 延迟可控 核心思想:平时允许可移动页面占用 CMA 区域,驱动申请时通过页面迁移快速回收连续空间 既避免专用内存池的浪费,又保证大块连续分配的可靠性

1.4 CMA 与伙伴系统的关系

CMA 并非替代伙伴系统,而是作为伙伴系统的补充层,两者协同工作:

对比维度 伙伴系统 (Buddy System) CMA
分配粒度 2^order 页(通常 ≤ 4 MB) 任意大小连续区域(可达数百 MB)
连续性保证 物理连续,但大块分配易失败 物理连续,通过迁移保证成功
内存利用率 高(所有页面均可分配) 高(空闲时页面被共享使用)
分配延迟 低(直接分配) 中等(可能触发页面迁移)
适用场景 通用内存分配 DMA、GPU、多媒体等大块连续需求
工程实践:在配置 CMA 区域大小时,通常设置为系统总内存的 5%~20%。例如 2 GB 内存的设备,CMA 区域配置为 128~256 MB 可满足大多数多媒体场景需求。过大的 CMA 区域会减少普通可分配内存,过小则无法满足驱动需求。

1.5 CMA 的软件架构

CMA 在内核中的实现涉及多个模块的协作,其软件架构分为三层:

  • 核心层 (mm/cma.c):提供 CMA 区域的创建、分配、释放、迁移等核心逻辑,维护每个 CMA 区域的位图和管理结构。
  • API 层 (include/linux/cma.h):暴露 cma_alloc()、cma_release()、cma_get_base() 等接口给驱动和内核其他子系统。
  • 驱动适配层:DMA 引擎 (dma_alloc_coherent)、V4L2 框架、DRM 图形框架等高层 API 内部调用 CMA 接口,对驱动开发者透明。
CMA 软件架构分层图 CMA 软件架构分层图 驱动适配层(高层 API) dma_alloc_coherent | V4L2 videobuf2 | DRM GEM | ALSA 对驱动开发者透明,内部调用 CMA 接口 CMA API 层 cma_alloc() | cma_release() | cma_get_base() | cma_get_size() include/linux/cma.h CMA 核心层 区域管理 | 位图操作 | 页面迁移 | 预留初始化 mm/cma.c + mm/cma.h 驱动通过高层 API 分配连续内存,CMA 核心层负责底层物理页面管理

1.6 CMA 的典型使用流程

驱动开发者使用 CMA 的典型流程如下:

  1. 配置 CMA 区域:通过内核命令行参数 cma=256M 或设备树 reserved-memory 节点指定 CMA 区域大小和位置。
  2. 驱动初始化:在驱动 probe 函数中调用 dma_set_coherent_mask() 设置 DMA 掩码,或直接调用 cma_get_base() 获取 CMA 区域信息。
  3. 分配连续内存:调用 dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL),内部通过 CMA 分配连续物理内存。
  4. 使用缓冲区:驱动将返回的虚拟地址和 DMA 地址传递给硬件进行 DMA 操作。
  5. 释放连续内存:调用 dma_free_coherent(dev, size, cpu_addr, dma_handle) 将内存归还 CMA 区域。
最佳实践:在驱动中优先使用 dma_alloc_coherent 而非直接调用 cma_alloc,前者提供更好的跨平台兼容性,且自动处理 cache 一致性。
// 驱动中使用 CMA 的典型代码片段
#include <linux/dma-mapping.h>

struct device *dev = &pdev->dev;
size_t buf_size = SZ_8M;  // 8 MB 连续缓冲区
dma_addr_t dma_handle;
void *cpu_addr;

// 分配连续 DMA 缓冲区(内部使用 CMA)
cpu_addr = dma_alloc_coherent(dev, buf_size, &dma_handle, GFP_KERNEL);
if (!cpu_addr) {
    dev_err(dev, "Failed to allocate DMA coherent memory\n");
    return -ENOMEM;
}

// 硬件初始化:将 dma_handle 写入 DMA 控制器寄存器
writel(dma_handle, hw->dma_addr_reg);

// ... 使用缓冲区进行 DMA 传输 ...

// 释放缓冲区
dma_free_coherent(dev, buf_size, cpu_addr, dma_handle);
性能数据:在 ARM64 嵌入式平台上,使用 CMA 分配 8 MB 连续内存的平均延迟约为 1.2 ms(包含可能的页面迁移时间),而直接使用伙伴系统分配同样大小内存的成功率在系统运行 1 小时后降至 30% 以下。CMA 将分配成功率提升至接近 100%,且延迟波动在可接受范围内。