2. DMA 基础原理:控制器架构、传输类型与描述符链

DMA(Direct Memory Access,直接存储器访问)是 Linux 内核性能优化的核心硬件机制之一。它允许外设或内存块之间直接交换数据,无需 CPU 逐字节搬运,从而将 CPU 从繁重的数据拷贝任务中解放出来。理解 DMA 的控制器架构、传输类型以及描述符链的工作方式,是后续分析缓存一致性问题和优化 DMA 性能的基石。

2.1 DMA 控制器架构

DMA 控制器(DMAC)是系统总线上的一个独立主控设备,能够发起总线事务。典型的 DMA 控制器内部包含以下关键组件:

组件 功能描述 性能影响
控制寄存器组 存储传输方向、数据宽度、突发长度、传输使能等配置 寄存器访问延迟直接影响启动速度
源地址寄存器 保存数据源地址(内存地址或外设 FIFO 地址) 地址对齐影响传输效率
目的地址寄存器 保存数据目标地址 地址递增模式决定数据布局
传输计数寄存器 记录剩余待传输的数据量(字节/字/块) 计数归零触发中断或链式跳转
状态与中断逻辑 检测传输完成、错误、超时等事件并生成中断 中断频率影响 CPU 负载
描述符获取引擎 从内存中自动抓取下一个描述符(链式 DMA) 描述符预取深度影响吞吐
架构要点:现代高性能 DMA 控制器(如 ARM 的 PL330、Intel 的 I/OAT)通常采用多通道设计,每个通道独立处理一条传输流,支持硬件描述符队列和自动链式跳转,从而在无需 CPU 干预的情况下完成复杂的数据搬运任务。
DMA 控制器内部架构 系统总线接口 内部数据/地址总线 控制寄存器组 方向 / 宽度 / 突发 使能 / 优先级 源地址寄存器 内存 / 外设 FIFO 递增 / 固定 目的地址寄存器 内存 / 外设 FIFO 递增 / 固定 传输计数寄存器 剩余字节数 状态与中断逻辑 完成 / 错误 / 超时 描述符获取引擎 预取 / 链式跳转

2.2 DMA 传输类型

根据数据源和目标的不同,DMA 传输分为三种基本类型。每种类型在总线占用、缓存一致性处理和性能特征上存在显著差异。

2.2.1 内存到内存(Memory-to-Memory)

  • 场景:数据拷贝、缓冲区移动、内存整理、零拷贝网络栈中的页拷贝。
  • 特点:源和目标均为系统内存,不需要外设参与。通常用于加速 memcpy() 或大块数据搬移。
  • 性能瓶颈:完全受限于内存带宽和总线竞争。CPU 缓存可能被冲刷,导致缓存污染。
  • Linux 实现:通过 dmaengine 框架的 DMA_MEMCPY 或 DMA_XOR 操作。

2.2.2 外设到内存(Peripheral-to-Memory)

  • 场景:网卡接收数据包、硬盘读取数据、音频采集、摄像头帧捕获。
  • 特点:外设作为源,通过 DMA 将数据写入内存缓冲区。外设通常使用流式传输,数据连续到达。
  • 性能瓶颈:外设速度、总线仲裁延迟、描述符提交速度。CPU 读取数据前必须执行缓存失效操作。
  • Linux 实现:网络驱动中的 NAPI 配合 DMA 环形缓冲区;块设备层的 bio 链。

2.2.3 内存到外设(Memory-to-Peripheral)

  • 场景:网卡发送数据包、硬盘写入数据、音频播放、显示刷新。
  • 特点:内存作为源,外设作为目标。CPU 在启动 DMA 前必须确保数据已从缓存写回内存(缓存冲刷)。
  • 性能瓶颈:缓存冲刷开销、外设 FIFO 深度、传输完成中断频率。
  • Linux 实现:网络驱动的 skb 数据映射;显示驱动的 framebuffer DMA。
传输类型对比表:
传输类型 源 目标 典型带宽瓶颈 缓存一致性操作 Linux DMA 操作类型
内存→内存 内存 内存 内存带宽、总线竞争 无(CPU 不直接访问) DMA_MEMCPY
外设→内存 外设 FIFO 内存 外设速率、描述符提交 CPU 读前需缓存失效 DMA_DEV_TO_MEM
内存→外设 内存 外设 FIFO 缓存冲刷、外设 FIFO 深度 CPU 写后需缓存冲刷 DMA_MEM_TO_DEV
DMA 三种传输类型 内存 A(源) 内存 B(目标) 内存→内存 外设(源) 内存(目标) 外设→内存 内存(源) 外设(目标) 内存→外设

2.3 DMA 描述符链

对于大块数据或连续流传输,单次 DMA 传输往往不够。描述符链(Descriptor Chain)机制允许将多个传输任务串联起来,由 DMA 控制器自动依次执行,无需 CPU 在每次传输完成后重新编程。

2.3.1 描述符结构

每个描述符通常包含以下字段(以 Linux dma_slave_config 和硬件描述符为例):

struct dma_async_tx_descriptor {
    dma_addr_t           src_addr;      // 源物理地址
    dma_addr_t           dst_addr;      // 目标物理地址
    size_t               len;           // 传输长度(字节)
    u32                  flags;         // 控制标志(中断使能、链尾等)
    struct dma_async_tx_descriptor *next; // 指向下一个描述符(软件链)
    // 硬件描述符额外包含:
    //   - next_desc_addr: 下一个描述符的物理地址
    //   - control: 传输配置(方向、宽度、突发大小)
};

2.3.2 链式工作流程

  1. 描述符构建:CPU 在内存中分配一组描述符,填充源地址、目标地址、长度和标志。
  2. 链式链接:将描述符通过 next 指针或硬件 next_desc_addr 字段串联成单向链表。
  3. 提交启动:CPU 将第一个描述符的地址写入 DMA 控制器的描述符起始地址寄存器,然后启动 DMA。
  4. 自动执行:DMA 控制器完成当前描述符后,自动从内存中获取下一个描述符并执行,直到遇到链尾标志。
  5. 完成通知:最后一个描述符可配置为产生中断,通知 CPU 整条链传输完成。
性能陷阱:描述符链的内存布局对性能影响极大。如果描述符分散在多个缓存行中,DMA 控制器每次获取下一个描述符时都会触发缓存行填充,增加延迟。最佳实践是将描述符数组连续存放并对齐到缓存行边界(通常 64 字节)。

2.3.3 环形描述符 vs 链表描述符

特性 环形描述符(Ring) 链表描述符(Linked List)
结构 固定大小的环形缓冲区,描述符连续存放 动态分配,通过指针链接,可分散在内存中
预取效率 高(硬件可预取连续描述符) 低(每次跳转可能触发缓存缺失)
灵活性 低(大小固定,不易动态扩展) 高(可动态增删描述符)
典型应用 网卡 RX/TX 队列、音频流 块设备请求、复杂数据流水线
Linux 驱动示例 igb、ixgbe 网络驱动 dw_dmac、pl330 通用 DMA 引擎
DMA 描述符链工作流程 CPU 构建描述符 描述符 1 src=A, dst=B len=4KB, next→2 描述符 2 src=C, dst=D len=8KB, next→3 描述符 3 src=E, dst=F len=2KB, INT! 提交描述符地址 DMA 控制器执行链 自动获取下一描述符 链尾触发中断通知 CPU

2.4 关键性能指标与优化方向

在后续章节深入缓存一致性之前,先建立 DMA 性能的量化基准:

  • 传输延迟:从 CPU 提交描述符到 DMA 完成传输的时间。受描述符获取延迟、总线仲裁、内存访问延迟影响。
  • 吞吐量:单位时间内完成的数据传输量。瓶颈通常在于内存带宽或外设接口速率。
  • CPU 卸载率:DMA 替代 CPU 搬运数据的比例。理想情况下接近 100%,但描述符管理和中断处理仍会占用少量 CPU。
  • 描述符提交速率:CPU 能够向 DMA 控制器提交描述符的速度。高吞吐场景下,描述符提交可能成为瓶颈。
优化切入点:在 Linux 内核中,通过 dma_alloc_coherent() 分配一致性内存可以避免缓存一致性问题,但会牺牲部分性能;而 dma_map_single() 配合流式映射则允许更细粒度的缓存控制。选择哪种映射方式,取决于传输方向和实时性要求。

理解 DMA 控制器架构、三种传输类型的差异以及描述符链的运作机制,是后续分析 DMA 性能瓶颈和缓存一致性维护的基础。下一章将深入 DMA 与 CPU 缓存之间的交互,揭示缓存一致性问题的根源。