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 传输,典型场景包括:

  1. 网络数据包发送/接收: SKB(Socket Buffer)中的数据缓冲区,每次发送前映射,发送完成后立即解映射。
  2. 块设备 I/O 请求: 来自块层 struct bio 的临时缓冲区,在 I/O 请求生命周期内映射。
  3. USB 控制传输: USB 控制端点使用的临时设置包或数据缓冲区。
  4. 音频数据流: 周期性音频缓冲区,每次填充后映射给 DMA 控制器播放。
  5. 传感器数据采集: 单次触发采集的缓冲区,采集完成后立即解映射。
最佳实践: 对于频繁、小尺寸的传输(如网络收发包),建议使用 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 图

dma_map_single 单次映射完整生命周期 1. 分配缓冲区 2. dma_map_single() 3. 检查返回值 4. 启动 DMA 传输 5. 等待传输完成 6. dma_unmap_single() 错误处理:释放/重试 关键说明: • 步骤 2 和 6 必须严格配对,size 和 direction 参数必须一致。 • 步骤 3 的返回值检查不可省略,尤其在 IOMMU 使能或内存紧张时。 • 步骤 5 完成后才能调用 dma_unmap_single,否则设备可能仍在访问内存。 • 错误分支(虚线)在 dma_mapping_error 返回 true 时执行。

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 框架的异步映射接口。