4. 一致性DMA映射

在Linux DMA编程中,映射方式决定了CPU与设备之间如何共享内存,也决定了缓存一致性由谁负责。流式DMA映射适合“一次传输、用完即弃”的场景,而一致性DMA映射则面向“长期共享、双向访问”的控制面数据。本节围绕dma_alloc_coherent与dma_free_coherent,讲清一致性内存的本质、分配流程、典型用法与常见误区。

一、核心原理:一致性内存到底“一致”在哪里

所谓一致性DMA内存,是指驱动通过dma_alloc_coherent()申请的一段内存,它保证CPU和设备在访问同一块缓冲区时,不需要驱动显式调用dma_sync_single_for_cpu()或dma_sync_single_for_device()来同步缓存。其背后依赖两种机制之一:

  • 硬件缓存一致架构:如x86平台,CPU与DMA控制器通过缓存一致性协议(如MESI)协同,DMA读写会参与缓存一致性维护,因此内存天然一致。
  • 非一致架构的软件规避:如ARM、PowerPC等平台,分配时通常将内存标记为非缓存(uncached)或写合并(write-combine),使CPU访问绕过数据缓存,从而避免“CPU看到旧值、设备看到新值”的错位。

因此,一致性DMA内存的“一致”不是免费午餐:它用牺牲CPU访问性能换来了无需手动同步的便利。这也是它适合控制面、不适合大数据吞吐的根本原因。

一致性DMA映射:分配、使用与释放路径 CPU 驱动读写 buf 无需 dma_sync DMA设备 读写 dma_handle 直接访问物理内存 一致性缓冲区 非缓存 / 硬件一致 CPU虚拟地址 ↔ DMA地址 长期共享,双向可见 分配阶段 dma_alloc_coherent() 返回 cpu_addr + dma_handle GFP_KERNEL / GFP_ATOMIC 可能触发内存回收 使用阶段 CPU 直接读写 cpu_addr 设备使用 dma_handle 无需 dma_sync_* 适合描述符/状态/寄存器 释放阶段 dma_free_coherent() 传入 size、cpu_addr、dma_handle 三者必须与分配时一致 先停设备再释放

二、方法流程:分配、使用与释放的标准动作

一致性DMA映射的API原型如下:

void *dma_alloc_coherent(struct device *dev,
                         size_t size,
                         dma_addr_t *dma_handle,
                         gfp_t flag);

void dma_free_coherent(struct device *dev,
                       size_t size,
                       void *cpu_addr,
                       dma_addr_t dma_handle);

典型使用流程分为四步:

  1. 分配:在设备probe或初始化阶段调用dma_alloc_coherent(),得到CPU侧虚拟地址cpu_addr和DMA侧总线地址dma_handle。注意dma_handle是设备视角的地址,不能直接给CPU解引用。
  2. 告知设备:把dma_handle写入设备寄存器,作为描述符基址或数据缓冲区地址。
  3. 双向访问:CPU通过cpu_addr读写,设备通过dma_handle读写,双方看到的内容始终一致,无需同步调用。
  4. 释放:在remove或出错路径中调用dma_free_coherent(),且必须保证设备已停止使用该内存。

关键点:dma_alloc_coherent()返回的cpu_addr与dma_handle是一对绑定关系。释放时必须原样传回,否则会造成内存泄漏或错误释放。

三、案例场景:什么时候该用一致性映射

一致性DMA映射最适合“控制面”和“小数据、频繁双向交互”的场景。以下是一个网卡驱动分配描述符环的典型片段:

struct tx_desc {
    __le64 addr;
    __le32 len;
    __le32 flags;
};

struct nic_priv {
    struct device *dev;
    struct tx_desc *tx_ring;
    dma_addr_t tx_ring_dma;
    int tx_ring_size;
};

static int nic_alloc_rings(struct nic_priv *priv)
{
    size_t size = priv->tx_ring_size * sizeof(struct tx_desc);

    priv->tx_ring = dma_alloc_coherent(priv->dev, size,
                                       &priv->tx_ring_dma,
                                       GFP_KERNEL);
    if (!priv->tx_ring)
        return -ENOMEM;

    /* 把 DMA 地址写入设备寄存器 */
    writel(priv->tx_ring_dma, priv->base + TX_RING_BASE);
    return 0;
}

static void nic_free_rings(struct nic_priv *priv)
{
    size_t size = priv->tx_ring_size * sizeof(struct tx_desc);

    /* 先停设备,再释放 */
    writel(0, priv->base + TX_RING_BASE);
    dma_free_coherent(priv->dev, size,
                      priv->tx_ring, priv->tx_ring_dma);
}

这里描述符环被CPU反复填写、被设备反复读取,且数据量小、访问频繁。如果改用流式映射,每次更新描述符都要调用dma_sync_single_for_device(),开销反而更大。因此一致性映射是更自然的选择。

对比维度 一致性DMA映射 流式DMA映射
典型API dma_alloc_coherent / dma_free_coherent dma_map_single / dma_unmap_single
缓存一致性 由架构或非缓存属性保证 需驱动显式sync
CPU访问性能 较低(可能非缓存) 较高(可缓存)
适用数据量 小到中等,控制面 大块数据,数据面
生命周期 长期持有,probe到remove 单次传输,map到unmap
典型对象 描述符环、状态寄存器、命令缓冲 网络包、磁盘块、大块采样数据

四、误区与复盘:一致性映射不是“万能缓冲区”

误区一:把大数据缓冲区也用dma_alloc_coherent。在ARM等非一致架构上,这块内存通常是非缓存的,CPU逐字节访问会非常慢。大块数据应使用流式映射,让CPU走缓存、设备走DMA,再用sync控制边界。

误区二:认为一致性内存“绝对不需要任何同步”。一致性只保证缓存层面不冲突,不保证访问顺序。如果CPU写完描述符后要通知设备,仍需使用wmb()或dma_wmb()保证写顺序,避免设备看到半成品描述符。

误区三:在原子上下文用GFP_KERNEL分配。dma_alloc_coherent()的flag参数若使用GFP_KERNEL,可能睡眠;在中断上下文应使用GFP_ATOMIC,但成功率会下降,最好在probe阶段预分配。

复盘建议:设计DMA缓冲区时先问三个问题——数据是控制面还是数据面?访问是长期共享还是单次传输?CPU是否需要频繁读写?若答案偏向“控制面、长期、频繁”,选一致性映射;否则优先流式映射。

总结来说,dma_alloc_coherent()与dma_free_coherent()提供了一种“省心但性能有代价”的DMA内存方案。理解它在不同架构上的实现差异,明确它适合描述符、状态和命令缓冲等控制面对象,才能在实际驱动中做出正确的映射选择。