4. dma_map_single API详解:函数原型、参数含义、返回值的处理、单次映射的使用场景
dma_map_single 是 Linux 内核流式 DMA 映射中最核心、最常用的 API 之一。它负责将内核虚拟地址(通常是 kmalloc 或 __get_free_pages 分配的缓冲区)映射为 DMA 总线地址,并完成必要的缓存一致性操作。理解该 API 的细节是编写正确、高效 DMA 驱动程序的基础。
4.1 函数原型与头文件
该 API 定义在 <linux/dma-mapping.h> 中,其函数原型如下:
dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
size_t size, enum dma_data_direction direction);
该函数返回一个 dma_addr_t 类型的 DMA 总线地址,供 DMA 控制器使用。若映射失败,返回的地址需要通过 dma_mapping_error 进行校验。
4.2 参数含义详解
| 参数 | 类型 | 含义与约束 |
|---|---|---|
dev |
struct device * |
指向进行 DMA 操作的设备结构体。用于关联 IOMMU/IOMMU 域、获取设备 DMA 掩码和缓存一致性属性。不可为 NULL。 |
cpu_addr |
void * |
内核虚拟地址,指向待映射的缓冲区。必须由 kmalloc、__get_free_pages 或 dma_alloc_coherent(非一致性映射时)返回。不能是栈地址或 vmalloc 地址。 |
size |
size_t |
映射的字节长度。必须大于 0,且不能超过设备 DMA 掩码支持的寻址范围。 |
direction |
enum dma_data_direction |
数据传输方向,决定缓存刷新/失效策略。可选值见下方表格。 |
4.2.1 direction 参数枚举值
| 枚举值 | 数值 | 含义 | 缓存操作 |
|---|---|---|---|
DMA_BIDIRECTIONAL |
0 | 双向传输(设备可读可写) | 映射时刷新缓存(写回+失效),同步时双向操作 |
DMA_TO_DEVICE |
1 | 数据从 CPU 到设备(如发送) | 映射时刷新缓存(写回),确保设备看到最新数据 |
DMA_FROM_DEVICE |
2 | 数据从设备到 CPU(如接收) | 映射时失效缓存,同步时失效缓存,防止 CPU 读到脏数据 |
DMA_NONE |
3 | 调试/未初始化状态 | 无操作,用于调试检测 |
方向选择错误的风险: 若将
DMA_FROM_DEVICE 误用为 DMA_TO_DEVICE,设备写入数据后 CPU 可能读到缓存中的旧数据(缓存一致性问题),导致数据损坏。反之,若将 DMA_TO_DEVICE 误用为 DMA_FROM_DEVICE,CPU 写入的数据可能被缓存失效操作丢弃,设备收到错误数据。
4.3 返回值的处理与错误检查
dma_map_single 返回 DMA 总线地址。但该地址可能无效,必须使用 dma_mapping_error 进行校验:
dma_addr_t dma_handle;
dma_handle = dma_map_single(dev, buffer, size, direction);
if (dma_mapping_error(dev, dma_handle)) {
dev_err(dev, "dma_map_single failed\n");
/* 错误处理:释放资源或返回 */
goto err_out;
}
错误检查的要点:
- 必须检查: 在 IOMMU 使能、内存紧张或设备寻址受限时,映射可能失败。
- 检查时机: 在
dma_map_single返回后立即检查,不可延迟。 - 错误处理: 映射失败时,缓冲区内容未被修改,可直接释放或重试。
- 返回值范围: 成功时返回的地址在设备 DMA 掩码范围内,且对齐到缓存行大小(通常 32 或 64 字节)。
4.4 单次映射的使用场景
dma_map_single 适用于“一次性”或“短生命周期”的 DMA 传输,典型场景包括:
- 网络数据包发送/接收: SKB(Socket Buffer)中的数据缓冲区,每次发送前映射,发送完成后立即解映射。
- 块设备 I/O 请求: 来自块层
struct bio的临时缓冲区,在 I/O 请求生命周期内映射。 - USB 控制传输: USB 控制端点使用的临时设置包或数据缓冲区。
- 音频数据流: 周期性音频缓冲区,每次填充后映射给 DMA 控制器播放。
- 传感器数据采集: 单次触发采集的缓冲区,采集完成后立即解映射。
最佳实践: 对于频繁、小尺寸的传输(如网络收发包),建议使用
dma_map_single + dma_unmap_single 配对,避免长期占用 DMA 地址空间。对于大块、长期使用的缓冲区(如帧缓冲),考虑使用 dma_alloc_coherent 分配一致性内存。
4.5 映射生命周期与配对规则
每个 dma_map_single 调用必须与一个 dma_unmap_single 调用严格配对:
/* 映射 */
dma_addr_t dma_handle = dma_map_single(dev, cpu_addr, size, direction);
/* ... DMA 传输 ... */
/* 解映射 */
dma_unmap_single(dev, dma_handle, size, direction);
配对规则:
- 参数一致:
dma_unmap_single的size和direction必须与映射时完全相同。 - 及时解映射: 在 DMA 传输完成后(通过完成回调或轮询确认)立即解映射,避免泄漏 DMA 映射资源。
- 禁止重复解映射: 对同一 DMA 地址重复调用
dma_unmap_single会导致内核崩溃或数据损坏。
4.6 单次映射流程 SVG 图
4.7 典型代码示例
以下是一个完整的网络驱动发送路径中使用 dma_map_single 的示例:
static int netdev_start_xmit(struct sk_buff *skb, struct net_device *ndev)
{
struct my_priv *priv = netdev_priv(ndev);
struct device *dev = priv->device;
dma_addr_t dma_handle;
int ret;
/* 1. 映射 SKB 数据缓冲区 */
dma_handle = dma_map_single(dev, skb->data, skb->len, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma_handle)) {
dev_err(dev, "Failed to map TX buffer\n");
dev_kfree_skb(skb);
return NETDEV_TX_OK;
}
/* 2. 将 DMA 地址写入描述符,通知 DMA 引擎 */
priv->tx_desc->addr = cpu_to_le32(dma_handle);
priv->tx_desc->len = cpu_to_le16(skb->len);
wmb(); /* 确保描述符写入完成后再通知硬件 */
writel(1, priv->regs + TX_START);
/* 3. 在完成回调中解映射 */
/* 注意:实际驱动中通常将 dma_handle 和 skb 存入完成队列 */
priv->tx_skb[priv->tx_idx] = skb;
priv->tx_dma[priv->tx_idx] = dma_handle;
priv->tx_idx = (priv->tx_idx + 1) % TX_RING_SIZE;
return NETDEV_TX_OK;
}
/* 中断处理中的完成回调 */
static void tx_complete(struct my_priv *priv, int idx)
{
struct device *dev = priv->device;
struct sk_buff *skb = priv->tx_skb[idx];
dma_addr_t dma_handle = priv->tx_dma[idx];
/* 4. 解映射 */
dma_unmap_single(dev, dma_handle, skb->len, DMA_TO_DEVICE);
/* 5. 释放 SKB */
dev_kfree_skb(skb);
priv->tx_skb[idx] = NULL;
}
性能提示: 在 SMP 系统中,
dma_map_single 可能涉及 TLB 刷新和缓存维护操作,开销较大。对于高吞吐场景,可考虑使用 dma_map_page 映射物理页,或使用 dmaengine 框架的异步映射接口。