背景

直接内存访问(Direct Memory Access,DMA)是一种高效的数据传输机制,它允许外设在无需 CPU 介入的情况下,直接读写系统内存。这样不仅避免了 CPU 在执行“取指/取数/送数”过程中的大量参与,还减少了中断处理、现场保护与恢复、以及寄存器与内存之间的频繁数据交换。

DMA 技术是实现 Zero Copy(零拷贝) 的关键手段之一。在 IO 设备与用户空间之间传输数据时,它显著减少了数据的拷贝次数和系统调用开销,从而提升整体系统性能

为什么 DMA 传输更快?

传统的数据传输方式包括程序查询(Polling)和中断(Interrupt)两种:

  • 程序查询方式:CPU 反复检查外设状态,等待其准备完成后再进行数据传输。这种方式效率低下,因为 CPU 可能长时间处于等待状态。
  • 中断方式:虽然避免了轮询带来的空转,但每次中断传输都需要 CPU 执行一系列操作(如转入中断服务程序、保存/恢复现场、返回主程序等),整体代价仍然较高

这两种方式的数据传输路径都是:

外设 → CPU → 内存

每个字节的传输都依赖 CPU 的直接参与,导致系统效率受限。

而 DMA 的数据传输路径则为:

外设 ↔ 内存(绕过 CPU)

在 DMA 模式下,CPU 将总线控制权交由 DMA 控制器接管。DMA 控制器在不经过 CPU 干预的情况下,直接控制数据在外设与内存之间的传输。由于绕过了 CPU 的指令执行与中断处理,传输速度大幅提升,接近于内存的最高访问速率

DMA 的工作流程

在一般情况下,系统总线由 CPU 控制。然而在 DMA 工作期间,CPU 会将总线控制权暂时交给 DMA 控制器,后者负责数据传输的全过程,包括传输字节数的控制、传输完成的判断与通知等

DMA 控制器的典型工作流程如下:

  1. DMA 请求:当外设有数据传输需求并准备就绪时,向 DMA 控制器发送请求(DMA Request, DRQ)
  2. 总线仲裁:DMA 控制器收到请求后,向 CPU 发出总线接管请求(Bus Request, BR)
  3. CPU 让出控制权:CPU 在当前总线周期结束后,响应请求,发出总线授权信号(Bus Grant, BG),表示释放对总线的控制
  4. 开始 DMA 传输:DMA 控制器获得总线控制权后,通知外设可以开始传输
  5. 数据传输:DMA 控制器根据预先设定的地址与方向控制信号,在外设与内存之间直接进行数据传输
  6. 传输完成:数据传输完成后,DMA 控制器发出传输结束信号,并撤销总线接管请求,CPU 随后重新收回总线控制权

基础 DMA 传输模式

DMA 控制器支持多种传输模式,以适应不同的性能与实时性需求。三种常见模式:

1. 单次传输(Single Transfer / Cycle Stealing)

  • 每次仅传输 一个字节或一个字(word)
  • 每完成一次传输,CPU 就暂时重新获得总线控制权
  • CPU 和 DMA 交替使用总线,因此又称“Cycle Stealing”模式
  • 优点:CPU 响应速度较快,不会长期被 DMA 占用
  • 缺点:DMA 传输效率低,适用于对实时性要求高、但传输量小的场景

2. 块传输(Block Transfer / Burst Mode)

  • DMA 控制器一次性占用总线,连续传输多个字节/字
  • DMA 在传输过程中独占总线,直到整个块数据传完
  • 优点:传输速度快,系统总线利用率高
  • 缺点:CPU 在此期间无法访问总线,可能延迟响应
  • 适用场景:大批量数据搬运,如从磁盘读取缓存块

3. 周期性传输(Demand / Cycle Mode)

  • 外设根据自己速度主动拉取总线,每次传输一个或多个字
  • 介于单次与块传输之间,带有“按需触发”的特性
  • 多用于如声卡、网卡这类实时性强、速率不固定的设备
模式名称 简要描述 特点
Single Mode 每次传输一个字节或一个字(word),然后释放总线 实时响应好,效率低
Block Mode 一次性传输一整个块的数据,传输期间占用总线 吞吐高,但会阻塞 CPU
Demand Mode 外设按需发起 DMA 请求,动态控制传输周期 动态性强,适应性高

这些模式都是“线性传输”模型:

一次 DMA 操作中传输的数据是一段连续的物理地址区间

Scatter-Gather DMA

Scatter-Gather DMA 是对传统 DMA 的扩展,支持:

  • 数据在内存中不是连续存放的(scatter,分散)
  • DMA 控制器依次处理多个内存片段,最终“聚集”到一起进行传输,或反向操作(gather→scatter)

传统 block DMA 一次只能传输物理上连续的一个块的数据, 完成传输后发起中断

scatter-gather DMA 允许一次传输多个物理上不连续的块,完成传输后只发起一次中断,比 block DMA 方式效率高,但需要硬件软件都实现(硬件控制器读取描述符链表,自动解析并传输)

场景举例

  • 一个网络报文由多个非连续缓冲区组成(比如 metadata、headers、payload 分布在不同位置)
  • 应用程序分配了一组小 buffer(页对齐但不连续)

传统 DMA 需要你 手动合并 这些 buffer 成一个连续块 Scatter-Gather 则允许 DMA 控制器一次性完成这组分散地址的传输

工作原理:DMA 描述符链表

Scatter-Gather DMA 的核心机制是:

使用一组描述符(Descriptor)或链表来描述多个物理地址片段

每个描述符通常包含:

struct dma_desc {
    void *src_addr;   // 源地址
    void *dst_addr;   // 目的地址
    size_t length;    // 传输长度
    struct dma_desc *next; // 指向下一个描述符
};

DMA 控制器启动后,会自动遍历这组描述符并按顺序处理多个传输任务,直到整个链表结束或传输完成

Scatter-Gather vs 基础 DMA 模式

特性 基础 DMA 模式 Scatter-Gather DMA
传输内存区域 单一、连续的物理内存 多个非连续的内存块
编程复杂度 简单,设置起始地址+长度 需要构造 DMA 描述符链表
控制方式 每次传输一个 buffer 一次配置,自动遍历多个 buffer
系统调用次数 多次(若有多个 buffer) 一次发起(支持 batch DMA)
中断次数 多个传输产生多个中断 可合并为一个中断
性能 中等 高吞吐、低延迟
应用场景 简单数据搬运、控制器配置 网络堆栈、GPU、DMA Engine、音视频流处理等

应用

应用领域 Scatter-Gather 作用
网络驱动(如网卡) 接收 buffer 分散在多个页 → SG DMA 自动收集并组合到 socket buffer 中
块设备(如 NVMe) 读写请求来自多个页缓存 → 构造 SG 链表提交给存储控制器
GPU 显存传输 用户空间中的图像块可能分布不连续 → SG DMA 用于批量从系统内存传输至 GPU
多媒体处理 多帧视频帧缓存 → DMA 控制器自动从多个 buffer 中收集数据送往编解码模块

基础 DMA 适合连续块传输,而 Scatter-Gather DMA 更适合处理复杂结构化数据、高性能多 buffer 场景,能显著降低 CPU 负担和系统延迟

DPDK

DPDK分片包采用链式管理,同一个数据包的数据,分散存储在不连续的块中(mbuf 结构)。要求 DMA 一次操作,需要从不连续的多个块中搬移数据。e1000 驱动发包部分代码:

uint16_t
eth_em_xmit_pkts(void *tx_queue, struct rte_mbuf **tx_pkts,uint16_t nb_pkts){
    //e1000驱动部分代码
    ...
    m_seg = tx_pkt;
    do {
        txd = &txr[tx_id];
        txn = &sw_ring[txe->next_id];
 
        if (txe->mbuf != NULL)
            rte_pktmbuf_free_seg(txe->mbuf);
            txe->mbuf = m_seg;
 
        /*
        * Set up Transmit Data Descriptor.
        */
        slen = m_seg->data_len;
        buf_dma_addr = rte_mbuf_data_iova(m_seg);
 
        txd->buffer_addr = rte_cpu_to_le_64(buf_dma_addr);
        txd->lower.data = rte_cpu_to_le_32(cmd_type_len | slen);
        txd->upper.data = rte_cpu_to_le_32(popts_spec);
 
        txe->last_id = tx_last;
        tx_id = txe->next_id;
        txe = txn;
        m_seg = m_seg->next;
    } while (m_seg != NULL);
 
    /*
    * The last packet data descriptor needs End Of Packet (EOP)
    */
    cmd_type_len |= E1000_TXD_CMD_EOP;
    txq->nb_tx_used = (uint16_t)(txq->nb_tx_used + nb_used);
    txq->nb_tx_free = (uint16_t)(txq->nb_tx_free - nb_used);
    ...
}

Linux 应用层 Vectored I/O

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <unistd.h>
#include <sys/uio.h>

int main(int argc, char *argv[]){
	const char buf1[] = "Hello, ";
	const char buf2[] = "Wikipedia ";
	const char buf3[] = "Community!\n";

	struct iovec bufs[] = {
		{ .iov_base = (void *)buf1, .iov_len = strlen(buf1) },
		{ .iov_base = (void *)buf2, .iov_len = strlen(buf2) },
		{ .iov_base = (void *)buf3, .iov_len = strlen(buf3) },
	};

	if (writev(STDOUT_FILENO, bufs, sizeof(bufs) / sizeof(bufs[0])) == -1)	{
		perror("writev()");
		exit(EXIT_FAILURE);
	}

	return EXIT_SUCCESS;
}

优势对比

特性 传统 I/O (read, write) Vectored I/O (readv, writev)
内存布局支持 单一连续缓冲区 多个非连续缓冲区(iovec 数组)
系统调用次数 多个 一个
用户空间拷贝效率 可能多次复制 一次性拷贝多个缓冲区
典型应用场景 普通文件读写、一次一块 网络协议(如 HTTP 头+体)、零拷贝优化

实际应用场景

  • Web服务器:发送 HTTP 响应时,可以把 HTTP 头和文件内容分成两个缓冲区,用 writev() 一次发送出去,避免手动合并
  • 日志系统:结构化日志输出时可分别写入时间戳、级别、正文等字段
  • 性能优化:减少系统调用数量,减少内存拷贝和 CPU cache pollution