4. DMA基础原理

在 PCIe 设备驱动中,CPU 直接搬运数据会带来大量内存读写和中断开销。DMA(Direct Memory Access,直接内存访问)让设备在不经过 CPU 逐字节搬运的情况下,直接读写系统内存,从而把 CPU 从数据搬运中解放出来。理解 DMA 控制器、地址映射、一致性内存、流式 DMA、DMA 方向与传输描述符,是后续做 PCIe DMA 传输优化的基础。

一、DMA 控制器与 PCIe 设备的关系

DMA 控制器是设备内部负责发起内存读写的硬件模块。在 PCIe 场景中,它通常集成在网卡、NVMe 控制器、FPGA 加速卡或 GPU 内部。CPU 通过配置寄存器告诉 DMA 控制器“从哪里读、写到哪里、传多少、完成后如何通知”,DMA 控制器随后以 Bus Master 身份向 PCIe 根复合体发起 Memory Read / Memory Write TLP 请求。

因此,DMA 传输的发起方是设备,而不是 CPU。CPU 只负责准备缓冲区和描述符,并在传输完成后处理中断或轮询状态。PCIe 配置空间中的 Bus Master Enable 位必须置位,设备才能发起 DMA;否则设备只能响应 CPU 的访问,不能主动读写内存。

二、地址映射:CPU 地址、总线地址与设备地址

驱动代码中拿到的指针是 CPU 虚拟地址,而 DMA 控制器看到的是总线地址(也称 DMA 地址)。两者之间需要经过 IOMMU 或 DMA 映射层转换。Linux 提供两类映射接口:一致性映射(dma_alloc_coherent)和流式映射(dma_map_single / dma_map_sg)。

如果系统没有 IOMMU,总线地址通常等于物理地址;如果启用 IOMMU,总线地址是 I/O 虚拟地址,由 IOMMU 页表映射到物理页。驱动绝不能把 CPU 虚拟地址直接写入 DMA 寄存器,否则设备会访问错误的内存区域,轻则数据错乱,重则触发 IOMMU 缺页或系统崩溃。

核心原则:CPU 用虚拟地址访问内存,设备用 DMA 地址访问内存,二者必须通过 DMA API 建立映射关系。

三、一致性内存与流式 DMA

一致性内存由 dma_alloc_coherent 分配,保证 CPU 与设备看到的数据一致,不需要显式刷新缓存。它适合长期存在、频繁访问的控制结构,例如描述符环、状态寄存器和命令队列。缺点是分配成本较高,且可能占用连续内存。

流式 DMA 由 dma_map_single 或 dma_map_sg 建立临时映射,适合一次性或批量数据传输。使用流式 DMA 时必须遵守方向语义:在设备读取前执行 dma_sync_single_for_device,在 CPU 读取前执行 dma_sync_single_for_cpu。否则 CPU 缓存中的旧数据可能覆盖设备写入的新数据,造成“数据看起来没更新”的典型问题。

对比项 一致性 DMA 流式 DMA
典型接口 dma_alloc_coherent dma_map_single / dma_map_sg
缓存一致性 硬件或架构保证一致 需显式 sync 操作
适用对象 描述符环、控制块、状态区 数据缓冲区、SKB、大块传输
生命周期 长期持有,驱动卸载时释放 单次传输,映射后及时 unmap
性能关注点 分配开销、内存占用 映射/解映射开销、cache 刷新

四、DMA 方向与传输描述符

DMA 方向决定数据流动和缓存维护策略。Linux 定义四种方向:DMA_TO_DEVICE 表示内存到设备,DMA_FROM_DEVICE 表示设备到内存,DMA_BIDIRECTIONAL 表示双向,DMA_NONE 用于调试。方向写错会导致缓存刷新方向错误,是 DMA 数据异常的常见根因。

传输描述符是 DMA 控制器执行任务的“指令包”。一个描述符通常包含源地址、目的地址、传输长度、控制标志和下一个描述符指针。驱动把多个描述符组织成环(Descriptor Ring)或链(Descriptor Chain),设备通过 Head/Tail 指针判断哪些描述符可执行、哪些已完成。

struct dma_desc {
    u64 src_addr;      /* DMA 地址,不是 CPU 虚拟地址 */
    u64 dst_addr;      /* DMA 地址 */
    u32 len;           /* 传输字节数 */
    u32 flags;         /* 中断使能、链结束、方向等 */
    u64 next_desc;     /* 下一个描述符的 DMA 地址 */
};

/* 典型流式映射流程 */
dma_addr_t dma = dma_map_single(dev, buf, len, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma)) {
    return -ENOMEM;
}
writel(dma, dev->regs + DMA_SRC);
writel(len, dev->regs + DMA_LEN);
writel(DMA_START, dev->regs + DMA_CTRL);
/* 传输完成后 */
dma_unmap_single(dev, dma, len, DMA_TO_DEVICE);

五、典型场景:PCIe 网卡收包

以 PCIe 网卡收包为例:驱动启动时用 dma_alloc_coherent 分配接收描述符环,并用 dma_map_single 为每个接收缓冲区建立 DMA_FROM_DEVICE 映射。网卡收到报文后,DMA 控制器把数据写入缓冲区,更新描述符状态位,并产生中断。驱动在中断处理中读取描述符,执行 dma_sync_single_for_cpu,再把数据交给协议栈,最后重新映射缓冲区并补充到环中。

这个流程中,描述符环用一致性内存,数据缓冲区用流式 DMA,方向为 DMA_FROM_DEVICE。若驱动忘记 sync 或方向写成 DMA_TO_DEVICE,就会出现“网卡显示收包但协议栈看不到数据”或“数据偶发错乱”的问题。

六、常见误区与复盘

  • 把虚拟地址写入 DMA 寄存器:设备无法识别 CPU 虚拟地址,必须使用 dma_map_* 返回的 DMA 地址。
  • 流式 DMA 忘记 sync:CPU 与设备缓存视图不一致,导致数据旧值覆盖新值。
  • 方向写反:DMA_TO_DEVICE 与 DMA_FROM_DEVICE 混用,缓存刷新方向错误。
  • 映射后未检查 dma_mapping_error:IOMMU 映射失败时继续使用无效地址,可能触发设备异常。
  • 描述符环未按 cacheline 对齐:CPU 与设备同时修改同一 cacheline,产生伪共享和状态竞争。
调试 DMA 问题时,优先确认三件事:DMA 地址是否来自 DMA API、方向是否正确、sync 调用是否覆盖了 CPU 与设备交替访问的边界。
PCIe DMA 传输核心结构:地址映射、方向与描述符 CPU / 驱动 虚拟地址 buf dma_map_single() dma_sync_*() 中断 / 轮询完成 DMA 映射 / IOMMU CPU 虚拟地址 → DMA 地址 一致性映射:长期有效 流式映射:单次有效 方向:TO / FROM / BIDIR PCIe 设备 / DMA 控制器 Bus Master 发起 TLP Memory Read / Write 读取描述符环 写回完成状态 传输描述符环(Descriptor Ring) Desc 0 src/dst/len Desc 1 flags/next Desc 2 OWN 位 Desc 3 EOP/中断 环形回绕:Head 追 Tail,设备与驱动交替更新 1. 准备缓冲区 dma_alloc_coherent / map 2. 填写描述符 DMA 地址 + 长度 + 方向 3. 启动并同步 doorbell + sync + 中断 方向: DMA_TO_DEVICE(内存→设备) DMA_FROM_DEVICE(设备→内存) DMA_BIDIRECTIONAL(双向)

掌握以上结构后,后续优化 PCIe DMA 传输时,就可以从描述符环深度、映射粒度、sync 次数、方向选择和中断合并等角度逐项分析,而不是只盯着“带宽数字”本身。