从设备树到内核启动:一步步拆解Linux内核中CMA连续内存区域的创建与初始化全过程
Linux内核CMA连续内存分配器深度解析:从设备树配置到伙伴系统整合
引言
在现代嵌入式系统和多媒体设备开发中,大块连续物理内存的获取一直是开发者面临的棘手问题。当摄像头需要处理4K视频流、GPU渲染复杂场景或硬件编解码器处理高码率内容时,传统的内存分配机制往往难以满足需求。这就是Contiguous Memory Allocator(CMA)诞生的背景——一个精巧的内核子系统,它像一位内存魔术师,在看似碎片化的物理地址空间中变出连续的空白画布。
不同于简单的内存预留机制,CMA的独特之处在于其"共享"设计哲学:当设备不使用时,这些预留区域依然可供普通进程分配可移动页面;当设备驱动需要时,又能通过页面迁移回收这些内存。这种动态平衡使得CMA既保证了关键设备的性能需求,又避免了宝贵内存资源的闲置浪费。本文将深入Linux内核源码,揭示CMA从设备树解析到最终融入伙伴系统的完整生命周期,为开发者提供定制化内存管理的技术蓝图。
1. CMA的核心架构与设计哲学
1.1 为什么需要CMA
在深入代码之前,理解CMA解决的核心问题至关重要。现代SoC中的多媒体加速器(如GPU、VPU)、图像处理器(ISP)和高清视频编解码器通常具有以下特性:
- 硬件限制:许多嵌入式设备缺乏scatter-gather DMA能力,必须使用物理连续内存
- 大缓冲区需求:一帧4K YUV420视频需要约12MB内存(3840×2160×1.5字节)
- 特殊对齐要求:某些处理器要求内存起始地址对齐到特定边界(如64KB)
// 典型的多媒体驱动内存申请代码示例
struct page *pages = dma_alloc_from_contiguous(dev, nr_pages, align, GFP_KERNEL);
if (!pages) {
dev_err(dev, "Failed to allocate %zu pages\n", nr_pages);
return -ENOMEM;
}
1.2 CMA数据结构全景
CMA在内核中用struct cma表示,这个简洁的结构体承载着复杂的内存管理逻辑:
struct cma {
unsigned long base_pfn; // 起始页帧号
unsigned long count; // 区域总页数
unsigned long *bitmap; // 分配状态位图
unsigned int order_per_bit; // 每个bit代表的页数(2^order)
struct mutex lock; // 并发保护锁
const char *name; // CMA区域名称
};
关键设计亮点在于order_per_bit和位图的配合使用。假设order_per_bit为3,则每个位图bit管理8个页面(2³),这种设计:
- 大幅减少位图内存占用(管理1GB内存仅需4KB位图)
- 适合大块连续分配场景(多媒体设备通常需要MB级内存)
- 保持对中小规模分配的兼容性
2. CMA区域创建的两种路径
2.1 设备树(DTS)配置方式
在现代ARM嵌入式系统中,设备树是硬件描述的标准方式。CMA区域的典型DTS定义如下:
reserved-memory {
#address-cells = <2>;
#size-cells = <2>;
ranges;
linux,cma {
compatible = "shared-dma-pool";
reusable;
size = <0x0 0x20000000>; // 512MB
alignment = <0x0 0x200000>; // 2MB对齐
linux,cma-default;
};
};
内核启动时,通过以下调用链完成解析:
rmem_cma_setup():注册reserved memory操作回调cma_init_reserved_mem():从设备树提取base/size参数cma_declare_contiguous():最终创建CMA区域
注意:DTS中的
reusable属性是关键,它允许该区域被普通内存管理系统使用
2.2 内核参数与静态配置方式
对于没有设备树的传统系统,可以通过内核命令行或编译选项配置CMA:
# 内核启动参数示例
cma=128M@16M # 在16MB处分配128MB CMA区域
对应的代码路径在drivers/base/dma-contiguous.c中:
static int __init early_cma(char *p)
{
// 解析"size[@base][@alignment]"格式参数
if (!size)
return 0;
pr_info("Using %lu MiB CMA area at base %lu MiB\n",
size / SZ_1M, base / SZ_1M);
cma_declare_contiguous(base, size, limit, 0, 0, false, "cmdline");
}
两种配置方式的对比:
| 特性 | 设备树(DTS)方式 | 内核参数方式 |
|---|---|---|
| 配置灵活性 | 高(可定义多个区域) | 低(通常全局一个区域) |
| 内存布局可视化 | 好(与硬件描述集成) | 差(纯文本参数) |
| 启动后修改 | 不可修改 | 不可修改 |
| 多架构支持 | ARM/ARM64主流 | 所有架构通用 |
| 对齐要求指定 | 明确(alignment属性) | 可选(@alignment后缀) |
3. CMA初始化与伙伴系统整合
3.1 启动阶段的CMA初始化
CMA的初始化发生在内核启动的core_initcall阶段,关键函数调用序列如下:
cma_init_reserved_areas():扫描所有已声明的CMA区域init_cma_reserved_pageblock():处理每个保留的页面块__free_pages()+adjust_managed_page_count():将页面注入伙伴系统
这个过程的精妙之处在于内存状态的转换:
保留内存 → 可移动类型页面 → 加入伙伴系统可用列表
3.2 与伙伴系统的协同机制
CMA区域在伙伴系统中的管理遵循特殊规则:
- 页面类型标记:所有CMA页面被标记为
MIGRATE_CMA类型 - 隔离处理:
__free_one_page()中特殊处理CMA页面 - 防碎片策略:避免CMA页面与非可移动页面混合
// mm/page_alloc.c中CMA页面的特殊处理
if (is_migrate_cma(migratetype)) {
__mod_zone_freepage_state(zone, 1 << order, migratetype);
set_pageblock_migratetype(page, MIGRATE_CMA);
}
这种设计带来的优势是:
- 内存利用率最大化:CMA区域在非使用状态可作为普通内存
- 碎片抵抗力强:通过迁移类型隔离保证可回收性
- 分配效率平衡:大块连续分配时无需全内存扫描
4. CMA分配与释放的内部机制
4.1 分配流程深度剖析
当驱动调用cma_alloc()时,内核执行以下关键步骤:
-
内存迁移准备:
- 扫描CMA区域中已分配的普通页面
- 通过
alloc_contig_range()尝试迁移这些页面
-
实际分配操作:
- 获取CMA区域锁(防止并发访问)
- 扫描位图寻找足够大的空闲块
- 标记已分配位图区域
// CMA分配核心代码简化版
page = cma_alloc(cma, count, align);
if (page) {
if (clear_pages)
clear_gfp_mask = __GFP_ZERO;
ret = alloc_contig_range(pfn, pfn + count, MIGRATE_CMA,
GFP_KERNEL | clear_gfp_mask);
if (ret == 0) {
bitmap_set(cma->bitmap, start, bitmap_count);
return pfn_to_page(pfn);
}
}
4.2 释放过程的实现细节
内存释放是分配的逆过程,但有几个关键注意点:
- 引用计数机制:CMA区域采用
page->_refcount跟踪使用情况 - 位图清理:仅当页面完全无人使用时才清除位图标记
- 内存类型恢复:释放的页面重新标记为
MIGRATE_CMA
// 典型驱动释放代码示例
void release_buffer(struct device *dev, struct page *pages, size_t size)
{
if (dma_release_from_contiguous(dev, pages, size >> PAGE_SHIFT))
return;
// 备用释放路径
dma_free_coherent(dev, size, page_address(pages), page_to_phys(pages));
}
5. CMA与DMA子系统的深度集成
5.1 DMA API的透明整合
CMA最强大的特性之一是与DMA子系统的无缝集成。当驱动使用标准DMA API时,内核自动尝试以下分配路径:
- 一致性内存池(如果设备需要non-cacheable内存)
- CMA区域(当允许阻塞且CMA可用时)
- 普通伙伴系统(回退到常规分配)
- SWIOTLB(作为最后手段)
这种分层策略可通过以下内核参数调整:
# 禁用CMA回退路径
dma_perdev_coherent=1
5.2 性能优化实践
在实际项目中,我们通过以下技巧优化CMA性能:
-
区域划分:为不同设备分配独立CMA区域
vpu_cma: vpu@0x30000000 { compatible = "shared-dma-pool"; reg = <0x0 0x30000000 0x0 0x10000000>; // 256MB no-map; }; -
预分配策略:在系统空闲时预先分配关键缓冲区
// 启动时预分配 static int __init prealloc_cma(void) { cma_alloc(dev, PREALLOC_SIZE, 0, GFP_KERNEL); return 0; } late_initcall(prealloc_cma); -
水位线监控:通过
/proc/meminfo跟踪CMA使用CmaTotal: 262144 kB CmaFree: 130112 kB
6. 调试与问题排查技巧
6.1 常见问题场景
- 分配失败:检查
dmesg中的CMA统计信息 - 性能下降:可能因频繁页面迁移导致
- 死锁风险:当CMA分配路径与文件系统回写路径交叉时
6.2 调试工具集
-
CMA信息接口:
cat /proc/meminfo | grep Cma -
页面迁移跟踪:
echo 1 > /sys/kernel/debug/tracing/events/migrate/mm_migrate_pages/enable -
区域详细信息(需要内核配置):
cat /sys/kernel/debug/cma/cma-0/used
6.3 真实案例:视频处理延迟优化
在某4K视频处理项目中,我们发现CMA分配有时需要数百毫秒。通过分析,问题根源在于:
- 系统内存压力大时,页面迁移成本高
- CMA区域与频繁访问的页面混合
解决方案包括:
- 增加CMA区域大小(从128MB到256MB)
- 调整
/proc/sys/vm/compact_memory触发主动压缩 - 为视频处理保留专用CMA区域
7. CMA高级配置与替代方案
7.1 多区域配置策略
对于复杂系统,可以配置多个CMA区域服务不同设备:
reserved-memory {
gpu_cma: gpu@0x20000000 {
size = <0x0 0x10000000>; // 256MB
};
vpu_cma: vpu@0x30000000 {
size = <0x0 0x8000000>; // 128MB
};
};
&gpu {
memory-region = <&gpu_cma>;
};
&vpu {
memory-region = <&vpu_cma>;
};
7.2 替代方案比较
当CMA不适用时,开发者可以考虑:
-
一致性内存池:
coherent_pool=2M # 内核启动参数 -
ION内存分配器(Android系统):
int fd = open("/dev/ion", O_RDONLY); ioctl(fd, ION_IOC_ALLOC, &allocation_data); -
保留大页内存:
hugepagesz=2M hugepages=256
方案对比表:
| 特性 | CMA | 一致性内存池 | ION分配器 | 大页内存 |
|---|---|---|---|---|
| 动态共享 | 是 | 否 | 是(Android) | 否 |
| 是否需要预留 | 是 | 是 | 是 | 是 |
| 分配粒度 | 页(通常4K) | 字节级 | 任意大小 | 大页(2M/1G) |
| 适用场景 | 通用Linux | 小缓冲区 | Android多媒体 | 数据库等 |
8. 前沿发展与未来趋势
随着Linux内存管理子系统的演进,CMA技术也在持续优化:
- CMA与ZONE_MOVABLE的深度整合:提升大块分配成功率
- 动态CMA区域调整:运行时根据需求扩展/收缩区域
- 智能预取机制:预测设备需求提前准备内存
- 异构内存支持:在NUMA系统中优化CMA位置
在最近的内核版本中(5.10+),CMA有几个值得关注的改进:
- 性能优化:减少页面迁移时的锁竞争
- 调试增强:更详细的
sysfs接口 - 与DMA-BUF的集成:改进多媒体管道内存共享
// 新内核中的CMA改进示例:延迟分配支持
ret = cma_alloc_late(dev, size, align);
if (ret) {
dev_info(dev, "Using late CMA allocation\n");
}
对于需要深度定制内存管理的开发者,建议关注以下内核邮件列表:
- Linux Memory Management mailing list (linux-mm@kvack.org)
- ARM Kernel mailing list (linux-arm-kernel@lists.infradead.org)
- DMA API maintainers (iommu@lists.linux-foundation.org)
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)