1. 从一块乱写内存的扩展卡说起:IOMMU 到底挡的是什么
几年前遇到过一个让我印象很深的问题:一台跑着几台虚拟机的宿主,插了一块二手的万兆网卡,前两周一切正常,第三周开始出现随机的进程崩溃、压缩包解压报 CRC 错误、数据库偶发校验失败。换内存、换系统盘、跑 memtest 全部通过,折腾了整整两天,最后是在内核日志里翻到一行以前从没认真看过的报错,才把方向从"内存坏了"转到"DMA 越界"上。
那行日志来自IOMMU(Input/Output Memory Management Unit,输入输出内存管理单元)。它替我们抓到了那个凶手:网卡固件在处理某个边界长度时,描述符算错了一位,往一块不属于它的物理内存里写了数据。如果这台机器的 IOMMU 没开,这个错误会一直静默地破坏内存,直到某天数据彻底对不上,而你会永远怀疑是内存条的问题。
所以这篇文章想聊的不是"IOMMU 是什么"这种教科书定义,而是一个更实际的问题:在什么情况下,开启 IOMMU 是必须的;在什么情况下,开了反而给自己找麻烦;以及开与不开之间,你到底交换了什么。无论你是折腾虚拟化直通的玩家,还是维护几台物理服务器的运维,或者只是给家用 NAS 加块扩展卡,这套判断逻辑都用得上。
1.1 没有 IOMMU 时,设备的 DMA 是一张空白支票
要理解 IOMMU 的价值,得先知道没有它的时候世界有多"裸奔"。CPU 访问内存要经过 MMU 做虚拟地址到物理地址的翻译,这一步有页表、有权限位、有用户态和内核态的隔离,写错了立刻触发缺页异常,程序被杀掉。但设备发起的 DMA 完全不走这条路。
一块支持总线主控(Bus Master)的网卡、阵列卡、NVMe 盘,它们的 DMA 引擎拿到的是驱动填进描述符里的地址。在传统模式下,这个地址就是物理地址——设备说"我要把这块数据写到 0x7f3a0000",内存控制器就老老实实照办,没有任何中间人过问"你是哪个设备""这块内存归不归你""你是读还是写"。
这意味着什么?意味着一个固件有 bug 的网卡、一块山寨的扩展卡、一个描述符环算错位的驱动,就能往任意物理地址上覆盖数据。而且这种破坏有三个非常讨厌的特征:随机性(取决于当时的物理内存布局)、延迟性(可能写坏的是几小时后才被读取的缓存)、误导性(报错的地方和出事的地方完全无关)。你看到的可能是某个进程段错误,真正的问题源头却是另一块卡。
提示:很多"玄学不稳定"的机器,最后查出来都是 DMA 越界或者地址位宽不匹配。判断方法很简单——看内核日志里有没有 IOMMU fault 或者 DMA 相关的告警。当然,前提是你得先把 IOMMU 打开,它才有能力告警。
1.2 IOMMU 的三层职责:翻译、检查、隔离
IOMMU 的结构和 CPU 的 MMU 很像,只是服务的对象从"进程"换成了"设备"。它挂在内存控制器和 I/O 总线之间,所有设备的 DMA 请求都要先过它这一关。它干三件事:
第一件是地址翻译。设备驱动不再往描述符里填物理地址,而是填一个 IOVA(I/O Virtual Address)。设备拿着 IOVA 发起请求,IOMMU 查自己的页表,翻译成真实物理地址。这一层和 CPU 页表几乎同构,也支持多级页表和大页。
第二件是权限检查。每个页表项上都有读写权限位。设备只有读权限的区域,写请求会被直接拦下并产生 fault。这就把"设备能干什么"从"物理上能到达哪里"收窄成了"驱动明确授权的那几页"。
第三件是隔离。IOMMU 把设备划分成不同的 domain,每个 domain 一套独立页表。A 设备的页表里根本没有 B 设备缓冲区的映射,所以 A 就算疯了一样乱发请求,也碰不到 B 的数据。这一条是虚拟化设备直通的基础,后面会展开讲。
打个生活化的比方:没有 IOMMU 的时候,快递员知道你家的门牌号,可以自己上门、自己开门、自己翻你抽屉。有了 IOMMU,你只给他一个快递柜编号,他只能把包裹放进那个柜子,柜子以外的区域他既进不去也不知道在哪。柜号和门牌号的对应关系,只有小区的管理处(IOMMU)知道。
1.3 开了和没开,实际差别在哪
把差别摊开成一张表会更直观:
| 维度 | 未开启 IOMMU | 开启 IOMMU |
|---|---|---|
| 设备看到的地址 | 物理地址 | IOVA(虚拟地址) |
| DMA 越界后果 | 静默破坏任意内存 | 被拦截并产生 fault 日志 |
| 设备间隔离 | 无,互相可访问 | 按 domain 隔离 |
| 设备直通虚拟机 | 无法安全实现 | 可以(配合 VFIO) |
| 外部接口 DMA 防护 | 基本没有 | 可在固件配合下启用 |
| DMA 映射开销 | 接近零 | 建立/销毁映射有成本 |
| 故障可观测性 | 几乎为零 | fault 日志可定位到具体设备 |
| 部分老设备兼容性 | 无影响 | 可能出现映射失败或走 bounce buffer |
这张表里最容易被低估的是倒数第二行——可观测性。开了 IOMMU 之后,硬件层面的越界访问会变成一条明确的日志,带上设备的 BDF(总线:设备:功能号)、出错地址、访问类型和原因码。这对排查疑难杂症的价值,有时候比隔离本身还大。
而最容易被高估的是最后一行。很多人担心"开了 IOMMU 性能掉一半",这个说法在十几年前有一定道理,现在的硬件代次和内核实现下,绝大多数场景的性能影响在个位数百分比以内。真正会掉性能的是另一件事:设备 DMA 能力受限时被迫走 bounce buffer,这个后面单独讲。
2. 设备直通场景:为什么把硬件交给虚拟机就绕不开 IOMMU
如果说普通用户开 IOMMU 是"顺手加个保险",那对做虚拟化直通的人来说,IOMMU 就是硬性前置条件,没有它整个方案根本不成立。这一节把直通这条路走一遍,顺便解释为什么有些卡能直通、有些卡怎么折腾都不行。
2.1 设备直通的最小必要条件
在 Linux 上做设备直通,主流路径是 VFIO 框架:把目标设备从原驱动上解绑,绑到vfio-pci上,然后把设备对应的 IOMMU group 整个交给虚拟机。虚拟机里的驱动以为自己在一块真实的物理设备上工作,实际上它的所有 DMA 请求都被 IOMMU 翻译到了虚拟机自己的内存区域里。
这里的关键在于"整个 group"这四个字。IOMMU 的隔离粒度不是单个设备,而是 group。同一个 group 内的设备共用一套地址翻译结构,IOMMU 没有办法区分"这个请求是 group 里 A 设备发的还是 B 设备发的"。所以你要把 group 里的某一个设备给虚拟机,就必须把整个 group 都给出去。
很多人第一次做直通失败,报的就是这个错:
# 直接把单块设备交给虚拟机时常见报错 vfio-pci 0000:03:00.0: not a valid IOMMU group member # 或者 qemu-system-x86_64: -device vfio-pci,host=03:00.0: vfio 0000:03:00.0: failed to add group意思很直白:这块设备所在的 group 里还有别的成员,你得一起处理。
2.2 IOMMU Group 是怎么划分出来的
group 的划分依据来自固件提供给内核的 DMA 重映射报告表(Intel 平台叫 DMAR,AMD 平台叫 IVRS)。内核解析这张表,结合 PCIe 拓扑,把设备分组。分组的基本原则是:如果两个设备之间的 DMA 请求无法被 IOMMU 区分来源,它们就在同一个 group。
什么情况会导致无法区分?典型的是它们挂在同一个 PCIe 桥下面,而这个桥又不支持 ACS(Access Control Services)。ACS 是 PCIe 的一个可选特性,作用是让下游端口的请求带上来源标识,从而让 IOMMU 能分辨"这个请求是从哪个下游口来的"。支持 ACS 的交换机和根端口,能让每个下游设备独立成组;不支持的话,整条链路下面的设备就打包在一起了。
所以你会发现一个很有意思的现象:同一块卡插在不同插槽上,直通结果可能完全不同。插在 CPU 直连的 PCIe 槽上,往往能独占一个 group;插在通过 PCH 扩展出来的槽上,可能和旁边的网卡、USB 控制器挤在一个 group 里,怎么调都不行。
2.3 分组不理想时的三条路,以及各自的代价
分组不理想的时候,实际能走的路就三条,每条都有代价:
第一条是换插槽。成本最低、风险最小,优先试。物理机箱打开,把卡从 PCH 扩展槽挪到 CPU 直连槽,再跑一遍分组查询脚本看结果。很多时候这一步就解决了。
第二条是用 ACS 覆盖补丁。社区里有给内核打的pcie_acs_override补丁,可以让内核忽略固件报告的拓扑限制,强制把设备拆成独立组。命令写法大致是:
# 内核启动参数(需要打了对应补丁的内核) pcie_acs_override=downstream,multifunction这条路要特别谨慎。它做的事本质上是绕过硬件层面的隔离能力,让 IOMMU 以为设备是隔开的,实际上硬件上并没有。如果 group 里的另一个设备被分配给了别的地方,两者的 DMA 是能互相看见的。自己做实验无所谓,放到多租户或者生产环境里就是安全隐患。我的态度是:能不覆盖就不覆盖,实在要覆盖,只给这台机器自己用,不接入任何不可信的工作负载。
第三条是放弃直通,改用 virtio 或者 SR-IOV。virtio 走的是软件模拟路径,性能不如直通但兼容性极好;SR-IOV 需要硬件支持虚拟功能,一块物理卡虚拟出多个 VF,每个 VF 天然独立成组,是数据中心里更干净的方案。代价是网卡、阵列卡要选支持 SR-IOV 的型号,价格通常高一档。
| 方案 | 隔离强度 | 性能 | 兼容性 | 适用场景 |
|---|---|---|---|---|
| 整组直通 | 高 | 接近原生 | 受分组限制 | 单卡独占、独立插槽 |
| ACS 覆盖后直通 | 中低 | 接近原生 | 好 | 单机实验、受控环境 |
| virtio 半虚拟化 | 高(软件层) | 中等 | 极好 | 通用虚拟化、网络吞吐一般 |
| SR-IOV | 高(硬件隔离) | 高 | 需硬件支持 | 多虚拟机共享一块物理卡 |
这张表的重点不是让你选"性能最高"的那个,而是提醒你:隔离强度和性能是可以分开权衡的两个维度。很多人只盯着性能跑分,忽略了隔离被削弱带来的连带风险。
3. Intel、AMD、ARM 三个平台的开启方式差异
确认了"为什么要开",接下来是"怎么开"。这部分看似只是抄几行参数,实际上不同平台的坑点差别挺大,尤其是固件层的选项命名,各厂商基本是自由发挥。
3.1 固件层要先打开,内核参数才有意义
一个非常常见的困惑:明明在内核参数里写了intel_iommu=on,重启后查日志却什么都没有。原因通常是固件里没开。
Intel 平台在主板固件里的选项一般叫这几个名字之一:
VT-d或Intel VT for Directed I/OIntel Virtualization Technology for Directed I/O- 有些消费级主板藏在
Advanced→CPU Configuration下面,还有的干脆叫IOMMU
AMD 平台的选项名一般是IOMMU或者AMD-Vi。ARM 服务器平台通常在固件里默认开启 SMMU,没有单独的开关。
需要额外注意一点:很多消费级主板在开启 VT-d 之后,会把选项旁边标注一行警告说"可能影响兼容性",这是厂商的保守表达。实际影响主要体现在老旧扩展卡上,后面第六节会讲怎么处理。
3.2 内核启动参数怎么写,以及为什么常常带 iommu=pt
最经典的写法是这两组:
# Intel 平台 intel_iommu=on iommu=pt # AMD 平台 amd_iommu=on iommu=ptiommu=pt(pass-through)的含义是:对宿主机自己使用的设备,使用恒等映射(identity mapping),也就是让 IOVA 直接等于物理地址,不做真正的翻译。只有被交给虚拟机的设备才建立完整的翻译页表。
为什么要这样?因为对宿主机自己的设备来说,地址翻译带来的隔离收益很小(这些设备本来就在同一个信任域里),但每次 DMA 映射都要改页表、发 IOTLB 失效、等确认,在高频小包场景下这是实打实的开销。用iommu=pt可以把这部分开销压到最低,同时保留完整的 IOMMU 框架,直通设备照常工作。
注意:
iommu=pt不是"关掉 IOMMU",它只是对宿主设备用恒等映射。设备直通的隔离能力、DMA 越界的拦截能力都还在。这个参数被误解过很多次,包括我自己早期也以为它是个性能妥协开关。
在较新的内核上,这个参数有了新的表达方式:
# 新内核(5.15 之后建议改用这两个) iommu.passthrough=1 iommu.strict=0两套写法的功能有重叠,具体哪个生效取决于你的内核版本。稳妥的做法是先只用一组,确认生效后再调整,不要把一堆参数从网上抄一遍全塞进去,出问题的时候根本不知道是哪个参数导致的。
3.3 ARM 平台与内核编译选项
ARM 服务器和部分嵌入式平台用的是 SMMU(System MMU),架构上对应 x86 的 IOMMU。开启方式不太一样:设备树或者 ACPI 表里描述 SMMU 节点,内核配置里打开对应驱动。
关键的内核配置项有几个,值得单独确认:
| 配置项 | 作用 | 说明 |
|---|---|---|
CONFIG_INTEL_IOMMU | Intel VT-d 支持 | x86 Intel 平台必需 |
CONFIG_INTEL_IOMMU_DEFAULT_ON | 默认开启 VT-d | 决定不写参数时是否生效 |
CONFIG_AMD_IOMMU | AMD-Vi 支持 | x86 AMD 平台必需 |
CONFIG_ARM_SMMU_V3 | ARM SMMUv3 支持 | ARM 服务器平台必需 |
CONFIG_IOMMU_DEFAULT_PASSTHROUGH | 默认 passthrough | 相当于内核层写死iommu=pt |
CONFIG_IOMMU_DEFAULT_DMA_LAZY | 默认懒刷新 | 影响性能和隔离强度的取舍 |
CONFIG_IOMMU_SUPPORT | IOMMU 框架总开关 | 前面几项依赖它 |
发行版内核一般已经把这些配好了,自己编译内核的时候才需要逐项检查。判断方法很直接,看/boot/config-$(uname -r)里对应项是不是=y。
grep -E "CONFIG_(INTEL|AMD)_IOMMU|CONFIG_ARM_SMMU|CONFIG_IOMMU_SUPPORT" /boot/config-$(uname -r)3.4 开启后怎么确认真的生效了
参数写完、重启完,别急着高兴,先验证。三条命令就够:
# 看内核是否识别到 IOMMU 硬件 dmesg | grep -i -e DMAR -e IOMMU # 看 IOMMU 子系统是否注册成功 ls /sys/class/iommu # 看设备分组是否已经建立 find /sys/kernel/iommu_groups/ -maxdepth 1 -type d | sort -V第一条命令的正常输出里应该能看到类似这样的内容:
DMAR: IOMMU enabled DMAR: Intel(R) Virtualization Technology for Directed I/O DMAR-IR: Enabled IRQ remapping in x2apic mode如果只有固件的 DMAR 表信息,没有IOMMU enabled这一行,说明固件开了但内核参数没生效,或者被别的参数覆盖了。如果连 DMAR 表都没打印,那基本是固件层没开。
第二条命令如果返回空,说明 IOMMU 子系统没有注册任何实例。第三条命令返回的目录数量一般和平台相关,几十个到上百个都正常——数量本身就是分组细粒度的体现。
想看得更直观,可以用这个脚本把每个 group 的成员列出来:
for g in /sys/kernel/iommu_groups/*; do echo "IOMMU Group ${g##*/}:" for d in "$g"/devices/*; do printf "\t%s\n" "$(lspci -nns "${d##*/}")" done done这个脚本我建议存成文件,每次换插槽或者换卡都跑一遍,比记在脑子里靠谱。
4. 中断重映射:最容易被忽略但最不能省的一环
聊 IOMMU 的时候,绝大多数资料都在讲 DMA 地址翻译,很少有人把中断重映射(Interrupt Remapping)单独拎出来说。但它在直通场景里的重要性不比地址翻译低,而且它是很多"参数明明加对了、直通就是不工作"问题的真正原因。
4.1 MSI/MSI-X 中断本身就是一次内存写
这一点是理解中断重映射的钥匙。现代设备用的 MSI/MSI-X 中断,本质上是设备往一个特定地址写一个特定数据,这个写操作由芯片组转发给 CPU 的中断控制器,最终触发一个中断向量。既然是内存写,那它天然就要经过 IOMMU 的检查。
问题来了:如果 IOMMU 开启了地址翻译,但中断重映射没开,设备发出的中断写会带着什么样的地址?这个地址由谁翻译、翻译成什么?没有中断重映射的时候,这套机制是不完整的,芯片组只能按固定规则处理,结果是中断可能被送到错误的 CPU、错误的目标,或者在虚拟化场景下被注入到错误的虚拟机里。
更严肃的问题在于安全。中断注入如果不受控,理论上一个被直通给虚拟机的设备,可以通过构造特定的中断写来影响宿主机或者其他虚拟机的中断处理流程。中断重映射的作用就是给每个中断源分配一个标识,让中断控制器能验证"这个中断确实来自这个设备,确实应该投递给这个目标"。
4.2 中断重映射不开时直通会出什么问题
实际表现通常是这几类:
- 虚拟机启动时直接报
vfio: failed to enable interrupt remapping,直通设备起不来 - 设备能直通进去,但中断收不到,虚拟机里表现为网卡收包卡死、阵列卡 IO 超时
- 直通设备能工作,但高负载下偶发中断丢失,日志里出现
irq 某某: nobody cared - 宿主机在设备热插拔时出现中断相关的告警
内核日志里验证中断重映射是否开启,看这一行:
dmesg | grep -i "DMAR-IR" # 正常输出 # DMAR-IR: Enabled IRQ remapping in x2apic mode如果没有这一行,或者显示xapic mode之外的内容,就需要检查中断控制器的工作模式。
4.3 x2apic 与 IRQ 重映射的搭配关系
中断重映射和 CPU 的中断控制器模式是绑定的。传统 xAPIC 模式下的中断投递方式比较受限,能表达的 CPU 数量有限。x2APIC 模式扩展了寻址能力,也是中断重映射正常工作更常见的搭配。
如果内核日志里显示中断重映射没启用,可以顺着这几个方向查:
- 固件里是否有独立的"中断重映射"选项被关掉了(多数平台跟随 VT-d/IOMMU 总开关)
- 内核参数里有没有
intremap=off,这个参数一般是排查问题时临时加的,很容易忘了删 - CPU 是否处于 x2APIC 模式,
dmesg | grep -i x2apic能看到 - 内核配置里
CONFIG_IRQ_REMAP是否开启
# 临时排查时才会用的参数,正常情况下不要加 # intremap=off提示:
intremap=off这类参数在社区帖子里经常作为"解决某个奇怪问题的偏方"出现。如果你的目标是设备直通,不要用这个偏方,它会让直通方案失去必要的基础。看到别人帖子里有这个参数,先想想他的目标是不是和你一样。
5. 性能账:IOMMU 的开销究竟在哪里
关于 IOMMU 的性能影响,网上流传的说法跨度极大——有说"几乎没有影响"的,也有说"吞吐掉三成"的。两个说法都有道理,因为它们的测试场景完全不同。搞清楚开销的来源,比记住某个百分比有用得多。
5.1 DMA 映射与 IOTLB 的基本开销
IOMMU 的性能成本主要来自三个环节:
第一是映射建立和销毁。驱动调用 DMA 映射接口时,内核需要在 IOMMU 页表里找到空闲的 IOVA 区间,建立页表项。设备用完再解除映射,又要回收区间、清页表项。这部分是 CPU 开销,和映射频率成正比。
第二是 IOTLB 失效。IOMMU 内部有自己的地址翻译缓存(IOTLB),类似 CPU 的 TLB。页表改了以后,缓存里的旧条目必须失效,否则设备会用到过期的翻译结果。失效操作需要发命令给 IOMMU 硬件并等待确认,这个等待在高频场景下会累积。
第三是 IOTLB 缺失时的查表。设备发起的 DMA 地址如果不在 IOTLB 里,IOMMU 要自己去内存里走一遍页表,多级页表就是多次访存。这个延迟直接加在设备的 DMA 路径上。
对照来看就很清楚:映射次数少、每次数据量大的场景(比如大块顺序 IO、大帧网络传输),IOMMU 开销几乎可以忽略;映射频繁、每次数据量小的场景(比如高频小包转发、大量小随机 IO),开销才会显现。
5.2 strict 与 lazy 两种刷新策略的取舍
内核提供了一个直接影响这个开销的开关:失效操作的执行时机。
**严格模式(iommu.strict=1,长期是默认值)**下,每次解除映射都会立即执行 IOTLB 失效并等待完成。安全边界很清晰:映射一解除,设备马上就无法访问那块内存。
懒模式(iommu.strict=0或者内核配置里的CONFIG_IOMMU_DEFAULT_DMA_LAZY)下,失效操作会被批量延迟执行一段时间。好处是大幅度减少了等待次数,性能提升在部分场景下很明显;代价是存在一个时间窗口:映射已经解除了,但 IOMMU 的缓存里还留着旧条目,设备在这段时间里仍然能访问那块刚被释放的内存。
这个窗口期意味着什么?在设备被移除、内存被回收、重新分配给别的用途的场景下,理论上存在被旧设备访问到的可能。所以在做设备热插拔、直通设备动态回收这类操作时,lazy 模式需要额外注意。
# 严格模式(默认,安全优先) iommu.strict=1 # 懒模式(性能优先,需要评估风险) iommu.strict=0我的建议是:如果这台机器上有不受信任的工作负载,或者你会做设备热插拔,用严格模式;如果是一台纯性能导向的单用户机器,可以试懒模式,但要清楚你放弃了什么。有些发行版在新版本里把默认改成了懒模式,升级内核之后性能突然变好或者某些奇怪问题突然出现,可以先查一下这个默认值有没有变。
5.3 大页与 ATS/PASID:让 IOMMU 少干活
除了改刷新策略,还有几层手段可以减少 IOMMU 的工作量:
使用大页映射。IOMMU 页表支持 2MB 甚至 1GB 的大页。同样一块内存,用 2MB 页只需要一个页表项,用 4KB 页要五百多个。页表项少了,覆盖同样地址范围所需的 IOTLB 条目也少,缺失率自然下降。这一条在设备 DMA 访问大块连续内存时效果明显。
ATS(Address Translation Services)。支持 ATS 的设备可以在自己内部缓存地址翻译结果,需要时直接问 IOMMU 要一份,之后就不用每次都走 IOMMU 查表了。这是把翻译成本从每次访问摊薄到每次缓存填充。前提是设备和 IOMMU 都支持,且固件正确描述。
PASID(Process Address Space ID)。让设备的请求带上进程标识,实现设备和 CPU 共享同一套地址空间(SVA)。对 GPU、加速卡这类需要和用户态进程直接交换数据的设备价值很大,普通网卡和存储卡用不上。
实际能用到哪一层,取决于硬件代次。近几年的服务器平台基本都支持 ATS,消费级平台支持情况参差不齐,查看方式可以从lspci -vv里找ATS相关的 Capabilities 字段。
5.4 哪些负载真正受影响,哪些是心理作用
给几个具体的判断依据,方便你对号入座:
| 场景 | IOMMU 开销感受 | 说明 |
|---|---|---|
| 大文件顺序读写 | 基本无感 | 映射次数少,单次数据量大 |
| 数据库随机小 IO | 轻微 | 取决于 IO 深度,通常低于 5% |
| 万兆网络常规收发包 | 轻微 | 使用iommu=pt后更小 |
| 25G/100G 小包转发 | 明显 | 需要关注大页和刷新策略 |
| NVMe 直通给虚拟机 | 轻微到中等 | 取决于队列深度 |
| 使用 bounce buffer 的老设备 | 严重 | 这才是真正的性能杀手 |
最后一行单独说一下。有些老设备只有 32 位 DMA 寻址能力,而系统内存大于 4GB,内核就必须用 bounce buffer(SWIOTLB)——把数据先拷到低地址区域再让设备访问。这个拷贝开销远大于 IOMMU 本身的翻译开销。开启 IOMMU 之后,内核可能更倾向于使用 SWIOTLB 而不是自己想办法绕过去,所以在这种老设备上,开启 IOMMU 后的性能下降主要来自 bounce buffer,而不是翻译本身。
# 看是否启用了 SWIOTLB 以及相关统计 dmesg | grep -i swiotlb cat /sys/kernel/debug/swiotlb/io_tlb_nslabs 2>/dev/null如果你的场景里出现了 SWIOTLB,优先考虑换设备或者加装支持 64 位 DMA 的控制器,而不是纠结 IOMMU 参数。
6. 开启后翻车的几种典型情况和排查链路
IOMMU 属于那种"平时不出声、出事就让人一头雾水"的硬件特性。这一节把几种常见的翻车现场和排查顺序整理出来,遇到问题时按顺序走,比盲目搜帖子快得多。
6.1 开了之后进不去系统或者卡在启动阶段
第一种情况是加了参数之后系统起不来。表现可能是启动卡在某个驱动初始化、黑屏、或者直接崩到救援模式。原因通常是固件实现的 IOMMU 有缺陷,内核在建立初始映射时失败。
处理顺序:
- 先只加一个参数。不要一次把
intel_iommu=on iommu=pt iommu.strict=0全加上,只加intel_iommu=on,看能不能起来。能起来的话,再逐个加后面的。 - 加
iommu=soft试试。这个参数让内核使用软件实现的 SWIOTLB 代替硬件 IOMMU,绕过有问题的固件实现。代价是性能损失,但能让系统先跑起来。 - 检查有没有和虚拟化相关的参数冲突。有些平台的固件在开启虚拟化扩展和 IOMMU 时行为不一致。
- 确认固件版本。主板厂商的固件更新日志里经常有"修复 IOMMU 相关问题"这种条目,遇到诡异现象值得先去官网查一下。
# 现代 Linux 用这条在各启动项里临时追加参数做测试 # 在内核选择界面按 e 进入编辑,在 linux 行末尾追加 intel_iommu=on6.2 DMAR/IOMMU fault 日志怎么读
如果系统能起来,但运行中出现问题,日志就是最主要的线索来源。一条典型的 fault 日志长这样:
DMAR: [DMA Read] Request device [03:00.0] fault addr fff00000 [fault reason 06] PTE Read access is not set逐段拆开看:
| 字段 | 含义 | 怎么用 |
|---|---|---|
DMA Read | 访问类型 | 读或写,判断设备在做什么 |
03:00.0 | 设备 BDF | 用lspci -s 03:00.0定位是哪块设备 |
fault addr | 出错地址 | 结合驱动代码看这块地址应该属于谁 |
fault reason | 原因码 | 权限不足、页表项不存在、地址位宽不符等 |
拿到 BDF 之后,这一步一定要做:
lspci -s 03:00.0 -nn lspci -s 03:00.0 -vv | head -40第一条确认是什么设备,第二条看它的 DMA 能力和链路信息。大多数 fault 日志指向的都是驱动 bug 或者设备固件 bug,而不是 IOMMU 本身有问题。IOMMU 在这里扮演的是"报警器",把原本会静默破坏内存的错误暴露出来了。所以看到 fault 日志的第一反应不该是"把 IOMMU 关掉",而是"终于抓到了"。
当然也有例外。如果 fault 是在设备正常工作时大量、规律地出现,而且换到老旧内核上不出现,那可能是内核驱动和 IOMMU 子系统的配合问题,这种情况下去查对应驱动的更新记录比查 IOMMU 更有用。
6.3 老设备、扩展卡和固件缺陷
有几类设备在开启 IOMMU 之后特别容易出问题:
- 只支持 32 位 DMA 的老网卡、老阵列卡:被迫走 bounce buffer,性能骤降,日志里能看到 SWIOTLB 相关输出
- 固件里 ATS 描述有误的设备:IOMMU 按固件描述建立映射,描述错了就翻译错,表现为间歇性数据传输错误
- PCIe 桥后面挂一堆设备的扩展卡:分组混乱,直通困难
- 某些 USB 控制器和采集卡:对映射约束有特殊要求,容易出现映射失败
这几类问题的共同处理思路是:先用iommu=pt缩小影响范围,如果iommu=pt下问题消失、完整翻译下问题出现,那基本能确定是这个设备的映射能力问题,可以考虑换设备或者对这块设备单独放宽约束。
6.4 一套可复用的排查顺序
把上面的经验整理成一个固定流程,遇到 IOMMU 相关问题按这个顺序走:
- 确认是否真的开启:
dmesg | grep -i iommu,看IOMMU enabled和DMAR-IR两行 - 确认分组情况:跑一遍分组脚本,看目标设备是否独立成组
- 确认中断重映射:日志里有没有
Enabled IRQ remapping - 看 fault 日志:
journalctl -k | grep -i -e DMAR -e "IOMMU fault" - 临时降级测试:加
iommu=pt或iommu=soft看问题是否变化,用来区分是翻译问题还是框架问题 - 检查设备能力:
lspci -vv看 DMA 位宽、ATS、ACS 支持情况 - 最后才考虑关掉:把前六步的结论记下来,再决定是否值得为了某个设备放弃 IOMMU
这个顺序的核心逻辑是先缩小范围再下结论。跳过前面几步直接关掉 IOMMU,等于把一个能帮你抓 bug 的工具扔掉,下次遇到同样的问题还得从头开始猜。
7. 到底该不该开:按场景给出取舍建议
回到标题本身的问题。经过前面这些展开,答案其实已经很清楚了:IOMMU 不是一个"开了就变好"的开关,它是一个把设备访问从无序变成有序的基础设施。需不需要它,取决于你的设备是否可信、是否要做设备隔离、以及你是否在意故障可观测性。
7.1 家用 NAS 和轻量虚拟化
这是最常见的场景。一台机器上跑着存储服务、几个容器、可能还有一两台虚拟机。
建议开,并且建议带上iommu=pt。理由有三点:一是这类机器常插各种扩展卡(网卡、HBA、扩展 SATA 控制器),来源杂、质量参差,DMA 越界的概率不低;二是如果以后想把某块盘或者某块网卡直通给虚拟机,开了就不用重装重新折腾;三是iommu=pt已经把宿主设备的开销压到很低,日常使用基本感觉不到差别。
需要留意的是,这类机器的固件往往是消费级主板,VT-d 选项可能藏得比较深,或者需要先开启虚拟化扩展才能看到 IOMMU 选项。
7.2 多虚拟机宿主和长期运行的服务器
必须开,而且建议使用严格刷新模式。这类环境下隔离本身就是需求的一部分,不是可选项。多个虚拟机共享同一套硬件,如果设备之间没有 IOMMU 隔离,一个虚拟机的设备理论上能读写另一个虚拟机的内存。这不是理论担忧,而是实实在在的攻击面。
同时这类环境对故障可观测性的要求也更高。生产机器上出现一个无法解释的数据损坏,排查成本可能是几天的停机。有 IOMMU fault 日志在手,定位时间能压缩到分钟级别。
7.3 普通桌面和笔记本
看情况。如果你是普通办公和娱乐用途,不涉及虚拟化和设备直通,开不开的差别主要体现在两方面:
一方面,现代平台开启 IOMMU 之后还有一个不太被提及的收益——外部接口的 DMA 防护。笔记本的雷电接口、部分支持 PCIe 的外接扩展坞,这些接口允许外部设备直接发起 DMA。在固件配合的前提下,IOMMU 可以对这些外部设备的访问范围做限制。这是平台层面的一项防护能力,不是软件能替代的。
另一方面,个别笔记本平台的固件在开启 IOMMU 后会出现休眠唤醒异常、外接显示器识别问题等。如果遇到这类情况,可以先在固件里关掉再观察,确认是不是 IOMMU 引起的。
我的建议是:能开就开,遇到具体问题再单独处理。因为不开的话,遇到问题连日志都没有,只能靠猜。
7.4 我的默认策略
折腾了这些年,我现在的默认做法是这样:新装一台机器,第一件事是进固件把虚拟化相关选项全部打开,装完系统第一件事是确认dmesg里有IOMMU enabled和Enabled IRQ remapping in x2apic mode这两行。参数上先用intel_iommu=on iommu=pt这套最保守的组合,等有明确需求再考虑调整刷新模式。
分区上留一个专门跑虚拟机的环境,把可能要直通的设备(网卡、HBA、显卡)尽量插在 CPU 直连的 PCIe 槽上,装机的时候就规划好,省得后期为了调分组把机箱拆来拆去。
踩过几次坑之后我越来越觉得,IOMMU 这类基础能力最值得投入的地方不是"调优",而是**"在有需要的时候它已经在那里"**。很多硬件层面的问题,有没有 IOMMU 决定了你是花两个小时定位,还是花两天换零件。
最后分享一个小习惯:每次换硬件、换插槽、更新固件或者升级内核之后,我都会把那三条确认命令跑一遍,把分组脚本的输出存到一个文本文件里,文件名带上日期。看起来有点多余,但真的遇到问题时,有一份"之前是正常的"的输出做对比,排查效率完全不一样。