4. DMA 引擎架构:控制器工作原理与传输模式

DMA(Direct Memory Access)是块设备高性能 I/O 的核心硬件机制。它允许外设与系统内存之间直接传输数据,无需 CPU 逐字节搬运,从而将 CPU 从繁重的数据拷贝中解放出来,专注于上层业务逻辑。本章深入剖析 DMA 控制器(DMAC)的内部工作原理、关键寄存器与描述符链机制,并详细对比三种基本传输模式:内存到设备、设备到内存、内存到内存。

4.1 DMA 控制器工作原理

DMA 控制器是系统总线上的一个独立总线主控设备。它通过“窃取”总线周期(Bus Mastering)来执行数据传输。一个典型的 DMA 传输流程包含以下阶段:

  1. CPU 初始化 DMAC:CPU 配置 DMAC 的源地址、目标地址、传输长度、传输方向等参数。
  2. 外设发起 DMA 请求:当外设(如 NVMe 控制器、网卡)准备好接收或发送数据时,向 DMAC 发送硬件 DMA 请求信号(DMA Request)。
  3. DMAC 获取总线控制权:DMAC 向 CPU 发出总线请求(Bus Request),CPU 在完成当前总线周期后让出总线控制权(Bus Grant)。
  4. DMAC 执行数据传输:DMAC 直接控制地址总线和数据总线,在源和目标之间传输数据。每传输一个单元(字节/字/双字),DMAC 递增地址并递减计数器。
  5. 传输完成与中断:当计数器归零时,DMAC 释放总线控制权,并向 CPU 发送中断信号,通知传输完成。
关键指标: DMA 传输的吞吐量取决于总线带宽、DMAC 的时钟频率以及描述符链的处理效率。现代高性能 DMAC 支持多通道并发和硬件描述符预取,以隐藏总线延迟。

4.1.1 DMAC 核心寄存器与描述符链

现代 DMA 控制器通常采用描述符链(Descriptor Chain)架构,而非传统的寄存器组模式。描述符链允许 DMAC 自动处理多个不连续的缓冲区,无需 CPU 逐个配置。

寄存器/描述符字段 功能说明 典型位宽
源地址寄存器 (SAR) 存储数据源的内存地址或外设 FIFO 地址 32/64 位
目标地址寄存器 (DAR) 存储数据目标的内存地址或外设 FIFO 地址 32/64 位
传输控制寄存器 (CTL) 配置传输方向、数据宽度、突发长度、中断使能 32 位
描述符指针寄存器 (DPR) 指向内存中描述符链表的基地址 32/64 位
状态寄存器 (SR) 反映当前传输状态、错误标志、完成标志 32 位

描述符链的工作模式如下:每个描述符包含源地址、目标地址、传输长度、控制标志以及指向下一个描述符的指针。DMAC 自动从内存中加载描述符,执行传输,然后跳转到下一个描述符,直到遇到链尾标志。

性能优势: 描述符链消除了 CPU 在每次 DMA 传输后的干预。对于大量小 I/O 请求(如 4KB 随机读写),描述符链可以显著降低 CPU 开销,提升 IOPS。
DMA 描述符链工作流程 CPU DMAC 内存 外设 配置描述符 DMA 请求 读取描述符 描述符 1 描述符 2 描述符 N 描述符链优势 • 支持 Scatter/Gather 传输 • 减少 CPU 中断次数 • 硬件自动预取下一个描述符

4.2 DMA 传输模式详解

根据数据流的方向,DMA 传输分为三种基本模式。理解这些模式对于块设备驱动开发者至关重要,因为它们直接影响数据路径的设计和缓存一致性策略。

4.2.1 内存到设备 (Memory-to-Device)

此模式用于将数据从系统内存传输到外设。典型场景包括:NVMe 提交队列(SQ)条目写入、网卡发送描述符、GPU 纹理上传。CPU 首先在内存中准备好数据缓冲区,然后配置 DMAC 将数据搬运到外设的 FIFO 或寄存器空间。

  • 数据流:内存 → DMAC → 外设
  • 缓存策略:CPU 在启动 DMA 前必须执行缓存刷新(Cache Flush)或使用非缓存一致性内存区域,确保外设看到最新的数据。
  • 性能关注点:内存读取延迟、总线写带宽。

4.2.2 设备到内存 (Device-to-Memory)

此模式用于将外设数据读入系统内存。典型场景包括:NVMe 完成队列(CQ)条目读取、网卡接收数据包、磁盘读取数据。外设通过 DMA 将数据直接写入内存中的预分配缓冲区。

  • 数据流:外设 → DMAC → 内存
  • 缓存策略:CPU 在访问 DMA 写入的内存区域前,必须执行缓存失效(Cache Invalidate)操作,避免读取过时的缓存数据。
  • 性能关注点:内存写带宽、缓存一致性开销。

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

此模式用于在系统内存的两个不同区域之间搬运数据,无需 CPU 参与。典型场景包括:帧缓冲拷贝、加密/压缩引擎的数据搬移、用户态与内核态缓冲区拷贝(在支持用户态 DMA 的架构中)。

  • 数据流:内存区域 A → DMAC → 内存区域 B
  • 缓存策略:源区域需刷新,目标区域需失效,或使用非缓存区域。
  • 性能关注点:内存带宽利用率、DMAC 内部 FIFO 深度。
避坑指南: 内存到内存 DMA 通常比 CPU 使用 memcpy() 慢,因为 DMAC 需要经过系统总线仲裁,且无法利用 CPU 的 L1/L2 缓存。仅在 CPU 需要同时处理其他任务或数据量极大(>1MB)时,才考虑使用内存到内存 DMA。
三种 DMA 传输模式对比 模式1: 内存 → 设备 内存 外设 DMA 模式2: 设备 → 内存 外设 内存 DMA 模式3: 内存 → 内存 内存A 内存B DMA 三种模式关键对比 特性 Mem→Dev Dev→Mem Mem→Mem 缓存操作 Flush Invalidate Flush+Inv 典型延迟 低 低 中 CPU 卸载效果 高 高 中 适用场景 NVMe SQ NVMe CQ 帧缓冲

4.3 块设备驱动中的 DMA 编程要点

在 Linux 块设备驱动中,使用 DMA 需要遵循一套标准 API 和内存管理规则。以下是一个简化的 DMA 传输初始化代码片段,展示了从内存到设备的传输流程:

/* 简化的块设备 DMA 写操作示例 */
static int blkdev_dma_write(struct device *dev, void *buf, size_t len, dma_addr_t dma_dev_addr)
{
    struct dma_chan *chan;
    struct dma_async_tx_descriptor *tx;
    dma_addr_t dma_buf;
    enum dma_ctrl_flags flags = DMA_CTRL_ACK | DMA_PREP_INTERRUPT;

    /* 1. 分配 DMA 一致性内存或映射流式 DMA 缓冲区 */
    dma_buf = dma_map_single(dev, buf, len, DMA_TO_DEVICE);
    if (dma_mapping_error(dev, dma_buf))
        return -ENOMEM;

    /* 2. 获取 DMA 通道(通常与设备绑定) */
    chan = dma_request_chan(dev, "tx");
    if (IS_ERR(chan)) {
        dma_unmap_single(dev, dma_buf, len, DMA_TO_DEVICE);
        return PTR_ERR(chan);
    }

    /* 3. 准备 DMA 描述符 */
    tx = dmaengine_prep_slave_sg(chan, &(struct scatterlist){
        .page_link = 0,
        .offset = offset_in_page(buf),
        .length = len,
        .dma_address = dma_buf,
    }, 1, DMA_MEM_TO_DEV, flags);
    if (!tx) {
        dma_release_channel(chan);
        dma_unmap_single(dev, dma_buf, len, DMA_TO_DEVICE);
        return -ENOMEM;
    }

    /* 4. 提交传输并等待完成 */
    tx->callback = dma_callback;  /* 注册完成回调 */
    dmaengine_submit(tx);
    dma_async_issue_pending(chan);

    /* 5. 等待传输完成(同步方式示例) */
    dma_wait_for_async_tx(tx);

    /* 6. 清理 */
    dma_unmap_single(dev, dma_buf, len, DMA_TO_DEVICE);
    dma_release_channel(chan);

    return 0;
}
性能调优建议:
  • 使用 Scatter/Gather 列表:对于非连续物理内存,使用 dmaengine_prep_slave_sg() 而非单缓冲区接口,减少 DMA 传输次数。
  • 批量提交:在块设备请求队列中,将多个 BIO 合并为一次 DMA 传输,提高总线利用率。
  • 选择正确的 DMA 方向:DMA_TO_DEVICE 对应内存到设备,DMA_FROM_DEVICE 对应设备到内存,错误的方向会导致数据损坏。
  • 避免频繁的 DMA 映射/取消映射:对于热路径,考虑使用 DMA 池或预映射缓冲区。

4.4 本章小结