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 的核心原理是 预留 + 迁移 + 回收,通过三个阶段的协同工作来对抗内存碎片化:
- 预留阶段:系统启动时从物理内存中划分一块连续区域作为 CMA 区域(通过内核参数
cma=或设备树reserved-memory节点配置)。 - 共享阶段:CMA 区域内的物理页面被加入伙伴系统,允许可移动页面(MOVABLE 类型)占用这些页面,提高内存利用率。
- 回收阶段:当驱动调用
dma_alloc_coherent()或cma_alloc()时,CMA 将占用该区域的页面通过migrate_pages()迁移到其他区域,回收出连续物理空间。
注意:CMA 依赖页面迁移机制,因此被占用的页面必须是可移动的(MIGRATE_MOVABLE)。如果 CMA 区域被不可移动页面(如内核栈、slab 对象)长期锁定,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 接口,对驱动开发者透明。
1.6 CMA 的典型使用流程
驱动开发者使用 CMA 的典型流程如下:
- 配置 CMA 区域:通过内核命令行参数
cma=256M或设备树reserved-memory节点指定 CMA 区域大小和位置。 - 驱动初始化:在驱动 probe 函数中调用
dma_set_coherent_mask()设置 DMA 掩码,或直接调用cma_get_base()获取 CMA 区域信息。 - 分配连续内存:调用
dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL),内部通过 CMA 分配连续物理内存。 - 使用缓冲区:驱动将返回的虚拟地址和 DMA 地址传递给硬件进行 DMA 操作。
- 释放连续内存:调用
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%,且延迟波动在可接受范围内。