☰
第 1 篇 从 PIO 到 DMA:CPU 为何将数据搬运交由专用硬件
2026/10/14 10:31:10 网站建设 项目流程

本文属于第一册“硬件原理”,回答第 0 篇提出的问题 1:

  • CPU 为何将数据搬运任务交由专用硬件完成?
  • 承担该任务的 DMA 引擎具体是什么?

我们先分析由 CPU 执行数据搬运的代价,说明 DMA 的必要性;随后说明 DMA 引擎的基本工作方式、职责边界与物理形态。文中出现的诸多 DMA API 无需此刻深入,后文会专门讲解,此处留下印象即可。

1. 问题回顾

第 0 篇所设定的场景中,网卡接收到一帧数据后,首先将其暂存于片上包缓冲区(RX FIFO)。该缓冲区容量有限,数据帧须尽快传送至主机内存,由协议栈与应用处理。

完成这一传送存在两种方式:一是由 CPU 逐次读取设备中的数据并写入内存,二是由设备内的硬件引擎直接写入内存。前者称为程序化 I/O(Programmed I/O,PIO),后者即 DMA。要回答问题 1,需要先说明前一种方式的代价所在。

2. 程序化 I/O(PIO)的数据搬运方式

2.1 PIO 的基本过程

PIO 一词有狭义与广义两种用法。狭义的 PIO 专指 x86 平台通过in/out指令访问独立 I/O 端口空间的方式;广义的 PIO 泛指一切由 CPU 执行指令完成数据传送的方式,包括通过 MMIO(Memory-Mapped I/O)访问设备寄存器。现代 PCIe 设备主要使用 MMIO,本文采用广义用法。

设网卡提供以下寄存器:状态寄存器RX_STATUS指示是否有帧到达,长度寄存器RX_LEN给出帧长,数据寄存器RX_DATA为 FIFO 端口,每次读取返回下一个 8 字节,控制寄存器RX_CTRL用于通知网卡该帧已读取完毕。以 PIO 方式接收一帧数据的过程如下:

/* 示意代码:以 PIO 方式从网卡读取一帧数据 */staticu32pio_rx_frame(void__iomem*regs,u64*buf){u32 len,i;while(!(readl(regs+RX_STATUS)&RX_READY))cpu_relax();len=readl(regs+RX_LEN);for(i=0;i<DIV_ROUND_UP(len,8);i++)buf[i]=readq(regs+RX_DATA);/* 每次读取均为一次 PCIe 往返 */writel(RX_DONE,regs+RX_CTRL);returnlen;}

在该过程中,数据经由 CPU 寄存器中转:每 8 字节对应一条 MMIO 读指令与一条普通的内存写指令。数据路径为“设备 → CPU → 内存”,CPU 位于数据路径之上。

2.2 PIO 的三项开销

(1)CPU 占用与数据量成正比。每传送一个访问宽度的数据,CPU 均需执行一次读与一次写。帧长为L LL、单次访问宽度为w ww时,所需 MMIO 读的次数为⌈ L / w ⌉ \lceil L/w \rceil⌈L/w⌉。在此期间,CPU 无法执行协议处理或应用逻辑。

(2)单次 MMIO 读的延迟高,且无法并行。设备寄存器所在的地址区间通常映射为不可缓存(Uncached,UC)类型。原因在于设备寄存器的读写可能带有副作用,例如读取RX_DATA会使 FIFO 弹出下一项数据;若允许缓存或预取,将破坏这一语义。因此,每次 MMIO 读都必须实际到达设备。在 PCIe 上,读请求属于 non-posted 事务:CPU 发出 Memory Read 请求后,须等待设备返回携带数据的完成报文(Completion)。该往返延迟通常为数百纳秒至 1 微秒以上,取决于平台以及设备在 PCIe 拓扑中的位置。此外,UC 类型的访问须严格按程序顺序执行,后一次读须等待前一次读完成,无法通过乱序执行或多次请求并行来隐藏延迟。

(3)总线传输效率低。PCIe 以事务层包(TLP)为传输单位。每个 TLP 除数据载荷外,还包含 12 或 16 字节的头部,以及数据链路层与物理层附加的序号、LCRC 与帧界定符,合计约 20 至 30 字节。一次 8 字节的 MMIO 读需要一个读请求 TLP 与一个完成 TLP,有效载荷占比不足 20%。相比之下,DMA 写入时单个 TLP 的载荷可达最大载荷长度(Max Payload Size,常见为 256 字节),有效载荷占比在 90% 左右。

2.3 量化估算

以 1500 字节的帧、8 字节访问宽度、每次 MMIO 读 0.5 至 1 微秒计算:

项目数值
每帧所需 MMIO 读次数⌈ 1500 / 8 ⌉ = 188 \lceil 1500/8 \rceil = 188⌈1500/8⌉=188次
每帧 PIO 读取耗时约 94 至 188 µs
PIO 可达的接收吞吐约 8 至 16 MB/s,即 64 至 128 Mbps
100Gbps 线速下的帧间隔1538 × 8 / 10 11 ≈ 123 1538 \times 8 / 10^{11} \approx 1231538×8/1011≈123ns

表中帧间隔按以太网线上长度 1538 字节计算,即 1500 字节载荷加 14 字节帧头、4 字节 FCS、8 字节前导码与 12 字节帧间隙。

由此可见,即使 CPU 不执行任何其他任务,PIO 方式可达的吞吐与 100Gbps 线速仍相差约三个数量级。读取一帧所需的约 100 µs 内,线路上将到达近千帧数据,而容量为 1 MB 的 RX FIFO 在 100Gbps 线速下约 80 µs 即被写满,此后到达的帧将被丢弃。该瓶颈由单次访问的延迟决定,而非由总线带宽决定;且由于 FIFO 端口的读取须保持顺序,增加 CPU 核数亦无法改善。

2.4 历史实例与写方向的特殊性

上述问题在历史上已有实际体现。早期 ATA 硬盘接口以 PIO 方式传送数据,PIO mode 4 的传输速率上限约为 16.7 MB/s,且传输期间 CPU 占用率接近饱和。其后的 Ultra DMA 模式由控制器以总线主控方式直接访问内存,UDMA/133 的速率上限达到 133 MB/s,CPU 占用率亦大幅下降。在 Linux 早期版本中,常需通过hdparm -d1显式启用硬盘的 DMA 模式。

需要说明的是,MMIO 写与 MMIO 读的特性并不相同。PCIe 的内存写属于 posted 事务,CPU 发出后无需等待完成报文;若将设备地址区间映射为写合并(Write-Combining,WC)类型,CPU 的多次写入还可合并为一个较大的 TLP。因此,对于少量数据,PIO 写的开销可以接受,后文所述的部分优化即利用了这一特性。但 WC 无法改善读方向的效率,且数据仍须由 CPU 逐条指令写出,接收方向的问题依然存在。

3. DMA 的基本思路

3.1 三个阶段

DMA 的基本思路是:将数据传送交由能够自行发起总线事务的硬件完成,CPU 仅负责与数据量无关的配置与收尾工作。一次 DMA 传输可分为三个阶段:

  1. 配置:CPU 告知 DMA 引擎源地址、目的地址与长度,并启动传输。
  2. 执行:DMA 引擎自行发起总线事务完成数据传送,CPU 在此期间可执行其他任务。
  3. 完成通知:DMA 引擎通过中断或状态位通知 CPU 传输已完成。

与 PIO 相比,CPU 的开销由与数据量成正比,变为每次传输的固定开销;数据路径不再经过 CPU。图 1 对比了两种方式的数据路径。

图 1:PIO 与 DMA 的数据路径对比

3.2 以网卡接收为例

现代网卡通过描述符环(descriptor ring)组织 DMA 传输。描述符是位于主机内存中的一组固定格式的结构,每个接收描述符记录一个接收缓冲区的地址,并预留供网卡回写长度与状态的字段。驱动的准备工作如下:

/* 示意代码:以 DMA 方式准备接收 */structrx_desc{__le64 addr;/* 接收缓冲区的设备地址 */__le16 len;/* 由网卡回写:帧长 */__le16 status;/* 由网卡回写:完成标志等 */__le32 rsvd;};for(i=0;i<RING_SIZE;i++){ring[i].addr=cpu_to_le64(buf_dma[i]);ring[i].status=0;}writel(RING_SIZE-1,regs+RX_TAIL);/* 门铃:告知网卡可用描述符的范围 */

代码中的buf_dma[i]是接收缓冲区在设备视角下的地址,其来源是第 2 篇讨论的主题。对应上述三个阶段,网卡接收一帧数据的过程如下:

  1. 配置:驱动将接收缓冲区的地址填入描述符,并通过一次 MMIO 写更新尾指针寄存器。该寄存器通常称为门铃(doorbell)。一次门铃写入可同时提交多个描述符,其开销分摊至多帧数据。
  2. 执行:帧到达后,网卡通过 DMA 读取下一个可用描述符,获得缓冲区地址;随后以若干个 Memory Write TLP 将帧数据写入该地址;最后回写描述符中的长度与状态字段。内存写属于 posted 事务,网卡无需等待完成报文,可连续发出。
  3. 完成通知:网卡发出 MSI-X 中断。MSI-X 本身同样是一次向特定地址的内存写事务。中断处理程序(在 Linux 中由 NAPI 轮询机制接管后续处理)读取描述符状态,确认数据就绪后将缓冲区交给协议栈,并补充新的接收缓冲区。

在该过程中,CPU 未执行任何数据传送指令,也无需进行 MMIO 读。PCIe 的排序规则保证同一设备发出的 posted 写按序到达,因此当 CPU 观察到描述符状态已更新时,此前写入的帧数据亦已到达内存。但“已到达内存”并不等同于“CPU 读取到的是最新数据”,二者之间的差异即问题 3 所讨论的 cache 一致性问题。

3.3 DMA 引擎的职责边界

DMA 引擎的职能仅为按给定的地址与长度发起读写事务。围绕一次 DMA 传输的其他事项均由系统中的其他部分承担,这些事项构成了本专栏后续各篇的主要内容:

事项是否由 DMA 引擎负责实际承担者相关篇目
按给定地址与长度发起读写事务是DMA 引擎本篇
确定描述符中填写的地址否驱动(经由 DMA API)第 2 篇
保证 CPU cache 与内存数据一致否硬件一致性协议或软件维护第 3、4 篇
校验访问地址的合法性否IOMMU第 6 篇
选择缓冲区的分配与映射方式否驱动第 8 篇
判断缓冲区当前是否可被访问否驱动遵循所有权约定第 9 篇

上表的要点在于:DMA 引擎对其所访问的地址不作任何语义判断。地址错误时,DMA 引擎同样会如实执行写入,从而破坏无关的内存数据。这一特性决定了 DMA 相关问题的排查难度,也说明了 IOMMU 存在的必要性。

3.4 DMA 未节省的开销

DMA 使 CPU 不再参与数据传送,但以下开销依然存在:

  • 内存带宽:数据仍需写入内存,CPU 处理时仍需从内存读取。部分平台(如 Intel 的 DDIO 技术)将 DMA 写入的数据直接置入末级 cache,以减少内存访问,其与一致性的关系将在第 4 篇讨论。
  • 配置与通知的固定开销:描述符准备、门铃写入与中断处理均有固定开销。对于小数据量传输,该固定开销可能超过数据传送本身。因此,部分网卡在发送方向支持将小报文直接内联于描述符中,并通过 WC 映射以 MMIO 写入设备,从而省去设备通过 DMA 读取描述符与数据的两次往返。这一做法利用的正是 2.4 节所述的 MMIO 写特性。

4. DMA 引擎的三种物理形态

“DMA 引擎”是一个功能概念,指能够自行发起内存读写事务的硬件逻辑。其物理实现存在三种主要形态,区别在于由哪一部件发起总线事务,以及该部件与外设功能的耦合程度,如图 2 所示。

图 2:DMA 物理实现的三种主要形态

4.1 独立 DMA 控制器

在该形态中,DMA 控制器(DMAC)是系统总线上的一个独立部件,外设自身不具备发起总线事务的能力。外设通过请求与应答信号线(DREQ/DACK)向 DMA 控制器申请传送,由控制器在外设的数据寄存器与内存之间完成数据传送。对外设而言,这一过程与 CPU 访问其寄存器并无区别。

典型实例是 IBM PC 中的 Intel 8237。该控制器配合页寄存器仅能产生 24 位地址,即只能访问物理内存的低 16 MB。Linux 中的ZONE_DMA即源于这一限制,第 11 篇讨论的 DMA 掩码问题亦可追溯至此。在现代 SoC 中,ARM PL330、STM32 DMA 等控制器为 UART、SPI、I2S 等低速外设提供服务。此外,Intel 的 I/OAT 与 DSA 属于用于内存到内存拷贝的独立引擎,同样归入此类。

Linux 通过 dmaengine 框架管理此类控制器。控制器驱动作为提供者注册 DMA 通道,外设驱动作为使用者,通过dma_request_chan()获取通道,再依次调用dmaengine_slave_config()、dmaengine_prep_slave_sg()、dmaengine_submit()与dma_async_issue_pending()配置并启动传输。

4.2 总线主控外设

在该形态中,外设自身即可发起总线事务,即总线主控(bus master)。PCI/PCIe 设备普遍采用这一形态,网卡、NVMe 控制器、AHCI 控制器均属此类。PCI 配置空间 Command 寄存器的第 2 位为 Bus Master Enable,该位置位之前,设备不得发出内存请求。Linux 驱动通过pci_set_master()置位该位。

此类设备的 DMA 逻辑与设备自身的协议数据结构紧密耦合:网卡使用描述符环,NVMe 使用提交队列与完成队列(SQ/CQ)以及 PRP/SGL 描述数据缓冲区,AHCI 使用命令列表与 PRDT。驱动不经过 dmaengine 框架,而是直接调用 DMA 映射 API(如dma_alloc_coherent()、dma_map_single()、dma_map_sg())获得设备地址,再将其填入设备特定的数据结构。该形态是 PCIe 系统中 DMA 的主要形式,也是本专栏第三册与第四册讨论的主要对象。

4.3 设备内的专用拷贝引擎

GPU 与部分加速器内部集成了通用的拷贝引擎,如 AMD GPU 的 SDMA(System DMA)与 NVIDIA GPU 的 Copy Engine。从 PCIe 总线的角度看,GPU 仍是一个总线主控设备;但此类引擎具有以下不同于 4.2 节的特点:

  • 通用性:引擎不与特定的设备协议绑定,而是执行驱动提交的拷贝、填充等通用命令。驱动将命令包写入环形缓冲区,再通过门铃通知引擎执行。
  • 地址范围:源与目的可以均位于显存,可以分属显存与系统内存,在支持 P2P 的平台上还可以是另一设备的 BAR 空间。显存内部的拷贝不经过系统总线。
  • 并行性:拷贝引擎与计算单元相互独立,数据传送可与计算重叠执行。

以 amdgpu 驱动为例,SDMA 环用于 TTM 在显存与系统内存之间迁移缓冲区(amdgpu_copy_buffer())、更新 GPU 页表等任务。异构场景下的相关问题将在第五册讨论。

4.4 三种形态的比较

形态典型实例由谁发起总线事务编程方式Linux 对应框架
独立 DMA 控制器8237、PL330、STM32 DMA、Intel DSADMA 控制器配置通道寄存器或描述符链dmaengine
总线主控外设网卡、NVMe、AHCI外设自身设备特定的描述符或队列pci_set_master()与 DMA 映射 API
设备内的专用拷贝引擎AMD SDMA、NVIDIA Copy Engine设备内的拷贝引擎命令包与环形缓冲区设备驱动自行管理(如 amdgpu 的 SDMA 环)

三种形态在 CPU 与 DMA 引擎的分工上是一致的:CPU 负责配置,引擎负责传送,传送完成后通知 CPU。区别仅在于引擎所处的位置及其编程接口。

5. 常见误区

5.1 DMA 并非内存拷贝函数

memcpy()是同步操作:由 CPU 执行,函数返回时拷贝已经完成。DMA 则是异步操作:CPU 仅提交传输请求,数据传送由另一个总线发起者独立完成,CPU 须通过中断或轮询获知完成。由此产生两项约束:

  • 缓冲区在传输期间必须保持有效。若驱动在 DMA 完成前释放或重用缓冲区,设备仍会向原地址写入数据,从而破坏已分配给其他用途的内存。此类错误的现象与成因往往相距甚远,难以定位。
  • CPU 与设备并发访问同一内存。由此引出 cache 一致性(问题 3)与缓冲区所有权交接(问题 9)等问题。

同理,Linux 中dma_map_single()等映射接口并不传送数据。其作用是为已有的缓冲区建立设备可访问的地址,并在需要时执行 cache 维护操作。例外情况是反弹缓冲(bounce buffer):当设备无法访问缓冲区所在的物理地址时,映射接口会由 CPU 将数据复制至设备可访问的区域,该机制将在第 11 篇讨论。

5.2 DMA 并不必然快于 CPU 拷贝

对于主机内存之间的拷贝,现代 CPU 单核借助向量指令即可达到每秒十余 GB 的速率,单个 DMA 通道的吞吐未必更高;对于小数据量拷贝,DMA 的配置与通知开销更会占据主导。Linux 曾提供 NET_DMA 机制,利用 Intel I/OAT 引擎卸载 TCP 接收路径上的内存拷贝,后因收益有限及正确性问题于 3.19 版本移除。

DMA 的价值在于以下两点:其一,对于外设数据传送,DMA 避免了高延迟的 MMIO 读,是实现高吞吐的唯一可行方式;其二,DMA 使 CPU 从数据传送中解脱,可在传输期间执行其他任务。因此,评估是否使用 DMA 时,应关注 CPU 时间是否有更有价值的用途,而非单纯比较两者的传送速率。

6. 小结

对问题 1 的回答可归纳如下:

  • CPU 不适合承担外设数据传送。以 PIO 方式传送数据时,CPU 占用与数据量成正比;MMIO 读为不可缓存的 non-posted 事务,单次延迟达微秒量级且只能串行执行;小 TLP 的总线效率亦较低。在 100Gbps 网卡的场景下,PIO 可达的吞吐与线速相差约三个数量级。
  • DMA 将 CPU 的开销由与数据量成正比变为固定开销。CPU 负责配置与收尾,数据由能够自行发起总线事务的硬件直接写入内存,传输过程分为配置、执行与完成通知三个阶段。
  • DMA 引擎仅负责发起读写事务。地址的确定、cache 一致性、访问合法性与缓冲区所有权均由系统的其他部分承担,这些事项构成了本专栏后续各篇的内容。
  • DMA 引擎存在三种物理形态:独立 DMA 控制器、总线主控外设与设备内的专用拷贝引擎。PCIe 系统中以总线主控外设为主。

本文多次提及“设备地址”。网卡在描述符中获得的地址,与 CPU 所使用的虚拟地址、物理地址是否为同一概念,即为问题 2,也是下一篇讨论的内容。


系列导航:

  • 上一篇《第 0 篇 一帧数据的传输路径:本专栏讨论的 15 个核心问题》
  • 下一篇《第 2 篇 总线主控与设备地址:DMA 事务中地址的来源》

关联阅读:

  • 《深入理解 amdgpu SDMA 引擎专栏》:分析了amdgpu GPU 中的 SDMA 引擎和驱动实现

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询