3. DMA基本原理

在PCIe系统中,CPU如果逐字节搬运数据,会把大量时钟周期消耗在加载、存储和循环判断上,带宽利用率极低。DMA(Direct Memory Access,直接内存访问)的核心思想是:把“搬数据”这件事从CPU手中交给一个专门的硬件引擎——DMA控制器(DMAC)。CPU只负责“下命令”,DMAC负责“干活”,数据在内存与外设、内存与内存之间高速流动,CPU则被解放出来去做计算、协议处理和调度。

理解DMA,必须同时看清五个关键点:DMAC扮演什么角色、谁在总线上发起访问、地址如何映射、数据往哪个方向流、以及描述符与链表如何组织批量传输。下面逐一展开。

一、DMA控制器的角色:从“执行者”到“调度者”

DMA控制器本质上是一个带有总线主控能力的专用状态机。它内部通常包含:通道寄存器组、源地址寄存器、目的地址寄存器、传输长度寄存器、控制/状态寄存器、描述符取指单元、数据搬运引擎和中断/完成通知逻辑。CPU通过配置这些寄存器来启动一次传输,DMAC随后独立地产生读写请求、推进地址、递减长度,并在结束时置位完成状态或产生中断。

在PCIe场景中,DMAC往往位于Endpoint内部,例如网卡、NVMe SSD、FPGA加速卡或采集卡中。它一侧连接PCIe事务层,另一侧连接本地存储或数据通路。CPU通过MMIO写寄存器或写描述符来驱动它,DMAC则通过PCIe Memory Read/Write TLP访问主机内存。因此,DMAC既是“外设内部的数据搬运工”,也是“PCIe总线上的主控代理”。

关键认知:DMA不是“没有CPU参与”,而是“CPU只参与控制路径,不参与数据路径”。控制路径走寄存器/描述符,数据路径走DMA引擎与总线。

二、总线主控:谁有权发起PCIe访问

PCIe总线上的事务分为两类:一类由CPU或RC(Root Complex)发起,访问Endpoint的配置空间、BAR空间;另一类由Endpoint发起,访问主机内存。后者要求Endpoint具备总线主控(Bus Master)能力。在PCI配置空间中,Command寄存器的Bit 2是Bus Master Enable(BME)。只有BME被置1,Endpoint才能发起Memory Read/Write请求;否则即使DMAC内部逻辑就绪,TLP也无法合法发出。

因此,驱动初始化时通常要完成三步:第一,映射BAR,找到DMAC寄存器基址;第二,设置Command寄存器,打开Memory Space Enable和Bus Master Enable;第三,分配并填写描述符,启动DMA。若忘记打开BME,典型现象是寄存器可读写、中断可产生,但DMA传输永远不完成或直接报UR(Unsupported Request)。

三、地址映射:CPU虚拟地址、物理地址与设备地址

DMA传输使用的是设备可见地址,而不是CPU虚拟地址。CPU访问内存用虚拟地址,经MMU翻译为物理地址;而DMAC发出的PCIe TLP中携带的是主机物理地址(或IOMMU翻译后的地址)。因此驱动必须把缓冲区“映射”为设备可用的DMA地址。

在Linux中,常用接口包括:dma_alloc_coherent()用于分配一致性内存,返回虚拟地址和DMA地址;dma_map_single()用于映射已有缓冲区;dma_map_sg()用于散列表映射。若系统启用IOMMU,DMA地址还会经过IOMMU页表二次翻译,驱动拿到的DMA地址是IOVA,而非真实物理地址。

dma_addr_t dma_handle;
void *cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
/* 将 dma_handle 写入DMAC的源/目的地址寄存器 */
writel(lower_32_bits(dma_handle), dmac_base + SRC_ADDR_L);
writel(upper_32_bits(dma_handle), dmac_base + SRC_ADDR_H);

地址映射的常见误区是:直接把virt_to_phys()的结果写给DMAC。这在有IOMMU、有高端内存或使用vmalloc缓冲区时会出错。正确做法是始终使用DMA API,让内核处理缓存一致性和地址翻译。

四、传输方向与描述符、链表机制

DMA传输方向通常分为三种:内存到设备(Mem-to-Dev),如发送数据包;设备到内存(Dev-to-Mem),如接收数据、采集样本;内存到内存(Mem-to-Mem),如加速卡内部搬移或数据重排。方向决定了源/目的地址寄存器的填写方式,也决定了缓存一致性操作的方向:Mem-to-Dev通常需要dma_sync_single_for_device(),Dev-to-Mem则需要dma_sync_single_for_cpu()。

当传输量大、缓冲区分散时,逐个寄存器启动一次DMA效率很低。于是引入描述符(Descriptor):一个描述符就是一条传输指令,包含源地址、目的地址、长度、控制标志和下一个描述符指针。多个描述符通过“下一个指针”串成链表(Descriptor Chain / Ring),DMAC自动取指、执行、推进,直到遇到结束标志或回到环头。

描述符链表带来三个好处:第一,一次配置可搬运大量分散数据;第二,支持Scatter-Gather,把多个不连续物理页拼成一次逻辑传输;第三,支持环形队列,实现生产者-消费者模型,适合网卡、NVMe等高吞吐场景。

DMA传输核心关系:控制路径、数据路径与描述符链表 CPU / 驱动 配置寄存器 填写描述符 DMA控制器 DMAC 源/目的地址寄存器 长度寄存器 · 控制/状态 描述符取指单元 数据搬运引擎 PCIe 总线 / Root Complex Bus Master Enable (BME) Memory Read / Write TLP IOMMU 地址翻译 主机内存 DMA缓冲区(一致性/流式) DMA地址 = 物理地址或IOVA 描述符链表 / 环形队列(Descriptor Ring) Desc 0 src / dst / len flags · next Desc 1 src / dst / len flags · next Desc N src / dst / len flags · next 环形回绕:next 指向环头,持续消费 MMIO 配置 发起 TLP 数据读写 取描述符 控制路径 DMA引擎 数据路径

五、案例与场景:一次网卡接收DMA的完整链路

以PCIe网卡接收数据为例。驱动初始化时分配一组接收缓冲区,并为每个缓冲区填写一个描述符,组成接收环(RX Ring),然后把环基地址写入网卡DMAC。当数据包到达,网卡MAC把数据写入下一个可用描述符指向的缓冲区,同时更新状态位并产生中断。驱动在中断处理中读取描述符状态,把缓冲区交给协议栈,然后重新填充描述符,形成循环。

这个过程中,CPU只处理“描述符状态”和“缓冲区管理”,数据本身由DMAC通过PCIe Write TLP直接写入主机内存。若描述符环耗尽,网卡会丢弃数据包并置位溢出计数;若BME未开,DMAC无法发起写请求,表现为收包中断不来或DMA错误。

关键要素 作用 典型配置/检查点
DMAC 执行数据搬运,推进地址与长度 寄存器基址、通道使能、完成中断
Bus Master Enable 允许Endpoint发起PCIe访问 Command寄存器Bit 2置1
DMA地址映射 把CPU缓冲区转为设备可见地址 dma_alloc_coherent / dma_map_single
传输方向 决定源/目的与缓存同步方向 Mem-to-Dev / Dev-to-Mem / Mem-to-Mem
描述符链表 组织批量、分散、环形传输 src/dst/len/flags/next,环回指针

六、误区与复盘

  • 误区一:认为DMA不需要CPU。CPU仍需配置寄存器、填写描述符、处理完成中断;只是不逐字节搬运数据。
  • 误区二:忽略BME。寄存器能读写不代表能发起总线事务,BME未开时DMA必然失败。
  • 误区三:用虚拟地址或错误物理地址填DMAC。必须使用DMA API返回的DMA地址,并考虑IOMMU。
  • 误区四:忘记缓存同步。流式DMA缓冲区在设备读写前后需要正确的sync操作,否则读到旧数据或覆盖新数据。
  • 误区五:描述符环无回绕或未更新。环形队列必须正确维护生产/消费指针,否则会停摆或覆盖未处理数据。
调试DMA问题时,建议按“BME是否打开 → DMA地址是否正确 → 描述符是否有效 → 方向与缓存同步是否匹配 → 完成状态/中断是否上报”的顺序逐层排查,能快速定位大多数传输失败。

掌握DMAC角色、总线主控、地址映射、传输方向和描述符链表这五个要点,就抓住了PCIe DMA传输的骨架。后续无论是编写驱动、调试FPGA加速卡,还是优化NVMe与网卡吞吐,都可以回到这张骨架上定位问题。