4. CMA初始化流程
CMA 的初始化是多媒体驱动和 DMA 子系统能够正常使用连续内存的前提。整个初始化流程分为三个阶段:内核启动参数解析、设备树节点解析以及核心函数 cma_init_reserved_areas() 的执行。本章将深入分析这三个阶段的具体实现与配置方法。
cma_init_reserved_areas() 完成区域的管理结构初始化。
4.1 内核启动参数配置
内核提供了两个主要的启动参数来定义 CMA 区域:cma= 和 cma_region=。前者用于快速配置一个全局默认 CMA 区域,后者用于配置多个自定义区域。
| 启动参数 | 格式 | 说明 | 示例 |
|---|---|---|---|
cma= |
cma=nn[MG]@[start]-[end] |
设置全局默认 CMA 区域的大小和可选的物理地址范围 | cma=64M 或 cma=128M@0x40000000-0x80000000 |
cma_region= |
cma_region= |
定义命名的 CMA 区域,支持多个区域,驱动可通过名称引用 | cma_region=video:32M@0x50000000 |
cma= 参数快速验证驱动功能。当需要为不同 IP 核(如 VPU、ISP、GPU)分配独立 CMA 区域时,再切换到 cma_region= 进行精细化管理。
内核在 setup_arch() 阶段调用 early_init_dt_scan_chosen() 解析启动参数,将 cma= 的值存入 cma_cmdline 全局变量,将 cma_region= 的值存入 cma_regions_cmdline 链表。这些参数将在后续的 cma_init_reserved_areas() 中被读取。
4.2 设备树(Device Tree)中的 CMA 节点配置
在嵌入式 ARM/ARM64 平台中,设备树是配置 CMA 的首选方式。通过 reserved-memory 节点,可以灵活定义多个 CMA 区域,并指定它们与具体设备驱动的绑定关系。
以下是一个典型的多媒体平台设备树 CMA 配置示例:
/ {
reserved-memory {
#address-cells = <2>;
#size-cells = <2>;
ranges;
/* 全局默认 CMA 区域 */
linux,cma {
compatible = "shared-dma-pool";
reusable;
size = <0x0 0x10000000>; /* 256MB */
alloc-ranges = <0x0 0x40000000 0x0 0x80000000>;
linux,cma-default;
};
/* VPU 专用 CMA 区域 */
vpu_cma: vpu_region@a0000000 {
compatible = "shared-dma-pool";
reusable;
reg = <0x0 0xa0000000 0x0 0x04000000>; /* 64MB at 0xa0000000 */
};
/* ISP 专用 CMA 区域 */
isp_cma: isp_region@b0000000 {
compatible = "shared-dma-pool";
reusable;
reg = <0x0 0xb0000000 0x0 0x02000000>; /* 32MB at 0xb0000000 */
};
};
/* VPU 设备节点引用专用 CMA 区域 */
vpu: vpu@a0000000 {
compatible = "vendor,vpu";
memory-region = <&vpu_cma>;
/* ... 其他属性 */
};
/* ISP 设备节点引用专用 CMA 区域 */
isp: isp@b0000000 {
compatible = "vendor,isp";
memory-region = <&isp_cma>;
/* ... 其他属性 */
};
};
reusable:表示该区域可被 CMA 管理,即内存可在不使用时被页分配器回收,在需要时由驱动迁移页面。linux,cma-default:标记该区域为系统默认 CMA 区域,所有未指定memory-region的 DMA 分配请求将默认使用此区域。alloc-ranges:限制 CMA 区域可分配的物理地址范围,常用于满足外设的地址寻址能力限制。
设备树解析流程在 arm64_memblock_init() 中调用 early_init_fdt_scan_reserved_mem() 完成。该函数遍历 /reserved-memory 节点,对 compatible = "shared-dma-pool" 且包含 reusable 属性的节点,调用 __reserved_mem_alloc_size() 在 memblock 中预留物理内存。
4.3 cma_init_reserved_areas() 函数分析
cma_init_reserved_areas() 是 CMA 初始化的核心函数,在 mm_init() 阶段被调用。它负责将之前通过启动参数或设备树预留的物理内存区域,初始化为完整的 CMA 管理结构。
该函数的主要执行流程如下:
- 遍历全局 reserved_mem 数组:遍历所有通过设备树或
cma_region=创建的struct reserved_mem条目。 - 检查 CMA 标志:判断每个 reserved_mem 条目是否包含
RESERVEDMEM_CMA标志,该标志在设备树解析阶段由reusable属性触发设置。 - 调用 cma_init_reserved_mem():对符合条件的条目,调用此函数创建
struct cma结构体,并初始化位图(bitmap)管理。 - 处理默认 CMA 区域:如果通过
cma=启动参数指定了全局默认区域,且设备树中未定义linux,cma-default,则在此处创建默认 CMA 区域。 - 注册到全局 cma 数组:将初始化完成的
struct cma *指针存入cma_areas全局数组,供后续dma_alloc_contiguous()等分配函数使用。
核心函数 cma_init_reserved_mem() 的关键代码逻辑如下:
int __init cma_init_reserved_mem(phys_addr_t base, phys_addr_t size,
unsigned int order_per_bit,
struct cma **res_cma)
{
struct cma *cma;
int ret;
/* 1. 检查参数合法性:地址对齐、大小有效性 */
if (size == 0 || !IS_ALIGNED(base, CMA_MIN_ALIGNMENT_BYTES))
return -EINVAL;
/* 2. 分配 struct cma 结构体 */
cma = kzalloc(sizeof(struct cma), GFP_KERNEL);
if (!cma)
return -ENOMEM;
/* 3. 初始化 CMA 区域参数 */
cma->base_pfn = PFN_DOWN(base);
cma->count = size >> PAGE_SHIFT;
cma->order_per_bit = order_per_bit;
/* 4. 分配并初始化位图(bitmap) */
cma->bitmap = kzalloc(BITS_TO_LONGS(cma->count) * sizeof(long),
GFP_KERNEL);
if (!cma->bitmap) {
kfree(cma);
return -ENOMEM;
}
/* 5. 初始化锁和迁移通知链 */
mutex_init(&cma->lock);
INIT_LIST_HEAD(&cma->preferred_pages);
/* 6. 将 CMA 区域标记为 MIGRATE_CMA 类型 */
ret = cma_activate_area(cma);
if (ret)
goto error;
/* 7. 返回创建的 cma 结构体指针 */
*res_cma = cma;
pr_info("CMA: reserved %ld MiB at %pa\n", (unsigned long)size / SZ_1M, &base);
return 0;
error:
kfree(cma->bitmap);
kfree(cma);
return ret;
}
struct cma:每个 CMA 区域的核心管理结构,包含基址页帧号(base_pfn)、页面总数(count)、位图(bitmap)以及用于页面迁移的锁。cma_areas[]:全局 CMA 区域数组,系统所有 CMA 区域都注册在此,索引从 0 开始。cma_area_count:全局 CMA 区域数量。
4.4 初始化流程总结
CMA 的完整初始化流程可以归纳为以下三个阶段:
| 阶段 | 调用时机 | 主要函数 | 产出 |
|---|---|---|---|
| 参数解析 | setup_arch() 早期 | early_init_dt_scan_chosen() |
解析 cma= 和 cma_region= 启动参数,存入全局变量 |
| 设备树解析 | arm64_memblock_init() | early_init_fdt_scan_reserved_mem() |
遍历 /reserved-memory 节点,通过 memblock 预留物理内存,创建 struct reserved_mem |
| CMA 结构初始化 | mm_init() | cma_init_reserved_areas() |
创建 struct cma,初始化位图,标记 MIGRATE_CMA 类型,注册到全局数组 |
cma: 或 CMA: 关键字,可以查看 CMA 区域的初始化状态。例如:[ 0.000000] cma: Reserved 256 MiB at 0x0000000040000000。如果未看到相关日志,说明 CMA 配置可能未被正确解析。
理解 CMA 的初始化流程,对于排查多媒体驱动中的 DMA 分配失败问题至关重要。例如,如果驱动在 dma_alloc_contiguous() 中返回 NULL,通常是因为 CMA 区域未正确初始化、区域大小不足或物理地址范围不满足外设要求。此时应首先检查启动参数或设备树配置是否正确,然后确认 cma_init_reserved_areas() 是否成功执行。