4. 设备树中配置CMA:节点语法、reg属性、alignment与no-map标志

在嵌入式Linux系统中,CMA(Contiguous Memory Allocator)区域的配置通常通过设备树(Device Tree)的 reserved-memory 节点完成。设备树为内核提供了描述系统内存布局的标准化方式,CMA区域的起始地址、大小、对齐要求以及映射属性都通过特定的DT属性和标志来定义。本节将深入解析这些核心语法元素。

4.1 reserved-memory 节点基本语法

设备树中,所有预留内存区域(包括CMA)都集中定义在根节点下的 reserved-memory 子节点中。每个具体的预留区域作为一个子节点,通过 compatible 属性标识其用途(如 shared-dma-pool 用于CMA),并通过 reg 属性描述物理地址范围。

关键结构: reserved-memory 节点是预留内存的容器,每个子节点代表一个独立的内存区域。CMA区域通常使用 compatible = "shared-dma-pool" 并配合 reusable 属性。
// 设备树中 CMA 区域的标准定义结构
/ {
    reserved-memory {
        #address-cells = <2>;   // 64位地址空间
        #size-cells = <2>;
        ranges;

        // 定义一个 CMA 区域
        linux,cma {
            compatible = "shared-dma-pool";
            reg = <0x00 0x10000000 0x00 0x10000000>; // 起始 0x10000000,大小 256MB
            alignment = <0x00 0x00400000>;           // 4MB 对齐
            reusable;                                 // 可复用(CMA 核心标志)
            linux,cma-default;                        // 标记为默认 CMA 池
        };
    };
};

4.2 reg 属性:定义物理地址与大小

reg 属性是设备树中描述设备物理地址空间的标准方式。对于CMA区域,reg 定义了两部分信息:起始物理地址和区域大小。其编码格式由父节点(reserved-memory)的 #address-cells 和 #size-cells 决定。

参数 说明 示例值(32位) 示例值(64位)
起始地址 CMA区域的物理基地址,必须与系统内存映射对齐 <0x10000000> <0x00 0x10000000>
区域大小 CMA区域的字节长度,通常为2的幂次 <0x10000000> (256MB) <0x00 0x10000000> (256MB)
地址单元 #address-cells 决定地址字段的32位字数量 1 2
大小单元 #size-cells 决定大小字段的32位字数量 1 2
实践建议: 在64位ARM系统中,即使物理内存低于4GB,也建议使用 #address-cells = <2> 和 #size-cells = <2>,以保持与未来扩展的兼容性。内核解析 reg 时会自动根据父节点属性调整读取宽度。

4.3 alignment 属性:对齐约束与性能

alignment 属性指定了CMA区域起始地址的对齐要求。内核在分配CMA区域时,会尝试将区域起始地址对齐到该属性指定的边界。对齐不仅影响内存布局,还直接关系到DMA操作的效率和TLB(Translation Lookaside Buffer)利用率。

  • 对齐粒度: 通常设置为页面大小(4KB)的倍数,常见值为 1MB、2MB、4MB 或 1GB。
  • 性能影响: 较大的对齐值(如 1GB)允许使用块映射(Block Mapping),减少页表开销,提升大块DMA传输性能。
  • 地址单元: 与 reg 类似,alignment 的编码宽度也由 #address-cells 决定。
  • 默认行为: 如果未指定 alignment,内核默认使用架构相关的对齐值(ARM64 通常为 2MB)。
// alignment 属性示例:不同对齐粒度
// 4KB 对齐(页面粒度)
alignment = <0x00001000>;

// 2MB 对齐(大页粒度,ARM64 默认)
alignment = <0x00200000>;

// 1GB 对齐(块映射粒度,适合大块 CMA)
alignment = <0x40000000>;
注意: 过大的对齐值可能导致CMA区域分配失败,因为系统可能无法找到满足对齐要求的连续空闲物理内存。建议根据实际硬件内存布局和DMA设备需求选择合理的对齐值。

4.4 no-map 标志:映射控制与安全

no-map 是一个布尔标志,指示内核不要为CMA区域创建页表映射。这意味着该区域在虚拟地址空间中不可见,只能通过物理地址直接访问。这一标志在以下场景中至关重要:

  • 安全内存: 防止CPU通过虚拟地址访问敏感数据(如加密密钥、安全监控数据)。
  • 设备独占内存: 某些外设(如GPU、VPU)需要独占物理内存,不希望CPU缓存干扰。
  • 多核共享: 在异构计算中,避免CPU映射导致缓存一致性问题。
标志 映射行为 典型用途 与CMA兼容性
无 no-map 内核创建线性映射,CPU可正常读写 标准CMA,驱动通过虚拟地址访问 完全兼容
no-map 内核不创建任何页表映射 安全内存、设备独占内存 需谨慎使用,驱动只能通过DMA或ioremap访问
no-map + reusable 区域可复用但无CPU映射 VPU/GPU专用CMA,避免CPU缓存污染 部分架构支持,需驱动配合

4.5 CMA区域配置完整示例与SVG结构图

以下是一个包含多个CMA区域的完整设备树配置示例,展示了不同用途的预留内存区域如何协同工作。

/ {
    reserved-memory {
        #address-cells = <2>;
        #size-cells = <2>;
        ranges;

        // 默认 CMA 区域:256MB,4MB 对齐,可复用
        linux,cma {
            compatible = "shared-dma-pool";
            reg = <0x00 0x10000000 0x00 0x10000000>;
            alignment = <0x00 0x00400000>;
            reusable;
            linux,cma-default;
        };

        // VPU 专用 CMA:128MB,2MB 对齐,no-map 防止CPU缓存干扰
        vpu,cma {
            compatible = "shared-dma-pool";
            reg = <0x00 0x20000000 0x00 0x08000000>;
            alignment = <0x00 0x00200000>;
            reusable;
            no-map;
        };

        // 安全内存区域:64MB,1MB 对齐,不可复用,无映射
        secure,memory {
            compatible = "shared-dma-pool";
            reg = <0x00 0x28000000 0x00 0x04000000>;
            alignment = <0x00 0x00100000>;
            no-map;
        };
    };
};
设备树 CMA 区域配置结构图 / (根节点) reserved-memory #address-cells = <2> #size-cells = <2>; ranges; linux,cma (默认CMA) compatible = "shared-dma-pool" reg = <0x10000000 0x10000000> alignment = <0x00400000> reusable; linux,cma-default vpu,cma (VPU专用) compatible = "shared-dma-pool" reg = <0x20000000 0x08000000> alignment = <0x00200000> reusable; no-map secure,memory (安全) compatible = "shared-dma-pool" reg = <0x28000000 0x04000000> alignment = <0x00100000> no-map (无reusable) 关键属性与标志说明 • reg:定义物理起始地址和大小,编码宽度由父节点 #address-cells/#size-cells 决定 • alignment:指定起始地址对齐要求,影响页表映射粒度和DMA性能 • reusable:标记区域可被页分配器复用(CMA核心特性),未使用时归还给系统 • no-map:禁止内核创建页表映射,用于安全内存或设备独占内存 • linux,cma-default:标记该区域为系统默认CMA池,驱动可自动使用

4.6 内核解析流程与属性校验

内核在启动过程中通过 early_init_dt_scan_reserved_mem() 函数解析 reserved-memory 节点。解析流程遵循以下步骤:

  1. 节点遍历: 遍历所有 reserved-memory 的子节点,检查 compatible 属性是否为 shared-dma-pool。
  2. 地址解析: 调用 of_read_number() 读取 reg 属性,根据父节点的 #address-cells 和 #size-cells 解码起始地址和大小。
  3. 对齐校验: 读取 alignment 属性(如果存在),校验起始地址是否满足对齐要求;若不满足,内核会尝试调整起始地址。
  4. 标志处理: 检查 no-map 和 reusable 标志,设置对应的内存区域标志位(如 MEMBLOCK_NOMAP)。
  5. 区域注册: 调用 memblock_reserve() 或 memblock_alloc_range() 将区域注册到内存分配器中。
内核源码参考: 解析逻辑位于 drivers/of/of_reserved_mem.c 中的 __reserved_mem_alloc_size() 函数。该函数负责处理 reg、alignment 和 no-map 的联合校验。

4.7 常见配置错误与调试方法

错误类型 现象 根因 解决方案
reg 地址重叠 内核启动报 "reserved memory: overlap" 多个CMA区域地址范围交叉 检查所有 reg 地址范围,确保无重叠
对齐失败 CMA区域分配失败,dmesg 显示 "alignment failed" 指定对齐值大于系统可用连续内存块 减小 alignment 值,或调整内存布局
no-map 冲突 驱动访问CMA区域时触发页错误 驱动未使用 ioremap 直接访问物理地址 驱动中调用 ioremap() 创建映射后再访问
地址单元不匹配 解析出的地址为0或异常大值 #address-cells 与 reg 编码宽度不一致 确保父节点和子节点的地址单元声明一致
调试技巧: 在启动参数中添加 memblock=debug 可以打印详细的内存区域注册信息,包括每个CMA区域的起始地址、大小和对齐状态。使用 cat /proc/iomem | grep "CMA" 可以查看已注册的CMA区域。