4. DMA API详解

在块设备驱动中,DMA(Direct Memory Access)是提升数据传输吞吐量的核心技术。Linux 内核提供了一套标准化的 DMA API,用于管理设备与内存之间的直接数据传输。本章将深入解析 dma_alloc_coherent、dma_map_single、dma_map_sg、dma_unmap_sg 等核心 API 的使用方法、适用场景以及内存屏障在 DMA 传输中的关键作用。

核心概念: DMA API 的核心目标是解决“设备可见地址”与“CPU 虚拟地址”之间的映射问题,并保证数据传输过程中的缓存一致性(Cache Coherency)。

4.1 DMA 地址与流式映射模型

理解 DMA API 之前,必须先区分两种基本的映射模型:一致性映射(Coherent Mapping) 和 流式映射(Streaming Mapping)。前者适用于长时间、频繁访问的缓冲区,后者适用于短时、一次性的数据传输。

特性 一致性映射(Coherent) 流式映射(Streaming)
典型 API dma_alloc_coherent dma_map_single / dma_map_sg
缓存一致性 硬件自动维护(或禁用缓存) 软件通过 sync 操作维护
分配开销 较高(可能分配大块连续内存) 较低(映射已有缓冲区)
适用场景 DMA 描述符环、控制结构 网络数据包、块设备 I/O 请求
生命周期 驱动加载到卸载 单次 I/O 操作
选择指南: 对于块设备驱动的 I/O 请求缓冲区,优先使用流式映射(dma_map_sg),因为它能高效处理分散/聚集(SG)列表,且不浪费宝贵的连续物理内存。

4.2 dma_alloc_coherent:一致性内存分配

dma_alloc_coherent 用于分配一段在 CPU 和设备视角下均保持缓存一致性的内存区域。该函数返回 CPU 虚拟地址,并通过第三个参数返回 DMA 总线地址。

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

// 示例:分配 64KB 一致性缓冲区
struct device *dev = &pdev->dev;
dma_addr_t dma_handle;
void *cpu_addr;

cpu_addr = dma_alloc_coherent(dev, 64 * 1024, &dma_handle, GFP_KERNEL);
if (!cpu_addr) {
    dev_err(dev, "Failed to allocate coherent DMA memory\n");
    return -ENOMEM;
}
// 使用 cpu_addr 和 dma_handle 初始化 DMA 描述符环
注意: 一致性内存通常来自预留的 DMA 区域或特殊内存池,分配大块内存时容易失败。建议仅在描述符环、状态寄存器等关键控制结构中使用。
一致性映射内存视图 CPU 地址空间 虚拟地址:0xFFFF8000_1000 缓存一致性区域 (无 cache 或硬件同步) 设备 DMA 地址空间 DMA 地址:0x8000_0000 直接访问物理内存 (无地址转换) MMU 映射 直接访问

4.3 dma_map_single:单缓冲区流式映射

当驱动需要将一块已有的内核缓冲区(如 kmalloc 或栈上分配的缓冲区)用于 DMA 传输时,使用 dma_map_single。该函数返回设备可用的 DMA 地址,并负责处理必要的缓存刷新操作。

// 原型
dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
                          size_t size, enum dma_data_direction dir);

// 示例:映射一个 4KB 的读缓冲区
void *buf = kmalloc(4096, GFP_KERNEL);
dma_addr_t dma_addr;

dma_addr = dma_map_single(dev, buf, 4096, DMA_FROM_DEVICE);
if (dma_mapping_error(dev, dma_addr)) {
    dev_err(dev, "dma_map_single failed\n");
    kfree(buf);
    return -EIO;
}

// 启动 DMA 读操作:设备从 dma_addr 写入数据
// ... 等待 DMA 完成 ...

// 同步数据给 CPU(关键步骤)
dma_sync_single_for_cpu(dev, dma_addr, 4096, DMA_FROM_DEVICE);

// 现在可以安全读取 buf 中的数据
process_data(buf);

// 传输结束后解除映射
dma_unmap_single(dev, dma_addr, 4096, DMA_FROM_DEVICE);
kfree(buf);
方向枚举:
  • DMA_TO_DEVICE:CPU 写入,设备读取(如写磁盘请求)。
  • DMA_FROM_DEVICE:设备写入,CPU 读取(如读磁盘完成)。
  • DMA_BIDIRECTIONAL:双向传输,开销最大,谨慎使用。

4.4 dma_map_sg 与 dma_unmap_sg:分散/聚集映射

块设备驱动中,一个 I/O 请求通常对应多个不连续的物理内存片段(bio_vec)。dma_map_sg 是处理这种分散/聚集(SG)列表的标准接口。它不仅能映射多个片段,还能合并物理上连续的片段,减少 DMA 描述符数量。

// 原型
int dma_map_sg(struct device *dev, struct scatterlist *sg,
               int nents, enum dma_data_direction dir);

// 示例:映射块设备请求的 SG 列表
struct scatterlist *sglist;
int nents, i, mapped;

// 假设 sglist 已由块层填充,nents 为原始片段数
mapped = dma_map_sg(dev, sglist, nents, DMA_TO_DEVICE);
if (mapped == 0) {
    dev_err(dev, "dma_map_sg failed\n");
    return -ENOMEM;
}

// 遍历映射后的 SG 列表(注意使用 sg_dma_address 和 sg_dma_len)
for_each_sg(sglist, sg, mapped, i) {
    dma_addr_t dma_addr = sg_dma_address(sg);
    unsigned int dma_len = sg_dma_len(sg);
    // 将 dma_addr 和 dma_len 填入硬件 DMA 描述符
    hw_desc[i].addr = cpu_to_le64(dma_addr);
    hw_desc[i].len  = cpu_to_le32(dma_len);
}

// 启动 DMA 传输...

// 传输完成后解除映射
dma_unmap_sg(dev, sglist, nents, DMA_TO_DEVICE);
API 输入参数 返回值 关键点
dma_map_sg 原始 SG 列表、片段数、方向 映射后的有效片段数(可能小于 nents) 使用 sg_dma_address() 获取 DMA 地址
dma_unmap_sg 原始 SG 列表、原始 nents、方向 void 必须传入原始 nents,而非映射后的数量
dma_sync_sg_for_cpu SG 列表、nents、方向 void DMA_FROM_DEVICE 后调用,使 CPU 可见最新数据
dma_sync_sg_for_device SG 列表、nents、方向 void DMA_TO_DEVICE 前调用,刷新 CPU 缓存到内存
dma_map_sg 映射流程 原始 SG 列表 片段 0: 4KB @ 0x1000 片段 1: 8KB @ 0x5000 片段 2: 2KB @ 0xA000 片段 3: 6KB @ 0xC000 dma_map_sg 映射后 DMA 列表 描述符 0: 12KB @ 0x8000 描述符 1: 8KB @ 0xA000 (合并后仅 2 个描述符) 硬件 DMA 引擎 读取描述符 0 传输 12KB 连续数据 读取描述符 1 传输 8KB 连续数据 硬件读取

4.5 内存屏障在 DMA 传输中的作用

内存屏障(Memory Barrier)用于保证 CPU 和 DMA 控制器对内存访问的顺序一致性。在以下场景中必须使用屏障:

  • 描述符写入后: 在将 DMA 描述符写入内存并通知硬件开始传输之前,必须确保描述符内容对设备可见。
  • 状态读取前: 在读取设备更新的状态字段(如完成标志)之前,必须确保 CPU 看到的是最新写入。
  • 缓存同步后: 在调用 dma_sync_single_for_cpu 之后,通常需要一个读屏障来保证数据可见性。
// 典型 DMA 启动序列(含屏障)
// 1. 填充描述符到一致性内存
desc->addr = cpu_to_le64(dma_addr);
desc->len  = cpu_to_le32(len);
desc->flags = cpu_to_le32(DESC_VALID);

// 2. 写屏障:确保描述符写入完成后再通知硬件
wmb();  // 写内存屏障

// 3. 通知硬件(写设备寄存器)
writel(1, host->regs + DOORBELL);

// --- 中断处理中 ---
// 4. 读屏障:确保读取状态前,设备已完成写入
rmb();  // 读内存屏障

// 5. 检查完成状态
if (desc->flags & cpu_to_le32(DESC_DONE)) {
    // 处理完成的数据
}
常见错误: 遗漏 wmb() 可能导致硬件读到过时的描述符内容;遗漏 rmb() 可能导致 CPU 看到陈旧的完成标志。在 ARM/ARM64 架构上,dma_alloc_coherent 返回的内存通常不需要额外屏障,但流式映射区域必须谨慎处理。

4.6 综合案例:块设备请求的 DMA 传输流程

以下代码片段展示了在块设备驱动中,如何将 dma_map_sg 与内存屏障结合,完成一次完整的读请求处理:

static int myblock_submit_bio(struct bio *bio)
{
    struct myblock_dev *dev = bio->bi_disk->private_data;
    struct scatterlist sglist[BIO_MAX_PAGES];
    struct bio_vec bvec;
    struct bvec_iter iter;
    int i = 0, mapped;

    // 1. 将 bio 的片段填入 SG 列表
    bio_for_each_segment(bvec, bio, iter) {
        sg_set_page(&sglist[i], bvec.bv_page,
                    bvec.bv_len, bvec.bv_offset);
        i++;
    }

    // 2. 映射 SG 列表到 DMA 地址
    mapped = dma_map_sg(dev->dma_dev, sglist, i,
                        bio_data_dir(bio) == WRITE ?
                        DMA_TO_DEVICE : DMA_FROM_DEVICE);
    if (mapped == 0) {
        bio_io_error(bio);
        return BLK_STS_IOERR;
    }

    // 3. 填充硬件描述符(使用 sg_dma_address / sg_dma_len)
    for_each_sg(sglist, sg, mapped, i) {
        dev->hw_desc[i].addr = cpu_to_le64(sg_dma_address(sg));
        dev->hw_desc[i].len  = cpu_to_le32(sg_dma_len(sg));
    }

    // 4. 写屏障 + 通知硬件
    wmb();
    writel(mapped, dev->regs + DMA_START);

    // 5. 等待中断(简化处理)
    wait_for_completion(&dev->dma_done);

    // 6. 如果是读操作,同步数据给 CPU
    if (bio_data_dir(bio) == READ) {
        dma_sync_sg_for_cpu(dev->dma_dev, sglist, i, DMA_FROM_DEVICE);
        rmb();  // 确保同步后的数据可见
    }

    // 7. 解除映射并结束 I/O
    dma_unmap_sg(dev->dma_dev, sglist, i,
                 bio_data_dir(bio) == WRITE ?
                 DMA_TO_DEVICE : DMA_FROM_DEVICE);
    bio_endio(bio);
    return BLK_STS_OK;
}
性能要点:
  • 尽量在 I/O 提交路径中批量映射 SG 列表,避免频繁调用 dma_map_single。
  • 对于高性能 NVMe 或 SSD 控制器,考虑使用 dma_map_sg_attrs 传递 DMA_ATTR_SKIP_CPU_SYNC 属性,由驱动手动控制同步时机。
  • 内存屏障的开销在大多数架构上很小,但过度使用(如每个描述符都加屏障)会降低性能。通常只在描述符链首尾和状态检查点使用。
块设备 DMA 传输完整流程 ① 填充 SG 列表 ② dma_map_sg ③ 填充描述符 ④ wmb() + 通知硬件 ⑤ 等待 DMA 完成 ⑥ dma_sync_sg_for_cpu ⑦ rmb() + 读取数据 ⑧ dma_unmap_sg 硬件 DMA 传输

4.7 常见陷阱与调试建议

陷阱 现象 解决方案
遗漏 dma_sync_* 调用 读数据时看到旧值或全零 在 DMA_FROM_DEVICE 后必须调用 dma_sync_sg_for_cpu
方向参数错误 数据损坏或 DMA 超时 严格区分 DMA_TO_DEVICE 和 DMA_FROM_DEVICE
使用 dma_unmap_sg 时传入映射后的数量 内存泄漏或 DMA 错误 始终传入原始 nents,而非 mapped
在中断上下文中调用 dma_alloc_coherent 调度时原子违规 使用 GFP_ATOMIC 标志,或预分配缓冲区
忽略 dma_mapping_error 检查 使用无效 DMA 地址导致硬件异常 每次 dma_map_* 后必须检查返回值
调试工具: 启用内核配置 CONFIG_DMA_API_DEBUG 和 CONFIG_DMA_API_DEBUG_SG,可以在运行时检测常见的 DMA API 误用,如重复映射、方向错误、未同步等。