乐于分享
好东西不私藏

CMA(连续内存分配器)源码解读:为什么它能解决 DMA 连续内存难题

CMA(连续内存分配器)源码解读:为什么它能解决 DMA 连续内存难题

1.  为什么 Linux 需要CMA

在 Linux 内核中,常规内存分配有两种:

  • kmalloc:物理连续(但受碎片影响,且上限较低)

  • vmalloc:虚拟连续,物理不连续

很多硬件(尤其是早期或简单设备)要求 DMA 缓冲区物理连续,否则无法用单个 DMA 描述符描述一段内存。即便是支持 scatter-gather 的设备,也常常对“连续”有性能偏好。

长期运行后,内存碎片化会让大块物理连续内存越来越难以分配。于是 CMA 的核心目标就非常清晰:

预留一块“专用的、连续的、不会被普通分配打碎”的物理内存池,供需要连续物理内存的设备使用。


2. CMA 的入口:谁调用 CMA?

在内核里,CMA 的使用通常来自 DMA 接口。典型路径是:

dma_alloc_coherent()  -> dma_alloc_attrs()    -> __dma_alloc_attrs()      -> dma_alloc_from_contiguous()        -> cma_alloc()

也就是说:CMA 不是直接被驱动调用的,而是通过 DMA API 作为“连续内存后端”被动触发。

所以你会发现:CMA 的存在对大多数驱动是透明的,只有在 DMA 需要连续物理内存时才会被动用到。


3. CMA 的核心数据结构:struct cma 与 struct cma_bitmap

在 mm/cma.c 中,CMA 的核心是 struct cma

struct cma {    unsigned long base_pfn;    unsigned long count;    unsigned long order_per_bit;    struct cma_bitmap *bitmap;    struct list_head list;    ...};
  • base_pfn:CMA 区域的起始物理页帧号(PFN)

  • count:区域大小(页数)

  • bitmap:分配位图,用于记录哪些页已被占用

  • order_per_bit:位图中每一位代表多少页(用于按“块”分配)

3.1 为什么用 bitmap?

CMA 的一个核心特点是:它必须支持“快速查找连续空闲页”。位图是最直接、最高效的方式。

CMA 的 bitmap 不是“每页一位”,而是“每块(block)一位”,原因是:

  • 大多数连续分配是按“块”进行的(比如 2^n 页)

  • 逐页位图会让查找变慢

  • 逐块位图减少扫描成本


4. CMA 的分配逻辑:cma_alloc() 的真实流程

cma_alloc() 的核心工作流程是:

  1. 查找符合条件的 CMA 区域

    • 按设备、按 NUMA、按 DMA 区域选择

  2. 从 bitmap 找到一段连续空闲块

  3. 标记为已分配

  4. 返回物理地址/页帧

最关键的一步是“查找连续空闲块”,在 cma.c 里对应的是:

  • cma_bitmap_alloc():位图分配

  • cma_find_contiguous():查找连续空闲区

4.1 先查找“合适的 CMA 区域”

Linux 支持多个 CMA 区域(多个 struct cma),通常会按:

  • 设备的 DMA 约束(dma_mask)

  • NUMA 节点

  • 设备所在的内存区域(reserved memory)

进行匹配。

这也是为什么在设备树里会看到类似:

memory-region = <&cma 0>;

或者:

reserved-memory {    compatible = "shared-dma-pool";#address-cells = <1>;#size-cells = <1>;    reg = <0x... 0x...>;};

这其实就是告诉内核:“给我预留一块连续物理内存作为 CMA 区域”。


5. CMA 的“预留”机制:启动阶段就决定了

CMA 之所以能保证连续性,是因为它在启动阶段就预留了一块物理内存,不参与普通内存分配。

预留方式有两种:

5.1 通过启动参数 cma=

一般是这样的:

cma=256M

这会在启动时从可用内存中预留 256MB 给 CMA,下面是我的设备上,我的设备上没有CMA 所以是0

5.2 通过设备树/ACPI 预留区域

很多嵌入式平台会通过 device tree 指定 CMA 区域:

reserved-memory {    cma: cma@0 {        compatible = "shared-dma-pool";        reg = <0x... 0x...>;    };};

这类方式更常见于 ARM/嵌入式平台。


6. CMA 的分配失败:不是“内存不够”,而是“连续不够”

这是很多人容易误解的地方:

CMA 分配失败通常不是因为“总内存不够”,而是因为:

  • CMA 区域里没有足够连续的空闲块

即使 CMA 区域还有很多零散空闲页,也可能因为碎片化导致无法分配。

这也说明了:

CMA 的连续性不是“动态整理”出来的,而是“预留 + 专用 + 尽量不碎片化”的结果。


7. CMA 的回收:为什么释放不一定能立即恢复连续性?

CMA 释放的核心是:

  • bitmap 清位

  • 但不会做“合并”之类的复杂操作

因此,CMA 区域仍可能被碎片化,尤其是在:

  • 频繁分配/释放不同大小的连续块

  • 多个设备共享同一 CMA 区域

这也是为什么很多系统在长期运行后仍可能出现 CMA 分配失败。


8. CMA 与 DMA 的关系:它解决了什么问题?

CMA 的存在使得 DMA 分配有了一个“连续物理内存的后端”。

在 dma_alloc_coherent() 的路径里:

  • 如果设备支持连续分配(或被配置为使用 CMA),内核会尝试从 CMA 分配

  • 如果 CMA 不可用或失败,可能会退回到普通分配或直接失败(取决于约束)

因此 CMA 的作用是:

让“需要连续物理内存的 DMA 设备”在运行时仍然能获得稳定的连续内存,而不必依赖启动时的静态分配。


9. CMA 的典型应用场景(为什么你会用到它)

CMA 最常见的应用场景包括:

9.1 视频/图像处理(VPU/ISP)

这些设备通常需要一段连续的缓冲区用于 DMA。

9.2 GPU/显示控制器

帧缓冲、图形缓冲通常需要连续物理内存(尤其是早期硬件)。

9.3 音频/网络/嵌入式外设

一些硬件不支持 scatter-gather,只能处理单段 DMA。


10. 常见误区与坑(工程实践)

10.1 误区:CMA 预留越大越好

预留越大意味着:

  • 系统可用内存越少

  • 对普通应用更不友好

CMA 的大小需要基于实际设备需求评估。

10.2 误区:CMA 能解决所有连续内存问题

CMA 只保证 CMA 区域内连续,但:

  • 仍可能碎片化

  • 仍可能分配失败

  • 仍需正确设置 DMA mask / IOMMU

10.3 坑:设备树 reserved-memory 误配置

很多嵌入式平台因为 reserved-memory 配置不当导致:

  • CMA 区域太小

  • CMA 区域不在正确的 NUMA 节点

  • DMA 访问范围不匹配

这些都会导致 CMA 分配失败。


11. CMA 的设计本质

CMA 的设计本质是一个工程妥协:

  • 预留一块连续内存:避免碎片化

  • 专用池:不被普通分配打碎

  • 按需分配:运行时仍可动态使用

  • 通过 DMA API 透明使用:不需要驱动直接管理

它解决的问题非常具体:

在运行时为 DMA 设备提供连续物理内存,而不必依赖静态分配或不可靠的 kmalloc。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » CMA(连续内存分配器)源码解读:为什么它能解决 DMA 连续内存难题

评论 抢沙发

2 + 8 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮