ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

MMU、IOMMU、SMMU区别与联系:从地址翻译到设备隔离

MMU、IOMMU、SMMU区别与联系:从地址翻译到设备隔离 这三个缩写放在一起很容易让人以为只是同一个东西在不同公司的花名。但实际上 MMU、IOMMU、SMMU 虽然干的都是“地址翻译”这件事服务的对象和解决问题的层次完全不同。尤其很多人在 JZ2440 这类 ARM9 板子上第一次接触 MMU紧接着又听别人聊服务器上的 IOMMU、ARM 那边的 SMMU很容易把这几个概念混成一锅粥。我打算直接从“地址到底是谁的地址”这个问题切进去把 CPU、设备和总线三个角色的视角拆开。你会发现一旦理解了“每个访问者都有自己的地址观”后面的页表、TLB、Stage 1/Stage 2 这些技术名词就全都串起来了。这篇文章适合三类人正在嵌入式板子上调 MMU 的开发者、做 Linux 驱动想搞懂 DMA 地址映射的人、以及做虚拟化或 ARM64 服务器底层软件的同学。我会从原理讲到裸机配置再讲到实际排障中怎么从日志定位问题尽量把这条线走通。1. 三种地址视角CPU、设备和总线各自看到的世界1.1 虚拟地址、物理地址和设备地址到底谁管谁先做一个最简单的实验。你在 Linux 里写一个用户态程序打印一个指针的值比如0x7ffe12345678。这个地址能直接拿去访问内存芯片吗不能。CPU 发出的这个地址要先经过 MMU 翻译变成物理地址才能驱动内存控制器去读写。这里的关键在于你平时写的所有地址都是你所在“角色”的虚拟地址不是内存条上的真实地址。CPU 看的是虚拟地址经过 MMU 变成物理地址这是第一层翻译。设备看的是什么呢在没有 IOMMU 的平台上设备比如网卡、磁盘控制器做 DMA 时它看到的地址就是物理地址没有任何中间层。也就是说CPU 侧早就有了一套复杂的“门禁系统”而设备侧完全是裸奔的。这就带来一个很实际的问题CPU 访问内存有 MMU 保护一个进程的野指针很难直接破坏另一个进程的物理内存除非发生严重的地址映射错误。但设备做 DMA 时只要驱动写错了 DMA 地址设备就会直接往物理内存里乱写可能把内核代码、别的进程数据、甚至页表本身都冲掉。这类问题在嵌入式调试中非常经典表现为“系统随机死机、报错位置不确定、关了 DMA 优化就好了”。后面我会详细讲这种问题怎么排查。1.2 为什么说“地址翻译”是隔离和虚拟化的地基如果只谈 CPU 的 MMU它的意义不光是“把虚拟地址变成物理地址”而是把每个进程关进一个独立的虚拟地址空间。进程 A 的0x1000和进程 B 的0x1000完全可以映射到不同的物理页互不干扰。这是一个非常强大的隔离模型现代操作系统没有它就活不下去。但隔离只是第一步。第二阶段是虚拟化虚拟机里的 guest 操作系统认为自己管理物理内存实际上它见到的只是宿主机给它的一块“伪物理地址”IPAIntermediate Physical Address。从 guest 的虚拟地址到真实的物理地址需要经过两级翻译。CPU 的 MMU 做了第一级VA - IPA第二级IPA - PA依然由 MMU 完成但用的是另一套页表。这套两阶段翻译的设计直接影响了 SMMU。因为在一个虚拟化系统里如果有设备要直通给虚拟机那设备发出的 DMA 地址本质上也是 guest 虚拟地址的一部分需要经历同样两阶段翻译。IOMMU/SMMU 做的核心工作就是把 CPU 已经玩了几十年的“翻译隔离”机制复制到设备侧。但要复制到设备侧会遇到很多 MMU 不需要解决的问题比如怎么知道这个 DMA 请求是谁发出来的一个网卡有多个队列每个队列要不要独立翻译这些就是在设备侧实现地址翻译时最先要回答的问题。1.3 三个名词的职责边界一句话概括我经常用一个不太严谨但很好记的方式来区分三者的定位MMUCPU 内部/内存控制器路径上的地址翻译器管“CPU 发出的地址”IOMMU设备访问内存路径上的地址翻译器管“外设 DMA 发出的地址”SMMUARM 体系下对 IOMMU 的一种具体实现同时承担了更多系统级内存管控职责相当于“ARM 版 IOMMU Plus”。严格来说IOMMU 是通用名词Intel 平台叫 VT-dAMD 平台叫 AMD-ViARM 平台实现为 SMMU。它们面对的问题高度一致但在具体寄存器接口、页表格式、虚拟化支持上有各自的差异。理解了 IOMMU 的通用模型再去看 SMMU 就不会发怵。2. MMUCPU侧地址翻译的工程设计与裸机实践2.1 多级页表为什么是必然选择5分钟算一笔账很多第一次接触 MMU 的人都会问为什么页表要做成多级直接一张大表虚拟地址的高位做索引一次性找到物理页不是更快吗我们来算一笔账。假设 32 位地址空间页面大小 4KB那么整个地址空间有 2^32 / 2^12 2^20 1048576 个物理页。如果一张页表项占 4 字节一张扁平页表就要 4MB。这还只是 32 位。如果是 64 位地址空间哪怕只使用 48 位扁平页表的大小会变成 2^48 / 4KB * 4B 256TB完全不可接受。所以必须把页表拆成多级。以 ARM64 常见的 4 级页表为例虚拟地址被切成 4 段索引每段 9 bit加页内偏移12 bit总共 48 位。顶级表只需要一页4KB里面 512 个表项指向第二级表第二级表再按需创建。一个进程真正用到的地址空间可能只有几十 MB那么它需要的页表总开销可能只有几十 KB而不是扁平页表的 4MB。这个“按需分配”的设计直接影响了大内存系统能否高效运行。多级页表的代价是一次地址翻译要走多级内存访问地址翻译本身的延迟变高了。这也是为什么 CPU 里一定要有 TLBTranslation Lookaside Buffer转换后备缓冲器来缓存最近使用的页表项。TLB 命中时虚拟地址到物理地址的翻译几乎不额外花时间TLB 未命中时硬件才开始一级一级地“走页表”page walk。2.2 TLB 与 ASID性能优化里容易被忽略的细节TLB 的基本原理类似哈希缓存但有一个特殊问题页表是跟随进程切换的。进程 A 和进程 B 的同一个虚拟地址指向的物理页可能完全不同。如果每次进程切换都把整个 TLB 刷掉性能会非常难看因为新进程一进来几乎每条指令都要触发 TLB miss。ARM 的解法是给 TLB 表项打上 ASIDAddress Space ID地址空间标识标签。TLB 里可以同时缓存多个进程的页表项通过 ASID 区分谁是谁。硬件在匹配虚拟地址时会同时检查 ASID如果匹配不上才算未命中。这样一来进程切换时不再需要冲刷所有 TLB 表项只需保留仍然有效的部分。在 ARMv7 上ASID 通常是 8 bit到了 ARMv8ASID 扩展到了 16 bit。这里有一个工程细节如果你在做裸机 RTOS没有充分使用 ASID那么在任务切换时还是要手动执行TLBI指令刷掉对应 ASID 的表项否则会出现“明明把内存改了某个任务读到的还是老数据”这种怪问题——查来查去最后发现是 TLB 缓存了我的页表项。2.3 在 JZ2440ARM920T上点亮 MMU最简可行配置JZ2440 用的 S3C2440 是一颗 ARM920T 内核ARMv4 架构。它的 MMU 比起现代 ARM64 简单很多非常适合用来建立第一手感觉。要在上面做最基本的“恒等映射”VA PA步骤如下。第一步准备段描述符表。ARM920T 的 L1 页表里每个描述符对应 1MB 地址空间。如果板子有 64MB 内存我只需要 64 个描述符就能把整段内存映射完。每个描述符的高位存物理段地址低 12 位放缓存属性、访问权限和 domain域信息。我通常把页表放在一个 16KB 对齐的内存区域因为 CP15 的 TTBRTranslation Table Base Register寄存器的低 14 位会被硬件忽略它要求页表基址按 16KB 对齐。第二步配置 domain 和 control 寄存器。ARM9 的 MMU 支持 16 个 domain每个 domain 有独立的访问权限控制。我习惯把所有映射都放到 domain 0并把 domain 0 配成 client 模式这样权限检查由描述符里的 APAccess Permission位决定。如果不配 domain直接让 domain 处于 no access 模式那系统一开 MMU 就会立即触发异常屏幕上毫无输出非常容易怀疑人生。第三步打开 MMU。ARM920T 的 CP15 C1 寄存器最底部的 M 位bit0是 MMU 的总开关。我写了一个很短的函数来置位void mmu_enable(void) { unsigned int val; __asm__ __volatile__( mrc p15, 0, %0, c1, c0, 0\n\t orr %0, %0, #1\n\t mcr p15, 0, %0, c1, c0, 0\n\t : r(val) : : memory); }启用 MMU 之后if 跳转地址一定要是虚拟地址。很多人在这里踩坑代码还在物理地址段0x30000000区域执行但 MMU 一开PC 会立刻被翻译成虚拟地址。如果页表里没有把当前位置的虚拟地址映射好CPU 取指直接失败。所以我的手写引导流程通常是先不开 MMU在物理地址下把页表填充好然后跳到一个专门进行虚拟地址恒等映射的代码段最后在那里打开 MMU。或者说更为稳妥的做法是开启 MMU 时使用一条 enable 指令立即跟一个跳转分支确保 PC 的后续取值地址在映射好的范围内。2.4 缓存属性与内存类型比开关 MMU 更容易出事的区域在 ARM 体系里页表描述符不只是做地址翻译还顺带定义了这段内存的 cache 属性、buffer 属性和 shareability共享性等。很多人把 MMU 理解为“地址翻译器”但它在现代 ARM 处理器上同时是“内存类型标定器”。同一块物理内存CPU 在访问厂商寄存器时应该映射为 Device 类型不能有缓存和写合并访问普通 DDR 时应该映射为 Normal 类型才能发挥 cache 的性能。这块很容易出错。比如一个外设的 FIFO 寄存器如果被映射成 cacheable 的 Normal 内存CPU 读它时可能拿到的是缓存里的旧值永远不会读到硬件的最新状态。反过来DMA 缓冲区如果被映射成 Device 类型CPU 读写它的性能会非常差。这个坑在裸机实验里不容易暴露但一旦上了 Linux几乎所有驱动都要面对。后面第 5 章我会专门讲 cache 和 DMA 一致性因为那是我认为嵌入式开发最重要的一个进阶点。3. IOMMU给外设DMA加上“访问围栏”3.1 没有 IOMMU 的年代一个DMA越界事故的复盘我可以讲一个很典型的事故。某块 PCIe 网卡驱动在初始化时申请了一个 DMA 缓冲区用来存放收到的包。由于驱动在计算 DMA 地址时有个整数溢出实际产生的 DMA 地址指向了某个完全不相干的内存区域。接下来发生了非常戏剧性的一幕网卡收到第一个数据包就把这块物理内存给覆盖了。系统为这个“错误”付出的代价是一块内核堆内存被写坏随即出现神秘的内存损坏报错连排查方向都很难确定。这比 CPU 侧的程序员写出野指针可怕得多。CPU 访问非法地址会触发 page fault 或者 segment fault至少你知道挂在哪一行。但 DMA 写坏内存时CPU 完全不知情因为硬件 DMA 引擎绕过 CPU 直接访问物理内存。没有 IOMMU就没有任何设备权限检查机制。你说这个 DMA 缓冲区只允许网卡访问可网卡根本不知道也不在乎你的“意图”。IOMMU 解决的就是这件事设备每一次 DMA 访问都要经过 IOMMU 的地址翻译和权限检查。设备发出的地址不是物理地址了而是一个“设备虚拟地址”也可以叫 I/O 虚拟地址IOVAIOMMU 负责把它翻译成物理地址同时检查该设备是否真的有权访问这个物理地址。3.2 IOMMU 翻译流程RID、页表与 IOTLB和 CPU 的 MMU 类似IOMMU 也维护着一棵或多棵页表。但 CPU MMU 是按进程切换页表而 IOMMU 的问题更复杂怎么知道一个 DMA 请求来自哪个设备这里引入了 RIDRequester ID的概念。在 PCIe 场景中RID 其实就是设备的 BDFBus/Device/Function编号比如0000:1f:00.0这样的字符串。IOMMU 硬件拿到一个 DMA 请求时先看 RID然后用 RID 去索引该设备对应的“设备上下文”或“流表条目”再从这条表目中拿到这个设备应该使用的页表基址。之后页表遍历、权限检查的过程就和 CPU 的 MMU 高度相似了。IOMMU 也有自己的 TLB常被称为 IOTLB。如果没有 IOTLB每个 DMA 包都要在硬件里走一遍多级页表性能会很难看。IOTLB 中缓存的表项通常会带上设备 ID 作为匹配键的一部分这样不同设备之间不会串缓存。更高级的 PCIe 设备还支持 ATSAddress Translation Services设备可以在 DMA 前主动向 IOMMU 查询地址翻译结果并缓存到自己的 ATC 里进一步减少每个 DMA 请求的翻译开销。3.3 IOMMU 与 MMU 的对比一张表看清本质下面这张表是我在自己笔记里反复用到的对照条理很清楚维度MMUIOMMU服务对象CPU 核心外设 / DMA 引擎输入地址虚拟地址VA设备地址 / IOVA输出地址物理地址PA物理地址PA请求者标识ASID / TTBR 上下文RID / StreamID页表格式与 CPU 架构强相关可以复用 CPU 页表格式也可以是厂商自定义格式是否需要软件维护硬件自动走页表操作系统负责刷新 TLB硬件自动走页表驱动/内核用不同接口维护映射核心价值进程隔离、内存保护、虚拟地址空间设备隔离、DMA 重映射、虚拟化直通这个表最值得注意的地方在“请求者标识”这一行。CPU 的地址翻译天然有一个“当前正在执行哪个进程”的上下文靠 ASID 就能区分。但设备 DMA 是没有“进程”概念的一个物理设备可以同时服务于多个虚拟机、多个用户态驱动所以它必须有一个明确的外部标识来区分不同的虚拟地址空间。3.4 从 VFIO 到设备直通虚拟化场景下的刚需虚拟机场景里光靠 CPU 的 MMU 还不够。你可能会想如果给虚拟机分配一块物理内存区域让它从某个起始物理地址开始使用然后让直通设备也去访问这块物理地址不就行了问题是虚拟机的 guest 操作系统不知道自己是在虚拟机里它会管理自己的物理内存。当 guest 里的驱动分配一个 DMA 缓冲区时它拿到的地址是 guest 物理地址其实对应宿主机里的一块“中间物理地址”。如果设备直通设备接到的 DMA 地址就是 guest 认为的物理地址。现在 guest 物理地址和宿主机物理地址之间差了一个偏移但设备并不知道这个偏移。所以现代虚拟化方案比如 VFIO会让设备直通给 VM 的同时在宿主机侧建立一套 IOMMU 映射把设备可用的 IOVA 空间映射到真正的宿主物理内存。这样设备在 DMA 时发出的地址先被 IOMMU 翻译成宿主物理地址。整个过程中还需要中断重映射防止 guest 通过设备中断向量注入恶意中断到宿主机。可以说IOMMU 解决了“直通模式下设备地址翻译和中断重映射”这两个绕不开的问题。4. SMMUARM体系下从IOMMU到系统内存管控的全面方案4.1 不只是 ARM 版的 IOMMUARM 的 SMMU 从名字看很容易被当成“ARM 的 IOMMU”它的核心功能确实在设备侧完成地址翻译和权限检查。但用起来之后你会发现SMMU 的复杂度和通用性远高于传统桌面平台的 IOMMU尤其是 SMMUv3 以后的实现。为什么因为在 ARM SoC 里可发起 memory access 的主设备bus master太多了比如 GPU、视频编解码器、加密引擎、各种 DMA 控制器。ARM 希望能用一套统一机制管理所有这些设备的内存访问而不只是“给 PCIe 网卡重映射地址”。所以 SMMU 在架构上支持非常复杂的流表Stream Table、上下文描述符Context Descriptor和多级转换并且和 ARM 的总线比如 CMN 网格互连深度绑定。我在调一个带 PCIe RC 的 ARM64 平台时发现 SMMU 就是那个平台的“内存访问总管”。每个 PCIe endpoint 的 DMA 和主机内部各加速器的 DMA最终都会走 SMMU 检查。如果一方配置错误整个平台的内存访问路径上都会出现故障。这和以前台式机上“IOMMU 只为一个 PCIe 子系统服务”的思维完全不同。4.2 Stage 1 / Stage 2 双级转换和 CPU 侧 MMU 的对称设计SMMU 最具特色的一点是完整支持两阶段地址转换用来和 ARM 虚拟化方案对齐。CPU 侧guest 的虚拟地址VA先经过 stage 1 翻译变成 IPA中间物理地址再经过 stage 2 翻译变成真正的物理地址PA。SMMU 侧设备发出的 DMA 地址先经过 stage 1 翻译对应 guest 的虚拟地址到 guest 物理地址再经过 stage 2 翻译对应 IPA 到 PA。这样设计的好处是在虚拟化场景下guest OS、hypervisor 和硬件可以“各管一段”。guest OS 的驱动负责配置 SMMU 的 stage 1hypervisor 负责配置 stage 2两者通过 IPA 衔接。如果只有单级翻译就要求 hypervisor 能够修改 guest 内核驱动的每一条 IOVA 映射记录那太痛苦了SMMU 的维护者评价“根本不可用”。在调试 stage 1/stage 2 时最大的坑是判断 fault 到底发生在哪个 stage。SMMU 的 event 记录里会明确标识 stage 1 fault 还是 stage 2 fault。如果你看到 stage 2 fault通常问题出在 hypervisor/VMM 的映射表看到 stage 1 fault多半是 guest 内驱动自己映射错误或者 DMA 描述符里的地址写错了。先分清是哪一级再往下看地址和 stream ID效率会高很多。4.3 StreamID、设置上下文与多队列隔离SMMUv3 的配置模型可以从“两条线”来理解。第一根线是 Stream Table EntrySTE每个设备在流表中有一条 STE里面放着该设备的配置信息最核心的是 stage 2 页表基址或指向上下文描述符的指针。第二根线是 Context DescriptorCD每个 CD 指向 stage 1 页表相当于一个独立的虚拟地址空间。为什么要分成 STE 和 CD 两层因为一个物理设备可能有多个功能function每个 function 又能创建多个 DMA 上下文。典型例子是高性能网卡上的多个 DMA 队列每个队列属于不同的网络 namespace 或者不同的虚拟机。SMMU 允许同一个物理设备的不同队列拥有不同的 CD因而拥有不同的地址转换结果。StreamID 字段在硬件里层层传递确保每一个 DMA 请求都能正确回到自己的地址空间。这一步在我实际使用 VFIO-mdev 这类设备时候理解得更清晰。mdevmediated device让一个物理设备被切分成多个虚拟设备每个虚拟设备有独立的地址空间底层依赖的就是 SMMU 的 StreamID 隔离能力。如果没有这种粒度控制多个虚拟设备共享同一张 DMA 页表任何一个 guest 的驱动都能通过 DMA 请求触达其他 guest 的内存。4.4 内存一致性SMMU 在总线层面要协调的事SMMU 还有一个常规 IOMMU 不太被强调的职责与缓存一致性总线的配合确保设备对共享内存的访问符合 ARM 内存模型。CPU 和 DMA 设备之间的一致性从来不是“地址翻译”能解决的它涉及 cache coherency 协议和总线上的监听snoop机制。如果你把一段 DMA 缓冲区映射成 cacheableCPU 会把它缓存起来。如果设备直接 DMA 写这块内存但 CPU 的 cache 里还残留旧数据CPU 稍后读到的还是旧值问题就来了。SMMU 不会自动替你做 cache flush它更多是“知道”这块内存是什么类型从而决定总线上要不要发起一致性操作。真正解决问题的是系统总线和驱动里对内存类型的正确设置。因此我处理 DMA 缓冲区始终记住一个原则如果需要 CPU 和设备频繁双向交换数据优先使用非 cacheable 映射或者使用一致的 DMA API如 dma_alloc_coherent让驱动层面不犯一致性错误。如果追求性能非要用 cacheable 映射就要自己管理 cache 的 clean/invalidate 操作而且要考虑 CPU 乱序执行和 DMA 并发访问时的时序平衡。4.5 ARM64 服务平台上 SMMU 的实际配置场景到了 ARM64 服务器级别SMMU 的使用场景越来越像传统 x86 上的 IOMMU但多了很多 ARM 生态特有的问题。一个典型例子是在 Linux 内核中你要通过设备树或 ACPI 表告诉 SMMU 驱动“这个设备是否需要 DMA 翻译”“它支持的最大粒度是多少”“要不要启用 ATS”。这些参数没配对设备 DMA 可能完全不通。配置 SMMU 时我最关注的三个参数是StreamID 位数它决定设备流表的索引范围队列深度尤其是 event queue 和 command queue 的深度这直接影响故障上报能力页表粒度SMMUv3 支持 4KB、64KB 等不同粒度选择时既要考虑设备 DMA 特性也要考虑 IOTLB 的命中率。在 Linux 上我会先用dmesg看 SMMU 驱动 probe 是否成功然后通过/sys/kernel/iommu_groups/查看设备分组情况。如果某个设备的 DMA 一直失败而日志里出现 SMMU event 字段里的 fault 记录那就说明翻译路径没配好先查设备到底是哪个 stream ID再查对应 STE/CD 是否正确比到处猜强得多。5. 调试实战从JZ2440裸机到Linux SMMU故障定位的经验5.1 JZ2440 的 MMU 调试先做恒等映射再谈其他对于 JZ2440 这类 ARM9 板子我最想给出的建议就是开启 MMU 之前永远先做恒等映射。也就是说先把当前正在执行代码所在的物理地址段映射成它本身的虚拟地址保证代码在 MMU 开启后不会立刻迷路。我见过很多新手做的第一版 MMU 实验把内存映射到了别的地址然后一开 MMU 程序就跑飞。这种问题非常难定位因为没有打印、没有串口输出。正确的顺序是关 MMU、关 icache/dcache在物理地址下初始化页表对全部 DDR 地址段、外设地址段分别建立恒等映射设置 domain、配置 C1 控制寄存器开 MMUMMU 稳定工作后再考虑把某一段地址映射到不同的虚拟地址并测试。如果恒等映射之后系统正常工作但换了一个不恒等的映射就挂那问题多半出在映射属性和范围设置上而不是 MMU 本身。以前我在某个板子上遇到程序跳转后死在中断向量表附近查了半天发现是中断向量表所在的地址段没有进入段映射范围导致取指失败非常隐蔽。5.2 Cache 与 DMA 一致性陷阱和地址翻译同等重要这里有一个非常经典的问题我觉得每个做嵌入式或者驱动的人都应该反复在心里默念三遍MMU 的地址翻译和 cache 策略是两套机制但配置在同一张页表描述符里。你可以在同一个页表里让地址映射变得“正确”同时因为 cache 属性设错而让数据变得“陈旧”。具体场景驱动分配了一个 DMA 缓冲区CPU 往里面填数据然后把地址告诉 DMA 控制器。如果这段内存被映射为 write-back cacheableCPU 写入后数据可能还在 cache 里没写回 DDR。DMA 控制器去读 DDR读到的还是旧数据表现就是网卡发出的包全是垃圾。解决办法我总结有三条路径把 DMA 缓冲区映射为 non-cacheable 或 write-through最简单但性能一般在 CPU 写完后显式执行 cache clean在 DMA 完成后执行 cache invalidate这套流程 Linux 的dma_map_single/dma_unmap_single已经帮你封装好了使用硬件一致性互连CCI/CMN的设备让总线帮你在 DMA 和 CPU 之间 snoop 数据这需要在系统设计阶段就保证内存类型和总线协议匹配。很多人在裸机阶段忽略这一点是因为不开 cache 或者小规模数据看不出问题。一旦上了 Linux数据吞吐变大DMA 一致性 bug 就会以“偶发错包”“特定大小包必丢”的形式冒出来。我调试这类问题的时间往往比调试地址翻译本身还要多。5.3 怎么从 Linux 日志里识别 SMMU 故障现场Linux 里 SMMU 的故障上报很有辨识度。SMMU 硬件会把 DMA 地址翻译失败、权限错误、页表遍历错误等记录到 event queue驱动会把它们打印到内核日志。我见到最多的是arm-smmu-v3 ... event 0x10 received: Reason: F_TRANSLATION StreamID: 0x0012 DeviceID: 0x0001 Address: 0x8000abcd这里event 0x10表示一个 translation error也就是设备 DMA 请求的地址无法被当前页表翻译。拿到 StreamID 之后我会去/sys/kernel/iommu_groups/或者lspci -vvv里找到对应的设备确认它到底处在哪个 IOMMU 域。再进一步检查设备的 DMA 映射是否已经建立是软件没有 map还是 map 后又被 unmap 了。如果你看到的是F_PERMISSION权限错误说明页表项存在但设备的访问权限读、写与该页表项设置不符。这时候重点排查驱动是否错误地以只读方式映射了要写的内存。要区分F_TRANSLATION和F_PERMISSION非常关键因为它们对应完全不同的排查方向。5.4 三个让我印象深刻的排障案例最后分享三个我做底层调试时的实际案例因为它们分别代表了三个不同层次的坑。第一个是驱动错用了 ioremap 做 DMA 映射。驱动把一个物理地址通过ioremap映射到内核虚拟地址然后把虚拟地址当物理地址传给 DMA。在没有 IOMMU 的板子上碰巧能工作因为虚拟地址恰好落在某个可用的区域一旦开启 IOMMUDMA 马上失败。后来我直接改用dma_alloc_coherent分配缓冲区它的返回值自然就是 IOMMU 可翻译的地址。第二个是IOMMU 开启后性能暴跌。平台开了 SMMU从 SSV 传大文件时吞吐量从理论值掉了一半。用 perf 看 DMA 相关 cache 项并追踪 IOTLB miss 的频率最后发现是因为所有 DMA 缓冲区都分散在不同的物理页IOTLB 命中率极低。解决方式是改用 2MB 大页映射 DMA 区域或者使用连续内存分配器CMA让 DMA 缓冲区集中在连续物理内存上IOTLB 命中率一下就上来了。第三个是虚拟化环境下 stage 2 映射冲突。直通设备在 VM 内工作时 SMMU 报 stage 2 fault经过排查是 VMM 分配的 guest 物理内存区域和 pre-mapped 的 DMA 区域重叠导致 stage 2 页表出现歧义。这个问题在单虚拟机时很难察觉一旦跑多个 VM 就爆发。最后我把 VMM 的内存分配策略改成显式预留 DMA 窗口才彻底解决。这三个案例的共同点是问题都不在“硬件坏了”而在“地址映射的某个层面没有对齐”。理解 MMU、IOMMU、SMMU 的职责边界能让你在拿到一条 fault 日志时快速判断故障落在哪个层面直接去查对应那套页表和上下文描述符而不是漫无目的地试。我不会说什么“随着硬件发展这套机制会越来越复杂”之类的空话只想说一个朴素的个人体会地址翻译机制的演进本质上是“谁能够被信任”这个问题的演进。MMU 让进程之间互相信任边界变得更清晰IOMMU 让设备和系统之间有了隔离SMMU 则在 ARM 世界把这条信任链延伸到虚拟化和异构并行场景。你越早掌握这三者的分工与配合就越不会被底层平台的怪异行为吓住。
返回列表