DMA(Direct Memory Access,直接内存存取)是一种允许外设直接与计算机内存进行数据交换的技术,绕过了CPU的参与。这种机制的优势在于,它能够显著提高数据传输效率,减轻CPU的负担,从而使得计算机能够处理更多的任务和更高的性能要求。

DMA & CPU 基础概念

英文全称

  1. DMADirect Memory Access,直接内存访问
  2. CPUCentral Processing Unit,中央处理器

DMA 是什么📌

DMA(直接内存访问):是计算机主板上的一套硬件控制器,允许外设(硬盘、网卡、显卡)不经过 CPU,直接和内存之间搬运数据
典型场景:硬盘读文件、网卡接收网络数据包。
  • 没有 DMA:硬盘→CPU→内存,全部拷贝搬运工作由 CPU 亲自完成
  • 有 DMA:硬盘 ↔ 内存,DMA 控制器接管数据搬运,CPU 不参与数据复制

为什么 DMA 可以让 CPU “偷懒”👍

没有 DMA 的时候:

外设要读写内存数据,每一个字节都要经过 CPU 中转
 
CPU 要反复执行搬运指令,把数据从外设读到 CPU 寄存器,再写回内存,大量时间消耗在简单的数据拷贝,没法运算别的任务。

使用 DMA 工作流程:

  1. CPU 只做一次前期配置:告诉 DMA 控制器:数据从哪来、写到内存哪个地址、传输多少字节。配置完成 CPU 就可以去干别的计算任务。
  2. DMA 硬件接管总线,外设和内存之间直接批量搬运数据,搬运全过程不需要 CPU 介入
  3. DMA 传输全部完成后,给 CPU 发一个中断信号,通知 CPU:数据搬运完毕,可以处理这份数据了。
💡核心:CPU 只负责下达命令,枯燥重复的数据拷贝交给 DMA 硬件完成,CPU 解放出来执行程序运算,所以说 DMA 让 CPU 偷懒。

通俗类比

  • CPU:办公室主管;DMA:专职搬运工
  • 没有 DMA:主管亲自一箱一箱搬货物(硬盘的数据),搬货的时候没法处理文件;
  • DMA 模式:主管告诉搬运工,货物从仓库搬到几号货架,然后主管回去办公;搬运工自己搬完全部货物,敲一下门告诉主管 “搬完了”。

补充区分关键

  1. DMA不做数据计算,只做搬运,不会解析、修改数据,只负责数据复制转移。
  2. 传输结束依靠中断通知 CPU,CPU 再处理已经在内存里的数据。
  3. 现代计算机有 DMA 控制器,网卡、NVMe 硬盘、显卡都大量使用 DMA 提升 IO 性能。

简短背诵版(考试可用)

DMA 全称 Direct Memory Access 直接内存访问。DMA 控制器允许外设直接与内存交换数据,CPU 仅需初始化传输参数,数据传输由 DMA 硬件独立完成,传输结束通过中断通知 CPU,免除 CPU 大批量数据拷贝负担,释放 CPU 算力。
 
CPU 全称 Central Processing Unit 中央处理器。

DMA(Direct Memory Access)技术演进

核心主线:第三方共享 DMA → 总线主设备 DMA (Bus‑Master) → Scatter‑Gather 分散‑聚集 DMA → IOMMU 虚拟化 DMA → SoC 片上高级 DMA
 
本质演进目标:减少 CPU 干预、支持碎片化内存、提升传输带宽、增加 IO 安全隔离、适配虚拟化与多核异构。

1. 起源:大型机时代 DMA 雏形(1950‑1970)

代表机器:IBM‑709 大型机
  • 背景:早期全部 IO 都需要 CPU 参与搬运,大批量 IO 严重挤占 CPU 算力。
  • 实现:IO 通道 (I/O Channel),独立硬件单元,和 CPU 并行访问内存。
  • 特点:价格昂贵,只用于大型商用主机,没有进入微型计算机领域。
思想:把数据搬运工作交给独立硬件,CPU 只管下达任务,DMA 核心思想在此诞生。

2. 第一代:第三方 DMA(Third‑Party DMA / System DMA,8237A,ISA 总线,PC/XT 时代)

代表芯片:Intel 8237A,IBM‑PC 标配 DMA 控制器芯片
  • 架构:主板上一个全局共享 DMA 控制器芯片,所有 ISA 外设(声卡、软盘)共用这 4 路 DMA 通道。
  • 工作模式:外设不能直接抢总线,必须向 8237 申请,由 DMA 控制器接管总线完成内存‑外设搬运。
  • 局限⚠️
    1. 仅 20 位地址,只能访问 1MB 内存空间;32MB 以上高端内存无法访问,必须用弹跳缓冲区 Bounce Buffer 做内存拷贝,带来额外开销。
    2. 只支持物理连续内存块传输;每传完一块就触发一次中断,大量数据会产生巨量中断。
    3. 带宽很低,不适合硬盘、网卡等高速外设。
  • 结局:ISA 总线淘汰后,传统 8237 DMA 仅做兼容遗留,不再用于高速设备。

3. 第二代:第一方 DMA Bus‑Master DMA(PCI 总线时代,1990 年代)总线主模式

First‑Party DMA,也叫总线主控 DMA
  • 重大改变:DMA 控制器不再是主板上单独芯片,DMA 控制器直接集成在外设自身(网卡、SCSI 硬盘卡)。外设自己就可以申请总线控制权,直接访问内存,不再依赖中央共享 DMA 芯片。
  • PCI 总线支持 Bus‑Master,设备自己掌握 DMA 逻辑。
  • 优势:带宽大幅提升,摆脱 8237 的地址上限;适合高速网卡、磁盘控制器。
  • 遗留缺陷:
    1. 仍然要求缓冲区物理内存连续;操作系统内存碎片化后,很难分配大块连续物理内存。
    2. 设备直接访问物理内存地址,没有内存保护;驱动 bug、恶意硬件可以读写全部系统物理内存,存在安全风险。

4. 第三代 Scatter‑Gather DMA(SG‑DMA,分散‑聚集 DMA,PCI 后期广泛使用)

解决痛点:操作系统内存碎片化,很难分配大块物理连续内存。
  • 原理:驱动在内存中构造描述符链表,链表每一项记录一段分散物理内存的地址 + 长度。DMA 硬件读取描述符链表,自动把多段不连续物理内存当作一段逻辑连续的数据进行传输。
  • 关键优势:
    1. 不再要求传输缓冲区物理连续,完美适配操作系统分页内存;
    2. 多段数据传输只产生一次中断,极大降低中断开销;
    3. Linuxsendfile零拷贝技术底层就依赖 SG‑DMA,减少 CPU 拷贝开销。
现在绝大多数网卡、NVMe SSD 都标配 Scatter‑Gather DMA。

5. 第四代 IOMMU 增强 DMA(VT‑d / SMMU,2006 年后,x86/ARM64 虚拟化时代)

传统 Bus‑Master DMA 设备直接访问物理地址,安全漏洞大,不适合虚拟机。
 
IOMMU:IO 内存管理单元,类比 CPU 侧的 MMU。
  • 新增能力:IO 虚拟地址 IOVA → 物理地址 PA 的硬件地址翻译
  1. 给外设看到的是 IO 虚拟地址,设备不能直接触碰真实物理内存;
  2. 做内存访问权限隔离:限制设备只能访问分配给它的内存页,不能越权读写其他内存;
  3. 支持虚拟机直通设备,虚拟机内部驱动给的虚拟地址,IOMMU 翻译成主机物理地址;
  4. 不再强制要求物理连续内存,IOMMU 硬件完成地址转换。
  • Intel 叫 VT‑d;ARM 叫 SMMUv3;AMD 叫 IOMMU。
⚠️关闭 IOMMU,裸机 DMA 设备可遍历全部主机内存,有安全风险。

6. 现代片上高级 DMA(SoC 嵌入式、PCIe 4.0/5.0,2010 至今)

PC / 服务器 PCIe DMA

  • PCIe 继承 Bus‑Master+SG‑DMA,配合 IOMMU;支持相干 DMA,可直接操作 CPU Cache,减少刷 Cache 软件开销;大量用于 GPU、FPGA 加速卡、NVMe、高速网卡。

ARM SoC 内置 DMA:EDMA / UDMA / BDMA

  • 片上多通道 DMA,内存与串口、SPI、ADC、NPU 之间搬运;支持链表描述符;不需要经过系统总线绕远。
  • 异构计算场景:CPU 告诉 DMA,把 DDR 数据直接搬到 NPU 内部 SRAM,CPU 全程休眠,实现异构零拷贝。

DMA 演进对比总表

阶段 名称 核心特征 主要痛点 代表时代
0 IO 通道 大型机独立 IO 硬件,并行传输 成本极高,不面向微机 1950‑70 大型机
1 第三方 DMA 8237A 主板全局共享 DMA 控制器,外设共用通道 地址受限、必须连续内存、带宽低 ISA 总线,IBM‑PC
2 Bus‑Master 第一方 DMA 外设自带 DMA 控制器,自己抢占总线 必须物理连续内存;直接访问物理内存无保护 PCI 总线
3 Scatter‑Gather SG‑DMA 描述符链表,支持多段碎片化内存,一次中断 依然直接操作物理内存,缺少权限隔离 PCI、早期 PCIe
4 IOMMU DMA IO‑MMU 硬件地址翻译,IOVA 虚拟地址,权限隔离 硬件复杂,有少量硬件开销 虚拟化、PCIe 服务器
5 SoC 片上高级 DMA 多通道、链表 DMA、异构搬运、Cache‑Coherent DMA 平台定制化,不同 SoC 实现差异大 移动 SoC、FPGA、PCIe4.0/5.0

💡关键总结

  1. DMA 演进的两个主线:性能提升(带宽、减少中断、支持碎片化内存);安全隔离(IOMMU 引入 IO 虚拟地址,防止设备越权访问内存)。
  2. 8237 老式第三方 DMA 已经淘汰,现代高速设备全部是Bus‑Master + Scatter‑Gather DMA;服务器 / 虚拟化环境打开 IOMMU 做保护。
  3. DMA 始终只做数据搬运,不运算数据;CPU 工作永远是配置描述符链表,DMA 硬件搬运,完成中断通知 CPU

DMA 在操作系统层面演进

硬件 DMA 演进:8237 第三方 DMA → Bus‑Master → Scatter‑Gather → IOMMU;
 
操作系统侧核心矛盾:虚拟内存分页机制 vs DMA 只能识别物理地址、内存碎片化、Cache 一致性、虚拟化安全、异构设备内存共享。
 
重点覆盖 DOS、Windows、Linux、BSD,按时代梳理。

一、DOS 实模式时代(1980‑1995)

硬件基础:Intel 8237A(ISA 第三方 DMA)
  1. 系统特点:CPU 实模式,无内存分页,直接操作物理内存。
  2. DMA 工作:驱动直接操作 8237 端口寄存器编程;DMA 只能访问16MB 以下物理内存,缓冲区不能跨越 64KB 边界。
  3. 重大局限
    • 没有虚拟内存,没有分页;高于 16MB 内存不能直接 DMA
    • DOS 原生不支持 Bus‑Master PCI DMA,硬盘只能 PIO 模式,速度很低,UDMA 需要第三方驱动补丁才能启用;
    • 没有统一内核 DMA 子系统;每个硬件驱动自己直接操作硬件寄存器,驱动强耦合硬件。
  4. 问题:驱动写错寄存器直接系统崩溃,没有保护隔离。
小结:DOS 下 DMA 是裸硬件操作,无内核抽象层,受 8237 硬件硬限制。

二、Windows 系统 DMA 演进

Windows 95 / Windows98(V86 保护模式混合,1995‑2000)

  1. 同时兼容老式 ISA 系统 DMA(8237 模拟)和 PCI Bus‑Master DMA。
  2. 缺陷:
    • 内核没有完整统一 DMA 抽象;驱动要区分传统 System‑DMA 和 Bus‑Master;
    • 对 Scatter‑Gather 支持不完善;
    • 大量驱动 bug 会直接蓝屏;UDMA 总线主控需要手动在设备管理器开启。
  3. 没有 IOMMU 支持;设备直接访问全部物理内存,无安全隔离。

Windows 2000 / XP(真正保护模式内核,2000‑2010)

HAL(硬件抽象层)封装 DMA,操作系统提供统一 DMA 内核 API,驱动不再直接写硬件寄存器Microsoft ...。
  • 支持 4 类 DMA 接口:
     
    1)System‑DMA:模拟 8237 传统 ISA DMA;
     
    2)Packet‑DMA 数据包 DMA;
     
    3)Scatter‑Gather 分散聚集 DMA(PCI 设备网卡、SCSI);
     
    4)Common‑Buffer 共用缓冲区 DMA。
  • HAL 自动处理Bounce Buffer 弹跳缓冲区:当 32 位设备无法访问高于 4GB 内存,内核自动复制到低地址缓冲区,对驱动透明。
  • 局限:无 IOMMU 强制保护;关闭 IOMMU 情况下恶意 PCIe 设备可以 DMA 读取全部系统内存。

Windows Vista‑Win7(2007‑2012)

  1. 完善 VT‑d(Intel IOMMU)驱动框架,支持设备 DMA 地址翻译 IOVA。
  2. KMDF 驱动框架标准化 DMA 接口;新增DMA 验证器,内核调试检测驱动 DMA 使用错误(未 unmap、缓存同步错误)Microsoft ...。

Windows 8 / 10 / Windows11(2012 至今,SoC + 虚拟化安全)

  1. 全面支持 SoC 共享 DMA 控制器(System‑DMA),适配 ARM 平板片上 DMA;
  2. 内核 DMA 保护(Kernel DMA Protection):强制 IOMMU,阻止雷电 3/4 外接恶意 PCIe 设备 DMA 越权读写内存,安全防护成为默认选项Microsoft ...;
  3. Hyper‑V 深度对接 IOMMU,支持 PCIe 设备直通虚拟机,DMA 地址隔离;
  4. 完整支持 PCIe SG‑DMA、MSI‑X 中断、Cache‑Coherent DMA。
Windows 演进主线:裸硬件操作 → HAL 抽象封装 → SG‑DMA 支持 → IOMMU 安全隔离 → Kernel DMA Protection 安全加固。

三、Linux 内核 DMA 子系统演进(里程碑版本)

Linux 最大难点:跨 x86、ARM、RISC‑V 多架构,同时支持传统 ISA、PCI、PCIe、各类 SoC 片上 DMA,还要处理 swiotlb 弹跳缓冲、IOMMU、虚拟化、异构共享缓冲区。

1)Linux 2.0‑2.4(1990‑2003,早期内核)

  • DMA API 高度架构相关,x86、ARM 驱动接口不一样;驱动要手动处理物理地址;
  • 简单 Scatter‑Gather 支持;Bounce Buffer 实现在块设备层blk_queue_bounce
  • 没有统一 dma_map_* 系列 API;驱动经常错误使用virt_to_phys()直接给 DMA 硬件,在 IOMMU 环境直接失效。

2)Linux2.6 时代重大重构(2003‑2011)

通用 DMA Mapping 层诞生(里程碑)
  • 引入统一dma_map_single() / dma_map_sg() / dma_unmap_*一套 API,驱动不再关心底层硬件是 8237、Bus‑Master、有没有 IOMMU;内核层自动处理:
    • 硬件支持 IOMMU:生成 IOVA 虚拟 IO 地址;
    • 没有 IOMMU、设备寻址能力不足:自动调用SWIOTLB(软件 IO‑TLB)弹跳缓冲,对驱动透明完成拷贝,替代早期块层 bounce bufferThe Linux ...。
  • 区分Streaming DMA流式 DMA(大块 IO)、Coherent DMA一致 DMA(小控制结构),处理 Cache 一致性刷新;
  • DMA‑Engine 子系统:统一抽象片上内存‑内存 DMA,PL330 等 SoC DMA 控制器统一框架,驱动不需要关心具体 DMAC 硬件型号。

3)Linux 3.x‑4.x(2011‑2020,虚拟化、异构)

  1. IOMMU 子系统深度和 DMA‑mapping 融合,Intel VT‑d / ARM SMMUv3 完整支持;
  2. dma‑buf 子系统:DMA 缓冲区共享框架,GPU、视频编解码、NPU 多设备之间共享 DMA 缓冲区,fd 跨进程传递,实现异构零拷贝,安卓、DRM 显卡大量依赖此框架The Linux ...;
  3. NUMA 架构优化 DMA 内存分配,优先分配设备就近节点内存;
  4. 强化 dma_mask 设备寻址掩码机制,严格限制设备可访问地址范围。

4)Linux5.x‑6.x(2020‑至今)

  1. SWIOTLB 性能优化,支持动态扩展弹跳缓冲区;
  2. 6.16 内核:移除旧块层blk_queue_bounce(),弹跳缓冲全部收敛至 SWIOTLB 统一路径,消除两套 bounce 逻辑带来的 bug;
  3. 支持机密计算(AMD‑SEV / Intel‑TDX):加密内存场景,DMA 强制走 SWIOTLB 未加密弹跳池,外设不能直接访问虚拟机加密内存;
  4. RISC‑V 平台完善 DMA Mapping 与 IOMMU 支持。
💡Linux 最重要思想:驱动永远不能直接用 virt_to_phys 给 DMA 硬件;全部使用 dma_map 接口,由内核判断使用 IOMMU 硬件翻译,还是 SWIOTLB 软件弹跳缓冲

四、BSD 系列(FreeBSD)

  1. 早期:驱动直接操作 DMA 硬件;
  2. FreeBSD5‑7:引入统一busdma子系统,BSD 的 DMA 抽象层,类似 Linux dma‑mapping:
    • 封装 Scatter‑Gather、bounce buffer;驱动提交虚拟内存,busdma 自动生成硬件可使用 DMA 地址;
    • 支持 x86 VT‑d,ARM SMMU;
  3. 现代 FreeBSD:PCIe 设备、NVMe 网卡全部依托 busdma;支持设备直通虚拟化 DMA 隔离。

五、操作系统 DMA 演进核心趋势总览表

时代 关键痛点 操作系统解决手段
DOS 实模式 DMA 只能访问低 16MB 内存,驱动裸操作硬件 无内核抽象,驱动直接操作 8237 寄存器
早期保护模式 Win9x / 老 Linux 虚拟内存分页,DMA 只认物理地址 Bounce Buffer 弹跳缓冲,拷贝到设备可访问物理内存
2000 年代中期 碎片化内存,需要大量连续物理内存 内核提供 Scatter‑Gather API,把离散页组装成描述符链表
2000‑2010 多架构,驱动要写多套 DMA 代码 统一 DMA Mapping 抽象层,硬件差异在内核屏蔽
虚拟化时代 设备 DMA 可以越权读写主机内存 对接 IOMMU,IOVA 虚拟 IO 地址做权限隔离
异构计算(GPU/NPU/Codec) 多硬件之间 DMA 缓冲区反复拷贝 dma‑buf(Linux),实现多设备共享 DMA 缓冲区
机密计算 内存加密,外设不能直接访问加密内存 SWIOTLB 弹跳池,DMA 仅操作未加密缓冲区

💡核心总结

  1. 硬件 DMA 只认物理地址,完全不懂 CPU 侧虚拟分页,这是操作系统 DMA 所有复杂性根源。
  2. 操作系统做两件关键事:
    • ①软件适配:Bounce Buffer/SWIOTLB 解决设备寻址能力不足;Scatter‑Gather 解决内存碎片化;
    • ②安全隔离:IOMMU 把设备看到的 IOVA 和真实物理地址隔离,防止恶意 DMA 篡改内存。
  3. Linux:dma_map_*系列 API + SWIOTLB + IOMMU + dma‑buf;Windows:HAL DMA 抽象 + Kernel DMA Protection。

 

DMA 深度拆解:结构 · 底层原理 · 依赖关系 · 逻辑链路

DMA = Direct Memory Access,直接内存访问。 一句话本质:让"数据"在【外设 ↔ 内存】之间自己流动,把 CPU 从"搬运工"的岗位上解放出来,只保留它"指挥官"的职责。 下面按你要的四个维度逐层剖开,并在每层标注"它依赖谁、信号怎么流",最终串成可"在脑中跑一遍"的端到端链路。

〇、破题:先纠正一个根深蒂固的误解

❌ 流行说法:"DMA 传输不需要 CPU。"
✅ 准确说法:"DMA 传输不需要 CPU 搬运数据,但需要 CPU 发起、配置、收尾。"
CPU 在 DMA 里只干三件"轻活":① 配置(告诉 DMA 控制器:从哪搬、搬到哪、搬多少、怎么搬)→ ② 启动(按下开始键)→ ③ 收尾(收到完成中断后做后处理)。中间成千上万字节的逐字节搬运,CPU 一个周期都不参与。这才是"直接"二字的真义——数据通路绕过了 CPU 的执行单元与寄存器,而非绕过了 CPU 的存在。

为什么需要 DMA:PIO vs DMA(动机对比)

对比项 PIO(Programmed I/O,程序控制 I/O) DMA(直接内存访问)
数据通路 外设 → CPU 寄存器 → 内存 外设 ↔ 内存(直连
每字节 CPU 开销 1 次读 + 1 次写 + 循环判断 0(仅首尾各 1 次)
传输 1MB 的 CPU 占用 几乎 100%,CPU 被"绑死" < 0.1%,CPU 可并行算别的
瓶颈 CPU 速度 = 传输速度上限 总线/内存带宽 = 上限
适用 极少量、控制类数据 大块、高速、流式数据(磁盘/网卡/声卡/显存)
洞见:DMA 的本质是"用硬件状态机替代 CPU 的搬数据循环"。 CPU 的 for 循环被固化进了 DMA 控制器里的"地址自增 + 计数自减 + 总线驱动"逻辑。

一、结构拆解:DMA 系统由哪些"零件"组成

1.1 六大部件总览

   ┌──────────────────────────────────────────────────────────────┐
   │                       系 统 总 线 (System Bus)                 │
   │        地址总线(AB)  ‖  数据总线(DB)  ‖  控制总线(CB)            │
   └───┬──────────┬──────────┬────────────────────┬───────────────┘
       │          │          │          │          │
   ┌───┴───┐  ┌───┴────┐  ┌──┴───┐  ┌──┴───┐  ┌───┴────┐
   │  CPU  │  │  DMAC  │  │ 内存  │  │ 外设  │  │总线仲裁器│
   │(主设备)│  │(主设备) │  │(从设备)│  │(从/主)│  │ Arbiter │
   └───────┘  └────────┘  └──────┘  └──────┘  └────────┘
   发起/收尾   真正搬运     数据仓库   数据源头   决定"谁此刻
   配置DMAC   数据的人     被读写     /目的地    掌控总线"
部件 角色 关键内部结构
CPU 总线主设备 / 指挥官 配置 DMAC 寄存器、响应完成中断
DMAC(DMA Controller) 第二总线主设备 / 搬运工 通道、寄存器组、地址生成器、计数器、状态机
内存 从设备 / 数据仓库 提供物理地址空间(或经 IOMMU 的 IOVA 空间)
外设 从设备 或 主设备 发出 DREQ 请求 / 或自己当主设备发起传输
总线仲裁器 交通警 在 CPU 与 DMAC(及设备)间裁决总线归属
系统总线 数据高速公路 地址/数据/控制三组线,传输的物理载体

1.2 DMAC 的"内脏":寄存器组(核心中的核心)

每个 DMA 通道(Channel) 都有一套寄存器,CPU 配置它们 = 下达搬运命令:
寄存器 作用 传输中的变化
SAR(Source Address Reg,源地址) 数据从哪读 每传一拍 自动 +1/+N
DAR(Dest Address Reg,目的地址) 数据写到哪 每传一拍 自动 +1/+N
TCR / BCR(Transfer/Byte Count Reg,计数) 还要传多少 每传一拍 自动 −1,归零=完成
CCR(Control/Config Reg,控制) 传输方向、数据宽度、地址增减、模式、是否自增 静态配置
CSR(Status Reg,状态) 完成/错误/半完成标志 硬件置位,CPU/中断读
这就是"硬件状态机替代 for 循环"的物化:SAR++DAR++TCR--if(TCR==0) finish() 这四行伪代码,被焊死在了 DMAC 的硅片里。

1.3 三种物理形态(结构演进,必须分清)

形态 结构特征 谁当总线主设备 典型代表
① 独立 DMAC 芯片 外挂芯片,CPU 写其寄存器,外设发 DREQ DMAC Intel 8237(ISA 时代,4 通道,24 位地址)
② SoC 集成 DMA 引擎 片内通用 DMA 控制器,多通道,支持链表描述符 DMA 引擎 各 SoC 的 GDMA/EDMA/PL330
③ 设备 Bus-Mastering 无中央 DMAC,设备自己内置 DMA 引擎,直接发起总线事务 设备自身 PCIe 网卡/NVMe/GPU(PCIe 设备天然是 bus master)
关键演进:从"中央搬运工(DMAC)"到"人人皆主设备(bus mastering)"。 形态③里,"DMA 控制器"被拆散塞进了每个设备内部,结构上更分布式、带宽更高、延迟更低。

二、底层原理:数据究竟是怎么"自己流过去"的

2.1 核心机制:DMAC 在传输期"篡位"成总线主设备

正常时 CPU 掌控总线(发地址、发读写命令)。DMA 传输时发生主设备切换
  1. 外设(或软件)向 DMAC 发 DREQ(DMA Request);
  2. DMAC 向总线仲裁器发 HOLD / Bus Request("我要用总线");
  3. 仲裁器裁决后,让 CPU 释放总线,回 HLDA / Bus Grant 给 DMAC;
  4. DMAC 接管总线:它驱动地址总线(SAR/DAR)、驱动控制信号(MEMR/MEMW/IOR/IOW),数据在内存与外设间直接流过数据总线,CPU 全程旁观;
  5. 一拍传完,DMAC 释放或继续持有总线,更新 SAR/DAR/TCR;
  6. TCR 归零 → DMAC 发完成中断 → CPU 收回总线做收尾。
"直接"的物理含义:数据总线上的那一拍,两端是【内存】和【外设】,中间没有 CPU 寄存器中转。

2.2 总线占用策略:Cycle Stealing vs Burst(微妙但重要)

策略 行为 影响
Burst(突发/块传输) DMAC 一旦拿到总线就霸占到整块传完才释放 吞吐高,但 CPU 可能被"饿死"一段
Cycle Stealing(周期窃取/单传输) DMAC 每传 1 拍就还总线,CPU 插空跑 1 拍,再窃 1 拍 CPU 不被饿死,但传输被拉长、有切换开销
Transparent(透明) 只在 CPU 不访问总线的空隙偷传 对 CPU 完全无感,但传输率不可控
现代高速总线(PCIe、AXI/CHI)用仲裁 + 突发 + 乱序 + 多 outstanding 事务,已不是简单的"窃取/霸占"二分,而是多主设备并发、按 QoS 调度

2.3 传输模式

模式 说明
内存 ↔ 外设 最典型:磁盘/网卡与内存互搬
内存 → 内存 DMAC 同时驱动源读+目的写,CPU 完全不碰数据(memcpy 卸载)
外设 → 外设 较少见
Scatter-Gather / 链表描述符 DMAC 从内存读"描述符链表",自动串起多段不连续 buffer,无需 CPU 中途干预(见链路 B)

2.4 现代最隐蔽的原理坑:Cache 一致性

这是 80 年代教科书不讲、但今天最致命的一环。问题场景:
CPU 之前读过地址 X,X 的旧值躺在 CPU cache 里。现在 DMA 把新数据直接写进内存的 X(绕过 cache)。CPU 再读 X → 命中 cache → 读到旧值 → 错!
三种解决策略(底层原理的"高级篇"):
策略 机制 代价 适用
① 软件管理(non-coherent) 驱动在 DMA 前 flush(写回)cache、DMA 后 invalidate(作废)cache CPU 开销、易写错 老总线、低成本 SoC
② 硬件一致性(coherent interconnect) 设备 DMA 端口参与 snoop 协议(ARM ACE/CHI、Intel QPI/UPI),写内存时自动同步/作废 CPU cache 硬件复杂、硅面积 高性能 SoC、CPU 挂载设备
③ Non-cacheable 映射 把 DMA buffer 映射成不可缓存/Write-Combine 牺牲 cache 性能 简单场景、显存/帧缓冲
洞见:DMA 的"直接写内存"与 CPU 的"缓存内存"是两套并行的内存视图,一致性协议就是这两套视图的"对账机制"。 没有它,DMA 越快、错得越隐蔽。

三、依赖关系:DMA 能工作,到底"仰仗"谁

DMA 不是孤立模块,它站在一整条依赖栈上。任何一环缺失,DMA 要么不工作,要么"工作但出错"。

3.1 依赖栈(自底向上,标 硬/软 依赖)

┌─────────────────────────────────────────────────────────┐
│  L6 驱动/描述符  软件准备 buffer、描述符环、配置寄存器      [软]
├─────────────────────────────────────────────────────────┤
│  L5 中断子系统  完成/错误通知 CPU(GIC/APIC/MSI-X)        [硬]
├─────────────────────────────────────────────────────────┤
│  L4 Cache 一致性 snoop / flush-inv / coherent interconnect[硬*]
├─────────────────────────────────────────────────────────┤
│  L3 IOMMU/SMMU  IOVA→PA 翻译 + 访问隔离/保护             [硬*]
├─────────────────────────────────────────────────────────┤
│  L2 总线仲裁     裁决 CPU/DMAC/设备 谁掌控总线            [硬]
├─────────────────────────────────────────────────────────┤
│  L1 物理总线     地址/数据/控制线 + 时钟 + 电源域          [硬]
└─────────────────────────────────────────────────────────┘
        * 标"硬*"者:在简单/老系统可退化为软件方案,现代系统为硬依赖

3.2 依赖明细表("没有它会怎样")

依赖 类型 DMA 对它的依赖 缺失/失效的后果
物理总线 + 时钟/电源 数据/地址/控制的唯一载体 完全无法传输
总线仲裁器 让 DMAC/设备能"合法"成为主设备 总线冲突 / 拿不到总线 / 死锁
内存可寻址性 DMA 用物理地址或 IOVA 寻址 写到错误地址 → 内存破坏
IOMMU / SMMU 硬* ① 地址翻译(IOVA→PA)② 隔离保护(防设备越界 DMA)③ 虚拟化(GPA→HPA) 无隔离→DMA 攻击可读写任意物理内存;无虚拟化→设备无法直通
Cache 一致性 硬* 保证 CPU 视图与 DMA 视图一致 数据错乱(最隐蔽的 bug)
中断控制器 传输完成/半完成/错误 → 通知 CPU CPU 只能轮询,DMA 的"解放 CPU"优势大打折扣
描述符内存可访问 scatter-gather 时 DMAC 要读内存里的描述符(这本身是一次 DMA 读!) 链表断裂 / 取错描述符
驱动软件 分配一致内存、建描述符环、配寄存器、处理中断 DMA 引擎空转 / 配置错误

3.3 依赖关系图(谁触发谁)

   驱动(软)
     │ 配置寄存器 / 提交描述符 / 写 doorbell
     ▼
   DMAC/设备 ──申请总线──▶ 总线仲裁器 ──授权──┐
     │                                       │
     │ 读描述符/搬数据(用 IOVA)               │ 拿到总线
     ▼                                       ▼
   IOMMU ──翻译+检查──▶ 物理内存 ◀──一致性协议──▶ CPU Cache
     │
     │ 完成/错误
     ▼
   中断控制器(MSI-X/GIC) ──中断──▶ CPU(收尾)
一个常被忽略的依赖闭环:scatter-gather 模式下,DMAC 必须先"DMA 读"内存中的描述符,才知道接下来"DMA 搬"什么。 即"DMA 依赖 DMA 自己先跑一次"——这是描述符机制的自指依赖,调试链表 bug 时务必牢记。

四、逻辑链路:一次完整 DMA 传输的端到端时序

下面给两条链路:A 是经典/嵌入式形态,便于建立直觉;B 是现代 PCIe 真实形态,贴近工程现实。

链路 A:经典 DMAC 块传输(内存 → 外设,如 DAC 播放/串口发送)

阶段        主体        动作 / 信号
──────────────────────────────────────────────────────────────
① 准备      CPU/驱动    分配 buffer、填数据;写 DMAC 寄存器:
                        SAR=buf, DAR=外设数据口, TCR=N, CCR=方向/宽度
② 使能      CPU         置 CCR 的 enable 位("按下开始键")
③ 请求      外设        外设就绪 → 拉高 DREQ
④ 申请总线  DMAC        DMAC → 仲裁器:HOLD/BusReq
⑤ 让出总线  仲裁器/CPU  仲裁器裁决 → CPU 释放总线 → HLDA/BusGrant
⑥ 搬运循环  DMAC        重复 N 次:
                        · DMAC 驱地址(SAR/DAR)+控制(MEMR/IOW)
                        · 数据:内存→DB→外设(CPU 不参与)
                        · SAR++, DAR 不变(外设口固定), TCR--
⑦ 释放      DMAC        每拍/每块后视策略释放或续占总线
⑧ 完成      DMAC        TCR==0 → 置 CSR 完成位 → 发中断
⑨ 收尾      CPU/ISR     中断服务程序:清标志、回收 buffer、
                        通知上层"发送完成",必要时启动下一块

链路 B:现代 PCIe 设备 Bus-Mastering + 描述符环 + IOMMU + MSI-X(如网卡收包 / NVMe 读)

这是真实世界的 DMA:没有中央 DMAC,设备自己是主设备;用环形描述符队列实现"零 CPU 干预的连续传输";地址经 IOMMU;完成用 MSI-X
阶段     主体              动作 / 信号 / 数据流
────────────────────────────────────────────────────────────────────
① 初始化  驱动             分配"一致内存"的 描述符环(Ring) + 数据 buffer;
                          把 buffer 的【物理地址】经 IOMMU 映射为【IOVA】;
                          把 IOVA 填入描述符;配置设备的 环基址/大小;
                          配置 MSI-X 向量表
② 投递    驱动→设备        驱动把"待收/待写"描述符写好,更新 生产者索引,
                          写设备 doorbell 寄存器(一次 MMIO 写)
③ 取描述符 设备(DMA读)     设备作为主设备,发 TLP 读请求(IOVA)
                          → IOMMU 翻译 IOVA→PA + 权限检查
                          → 从内存取回描述符(★DMA 的自指依赖)
④ 搬数据  设备(DMA写/读)   按描述符指向的 buffer(IOVA) 发起数据 TLP:
                          收包=设备写内存;读盘=设备写内存(数据从盘来)
                          → IOMMU 翻译 → 数据落入内存
                          → coherent 互连同步/作废 CPU cache 对应行
⑤ 写回状态 设备(DMA写)     设备把"完成状态/长度"写回描述符或完成队列
⑥ 发中断  设备(MSI-X)     设备向 IOMMU/RC 指定的 中断地址 写一个值
                          (★MSI 本质=一次"定向 DMA 写")→ 触发 CPU 中断
⑦ 收尾    CPU/ISR+NAPI    中断→调度软中断/线程→读完成队列→
                          处理数据→回收描述符→更新 消费者索引→
                          若队列还有,继续;否则等下次中断/轮询
链路 B 的三个高价值洞见:
  1. 设备读描述符 = 一次 DMA:DMA 引擎的"指令"也存在内存里,由它自己取——这与 CPU 取指令同构,DMA 引擎其实是一台"只会搬数据的极简处理器"。
  2. MSI-X = 一次 DMA 写:现代中断不再是拉一根 IRQ 线,而是设备往一个特定内存地址写一个魔术值来触发中断。所以"发中断"在物理层就是一次受控的 DMA 写事务。
  3. IOMMU 是 DMA 的"MMU":正如 CPU 用 MMU 把虚拟地址翻成物理地址并做保护,设备用 IOMMU 把 IOVA 翻成 PA 并做隔离。没有 IOMMU,一个故障/恶意设备可 DMA 读写整台机器的任意物理内存(这就是 DMA 攻击的物理基础,也是虚拟化设备直通的安全前提)。

两条链路对照

维度 链路 A(经典 DMAC) 链路 B(PCIe bus-master)
主设备 中央 DMAC 设备自身
寻址 物理地址(直给) IOVA → IOMMU → PA
多段 buffer 需 CPU 多次重配 描述符环,CPU 零干预
完成通知 边带中断线 MSI-X(=DMA 写)
一致性 多为软件 flush/inv 多为硬件 coherent 或软件
典型场景 嵌入式/老外设 网卡/NVMe/GPU/现代一切高速设备

五、认知升级:常见误区清单

误区 纠正
"DMA 不需要 CPU" 需要 CPU 发起+配置+收尾,只是不搬数据
"DMA 比 CPU 搬数据'更快'" 单字节未必更快;DMA 赢在不占 CPU + 适合大块 + 可流水线
"DMA 直接给物理地址就行" 现代经 IOMMU 给 IOVA;裸给 PA 既不安全也无法虚拟化
"DMA 写完内存,CPU 立刻能读到" 不一定!要看 cache 一致性策略,否则读到 cache 旧值
"DMA 控制器只搬数据" scatter-gather 时它还读描述符写完成状态发 MSI——它是一台极简处理器
"中断和 DMA 是两回事" MSI/MSI-X 在物理层就是一种 DMA 写,二者同源
"所有设备都用中央 DMAC" PCIe 设备自带 DMA 引擎(bus mastering),无中央 DMAC

六、汇报 / 讲课要点提炼(PPT 核心页)

页 1 · 本质
DMA = 用硬件状态机替代 CPU 的搬数据循环;CPU 从"搬运工"变"指挥官",只配置-启动-收尾
页 2 · 结构
六部件:CPU / DMAC / 内存 / 外设 / 仲裁器 / 总线。DMAC 内脏 = SAR+DAR+TCR+CCR+CSR。三形态演进:独立芯片 → SoC 引擎 → 设备 bus-mastering
页 3 · 原理
传输期 DMAC 篡位成总线主设备,数据在内存↔外设间直连流过数据总线。坑:cache 一致性(软件 flush / 硬件 snoop / non-cacheable 三策)。
页 4 · 依赖
依赖栈:物理总线→仲裁→IOMMU→一致性→中断→驱动。IOMMU = DMA 的 MMU(翻译+隔离+虚拟化)。
页 5 · 链路
经典 9 步 vs 现代 7 步。三洞见:读描述符=DMA、MSI=DMA 写、IOMMU=DMA 的 MMU
页 6 · 一句话
DMA 不是"绕过 CPU",而是"绕过 CPU 的执行单元";它把数据通路从 CPU 手里夺走,却把控制权、地址翻译权、一致性对账权,分别交给了仲裁器、IOMMU 和一致性协议。

七、延伸锚点

方向 关键词 / 标准 / 文档
总线协议 AMBA AXI / ACE / CHI(ARM 一致性互连)、PCIe TLP 事务层
地址翻译/隔离 IOMMU(Intel VT-d / AMD-Vi)、SMMU(ARM)、PCIe ATS/PRI
中断 MSI / MSI-X、ARM GIC、x86 APIC
一致性 MESI/MOESI、snoop filterdirectory、coherent vs non-coherent DMA
软件框架 Linux DMA Engine APIdma_map/unmapIOMMU APIvfio(设备直通)
安全 DMA 攻击IOMMU 隔离、Thunderbolt/PCIe 热插拔防护
经典硬件 Intel 8237、ARM PL330 / PrimeCell DMA

把 DMA 拆到这一层,你会看到一个统一图景:DMA 控制器是一台"只会搬数据的极简 CPU"——它取"指令"(描述符)、操作"操作数"(buffer)、用"系统调用"(MSI 中断)回报结果;而 IOMMU、仲裁器、一致性协议,正是为这台极简 CPU 配套的 MMU、调度器、缓存对账机制。看懂这层同构,DMA 就不再是"一个外设功能",而是整套计算机体系结构在数据搬运维度上的微缩复刻

现代 DMA 真相:四大纵深一次打通

你说"需要",我理解为:把上一条埋下的三个钩子(IOMMU、MSI=DMA 写、Cache 一致性)连同软件实现,一次性兑现,讲透"真实世界里的 DMA"。 因此这一篇不写成四篇平行长文,而是把 ① IOMMU · ② PCIe 时序 · ③ Linux 源码 · ④ 一致性协议 串成一条因果链——对应"一次 DMA 从软件发起到硬件落地、再到 CPU 收悉"的完整生命周期。四块各占其位、互为因果。
若你其实只盯其中某一块(比如正调驱动、只想要 ③ 源码走读),文末告诉我编号,我单独深挖成一篇。下面先给"打通版"。

〇、先立主线:一次 DMA 的"因果链"全景

把四块按时间因果排好,你会看到它们根本不是四个知识点,而是同一次传输的四个阶段
[软件想搬数据]                      ← ③ Linux:dma_map / 提交描述符
        │  但软件手里只有"CPU 虚拟地址",设备不认识
        ▼
[地址翻译 + 隔离]                   ← ① IOMMU:VA/IOVA → PA,并设防
        │  设备拿到"总线地址(IOVA/PA)",真正上总线
        ▼
[总线事务]                          ← ② PCIe:MRd/MWr/Cpl + 属性位
        │  数据要落进内存,可 CPU cache 里可能有旧副本
        ▼
[视图对账]                          ← ④ 一致性:snoop / filter / NoSnoop
        │  搬完,设备要通知 CPU
        ▼
[完成回报 = 一次定向 DMA 写]         ← ② 的 MSI-X(闭环回②)
        │
        ▼
[CPU 收尾]                          ← ③ Linux:中断→sync→处理→unmap
一句话主线:软件发起(③) → 翻译设防(①) → 总线落地(②) → 视图对账(④) → 写中断闭环(②) → 软件收尾(③)。 记住这条链,下面四节就是给每个箭头"填肉"。

一、纵深① IOMMU:DMA 的"MMU",以及它为什么是安全与虚拟化的命门

1.1 它在链路里的位置

软件调用 dma_map_* 时,真正干地址翻译的就是 IOMMU(Intel 叫 VT-d,AMD 叫 AMD-Vi,ARM 叫 SMMU)。没有它,设备只能拿到"裸物理地址"——这在现代系统里既不安全不可虚拟化

1.2 为什么是"DMA 的 MMU"——同构对照

维度 CPU 侧 MMU 设备侧 IOMMU
谁发起访问 CPU 取指/读写 设备 DMA 读写
输入地址 虚拟地址 VA 总线地址 / IOVA
输出地址 物理地址 PA 物理地址 PA(或虚拟化下的 HPA)
翻译结构 页表(多级) IOMMU 页表(多级)
缓存 TLB IOTLB / ATC
保护 用户/内核隔离、NX 设备间隔离、读写权限位
缺页 Page Fault PRI / Page Request(高级)
洞见:IOMMU 不是"地址转换器"那么简单,它是把 CPU 那套"虚拟内存 + 保护 + 缺页"的整套红利,原样复制给了设备。 设备从此也能活在"被翻译、被隔离、被虚拟化"的世界里。

1.3 它解决的三件大事

大事 没有 IOMMU 有 IOMMU
① 隔离/安全 设备可 DMA 读写任意物理内存 → DMA 攻击(恶意 PCIe/Thunderbolt 设备偷密钥、改内核) 每个设备/组被限制在自己的 IOVA 窗口,越界即报错+阻断
② 虚拟化(设备直通) 客户机 OS 给设备的是 GPA,设备却需要 HPA,无法直通 IOMMU 做 GPA→HPA 二级翻译,设备"以为"自己在跟客户机玩,实际被宿主机 IOMMU 兜底 → vfio 设备直通的物理基础
③ 地址连续性 物理内存碎片化,设备要连续 buffer 极难凑 IOMMU 把不连续的物理页映射成连续的 IOVA,设备看到连续、内核不用凑 → 告别 bounce buffer / swiotlb 的多数场景

1.4 高级点:ATS / PRI(设备侧的"TLB + 缺页")

  • ATS(Address Translation Services):设备内部缓存 IOMMU 的翻译结果(ATC = Address Translation Cache),下次同地址 DMA 不必再问 IOMMU → 降低翻译延迟、减轻 IOMMU 压力。设备发的是带 AT=Translated 的 TLP。
  • PRI(Page Request Interface):设备发现 ATC 未命中,可向 CPU 请求把某页换入/建映射(Page Request),等 Page Response 后再访问 → 这是 SVM/SVA(共享虚拟内存) 的基础:让设备直接用 CPU 的虚拟地址、共享同一页表,零拷贝、零 pin。
这意味着:在 SVM 下,"DMA 地址"和"CPU 虚拟地址"第一次可以是同一个值——IOMMU 与 MMU 共享页表,设备成了"另一种 CPU"。这是 DMA 演化的最前沿。

1.5 误区

误区 纠正
"IOMMU 只为虚拟化" 它首先是安全隔离地址连续性的设施,虚拟化只是其一
"开了 IOMMU 就慢" 有 IOTLB/ATC 缓存,热路径翻译近乎免费;真正贵的是未命中 + 页表遍历
"IOMMU 关了就安全" 恰恰相反——关了 IOMMU,任何能发起 DMA 的设备都是 root

二、纵深② PCIe 时序:设备在总线上"到底发了什么"

2.1 它在链路里的位置

IOMMU 翻译完,设备手里有了 IOVA/PA,于是真正驱动总线——这一节看它在 PCIe 链路上发出的事务层包(TLP) 长什么样、怎么配对。

2.2 三类事务与 posted / non-posted(最关键的一对概念)

事务 TLP 类型 posted? 是否需要回包 典型用途
内存写 MWr MWr ✅ posted 否(发完即走) 设备把数据/描述符/MSI 写进内存
内存读 MRd MRd ❌ non-posted ✅ 需 Completion(CplD) 带回数据 设备读描述符、读 buffer
完成 Cpl/CplD CplD 对 MRd 的应答,数据在这里
洞见:PCIe 的"读"是两阶段的——设备先发 MRd(请求),对方稍后回 CplD(数据)。所以"读"在总线上是一对包,而"写"是一个包。这直接决定了:读有延迟、写可流水;也决定了 outstanding 与 tag 的意义。

2.3 outstanding 与 Tag:并发的钥匙

  • 因为读要等 completion,若一次只发一个读、等回包再发下一个,带宽会被往返延迟(RTT)吃光
  • 解法:outstanding——设备同时发出多个 MRd,每个带唯一 Tag;completion 回来时凭 Tag 配对。Tag 空间大小 = 可同时 in-flight 的读请求数,是设备并发能力的硬指标。
  • 这与 CPU 的乱序 + load buffer同构:用 tag 解耦"发出顺序"与"完成顺序"。

2.4 属性位:决定"正确性"与"性能"的开关(最易踩坑)

每个 TLP 头里有几个属性位,它们正是把①②④三块焊在一起的螺丝:
属性位 含义 与一致性(④)的关系 与性能的关系
NoSnoop 告诉互连:"别去 snoop CPU cache" ⚠️ 双刃剑:仅当软件已保证该区域 non-cacheable 或已 dma_sync 时才能置位,否则一致性破裂、读到旧值 省去 snoop 流量,更快
Relaxed Ordering (RO) 允许本事务越过前面的写 可能破坏"先写数据、后写 doorbell/状态"的顺序 → 需软件用 fence/强序写 兜底 提升并行、减少头阻塞
IDO 允许越过不同 Requester ID 的事务 多设备/多功能场景减少互相阻塞 提升多流并发
TH / 优先级 流量类别/优先级 QoS 调度
一个真实 bug 范式:驱动用 RO 写了数据,又用 RO 写了"完成标志",结果标志先于数据到达内存,CPU 看到标志去读数据 → 读到旧数据。修复 = 标志写用强序,或中间插 fence。 属性位用错,是 DMA 数据错乱里最难复现的一类。

2.5 尺寸参数

  • Max Payload Size (MPS):单个 MWr 数据段上限(128B~4KB),影响写效率与对齐。
  • Max Read Request Size (MRRS):单个 MRd 请求的字节数;注意:一个 MRd 可能被对方拆成多个 CplD 回来(受 Max Completion Boundary 等限制)——所以"一次读"在回程可能是"多包拼回"。

2.6 闭环:MSI-X 就是一次 MWr

回到上一条的洞见并兑现:设备发中断 = 发一个 posted MWr,写到 Root Complex 映射的"中断地址窗口",数据域里是中断向量号。 RC 收到这个写,识别地址落在中断窗口,于是不写内存,而是触发对应 CPU 的中断
所以从 TLP 视角看,"搬数据"和"发中断"是同一种动作(MWr),只是目的地址不同:写到 buffer 地址 = 搬数据;写到中断窗口地址 = 发中断。MSI 把"边带中断线"彻底消灭,统一进了内存写事务——这是 DMA 与中断在物理层的终极同源。

三、纵深③ Linux 源码视角:驱动"按下按钮"时,内核做了什么

3.1 它在链路里的位置

链路的两端都是软件:开头"想搬数据"、结尾"收悉收尾"。这一节把这两端用真实 API 与调用栈填实。

3.2 两类映射:streaming vs coherent(驱动的第一道选择题)

映射 API 一致性默认语义 cache 策略 适用
Streaming(流式) dma_map_single / dma_map_sg 单向、映射期 CPU 默认不可访问,需 dma_sync_* 切换所有权 通常 cacheable,靠 sync 对账 网卡收发包、磁盘 IO(一次性、方向明确)
Coherent(一致) dma_alloc_coherent 双向、CPU 与设备随时可见,无需 sync 通常 non-cacheable / write-combine 描述符环、doorbell 共享结构、控制块
洞见:coherent 不是" magically 一致",而是"用 non-cacheable 换一致性"——CPU 不缓存这块,自然没有 cache 旧值问题,代价是 CPU 访问它每次都走总线、更慢。所以"高频 CPU 读写 + 设备偶尔碰"的结构(如描述符环的状态字)要权衡,未必无脑 coherent。

3.3 dma_map_single 的真实路径(因果链的软件兑现)

驱动: dma_map_single(dev, cpu_addr, size, dir)
   │
   ├─ 把 cpu_addr(VA) 转成 page/offset
   │
   ├─ 调用 dev 的 dma_map_ops->map_page(...)        ← 抽象层,后端可换
   │       │
   │       ├─ 若挂 IOMMU:iommu_map / iommu_dma_map   ← ① 在这里发生
   │       │     · 分配 IOVA
   │       │     · 建/更新 IOMMU 页表 (VA/IOVA→PA)
   │       │     · 返回 dma_addr_t = IOVA
   │       │
   │       └─ 若无 IOMMU:dma_addr = phys_addr (+offset)  ← 直给物理地址
   │
   ├─ 按 dir 做 cache 维护(non-coherent 平台)        ← ④ 的软件侧
   │     · dir=TO_DEVICE   → clean(写回) cache,让设备读到 CPU 最新值
   │     · dir=FROM_DEVICE → invalidate cache,让 CPU 之后读到设备新值
   │
   └─ 返回 dma_addr_t  ← 这就是填进描述符/寄存器的"设备地址"
三个必须记住的语义:
  1. 返回值 dma_addr_t 既不是 VA 也不是 PA,是给设备用的总线地址(有 IOMMU 时是 IOVA)。把它当物理地址打印/使用是经典错误。
  2. dir 不是"建议",是所有权契约TO_DEVICE 期间 CPU 不许读、FROM_DEVICE 期间 CPU 不许读、BIDIRECTIONAL 两边都要 sync。违反 = 未定义。
  3. map 与 unmap 必须配对,且 unmap 前若 CPU 要看 FROM_DEVICE 的数据,必须先 dma_sync_single_for_cpu

3.4 sync 的本质:所有权的"交接棒"

dma_sync_single_for_device(...)   // CPU 写完 → 把所有权交给设备:clean cache
        ……设备 DMA 读/写……
dma_sync_single_for_cpu(...)      // 设备写完 → 把所有权交回 CPU:invalidate cache
硬件一致性互连(④) 的平台上,这些 sync 可能退化为空操作(因为 snoop 已自动对账);在非一致平台上,它们就是真正的 cache clean/invalidate 指令同一份驱动代码,靠 dma_map_ops 与平台一致性能力,自动适配两种硬件——这是 Linux DMA 抽象最优雅的地方。

3.5 收尾侧:中断 → NAPI/线程 → 处理 → 回收

设备 MWr 到中断窗口(MSI-X) → RC 触发中断 → 驱动 ISR
   ISR:通常只"关中断 + 调度软中断/线程"(顶半,要快)
        ↓
   软中断/NAPI poll / 内核线程(底半):
        · 读完成队列/描述符状态
        · dma_sync_*_for_cpu(FROM_DEVICE 时)   ← ④ 软件侧
        · 处理数据(上交协议栈/块层)
        · 回收描述符、补新 buffer、dma_map 新 buffer
        · 更新消费者索引、写 doorbell / 重新使能中断
这就是链路 B 第⑦步的软件展开。NAPI 的"中断+轮询混合" 正是为高包率设计:低负载用中断省 CPU,高负载切轮询避免中断风暴——本质是在"DMA 解放 CPU"之后,进一步解放 CPU 于中断本身

四、纵深④ 一致性协议:CPU 与设备的"内存对账机制"

4.1 它在链路里的位置

数据经②落到内存的瞬间,第④块登场:CPU cache 里若缓存了同一行,谁来保证两边不矛盾?

4.2 问题的物理图像(再画一次,但到协议级)

   CPU core0 cache line X = 旧值(脏/共享)        内存 X
        ▲                                          ▲
        │  CPU 读 X 命中 cache                      │ 设备 DMA 写 X = 新值
        └──────────── 矛盾!───────────────────────┘
                  两套视图,谁为准?

4.3 三种硬件解法(与③的软件解法对照)

解法 谁负责 机制 设备 TLP 属性 代价
A. 软件对账 驱动 ③ 的 sync:clean/invalidate 通常 NoSnoop 可置 CPU 开销、易错
B. 硬件 snoop(coherent 设备) 互连 设备 DMA 写 X 时,互连向持有 X 的 core 发 snoop,core 回写脏数据/作废该行,再让写落地 不置 NoSnoop 互连复杂、snoop 流量
C. non-cacheable 映射 页表属性 把 DMA 区标 UC/WC,CPU 根本不缓存 可 NoSnoop CPU 访问慢

4.4 coherent 设备的一次写:snoop 全流程

设备发 MWr(X) [NoSnoop=0]
   → 互连(一致性节点) 查 snoop filter / directory:X 是否在某 core cache?
        · 否 → 直接写内存,结束
        · 是 → 向该 core 发 Snoop(写/作废)
              core 若持有脏行 → 回写内存(或直送) + 作废本地行
              互连收到 snoop 响应 → 让设备的 MWr 落地
   → CPU 之后再读 X:cache 已作废 → 重新从内存取 → 拿到设备新值 ✓
洞见:snoop filter / directory 是"谁缓存了哪行"的索引,避免向所有 core 广播 snoop(广播在核数多时是灾难)。核数越多,directory 越比 broadcast 划算——这正是大型 SoC/服务器互连(CHI、CXL)的核心设计动机。

4.5 NoSnoop 的正确打开方式(②④ 的交汇点)

  • 置 NoSnoop = 你向硬件承诺:"这块内存此刻 CPU 没缓存 / 我已手动 sync 过,你别浪费流量去 snoop。"
  • 若承诺为假(CPU 其实缓存着且未 sync)→ 硬件信了你、没 snoop → 一致性破裂 → 读到旧值,且几乎无法复现
  • 所以:coherent 平台 + 正确驱动,通常让硬件自动管(不置 NoSnoop);只有 non-cacheable 区或已 sync 的 streaming 区,才安全地置 NoSnoop 换性能。 这就是②的属性位与④的协议必须协同的原因。

五、把四块焊死:一次"网卡收包"的完整因果回放

用一条最真实的链路,把①②④按时间顺序走一遍,验证它们确实是"同一次传输的四阶段":
T0 [③准备]  驱动早已 dma_alloc_coherent 建好 描述符环+buffer(或 dma_map 好 buffer),
            地址经①IOMMU 映射为 IOVA,填入描述符;使能队列。
T1 [②取描述符] 网卡发 MRd(描述符 IOVA) → ①IOMMU 翻译 IOVA→PA(+权限检查)
            → 内存回 CplD(描述符内容) [non-posted 两阶段]。
T2 [②收包写]  网卡把包数据 MWr(buffer IOVA),属性位按平台/驱动设定
            (coherent 区不置 NoSnoop / streaming 区视 sync 而定)。
T3 [①翻译]   MWr 的 IOVA 再经①IOMMU→PA。
T4 [④对账]   互连据 NoSnoop 位决定:snoop 持有该行的 core / 或直接落地;
            coherent 平台自动对账,CPU cache 与内存视图归一。
T5 [②写状态] 网卡 MWr 把"包长/状态"写回描述符(或完成队列)。
T6 [②=MSI-X] 网卡 MWr 到 中断窗口地址 → RC 触发 CPU 中断(写=中断,闭环)。
T7 [③收尾]  ISR→NAPI poll:读完成队列;若 streaming 则 dma_sync_for_cpu(④软件侧);
            上交协议栈;回收/补 buffer(dma_map→①);更新索引、写 doorbell。
看:①出现 3 次(取描述符、写数据、补 buffer)、②贯穿全程、④在 T4/T7、③在 T0/T7。四块不是章节,是同一条时间线上的不同角色。这就是"打通"。

六、误区总清单(四块合并,面试/答辩高频)

误区 纠正
"dma_addr 就是物理地址" 有 IOMMU 时是 IOVA;只有无 IOMMU 才等于 PA
"IOMMU 只为虚拟机" 首先是安全隔离 + 地址连续性,虚拟化是其应用之一
"PCIe 读和写一样快/一样形式" 读是 non-posted + completion 两阶段,写是 posted 单包
"NoSnoop 总是更快所以总开" 开错=一致性破裂;只在 non-cacheable 或已 sync 时安全
"RO/IDO 只是性能开关" 用错会乱序,破坏"数据先于标志",需 fence 兜底
"coherent 映射=免费一致" 它用 non-cacheable 换一致,CPU 访问更慢,要权衡
"sync 在所有平台都干活" 硬件一致平台上 sync 常退化为空,靠 snoop 对账
"MSI 是独立的中断机制" 物理层是一次定向 MWr,与搬数据同源
"设备读描述符不算 DMA" 算,且是 non-posted 读——DMA 引擎靠"自取指令"工作

七、汇报 / 讲课要点提炼(PPT 核心页)

页 1 · 主线
一次 DMA = 软件发起(③)→翻译设防(①)→总线落地(②)→视图对账(④)→写中断闭环(②)→软件收尾(③)。四块是一条因果链,不是四个知识点。
页 2 · ① IOMMU
DMA 的 MMU:翻译 + 隔离 + 虚拟化。关 IOMMU = 任何设备皆 root。前沿:ATS/PRI/SVM 让设备共享 CPU 页表。
页 3 · ② PCIe
写=posted 单包,读=non-posted+completion;outstanding+tag 换并发;NoSnoop/RO/IDO 是性能与正确性的双刃开关;MSI-X = 一次 MWr
页 4 · ③ Linux
streaming vs coherent 二选一;dma_map 返回 IOVAdir 是所有权契约;sync 是交接棒,硬件一致平台上可退化为空。
页 5 · ④ 一致性
三策:软件 sync / 硬件 snoop / non-cacheable;snoop filter/directory 抗广播风暴;NoSnoop 必须与④协同,否则读到旧值。
页 6 · 一句话
DMA 的"现代真相"是:软件用 dma_map 把虚拟地址托付给 IOMMU,IOMMU 翻译成受保护的总线地址,设备凭它在 PCIe 上以 posted/非posted 事务落地数据,一致性互连在背后默默对账 CPU cache,最后设备用一次内存写"伪装成中断"喊 CPU 来收尾——整条链,没有一环是孤立的。

八、延伸锚点

纵深 关键词 / 文档
① IOMMU Intel VT-d spec、ARM SMMUv3、Linux drivers/iommuvfioSVA/SVMATS/PRI
② PCIe PCIe Base Spec TLP 章、MPS/MRRSAERECRClspci -vvv 看 DevCtl/Cap
③ Linux include/linux/dma-mapping.hdma_map_opsNAPIDocumentation/core-api/dma-api.rst
④ 一致性 AMBA CHI / ACE、Intel CXL.cache/.memsnoop filter vs directory、MESI/MOESI
调试 iommu=pt/strict/relaxeddma_debugftrace 跟 dma_map、PCIe TLP 抓包(LeCroy/协议分析仪)

至此,上一条埋的三个钩子(IOMMU、MSI=DMA 写、一致性)连同软件实现,已在同一条时间线上全部兑现。你会注意到一个贯穿始终的同构:IOMMU≈MMU、Tag≈load buffer、snoop filter≈TLB/coherence directory、MSI≈syscall、描述符环≈指令流——DMA 引擎确是一台"只会搬数据的极简 CPU",而现代系统为它配齐了和真 CPU 同等规格的地址、并发、一致、中断基础设施。看懂这层同构,"DMA"二字就从"一个外设功能"升格为体系结构在数据搬运维度的完整复刻

关于"需要"的兜底

我默认按"打通版"给了整合篇。若你其实只想要其中某一块的"单点深挖",回我一个编号,我把它单独扩成一篇到"可对照源码/spec 逐行讲"的颗粒度:
  •  IOMMU internals:页表层级、domain/组、strict vs lazy unmap、vfio 直通全链路、SVA 页表共享。
  •  PCIe 时序:TLP 头逐字段、completion 拆分规则、RO/NoSnoop/IDO 的乱序矩阵与 fence 范式、AER 错误注入。
  •  Linux 源码走读:dma_map_single → iommu_dma_map → arch sync 的真实调用栈,配 dma-api.rst 逐条对照。
  •  一致性协议:CHI/ACE 下设备 DMA 的 snoop 时序图、snoop filter vs directory 的容量/延迟权衡、CXL 一致性域。

 

DMA(直接内存存取)简介

DMA(Direct Memory Access,直接内存存取)是一种允许外设直接与计算机内存进行数据交换的技术,绕过了CPU的参与。这种机制的优势在于,它能够显著提高数据传输效率,减轻CPU的负担,从而使得计算机能够处理更多的任务和更高的性能要求。

在传统的输入输出(I/O)操作中,数据通常是通过CPU在外设和内存之间进行搬运的。例如,当一个硬盘从内存中读取数据时,CPU必须先从内存读取数据,再将数据传送给硬盘。这种方式称为程序驱动I/O(Programmed I/O,PIO)。这种方法会占用CPU资源,导致性能下降,尤其是在需要频繁数据传输的情况下。

DMA的引入通过允许外设直接访问内存,减少了CPU的介入,从而提升了整体系统性能。

DMA 的工作原理

DMA工作时,数据传输通常由DMA控制器(DMA Controller,简称DMAC)来协调。DMA控制器负责在外设和内存之间进行数据传输,而CPU仅在启动DMA传输时进行干预,之后就能将工作交给DMA控制器处理。

  1. 初始化:首先,CPU会设置DMA控制器,指定数据传输的源地址、目的地址、数据传输的大小等信息。

  2. 数据传输:一旦DMA控制器收到指令,它便控制外设直接将数据写入内存(或从内存读取数据并传送到外设),在这个过程中,CPU不再参与数据的搬运。

  3. 中断通知:数据传输完成后,DMA控制器会向CPU发送一个中断信号,告知CPU数据传输已完成,CPU可以进行后续处理。

DMA 的工作模式

DMA通常有多种工作模式,根据数据传输的方式和DMA控制器的控制逻辑的不同,常见的DMA工作模式包括:

  1. 单次传输模式(Burst Mode)

    • 在此模式下,DMA控制器会一次性将一块数据全部传输到内存或外设。CPU在这段时间内通常会被暂停,直到数据传输完成后,才能恢复执行。这种模式适用于需要传输大量连续数据的情况。
  2. 连续传输模式(Cycle Stealing Mode)

    • 在此模式下,DMA控制器在每次数据传输完成后,会让CPU获得一个处理周期来执行任务。每次DMA完成一小块数据传输后,CPU就会得到机会执行一个指令。这样,CPU和DMA控制器交替工作,减少了CPU的空闲时间。
  3. 块传输模式(Block Mode)

    • DMA控制器在传输一块数据后,将把控制权交还给CPU。与单次传输模式不同,块传输模式允许DMA进行一次较长时间的传输,而不会中断CPU的任务太频繁。这种模式适用于需要相对较少、但较大数据块传输的应用场景。
  4. 直接传输模式(Demand Mode)

    • 在此模式下,DMA控制器会在每次数据传输时等待外设的请求,只有外设发送传输请求时,DMA控制器才会启动数据传输。

DMA 的优点

  1. 提高数据传输效率

    • 由于数据直接从外设传送到内存,或者反过来,DMA减少了CPU的干预,避免了数据搬运过程中CPU的多次操作。这样,数据传输变得更加高效,CPU可以用于其他任务。
  2. 减轻CPU负担

    • 在传统的I/O操作中,CPU需要不断干预和控制数据的传输,而DMA通过将数据搬运的任务交给专门的DMA控制器来完成,释放了CPU的资源,使其能够专注于更重要的计算任务。
  3. 提高系统响应性

    • 由于DMA控制器独立工作,系统可以在不占用CPU的情况下完成高速的数据传输,特别适用于需要高带宽的应用,如音视频数据流、硬盘访问等。

DMA 的应用

DMA技术广泛应用于各种需要高效数据传输的场景。以下是几个典型应用:

  1. 硬盘和存储设备

    • 硬盘、SSD等存储设备通常使用DMA来提高数据读写的效率。通过DMA,硬盘能够直接将数据读入内存,或者将内存中的数据直接写入硬盘,而无需CPU的介入。
  2. 音频和视频设备

    • 在音频和视频处理设备中,数据量巨大,且需要实时传输。DMA可以帮助音频卡、视频卡等设备直接将音视频数据传输到内存,或从内存传输到播放设备,避免CPU的过度负担。
  3. 网络适配器

    • 网络卡通过DMA直接将网络数据包传输到内存,或者将内存中的数据包发送到网络。这样可以大幅减少网络数据处理的延迟和CPU的负担。
  4. 图形卡(GPU)

    • 图形卡通常使用DMA技术来从系统内存读取图形数据或将渲染结果写回系统内存,减少图形处理中的延迟和CPU的参与。

DMA 的局限性

尽管DMA在很多领域中都有显著的优势,但它也存在一些局限性:

  1. 硬件要求

    • 需要专门的DMA控制器来管理数据传输,因此硬件支持和配置较为复杂。
  2. 内存冲突问题

    • 在多任务操作中,如果多个设备同时请求DMA访问内存,可能会出现内存访问冲突。这需要合理的内存访问控制和冲突管理机制。
  3. 处理复杂性

    • 对于涉及到多个外设和内存区域的传输,DMA的配置可能变得非常复杂,特别是在要求高效且无错误的数据传输时,必须小心处理。

DMA(直接内存存取)是一种高效的数据传输方式,能够绕过CPU直接在内存与外设之间进行数据交换。它不仅提高了数据传输的速度,还减轻了CPU的负担,使得计算机能够更高效地执行其他任务。DMA在硬盘、网络、音视频处理、图形渲染等多个领域都得到了广泛应用,为现代计算机系统提供了强大的性能支持。


 

posted @ 2024-11-09 12:09  suv789  阅读(860)  评论(0)    收藏  举报