3. Linux 内核 DMA API 概览

在 PCIe 驱动开发中,DMA(直接内存访问)是设备与主机内存之间进行高速数据传输的核心机制。Linux 内核提供了一套标准化的 DMA API,用于管理设备与 CPU 之间的内存一致性、地址映射以及数据传输。本章将深入剖析三个最核心的 DMA API:dma_alloc_coherent、dma_map_single 和 dma_map_sg,帮助您理解其工作原理、适用场景及常见故障点。

3.1 DMA 地址映射基础概念

在深入 API 之前,必须理解两个关键地址空间:CPU 虚拟地址(VA)和设备总线地址(DMA 地址)。PCIe 设备无法直接访问 CPU 的虚拟地址或物理地址,它只能通过其 BAR 空间或 ATS(地址转换服务)访问总线地址。DMA API 的核心职责就是在这两个地址空间之间建立正确的映射关系。

关键概念: DMA 地址 ≠ 物理地址。IOMMU(如 Intel VT-d、AMD-Vi)或 SWIOTLB 负责将设备可见的总线地址转换为系统物理地址。驱动开发者应始终使用 DMA API 返回的地址,而非直接操作物理地址。
DMA 地址映射架构 CPU 虚拟地址 (VA: 0x7f1234560000) MMU 转换 物理地址 (PA: 0x3f000000) IOMMU / SWIOTLB DMA 总线地址 (DMA: 0x80000000) 映射关系说明: • dma_alloc_coherent:分配物理连续内存,返回 CPU 虚拟地址和 DMA 地址,并保证一致性。 • dma_map_single:将已有的内核虚拟地址映射为 DMA 地址,需处理 cache 一致性。 • dma_map_sg:将分散/聚集列表映射为 DMA 地址,适用于一次传输多个非连续缓冲区。

3.2 dma_alloc_coherent:一致性 DMA 缓冲区

dma_alloc_coherent 用于分配一个物理连续且cache 一致性的内存区域。该 API 会同时返回 CPU 可以访问的虚拟地址和设备可以使用的 DMA 地址。由于硬件保证了 cache 一致性,驱动无需手动执行 cache 刷新操作,适用于描述符环、控制寄存器等对数据一致性要求极高的场景。

函数原型与参数

#include <linux/dma-mapping.h>

void *dma_alloc_coherent(struct device *dev, size_t size,
                         dma_addr_t *dma_handle, gfp_t flag);
参数 说明 常见值
dev 指向 PCIe 设备的 device 结构体指针 &pdev->dev
size 请求分配的字节数(必须为页大小的整数倍) PAGE_SIZE 或 PAGE_SIZE * n
dma_handle 输出参数,返回设备可用的 DMA 总线地址 传递给设备寄存器
flag 内存分配标志(GFP_KERNEL / GFP_ATOMIC) 中断上下文用 GFP_ATOMIC
返回值 CPU 可访问的虚拟地址(失败返回 NULL) 用于 memcpy / memset
最佳实践: 在 probe 函数中预先分配一致性缓冲区,并在 remove 函数中通过 dma_free_coherent 释放。避免在数据路径中频繁分配/释放,以减少内存碎片和延迟。

典型使用场景:描述符环

struct my_dev {
    struct pci_dev *pdev;
    void *desc_ring_cpu;      /* CPU 虚拟地址 */
    dma_addr_t desc_ring_dma; /* DMA 总线地址 */
};

static int my_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct my_dev *dev;
    int ret;

    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;

    dev->pdev = pdev;

    /* 分配 4 页大小的描述符环 */
    dev->desc_ring_cpu = dma_alloc_coherent(&pdev->dev,
                                            4 * PAGE_SIZE,
                                            &dev->desc_ring_dma,
                                            GFP_KERNEL);
    if (!dev->desc_ring_cpu)
        return -ENOMEM;

    /* 将 DMA 地址写入设备 BAR 寄存器 */
    pci_write_config_dword(pdev, PCI_BASE_ADDRESS_0,
                           lower_32_bits(dev->desc_ring_dma));
    pci_write_config_dword(pdev, PCI_BASE_ADDRESS_0 + 4,
                           upper_32_bits(dev->desc_ring_dma));

    pci_set_drvdata(pdev, dev);
    return 0;
}

static void my_remove(struct pci_dev *pdev)
{
    struct my_dev *dev = pci_get_drvdata(pdev);

    if (dev->desc_ring_cpu)
        dma_free_coherent(&pdev->dev, 4 * PAGE_SIZE,
                          dev->desc_ring_cpu, dev->desc_ring_dma);
}
dma_alloc_coherent 分配流程 驱动调用 API 分配物理连续页 建立 IOMMU 映射 返回 VA + DMA 地址 驱动使用缓冲区 注意:一致性缓冲区分配可能较慢(涉及页分配和 IOMMU 页表更新),建议在初始化阶段完成。

3.3 dma_map_single:流式 DMA 映射

dma_map_single 用于将已有的内核缓冲区(通常由 kmalloc、__get_free_pages 或静态数组分配)映射为 DMA 地址。与一致性分配不同,流式映射不保证 cache 一致性,驱动必须在适当的时机调用 dma_sync_single_for_device 和 dma_sync_single_for_cpu 来管理 cache。

函数原型与配对 API

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

void dma_unmap_single(struct device *dev, dma_addr_t dma_addr,
                      size_t size, enum dma_data_direction dir);

void dma_sync_single_for_cpu(struct device *dev, dma_addr_t dma_addr,
                             size_t size, enum dma_data_direction dir);

void dma_sync_single_for_device(struct device *dev, dma_addr_t dma_addr,
                                size_t size, enum dma_data_direction dir);
方向枚举 含义 cache 同步策略
DMA_TO_DEVICE CPU 写入,设备读取(如发送网络包) 映射前 flush cache;设备完成后无需操作
DMA_FROM_DEVICE 设备写入,CPU 读取(如接收网络包) 映射前 invalidate cache;设备完成后 invalidate
DMA_BIDIRECTIONAL 双向数据传输 映射前 flush+invalidate;每次切换方向都需同步
DMA_NONE 调试用途,无实际数据传输 无操作
常见故障: 忘记调用 dma_unmap_single 会导致 IOMMU 页表泄漏,最终耗尽 DMA 地址空间。在中断处理函数中完成数据传输后,务必及时解映射或同步 cache。

典型使用场景:网络数据包收发

/* 发送数据包 */
static int my_tx_packet(struct my_dev *dev, void *data, size_t len)
{
    dma_addr_t dma_addr;

    /* 将数据缓冲区映射为 DMA 地址 */
    dma_addr = dma_map_single(&dev->pdev->dev, data, len,
                              DMA_TO_DEVICE);
    if (dma_mapping_error(&dev->pdev->dev, dma_addr))
        return -ENOMEM;

    /* 将 DMA 地址写入设备发送描述符 */
    writel(lower_32_bits(dma_addr), dev->tx_reg + TX_DESC_ADDR_LO);
    writel(upper_32_bits(dma_addr), dev->tx_reg + TX_DESC_ADDR_HI);
    writel(len, dev->tx_reg + TX_DESC_LEN);
    writel(1, dev->tx_reg + TX_DESC_GO);  /* 触发发送 */

    return 0;
}

/* 发送完成中断处理 */
static irqreturn_t my_tx_done_irq(int irq, void *data)
{
    struct my_dev *dev = data;
    dma_addr_t dma_addr;
    size_t len;

    /* 从完成队列获取 DMA 地址和长度 */
    dma_addr = dev->tx_complete_dma;
    len = dev->tx_complete_len;

    /* 解映射,释放 DMA 地址空间 */
    dma_unmap_single(&dev->pdev->dev, dma_addr, len, DMA_TO_DEVICE);

    /* 通知上层协议栈数据已发送完成 */
    if (dev->tx_complete_cb)
        dev->tx_complete_cb(dev->tx_complete_ctx);

    return IRQ_HANDLED;
}

3.4 dma_map_sg:分散/聚集 DMA 映射

对于高性能设备(如 NVMe SSD、万兆网卡),一次 DMA 传输可能涉及多个非连续的物理内存片段。dma_map_sg 可以将一个 struct scatterlist 数组映射为设备可用的 DMA 地址列表,并返回合并后的有效片段数。

函数原型与 scatterlist 操作

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

void dma_unmap_sg(struct device *dev, struct scatterlist *sg,
                  int nents, enum dma_data_direction dir);

/* scatterlist 遍历宏 */
struct scatterlist *sg;
int i;

for_each_sg(sglist, sg, nents, i) {
    dma_addr_t dma_addr = sg_dma_address(sg);
    unsigned int len = sg_dma_len(sg);
    /* 将 dma_addr 和 len 写入设备描述符 */
}
参数 说明
sg 指向 scatterlist 数组的指针
nents 输入时的 scatterlist 条目数
返回值 映射后的有效 DMA 片段数(可能小于 nents,因为 IOMMU 可能合并相邻页)
性能优势: 使用 dma_map_sg 而非多次调用 dma_map_single 可以减少 IOMMU 页表操作次数,并允许硬件通过 scatter-gather 列表执行一次 DMA 传输多个片段,显著降低中断和 CPU 开销。

典型使用场景:NVMe PRP 列表

static int my_nvme_submit(struct my_dev *dev, struct scatterlist *sglist,
                          int nents, u64 slba, u16 nlb)
{
    struct scatterlist *sg;
    int i, mapped_nents;

    /* 映射 scatterlist */
    mapped_nents = dma_map_sg(&dev->pdev->dev, sglist, nents,
                              DMA_FROM_DEVICE);
    if (mapped_nents == 0)
        return -ENOMEM;

    /* 构建 PRP(物理区域页)列表 */
    i = 0;
    for_each_sg(sglist, sg, mapped_nents, i) {
        dma_addr_t dma_addr = sg_dma_address(sg);
        u32 len = sg_dma_len(sg);

        if (i == 0) {
            /* 第一个 PRP 写入命令的 PRP1 字段 */
            dev->cmd.prp1 = cpu_to_le64(dma_addr);
        } else if (i == 1) {
            /* 第二个 PRP 写入命令的 PRP2 字段 */
            dev->cmd.prp2 = cpu_to_le64(dma_addr);
        } else {
            /* 更多 PRP 写入 PRP 列表页 */
            dev->prp_list[i - 2] = cpu_to_le64(dma_addr);
        }
    }

    /* 提交命令到 SQ(提交队列) */
    return my_nvme_submit_cmd(dev, &dev->cmd);
}

/* 命令完成后的解映射 */
static void my_nvme_complete(struct my_dev *dev, struct scatterlist *sglist,
                             int nents)
{
    dma_unmap_sg(&dev->pdev->dev, sglist, nents, DMA_FROM_DEVICE);
}
三种 DMA API 对比 dma_alloc_coherent dma_map_single dma_map_sg 内存来源 内核分配新内存 已有内核缓冲区 scatterlist 数组 Cache 一致性 硬件保证 需手动同步 需手动同步 物理连续性 连续 取决于来源 非连续 适用场景 描述符环、控制结构 单缓冲区传输 大数据块、SG 传输 分配/映射开销 较高(页分配) 较低 中等(批量映射) 选择建议:控制路径用 dma_alloc_coherent;数据路径单缓冲区用 dma_map_single;大数据块或高性能场景用 dma_map_sg。 所有映射必须配对解映射,否则会导致 IOMMU 资源泄漏和设备访问错误。

3.5 常见故障模式与排查要点

  1. DMA 地址未对齐: 某些 PCIe 设备要求 DMA 缓冲区对齐到特定边界(如 64 字节、页大小)。使用 dma_alloc_coherent 时默认页对齐;使用 dma_map_single 时需确保原始缓冲区对齐。
  2. 忘记检查 dma_mapping_error: dma_map_single 和 dma_map_sg 可能因 IOMMU 地址空间耗尽而失败。必须检查返回值,否则设备将使用无效地址。
  3. 方向错误导致数据损坏: 使用 DMA_FROM_DEVICE 时,如果 CPU 在设备写入前读取 cache 中的旧数据,会导致数据不一致。务必在设备写入后调用 dma_sync_single_for_cpu。
  4. 中断上下文中使用 GFP_KERNEL: 在中断处理函数或持有自旋锁时,必须使用 GFP_ATOMIC,否则会导致内核调度错误。
  5. 64 位 DMA 地址截断: 在 32 位系统或未启用 64 位 DMA 支持的设备上,高 32 位地址会被忽略。使用 dma_set_mask_and_coherent 设置正确的 DMA 掩码。
调试技巧: 在 /sys/kernel/debug/dma-api/ 下可以查看 DMA API 的使用统计,包括分配次数、泄漏检测和错误计数。启用 CONFIG_DMA_API_DEBUG 内核选项可以在运行时检测常见的 DMA API 误用。