DMA传输原理:直接内存访问的概念、DMA引擎工作方式、PCIe DMA数据传输流程
我做了这么多年存储驱动,回头想想,DMA(Direct Memory Access,直接内存访问)这个机制,真是整个高性能IO的基石。你想想看,要是每次数据搬移都得CPU亲自下场,那SSD再快也没用——CPU早就累趴下了。
说白了,DMA就是让外设和内存之间直接传数据,不用CPU逐字节搬运。CPU只需要发个「开始吧」的指令,然后就干自己的活去了。等传输完成,DMA控制器发个中断通知一声,完事。
为什么CPU自己搬数据不行?
我见过不少刚入行的朋友,觉得CPU搬数据也没什么大不了的。其实这里有个巨大的性能鸿沟。做个简单算术:
| 传输方式 | 数据量 | CPU占用率 | 延迟 |
|---|---|---|---|
| CPU直接搬运 | 4KB | ~100% (忙等) | 微秒级 |
| PIO模式 | 4KB | ~80% (轮询) | 微秒级 |
| DMA传输 | 4KB | ~0% (释放) | 亚微秒级 |
| DMA传输 | 1MB | ~0% | 毫秒级(但CPU空闲) |
注意:不要被「毫秒级」吓到。这个延迟是从CPU发起请求到传输完成的时间,但CPU在这个时间段里可以处理其他任务。如果用PIO搬1MB数据,CPU会卡住数十毫秒——这在现代系统中完全不可接受。
DMA引擎的三种工作方式
DMA引擎不是只有一种形态。我在不同的项目里遇到过三种主流工作方式,各有各的适用场景:
- 飞拍方式(Flyby)——早期PCIe设备用得比较多。 典型流程:DMA控制器从源地址读一个数据,然后直接写到目标地址。数据不经过DMA控制器的内部寄存器。特点是延迟低,但只能做固定地址偏移的搬运。
- 块传输方式(Block Transfer)——现在最常用的模式。 我习惯叫它「整块搬」。DMA控制器一次性传输一块连续的数据。源地址和目标地址可以指定,大小也灵活。
- 分散-聚集方式(Scatter-Gather)——处理非连续内存的利器。 这个就厉害了。你传的数据可能在内存里是东一块西一块的,DMA引擎能通过一个描述符链表,把这些碎片自动拼接成连续的数据流。NVMe用这种方式特别多。
嗯,说起来我第一次接触Scatter-Gather是在调某个国产SSD的时候。当时上层应用申请的内存不连续,我还傻傻地想着用CPU先拼好再传……后来发现DMA本身就能搞定,真是白费了大力气。
建议:在实际项目中,如果能保证物理连续性,优先用块传输模式——逻辑简单,也容易定位问题。但当系统内存碎片较多时,Scatter-Gather才是正确出路。不要试图在驱动层做内存拼接,那是吃力不讨好。
PCIe DMA数据传输流程
好,现在我们来走一遍完整的PCIe DMA数据传输流程。我会结合NVMe的场景来说,这样更有代入感。
核心流程概览:
CPU设置描述符 → 写入门铃寄存器 → DMA引擎自动搬运 → 完成中断通知 → CPU处理完成
具体来说,分这么几步:
- Step 1: 准备阶段
CPU在内存中构建一个DMA描述符。描述符里包含:源地址(比如SSD内部缓存)、目标地址(系统内存缓冲区)、传输长度(比如4KB)。 - Step 2: 通知硬件
CPU往NVMe控制器的门铃寄存器写一个值。这个动作告诉控制器:「我有新的任务给你,开始干吧」。 - Step 3: DMA引擎接管
控制器内部的DMA引擎读取描述符,然后发起PCIe Memory Read事务(从SSD缓存读数据),再发起PCIe Memory Write事务(把数据写到系统内存)。这中间完全不需要CPU干预。 - Step 4: 中断通知
数据全部写完以后,DMA引擎往完成队列写一个完成状态,然后触发MSI/MSI-X中断。 - Step 5: 驱动收尾
CPU的中断服务程序被触发,检查完成状态,通知上层应用数据可用。
避坑指南:DMA传输中的常见陷阱
理论说完了,说点实际的。我这些年见过的坑,随便挑几个跟大家聊聊:
- 缓冲区的Cache一致性——我曾经在一个FPGA项目中,发现DMA写入的数据CPU读出来总是错。查了一天,问题出在CPU缓存上。CPU读取时命中了过时的缓存行。解决办法:要么用DMA内存区域标记为non-cacheable,要么在关键位置手动执行缓存刷新指令。
- 描述符链表断裂——Scatter-Gather方式依赖描述符链表的物理连续性。如果你的描述符链表本身位于可分页的内存中,并且DMA引擎不支持虚拟地址,那就麻烦了。一定要确保描述符物理连续,或者用IOMMU来做翻译。
- 中断风暴——高吞吐场景下,每笔传输都触发中断的话,CPU会崩溃在中断处理上。我习惯的做法是:启用中断合并(Interrupt Coalescing),等一定数据量或超时后再触发一次中断。
特别提醒:NVMe的PRP(物理区域页)和SGL(分散聚集列表)机制,本质上就是DMA描述符的不同实现方式。PRP适用于单次传输不超过4KB的情况,超过了就乖乖用SGL。我见过有人非要用PRP传大块数据,结果描述符数量爆增,性能反而下降。
小结
DMA传输的核心价值就是解放CPU。你只要把工作拆成:准备描述符 -> 触发开始 -> 等待完成 这三步,其他的交给硬件去做。对于NVMe场景,尤其要注意描述符的内存布局和Cache处理。记住一句话:DMA擅长的是简单重复劳动,我们要做的就是把这个劳动的说明书写得清楚明白。搞懂这些,后面讲均衡参数调优才有的放矢。