4. 一致性DMA映射(Coherent DMA):dma_alloc_coherent()函数详解

一致性DMA映射(Coherent DMA Mapping)是Linux内核为DMA操作提供的最直接、最安全的缓冲区分配方式。它保证在CPU和DMA控制器访问缓冲区时,不需要任何额外的缓存一致性维护操作(如手动flush或invalidate cache)。这种“一致性”是通过硬件和内核的协同配置实现的,其核心在于分配时对内存页表属性的特殊设置。

核心概念: 一致性DMA映射的“一致性”并非指数据在CPU和Device之间实时同步,而是指硬件保证任何时刻,CPU通过虚拟地址看到的缓存内容与DMA设备通过物理地址看到的内存内容是一致的。这通常通过将内存区域标记为非缓存(Non-Cacheable)或非缓冲(Non-Bufferable)来实现。

4.1 dma_alloc_coherent() 函数原型与参数

该函数是分配一致性DMA缓冲区的主要接口,定义在 <linux/dma-mapping.h> 中。其原型如下:

/**
 * dma_alloc_coherent - 分配一致性DMA缓冲区
 * @dev:      指向设备结构的指针,用于标识DMA设备
 * @size:     请求分配的缓冲区大小(字节)
 * @dma_handle: 输出参数,返回DMA总线地址(物理地址)
 * @gfp:      GFP内核内存分配标志(GFP_KERNEL / GFP_ATOMIC 等)
 *
 * 返回值: 内核虚拟地址(CPU可访问),失败返回 NULL
 */
void *dma_alloc_coherent(struct device *dev, size_t size,
                         dma_addr_t *dma_handle, gfp_t gfp);
参数 类型 说明
dev struct device * 执行DMA操作的设备。不能为NULL,内核需要它来获取IOMMU或总线信息。
size size_t 请求的字节数。内核会按页对齐分配,实际分配大小可能大于请求值。
dma_handle dma_addr_t * 输出参数。函数返回后,此指针指向DMA设备可直接使用的总线地址(物理地址)。
gfp gfp_t 内存分配标志。在进程上下文中通常使用 GFP_KERNEL;在中断上下文中必须使用 GFP_ATOMIC。

4.2 返回的虚拟地址与物理地址

dma_alloc_coherent() 成功返回后,会提供两个关键的地址值:

  • 返回值(虚拟地址): 这是CPU可以直接解引用访问的内核虚拟地址(属于内核线性映射区或vmalloc区)。驱动通过此地址读写缓冲区数据。
  • dma_handle(DMA地址): 这是DMA设备用来寻址的总线地址。在大多数平台上(无IOMMU),它等同于物理地址。驱动将此地址写入DMA控制器的源/目的地址寄存器。
关键区别: 虚拟地址用于CPU编程,DMA地址用于设备DMA。两者指向同一块物理内存,但CPU访问时经过MMU和Cache,而设备访问时直接通过总线。一致性映射保证了这两种访问路径看到的数据一致。

下面的SVG图展示了CPU和DMA设备通过一致性映射访问同一块物理内存的路径:

一致性DMA映射:CPU与DMA设备访问路径 一致性DMA映射:CPU与DMA设备访问路径 CPU 核心 DMA 设备 物理内存 (非缓存/非缓冲属性) CPU 虚拟地址访问 (绕过Cache,直接读内存) DMA 总线地址访问 (直接读写物理内存) ✅ 硬件保证:CPU与DMA看到的数据始终一致

4.3 内存属性:非缓存(Non-Cacheable)与非缓冲(Non-Bufferable)

一致性DMA映射的本质在于修改了页表项中的内存属性。当内核分配一致性缓冲区时,它会将对应的物理页框映射到内核虚拟地址空间,并清除该映射的 C(Cacheable) 和 B(Bufferable) 位。这导致CPU对该区域的访问完全绕过L1/L2 Cache和写缓冲。

内存属性 ARMv8 页表描述符 x86 PAT/MTRR 行为描述
非缓存(Non-Cacheable) AttrIndx 指向 MAIR 中 Normal Non-Cacheable 条目 PAT 设置为 WC(Write Combining)或 UC(Uncacheable) CPU读/写直接访问内存,不经过Cache。读操作不会填充Cache,写操作直接写入内存。
非缓冲(Non-Bufferable) 在 Non-Cacheable 基础上,设置 Shareability 为 Non-Shareable UC(Uncacheable)模式隐含非缓冲 CPU写操作必须等待写事务在总线上完成才返回。不会使用写缓冲合并写操作。
性能代价: 一致性DMA映射虽然简化了驱动开发,但代价是显著的性能损失。CPU对一致性缓冲区的每次读写都是慢速的总线事务,而不是快速的Cache命中。对于高频小数据量的控制/状态寄存器通信,这种代价可以接受;但对于大数据块传输(如网络数据包、视频帧),应使用流式DMA映射(Streaming DMA Mapping)配合手动Cache同步以获得更好性能。

4.4 分配流程与内部机制

内核在实现 dma_alloc_coherent() 时,经历以下关键步骤:

  1. 大小对齐: 将请求的 size 向上对齐到页大小(PAGE_SIZE)。
  2. 物理页分配: 调用底层内存分配器(如 __get_free_pages() 或 alloc_pages())获取连续的物理页框。
  3. 页表属性设置: 在建立内核虚拟地址到物理页框的映射时,将页表项中的 Cacheable 和 Bufferable 位清零。在ARM平台上,这通过设置MAIR(Memory Attribute Indirection Register)索引实现;在x86上,通过设置PAT(Page Attribute Table)条目实现。
  4. DMA地址转换: 如果系统有IOMMU/SMMU,内核会将物理地址映射到设备可见的I/O虚拟地址空间,并返回该I/O虚拟地址作为 dma_handle。如果没有IOMMU,则直接返回物理地址。
  5. 清零初始化: 内核使用 memset() 将分配的缓冲区清零,确保设备不会读到残留的敏感数据。

下面的SVG流程图展示了从驱动调用到内存属性设置的完整分配流程:

dma_alloc_coherent() 内部分配流程 dma_alloc_coherent() 内部分配流程 ① 驱动调用 dma_alloc_coherent() ② 大小对齐到 PAGE_SIZE ③ 分配连续物理页框 ④ 设置页表属性:清除 Cacheable + Bufferable 位 (ARM: MAIR → Normal Non-Cacheable | x86: PAT → UC/WC) ⑤ IOMMU转换 / 直接返回物理地址 ⑥ memset清零,返回虚拟地址 + DMA地址

4.5 使用示例与释放

以下是一个典型的驱动代码片段,展示如何分配和使用一致性DMA缓冲区:

#include <linux/dma-mapping.h>
#include <linux/slab.h>

struct my_device {
    struct device *dev;
    void *cpu_addr;      // CPU虚拟地址
    dma_addr_t dma_addr; // DMA总线地址
    size_t buf_size;
};

int my_driver_probe(struct my_device *mydev)
{
    struct device *dev = mydev->dev;
    size_t size = 4096; // 4KB缓冲区

    // 分配一致性DMA缓冲区
    mydev->cpu_addr = dma_alloc_coherent(dev, size,
                                         &mydev->dma_addr,
                                         GFP_KERNEL);
    if (!mydev->cpu_addr) {
        dev_err(dev, "Failed to allocate coherent DMA buffer\n");
        return -ENOMEM;
    }
    mydev->buf_size = size;

    // CPU写入数据(直接写,无需Cache同步)
    memset(mydev->cpu_addr, 0, size);
    strcpy((char *)mydev->cpu_addr, "DMA transfer data");

    // 将dma_addr写入DMA控制器的地址寄存器
    // writel(mydev->dma_addr, dma_reg_base + DMA_SRC_ADDR_REG);

    dev_info(dev, "Coherent DMA buffer: cpu=%px dma=%pad size=%zu\n",
             mydev->cpu_addr, &mydev->dma_addr, size);
    return 0;
}

void my_driver_remove(struct my_device *mydev)
{
    if (mydev->cpu_addr) {
        // 释放一致性DMA缓冲区
        dma_free_coherent(mydev->dev, mydev->buf_size,
                          mydev->cpu_addr, mydev->dma_addr);
        mydev->cpu_addr = NULL;
        mydev->dma_addr = 0;
        mydev->buf_size = 0;
    }
}
释放注意事项: 调用 dma_free_coherent() 时,必须传入与分配时完全相同的 dev、size、cpu_addr 和 dma_handle。任何参数不匹配都会导致内核崩溃或内存泄漏。建议在驱动移除路径中始终检查 cpu_addr 是否为NULL后再释放。

4.6 一致性DMA映射的适用场景

根据一致性DMA映射的特性,以下场景最适合使用:

  • 控制/状态寄存器通信: 如网卡的描述符环(Descriptor Ring)、USB的DMA描述符、音频设备的DMA控制块。这些数据结构通常较小(几十到几百字节),且CPU和设备需要频繁交换状态信息。
  • 小数据量周期性传输: 如I2S音频数据、低速ADC采样数据。一致性映射避免了每次传输都做Cache同步的开销。
  • 不需要高吞吐的场景: 当数据传输速率较低(< 10 MB/s)时,非Cache访问的性能损失可以接受。
  • 驱动开发初期: 一致性映射是最简单的DMA方式,适合在驱动原型阶段快速验证功能,后续再优化为流式映射。
场景 推荐映射类型 原因
网卡描述符环 一致性映射 CPU和设备频繁更新描述符状态,一致性保证避免Cache同步
大块网络数据包(> 1KB) 流式映射 数据量大,流式映射配合Cache同步可获得更高吞吐
音频DMA缓冲区 一致性映射 周期性小数据块,实时性要求高,避免同步延迟
视频帧缓冲区 流式映射 数据量大,一致性映射的Non-Cache访问会成为性能瓶颈
经验法则: 如果DMA缓冲区的大小小于一个Cache Line(通常64字节),或者CPU和设备对该缓冲区的访问频率超过每秒1000次,优先考虑一致性DMA映射。否则,流式DMA映射通常是更好的选择。