4. CMA初始化流程

CMA 的初始化是多媒体驱动和 DMA 子系统能够正常使用连续内存的前提。整个初始化流程分为三个阶段:内核启动参数解析、设备树节点解析以及核心函数 cma_init_reserved_areas() 的执行。本章将深入分析这三个阶段的具体实现与配置方法。

核心要点: CMA 的初始化发生在系统启动早期(mm_init 阶段),通过解析启动参数或设备树,在内存的 memblock 分配器中预留出物理连续区域,最终由 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 中预留物理内存。

CMA 设备树解析与内存预留流程 设备树 .dts 文件 early_init_fdt_scan_reserved_mem() 遍历 /reserved-memory 节点 检查 compatible="shared-dma-pool" 且包含 reusable 属性 __reserved_mem_alloc_size() memblock_reserve() 预留物理页 创建 struct reserved_mem 条目 关联到对应 device_node 说明 此阶段仅完成物理内存 预留,CMA 管理结构 在 cma_init_reserved_areas() 中初始化

4.3 cma_init_reserved_areas() 函数分析

cma_init_reserved_areas() 是 CMA 初始化的核心函数,在 mm_init() 阶段被调用。它负责将之前通过启动参数或设备树预留的物理内存区域,初始化为完整的 CMA 管理结构。

该函数的主要执行流程如下:

  1. 遍历全局 reserved_mem 数组:遍历所有通过设备树或 cma_region= 创建的 struct reserved_mem 条目。
  2. 检查 CMA 标志:判断每个 reserved_mem 条目是否包含 RESERVEDMEM_CMA 标志,该标志在设备树解析阶段由 reusable 属性触发设置。
  3. 调用 cma_init_reserved_mem():对符合条件的条目,调用此函数创建 struct cma 结构体,并初始化位图(bitmap)管理。
  4. 处理默认 CMA 区域:如果通过 cma= 启动参数指定了全局默认区域,且设备树中未定义 linux,cma-default,则在此处创建默认 CMA 区域。
  5. 注册到全局 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 区域数量。
cma_init_reserved_areas() 函数调用流程 cma_init_reserved_areas() 遍历 reserved_mem 数组 检查 RESERVEDMEM_CMA 标志 cma_init_reserved_mem() 分配 struct cma 和 bitmap cma_activate_area() 标记 MIGRATE_CMA 注册到 cma_areas[] 全局数组 初始化完成后的系统状态 • 物理内存已在 memblock 中预留,页分配器无法直接分配这些页面 • CMA 区域被标记为 MIGRATE_CMA 类型,可被页分配器回收用于移动页面 • 驱动可通过 dma_alloc_contiguous() 或 cma_alloc() 从指定区域分配连续内存

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() 是否成功执行。