3、DMA传输模式详解:内存到内存、内存到外设、外设到内存、循环模式(Scatter-Gather)
DMA(Direct Memory Access)传输模式是决定数据搬运效率与系统耦合度的核心设计维度。不同的传输模式适用于不同的硬件拓扑与业务场景。本章将深入剖析四种基础 DMA 传输模式:内存到内存、内存到外设、外设到内存,以及高级的 Scatter-Gather(循环/分散聚合)模式,并结合网络驱动中的数据加速场景进行工程化解读。
3.1 内存到内存(Memory-to-Memory)
内存到内存模式是最基础的 DMA 传输形式,通常用于在系统内存的不同区域之间高速复制数据块,无需 CPU 逐字节搬运。
适用场景:帧缓冲拷贝、加密/压缩中间缓冲区交换、用户态与内核态数据镜像。
传输流程
- CPU 配置 DMA 控制器:源地址(内存区域 A)、目的地址(内存区域 B)、传输长度。
- DMA 控制器向总线仲裁器请求总线控制权。
- 获得控制权后,DMA 直接从源地址读取数据,写入目的地址。
- 传输完成,DMA 控制器触发中断通知 CPU。
工程注意:内存到内存 DMA 在部分 SoC(如某些 ARM 平台)中可能不支持或性能低于预期,因为需要占用内存总线带宽,反而可能影响 CPU 对内存的访问延迟。使用前务必查阅芯片参考手册。
3.2 内存到外设(Memory-to-Device)
内存到外设模式是网络驱动中最常见的 DMA 方向之一。CPU 将待发送的数据包组织在内存中的 DMA 缓冲区,DMA 控制器负责将数据从内存搬运到网络接口控制器(NIC)的 FIFO 或内部缓冲区。
关键特征
| 属性 | 说明 |
|---|---|
| 数据流向 | 系统内存 → 外设(如网卡 TX FIFO) |
| 触发方式 | CPU 写外设控制寄存器,或外设通过 DMA 请求信号发起 |
| 典型应用 | 网卡发送数据包、GPU 上传纹理、USB 批量输出 |
| 同步机制 | 完成中断或轮询状态寄存器 |
性能优化点:在内存到外设传输中,应确保 DMA 缓冲区在物理地址上连续(或使用 IOMMU/SMMU 映射),避免外设无法访问跨页内存。同时,合理设置 TX 描述符环深度(如 256/512)可平衡延迟与吞吐量。
3.3 外设到内存(Device-to-Memory)
外设到内存模式是网络接收路径的核心。网卡接收到数据帧后,通过 DMA 直接将数据写入系统内存中预先分配的缓冲区,CPU 仅在收到中断后处理已就绪的数据。
与 PIO 模式对比
| 对比项 | DMA 模式 | PIO 模式(Programmed I/O) |
|---|---|---|
| 数据搬运者 | DMA 控制器(硬件) | CPU(逐字读取) |
| CPU 占用率 | 极低(仅中断处理) | 高(持续占用) |
| 吞吐量 | 高(可达到总线极限) | 低(受限于 CPU 速度) |
| 延迟 | 较低(硬件并行) | 较高(CPU 串行) |
| 适用场景 | 高速网络(1G/10G/25G+) | 低速设备或调试阶段 |
工程实践:在 Linux 网络驱动中,外设到内存 DMA 通常与 NAPI(New API)配合使用。网卡在中断中触发 NAPI 调度,轮询模式下批量处理多个接收包,减少中断次数,提升吞吐量。
3.4 循环模式与 Scatter-Gather(分散聚合)
Scatter-Gather 是 DMA 的高级传输模式,允许一次 DMA 操作访问多个非连续的内存缓冲区。它通过一个描述符链表(Descriptor Chain)来定义多个源/目的地址块,DMA 控制器自动遍历链表完成所有传输。
核心概念
- Scatter(分散):从多个不连续的内存源地址读取数据,聚合后发送到外设。
- Gather(聚合):从外设接收数据,分散写入多个不连续的内存目的地址。
- 描述符环(Descriptor Ring):循环缓冲区,每个描述符包含地址、长度、控制标志和指向下一个描述符的指针。
描述符结构示例
struct dma_desc {
u32 addr; // 缓冲区物理地址
u32 length; // 传输长度
u32 flags; // 控制标志(中断使能、链结束等)
u32 next_desc; // 下一个描述符指针(物理地址)
};
网络驱动中的 Scatter-Gather 应用
在 Linux 网络驱动中,Scatter-Gather 与 sk_buff 结构深度绑定。一个网络数据包可能分散在多个内存页中(如 TCP 分段、协议头与 payload 分离),通过 Scatter-Gather DMA,驱动可以直接将这些分散的页地址填入描述符,无需先拷贝到连续缓冲区。
关键 API:在 Linux 内核中,
dma_map_sg() 和 dma_unmap_sg() 用于将分散/聚合列表(SG list)映射到 DMA 地址空间。驱动通过 sg_dma_address() 和 sg_dma_len() 获取映射后的地址和长度,填入硬件描述符。
四种模式对比总结
| 模式 | 数据流向 | 内存连续性要求 | 典型硬件支持 | 网络驱动应用 |
|---|---|---|---|---|
| 内存到内存 | 内存 → 内存 | 源/目的连续 | 通用 DMA 控制器 | 帧缓冲拷贝、协议栈内部交换 |
| 内存到外设 | 内存 → 外设 | 源连续 | 网卡 TX 引擎 | 数据包发送 |
| 外设到内存 | 外设 → 内存 | 目的连续 | 网卡 RX 引擎 | 数据包接收 |
| Scatter-Gather | 双向(多块) | 无需连续 | 高级网卡(Intel/ Mellanox/ Broadcom) | 零拷贝、TSO、LRO、大包收发 |
避坑指南:Scatter-Gather 虽然强大,但描述符环的深度和缓冲区大小需要精心配置。过浅的描述符环会导致 DMA 引擎空转等待;过深则浪费内存。建议根据网络带宽和 MTU 计算:描述符深度 = 带宽 × 延迟 / 平均包长。例如 10Gbps 网络、100μs 延迟、1500 字节包长,深度约需 833,通常取 1024。