DMA技术入门:让CPU从搬运工变成指挥官

每次我跟年轻工程师聊DMA(直接内存访问),都会先问一个问题:CPU最擅长什么? 计算。那CPU最不擅长什么?搬数据。搬数据这活儿,说白了就是从一个地址复制到另一个地址,纯体力劳动,CPU干这个简直是大材小用。

我在早期做嵌入式项目时,遇到过CPU占用率居高不下的情况。一查性能计数器,好家伙,80%的CPU时间都在做内存拷贝。后来切到DMA,CPU占用率直接降到15%。嗯,这就是DMA的价值——让CPU从搬运工变成指挥官。

DMA工作原理:绕过CPU的专用数据通道

DMA控制器(DMAC)是一个独立于CPU的硬件模块。它内部有一组寄存器,用来描述“从哪里搬、搬到哪、搬多少”。CPU只需要初始化这些寄存器,然后一个启动命令,剩下的事DMA自己搞定。

传统方式下,从磁盘读数据到内存:

  • CPU发出读指令
  • 数据先到CPU内部寄存器
  • CPU再把数据写入内存
  • CPU全程忙转

DMA方式完全不一样:

  • CPU告诉DMA:数据从磁盘控制器 → 内存地址0x1000,长度64KB
  • CPU回去做别的事
  • DMA直接让磁盘控制器和内存对话
  • 传输完成,DMA发个中断通知CPU

核心要点: DMA传输中,CPU只在开始和结束时参与,中间数据路径完全不经过CPU。这就是“直接内存访问”的含义。

DMA传输模式:Flyby vs Fetch

DMA怎么搬数据?两种模式——Flyby(飞越)和Fetch(抓取)。这两个名字起得挺形象,我来拆解一下。

特性 Flyby 模式 Fetch 模式
数据路径 外设 ↔ 内存(直通) 外设 → DMAC → 内存
DMA控制器角色 控制者(仲裁总线) 中介者(存储转发)
速度 快(一次总线传输) 稍慢(两次总线传输)
硬件复杂度 低(无内部缓冲) 高(需FIFO或缓冲)
典型场景 高速设备(NVMe SSD、网卡) 低速设备、需要数据转换的场合

Flyby模式:数据从外设直接飞向内存,DMA只负责控制总线的使用权。举个例子,网卡收到一个数据包,DMA让网卡直接写到内存指定位置。数据全程不碰DMAC内部。这种模式延迟最低,吞吐量最高。

Fetch模式(也叫Flow-Through模式):DMA先读数据到自己的内部FIFO,再写出去。我在调试一个老旧的IDE硬盘控制器时,就碰到过这种模式。因为设备太慢,DMA需要先缓冲数据,等内存总线空闲了再写。你想想看,数据多了一次总线传输,延迟自然会增加。

小技巧: 现在的PCIe DMA控制器,基本都是Flyby模式。如果你在调试遇到性能瓶颈,先确认一下是不是被降级成了Fetch模式。我见过有人因为驱动参数配错,把高速网卡的Flyby模式强制搞成了Fetch,吞吐量直接腰斩。

DMA引擎与总线架构

DMA控制器不是独立存在的,它必须挂在总线上。不同芯片的DMA引擎架构差异很大,直接影响中断亲和性优化的最终效果。

常见的DMA引擎类型:

  • 系统DMA控制器: 传统多通道DMA,挂在系统总线上。x86上的ISA DMA就是典型,不过现代平台已经不常用。
  • 总线主控DMA: 设备自己带DMA引擎。比如NVMe SSD、网卡,它们内部的DMA控制器直接发起总线事务。这才是现代高性能系统的标配。
  • 多通道DMA引擎: 一个DMA控制器支持多个独立的DMA通道。每个通道可以独立配置源地址、目的地址和传输大小。我调试过的某款ARM SoC,DMAC有16个通道,挂载在AXI总线上。
DMA系统总线架构图 CPU 核心 系统总线(System Bus / AXI / PCIe) DMA 控制器 系统内存 (DRAM) I/O 设备 (网卡/SSD) DMA发起总线控制 数据经过DMA但不存储 Flyby模式:外设 ↔ 内存直连 Fetch模式:数据先到DMA缓冲 再由DMA写入内存

上图把DMA的核心关系画清楚了。CPU、内存、I/O设备都挂在系统总线上,DMA控制器也挂在上面。关键区别在于:CPU走的是程序驱动路径,DMA走的是硬件自动路径。

我在调试一个PCIe网卡时,发现它的DMA引擎内部有16个descriptor ring(描述符环),每个环对应一个独立的DMA通道。驱动把收发包的地址和长度写入描述符,网卡自己通过DMA引擎逐个执行。这种设计的好处是——多队列。每个队列可以绑定到一个CPU核心,这就是我们后面要讲的中断亲和性的基础。

注意: 有些芯片的DMA引擎有地址对齐限制。比如要求源地址和目的地址必须按4字节对齐。我遇到过一块FPGA加速卡,DMA内部对齐检查不严格,导致某些场景下数据错位。排查了整整两天才找到根因——其中一个描述符的地址是奇数偏移。

总线对DMA的影响:

  • PCIe总线: 支持多个DMA事务并行,每个设备有自己的DMA引擎。PCIe的TLP(事务层包)可以直接携带内存读写请求。
  • AXI总线(ARM): 支持乱序完成。DMA可以发出多个读请求,然后按任意顺序等待数据返回。我调过的一个SoC,AXI总线深度配置不合理,DMA吞吐一直上不去。
  • CCIX/CXL: 新一代缓存一致性总线。DMA可以直接访问远程内存,甚至缓存。这在高性能计算场景越来越常见。

最后说一个我踩过的坑。有次调试NVMe SSD性能,seq read才2GB/s,远低于标称的6GB/s。用perf一看,DMA中断全挤在CPU0上。整个CPU0被打到100%中断占用,其他CPU躺着看热闹。这就是典型的中断没做亲和性绑定。嗯,这个坑就是我接下来要给你讲的核心内容——怎么把DMA中断均匀分布到多核CPU上。