背景
直接内存访问(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 控制器的典型工作流程如下:
- DMA 请求:当外设有数据传输需求并准备就绪时,向 DMA 控制器发送请求(DMA Request, DRQ)
- 总线仲裁:DMA 控制器收到请求后,向 CPU 发出总线接管请求(Bus Request, BR)
- CPU 让出控制权:CPU 在当前总线周期结束后,响应请求,发出总线授权信号(Bus Grant, BG),表示释放对总线的控制
- 开始 DMA 传输:DMA 控制器获得总线控制权后,通知外设可以开始传输
- 数据传输:DMA 控制器根据预先设定的地址与方向控制信号,在外设与内存之间直接进行数据传输
- 传输完成:数据传输完成后,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