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映射的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);
典型使用流程分为四步:
- 分配:在设备probe或初始化阶段调用
dma_alloc_coherent(),得到CPU侧虚拟地址cpu_addr和DMA侧总线地址dma_handle。注意dma_handle是设备视角的地址,不能直接给CPU解引用。 - 告知设备:把
dma_handle写入设备寄存器,作为描述符基址或数据缓冲区地址。 - 双向访问:CPU通过
cpu_addr读写,设备通过dma_handle读写,双方看到的内容始终一致,无需同步调用。 - 释放:在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内存方案。理解它在不同架构上的实现差异,明确它适合描述符、状态和命令缓冲等控制面对象,才能在实际驱动中做出正确的映射选择。