2. CMA应用场景

连续物理内存(Contiguous Memory)在嵌入式Linux系统中是稀缺资源。多媒体编解码、GPU渲染、Camera ISP处理以及显示控制器等硬件模块,通常通过DMA(直接内存访问)操作数据,要求缓冲区在物理地址上连续。CMA(Contiguous Memory Allocator)正是为解决这一矛盾而设计——它允许系统在不需要大块连续内存时将其作为可移动页面使用,仅在驱动申请时动态迁移并锁定连续区域。

核心认知: CMA不是预留给某个驱动的“私有水池”,而是一个全局可回收的连续内存池。驱动在初始化时声明所需区域,在运行时按需分配,释放后页面可重新被内核回收用于常规分配。

2.1 典型驱动场景与内存需求对比

不同硬件模块对连续内存的需求量、分配时机和生命周期差异显著。下表归纳了四个主要场景的关键参数:

场景 典型连续内存需求 分配时机 生命周期 对齐要求
多媒体编解码(VPU/CODEC) 4~32 MB 每路流 解码/编码会话启动时 会话持续期间 4KB~2MB 对齐
GPU显存分配 16~256 MB 每帧缓冲 渲染任务提交时 帧生命周期(短时) 64KB~2MB 对齐
Camera ISP 8~64 MB 每帧 视频流开启时 流持续期间(循环缓冲) 32KB~1MB 对齐
显示控制器(Display Controller) 4~16 MB 每图层 显示模式设置时 常驻(直到模式切换) 1MB 对齐(典型)
关键观察: GPU场景的分配粒度最大且生命周期最短,对CMA的分配/释放频率要求最高;显示控制器场景则要求长期锁定,对碎片化容忍度最低。

2.2 多媒体编解码场景:VPU的连续缓冲需求

视频编解码器(VPU)通常包含硬件加速的DMA引擎,直接从DDR读取压缩码流并写入解码后的YUV/RAW帧。如果物理地址不连续,硬件无法通过一次DMA传输完成整帧数据搬运,导致性能骤降或功能异常。

/* 多媒体驱动中典型的CMA分配流程(伪代码) */
struct vpu_session {
    struct device *dev;
    struct cma *cma;
    dma_addr_t phys_addr;
    void *vaddr;
    size_t size;
};

int vpu_alloc_frame_buffer(struct vpu_session *sess, size_t size)
{
    /* 从设备关联的CMA区域分配连续内存 */
    sess->vaddr = dma_alloc_from_contiguous(sess->dev, size, 0);
    if (!sess->vaddr)
        return -ENOMEM;

    /* 获取物理地址并映射到IOMMU(如果存在) */
    sess->phys_addr = dma_map_single(sess->dev, sess->vaddr, size,
                                      DMA_FROM_DEVICE);
    /* 配置硬件寄存器 */
    writel(sess->phys_addr, sess->regs + VPU_FRAME_BASE);
    return 0;
}
避坑提示: 多媒体驱动在分配CMA缓冲时,务必考虑硬件对齐约束。部分VPU要求帧缓冲起始地址按2MB对齐,若CMA区域基地址未对齐,分配器可能返回非对齐地址,导致硬件DMA传输错误。建议在设备树中通过alignment属性指定对齐粒度。
多媒体编解码CMA分配流程 驱动初始化 声明CMA区域 分配连续内存 硬件DMA传输 释放缓冲 说明:驱动初始化时通过设备树声明CMA区域;会话启动时分配连续内存;DMA传输完成后释放缓冲,页面归还CMA池。

2.3 GPU显存分配场景:短生命周期与高频率分配

GPU驱动(如DRM/KMS、Vivante、Mali)在渲染每一帧时可能需要分配多个连续缓冲用于顶点数据、纹理和帧缓冲。与多媒体场景不同,GPU缓冲的生命周期极短(通常仅持续几帧),且分配频率极高。若CMA分配/释放路径存在瓶颈,直接导致帧率下降。

  • 挑战1:分配延迟敏感 —— 每帧分配必须在16.6ms(60fps)内完成,否则掉帧。
  • 挑战2:碎片化加剧 —— 频繁分配释放导致CMA区域产生外部碎片,大块分配失败率上升。
  • 挑战3:与CPU页面回收竞争 —— CMA迁移页面时可能触发大量LRU回收,增加延迟。
优化策略: GPU驱动通常采用缓冲池(Buffer Pool)模式,预先从CMA分配一批连续缓冲并缓存起来,渲染时直接从池中取出,渲染完成后再归还池中,避免每次分配都触发页面迁移。池大小需根据最大并发帧数动态调整。

2.4 Camera ISP场景:多帧循环缓冲与带宽约束

Camera ISP(图像信号处理器)通常需要多个连续缓冲构成循环队列:一个用于当前帧写入,一个用于上一帧处理,一个用于输出。每个缓冲必须物理连续,因为ISP的DMA引擎不支持SG(Scatter-Gather)模式。

/* Camera ISP循环缓冲配置示例 */
struct isp_buffer_ring {
    struct device *dev;
    struct cma *cma;
    dma_addr_t phys[ISP_RING_DEPTH];  /* 3~4个缓冲 */
    void *virt[ISP_RING_DEPTH];
    size_t buf_size;                  /* 单帧大小 */
    int head, tail;
};

int isp_alloc_ring(struct isp_buffer_ring *ring, size_t frame_size)
{
    int i;
    for (i = 0; i < ISP_RING_DEPTH; i++) {
        /* 从CMA分配连续内存 */
        ring->virt[i] = dma_alloc_from_contiguous(ring->dev,
                                                   frame_size, 0);
        if (!ring->virt[i])
            goto err_free;
        ring->phys[i] = virt_to_phys(ring->virt[i]);
    }
    return 0;

err_free:
    while (--i >= 0)
        dma_release_from_contiguous(ring->dev, ring->virt[i],
                                     frame_size);
    return -ENOMEM;
}
带宽注意: Camera ISP通常运行在数百MHz时钟频率下,对内存带宽要求极高。若CMA区域位于DDR带宽较低的地址范围(如某些SoC的“慢速域”),可能导致ISP无法在帧消隐期内完成数据搬运,产生帧丢失。建议在设备树中通过dma-ranges属性将CMA区域限定在高速DDR域。

2.5 显示控制器场景:长期锁定与模式切换

显示控制器(Display Controller)驱动在系统启动或显示模式切换时分配连续缓冲,用于存储帧缓冲(Framebuffer)或图层数据。这些缓冲通常长期锁定,直到显示模式改变或驱动卸载。因此,显示控制器对CMA区域的稳定性要求最高——一旦分配成功,不允许被迁移或回收。

显示模式 分辨率 色深 单帧大小(近似) 图层数
1080p60 1920x1080 32bpp ~8 MB 1~3
4K60 3840x2160 32bpp ~32 MB 1~4
8K30 7680x4320 32bpp ~128 MB 1~2
实践建议: 对于显示控制器场景,建议在设备树中为显示驱动预留专用CMA区域(通过shared-dma-pool且no-map),避免与其他驱动竞争。同时,在驱动初始化时一次性分配所有图层缓冲,后续模式切换仅更新指针,不重新分配内存。
CMA分配场景对比:生命周期与分配频率 分配频率低 分配频率高 → 生命周期长 生命周期短 显示控制器 长期锁定 多媒体编解码 会话级生命周期 Camera ISP 循环缓冲 GPU显存 帧级短生命周期 说明:显示控制器位于“低频+长生命周期”象限;GPU位于“高频+短生命周期”象限;多媒体和ISP居中。

2.6 场景驱动的CMA配置策略

根据上述场景特征,驱动开发者在配置CMA区域时应遵循以下策略:

  1. 专用区域 vs 共享区域: 显示控制器和Camera ISP建议使用专用CMA区域(shared-dma-pool + no-map),避免被其他驱动干扰;GPU和多媒体编解码可以共享一个大型CMA区域,利用统计复用降低总内存占用。
  2. 对齐粒度: 根据硬件DMA引擎的约束设置对齐。例如,某些VPU要求2MB对齐,GPU要求64KB对齐。在设备树中通过alignment属性指定。
  3. 预留大小: 根据最大并发场景计算。例如,支持4路1080p解码时,每路8MB,加上metadata,预留40MB较为安全。
  4. 分配策略: 高频分配场景(GPU)使用缓冲池;低频分配场景(显示控制器)使用一次性分配。