首页 / 资讯中心 / 文章详情

第 1 篇 从 PIO 到 DMA:CPU 为何将数据搬运交由专用硬件

第 1 篇 从 PIO 到 DMA:CPU 为何将数据搬运交由专用硬件 ★ FEATURED ARTICLE
本文属于第一册“硬件原理”回答第 0 篇提出的问题 1CPU 为何将数据搬运任务交由专用硬件完成承担该任务的 DMA 引擎具体是什么我们先分析由 CPU 执行数据搬运的代价说明 DMA 的必要性随后说明 DMA 引擎的基本工作方式、职责边界与物理形态。文中出现的诸多 DMA API 无需此刻深入后文会专门讲解此处留下印象即可。1. 问题回顾第 0 篇所设定的场景中网卡接收到一帧数据后首先将其暂存于片上包缓冲区RX FIFO。该缓冲区容量有限数据帧须尽快传送至主机内存由协议栈与应用处理。完成这一传送存在两种方式一是由 CPU 逐次读取设备中的数据并写入内存二是由设备内的硬件引擎直接写入内存。前者称为程序化 I/OProgrammed I/OPIO后者即 DMA。要回答问题 1需要先说明前一种方式的代价所在。2. 程序化 I/OPIO的数据搬运方式2.1 PIO 的基本过程PIO 一词有狭义与广义两种用法。狭义的 PIO 专指 x86 平台通过in/out指令访问独立 I/O 端口空间的方式广义的 PIO 泛指一切由 CPU 执行指令完成数据传送的方式包括通过 MMIOMemory-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(regsRX_STATUS)RX_READY))cpu_relax();lenreadl(regsRX_LEN);for(i0;iDIV_ROUND_UP(len,8);i)buf[i]readq(regsRX_DATA);/* 每次读取均为一次 PCIe 往返 */writel(RX_DONE,regsRX_CTRL);returnlen;}在该过程中数据经由 CPU 寄存器中转每 8 字节对应一条 MMIO 读指令与一条普通的内存写指令。数据路径为“设备 → CPU → 内存”CPU 位于数据路径之上。2.2 PIO 的三项开销1CPU 占用与数据量成正比。每传送一个访问宽度的数据CPU 均需执行一次读与一次写。帧长为L LL、单次访问宽度为w ww时所需 MMIO 读的次数为⌈ L / w ⌉ \lceil L/w \rceil⌈L/w⌉。在此期间CPU 无法执行协议处理或应用逻辑。2单次 MMIO 读的延迟高且无法并行。设备寄存器所在的地址区间通常映射为不可缓存UncachedUC类型。原因在于设备寄存器的读写可能带有副作用例如读取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 µsPIO 可达的接收吞吐约 8 至 16 MB/s即 64 至 128 Mbps100Gbps 线速下的帧间隔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/sCPU 占用率亦大幅下降。在 Linux 早期版本中常需通过hdparm -d1显式启用硬盘的 DMA 模式。需要说明的是MMIO 写与 MMIO 读的特性并不相同。PCIe 的内存写属于 posted 事务CPU 发出后无需等待完成报文若将设备地址区间映射为写合并Write-CombiningWC类型CPU 的多次写入还可合并为一个较大的 TLP。因此对于少量数据PIO 写的开销可以接受后文所述的部分优化即利用了这一特性。但 WC 无法改善读方向的效率且数据仍须由 CPU 逐条指令写出接收方向的问题依然存在。3. DMA 的基本思路3.1 三个阶段DMA 的基本思路是将数据传送交由能够自行发起总线事务的硬件完成CPU 仅负责与数据量无关的配置与收尾工作。一次 DMA 传输可分为三个阶段配置CPU 告知 DMA 引擎源地址、目的地址与长度并启动传输。执行DMA 引擎自行发起总线事务完成数据传送CPU 在此期间可执行其他任务。完成通知DMA 引擎通过中断或状态位通知 CPU 传输已完成。与 PIO 相比CPU 的开销由与数据量成正比变为每次传输的固定开销数据路径不再经过 CPU。图 1 对比了两种方式的数据路径。图 1PIO 与 DMA 的数据路径对比3.2 以网卡接收为例现代网卡通过描述符环descriptor ring组织 DMA 传输。描述符是位于主机内存中的一组固定格式的结构每个接收描述符记录一个接收缓冲区的地址并预留供网卡回写长度与状态的字段。驱动的准备工作如下/* 示意代码以 DMA 方式准备接收 */structrx_desc{__le64 addr;/* 接收缓冲区的设备地址 */__le16 len;/* 由网卡回写帧长 */__le16 status;/* 由网卡回写完成标志等 */__le32 rsvd;};for(i0;iRING_SIZE;i){ring[i].addrcpu_to_le64(buf_dma[i]);ring[i].status0;}writel(RING_SIZE-1,regsRX_TAIL);/* 门铃告知网卡可用描述符的范围 */代码中的buf_dma[i]是接收缓冲区在设备视角下的地址其来源是第 2 篇讨论的主题。对应上述三个阶段网卡接收一帧数据的过程如下配置驱动将接收缓冲区的地址填入描述符并通过一次 MMIO 写更新尾指针寄存器。该寄存器通常称为门铃doorbell。一次门铃写入可同时提交多个描述符其开销分摊至多帧数据。执行帧到达后网卡通过 DMA 读取下一个可用描述符获得缓冲区地址随后以若干个 Memory Write TLP 将帧数据写入该地址最后回写描述符中的长度与状态字段。内存写属于 posted 事务网卡无需等待完成报文可连续发出。完成通知网卡发出 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 所示。图 2DMA 物理实现的三种主要形态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 的 SDMASystem 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 引擎和驱动实现
阅读完成 · 觉得有帮助?
咨询建站