1. 从 DMA 讲起:IOMMU 到底管的是哪一段
IOMMU 这个缩写,很多人第一次是在 grub 内核参数里撞见的:intel_iommu=on。照着网上的教程加上、重启、然后 dmesg 里冒出一堆 DMAR 开头的日志,接着不管什么性能问题都开始怀疑它。这太常见了。
先说清楚 IOMMU 是什么的:它是 Input/Output Memory Management Unit,输入输出内存管理单元。CPU 那边有 MMU 管虚拟地址到物理地址的翻译,而 IOMMU 管的则是设备和内存之间的 DMA 访问。设备想读写内存时,不再直接拿物理地址去访问,而是先经过 IOMMU 这张"门禁表"做一次地址翻译和权限检查,合法才放行。
没有 IOMMU 的时代,DMA 设备写内存就像小区里随便进出的快递车——只要拿到一个物理地址就能直接访问,根本没人核对它有没有权限。这个设计在单机简单场景下没问题,可一旦牵扯到虚拟化、安全隔离或者大内存,麻烦就来了。
为了方便理解,可以把物理内存想象成一个仓库,每个货位就是一个物理地址。CPU 是这家仓库的内部员工,有工牌、有权限等级,想去哪就去哪,但也要过一道闸机(MMU)。外接设备比如网卡、GPU、NVMe 硬盘则像第三方物流的货车,没有 IOMMU 时,货车可以直接开进仓库乱翻;有了 IOMMU,仓库门口多了一个专门管货车的门卫,每一辆车要去哪个货位、能去哪些货位、一次最多装卸多少,都需要事先登记。
换句话说,IOMMU 本身不产生数据,它只做四件事:地址翻译、权限隔离、故障上报、中断重映射。这四个能力看似基础,但直接影响着虚拟化性能、直通设备稳定性,乃至整机安全。
最容易被误解的一点是 IOMMU 和 MMU 的关系。MMU 管的是 CPU 视角的内存视图,IOMMU 管的是设备视角的 DMA 视图,两者各管各的,互不替代。你的系统可以不开 CPU 虚拟化(就是 BIOS 里那个 VT-x),也可以没有任何虚拟机,但 IOMMU 依然可以独立开启,因为它的价值并不只服务虚拟化。
还有一个高频问题:为什么开了 IOMMU 之后,某些设备反而报 DMA fault?原因往往是设备驱动或者硬件自身没有按约定走 IOMMU 流程,而不是 IOMMU 功能坏了。IOMMU 干的是"严格执法"的活,它把以前被掩盖的非法 DMA 访问暴露出来了,所以你会看到一排排的 Fault 日志。这一点在后面的排查章节里会详细展开。
2. 为什么而开:安全隔离、设备直通与中断重映射三大驱动力
开启 IOMMU 不是没事找事。如果你只是普通桌面用户,不打游戏、不跑虚拟机、不搞透传,那开不开它的体验差异极小。但下面这三类需求,任何一个出现,IOMMU 都必须认真对待。
2.1 安全隔离:把 DMA 的"任意门"焊死
这是 IOMMU 最初也最本质的动机。做了安全方向的人都清楚,拿下 DMA 权限就约等于拿下了整台机器的内存。一个恶意或损坏的 PCIe 设备,理论上可以构造任意 DMA 请求,读取内核态敏感数据,甚至往内核内存里写 shellcode。这类攻击在真实环境中是有成熟利用链的,CTF 比赛里也经常出现。
IOMMU 一开,设备就只能访问被映射过的地址区间。对于普通驱动,内核会为它申请 DMA buffer 的那块区域建立映射;没映射过的地址,设备一旦尝试访问,IOMMU 直接阻断并上报 fault。这等于是在硬件层面给"任意 DMA"关上了门。
所以很多对安全合规有要求的服务器,不管虚拟化用不用,都会强制开启 IOMMU。注意这里说的"开启"是指完整模式,不能为了性能挂iommu=pt,因为 pt 模式下隔离粒度会大打折扣,这个后面会详细讲。
2.2 设备直通:虚拟化的性能救星
虚拟化场景是 IOMMU 最广为人知的用武之地。具体来说,当你希望虚拟机直接使用一块物理网卡、GPU 或者 NVMe 硬盘时,数据通路就不再想经过宿主机内核的模拟层了。设备直通(Device Passthrough)意味着设备要直接响应虚拟机发出的 DMA 请求,而虚拟机的内存物理地址往往是分散的,设备看到的地址如果不经过翻译,根本没法正确访问。
没有 IOMMU 的时候,虚拟化平台只能靠软件在宿主机内存里做 bounce buffer(弹跳缓冲),先把数据从 guest 内存抄到一片连续的物理内存里,再让设备读取,反过来再抄一次。数据来回拷贝,CPU 占用高、延迟大、吞吐量上不去,尤其是在万兆网卡、NVMe 这种高速设备上,性能损失非常难看。
有了 IOMMU,事情就变得优雅了:虚拟机里的设备驱动看到的地址,经过 IOMMU 的翻译表映射到真实的物理地址,设备直接访问,数据零拷贝。这也正是 VFIO 框架的核心依赖——VFIO 的直通能力如果没有 IOMMU 支撑,连设备分组都做不起来。可以说,没有 IOMMU 就没有现代意义上的高性能设备直通。
2.3 中断重映射:容易被忽视的第四项能力
IOMMU 不光管 DMA 数据,还管中断。Intel VT-d 规范里的 Interrupt Remapping 功能,会把设备的 MSI/MSI-X 中断请求重新映射到宿主机指定的中断向量上。这在虚拟化场景里尤其重要,否则设备直通后,虚拟机可以直接操控中断注入,往宿主机塞伪造的中断请求,这就是一个很经典的逃逸攻击面。
中断重映射启用后,dmesg 里会看到类似DMAR: Interrupt remapping enabled的日志。有些场景里,你只是想让 IOMMU 做 DMA remapping,不想承担中断重映射带来的复杂性,也可以通过nointremap内核参数关闭它。我个人的建议是不要轻易关,除非你有非常具体的兼容性问题需要排查。
3. 开启 IOMMU 的完整实操:BIOS、内核参数、验证命令
这个部分写给第一次接触 IOMMU 的人,已经折腾过的人可以直接跳到 4、5 节。先强调一句:IOMMU 的开关不是一个开关,而是"BIOS 开关 + 内核参数 + 设备驱动配合"三层链路,任何一层设置不对,最终效果都可能打了折扣。
3.1 BIOS 层面:VT-d 还是 AMD-Vi,别找错选项
Intel 平台的主板,IOMMU 的 BIOS 开关通常叫 "Intel Virtualization Technology for Directed I/O",缩写 VT-d。注意它和你开启 CPU 虚拟化扩展的那个 "Intel Virtualization Technology"(VT-x)是两码事,位置也往往不在同一个子菜单里。有些厂商主板的 VT-d 藏在 Chipset 或 Advanced -> Virtualization 配置下面,有些 OEM 服务器甚至默认就把 VT-d 关闭了,需要手动打开。
AMD 平台对应的选项通常是 "IOMMU" 或者 "AMD-Vi"。这里有个常见误区:AMD 平台开启虚拟化用的是 "SVM Mode",很多人在 BIOS 里看到 SVM 就以为 IOMMU 也开了,实际上 SVM 是 CPU 虚拟化扩展,和 IOMMU 不是一回事。AMD 平台的 IOMMU 开关要找独立选项,某些微星、华擎主板上它叫 "Overclocking" 菜单下的 "IOMMU" 子项,位置比较迷,找不到就用主板说明书搜索 AMD-Vi 或 IOMMU 关键词。
3.2 内核参数:intel_iommu=on 和 iommu=pt 怎么选
BIOS 开好之后,Linux 侧还需要通过内核 cmdline 显式开启。大多数现代发行版在 intel 平台默认是不开的,需要手动加参数。常用参数组合如下:
| 参数组合 | 效果 | 适用场景 |
|---|---|---|
intel_iommu=on | 完整开启 IOMMU,含 DMA remapping 与隔离 | 推荐默认使用,安全性最好 |
intel_iommu=on iommu=pt | 启动 IOMMU 但使用 pass-through 模式 | 对性能敏感、安全要求不高的场景 |
amd_iommu=on | AMD 平台开启 IOMMU | AMD 平台默认使用 |
nointremap | 关闭中断重映射 | 排查中断相关兼容性问题时临时用 |
igfx_off | 关闭内核对 iGPU 的 IOMMU 支持 | 解决多 GPU 直通时的 iGPU 异常 |
修改方式是在/etc/default/grub里的GRUB_CMDLINE_LINUX中追加,然后重新生成引导配置。Debian/Ubuntu 系执行update-grub,RHEL/CentOS 系执行grub2-mkconfig -o /boot/grub2/grub.cfg。如果你用的是 UEFI 引导,路径可能换成/boot/efi/EFI/<distro>/grub.cfg,保险起见先看当前系统实际读取的 grub.cfg 路径。
3.3 验证到底开没开:不要只看 dmesg 的一句话
装完参数重启后,第一件事不是急着跑虚拟机,而是确认核心功能真的生效。执行下面几条命令:
dmesg | grep -e DMAR -e IOMMU ls /sys/kernel/iommu_groups/ | head第一条如果有DMAR: IOMMU enabled这类输出,说明 IOMMU 软件层面已经工作。第二条如果列出一堆数字目录(比如 0、1、2、3 这种 group 编号),说明内核已经为设备分配了 IOMMU 域。这个命令非常重要,后面 4 节会反复用到。
一个容易踩的坑:ls /sys/kernel/iommu_groups/返回空,但dmesg里显示 IOMMU 已启用。这种情况通常是因为 BIOS 没开 VT-d/AMD-Vi,或者内核 cmdline 本身没有生效(比如 grub 配置写错位置)。只有 IOMMU 正常启用,/sys/kernel/iommu_groups/才会真正填充内容,我遇到过很多次"看了 dmesg 以为开了,结果 iommu_groups 目录空空如也"的情况,所以验证务必两条命令一起看。
4. IOMMU group 与 VFIO:直通设备的真正门槛
很多做设备直通的人都有这个体验:按照网上的教程把网卡加了 vfio-pci,重启后 QEMU 启动时仍然报错 "operation not permitted",或者设备明明在 lspci 里能看到,却无法从 vfio-pci 驱动接管。排查到最后,十有八九是 IOMMU group 的问题。
4.1 为什么直通的最小单位是 group 而不是单个设备
IOMMU 的分组逻辑是这样的:物理上处于同一个 PCIe 拓扑(比如同一个 Root Port 下的所有设备,或者同一个多功能设备的所有 Function)下的设备,通常会被划分到同一个 IOMMU group 里。这个 group 是硬件层面能够实现安全隔离的最小单元,也就是说,同一组里的设备共享同一个 IOMMU 保护域。
为什么要按组来分?因为如果两个设备在硬件上共享同一个 DMA 重映射单元,就不可能只放行其中一个、拦截另一个,安全性无从保证。硬着头皮把其中一个设备直通给虚拟机,另一个留在宿主机,理论上可能通过 PCIe 的某种机制互相干扰,内核为了安全直接禁止这种行为。这个"宁可不可用,不可不安全"的设计原则贯穿 VFIO 始终。
所以当你看到某个设备和一个不相关的 USB 控制器、声卡绑在同一个 group 里,不是内核分配失误,而是硬件拓扑决定的。查看分组的方法很简单:
find /sys/kernel/iommu_groups/ -type l | sort输出会列出每个 group 下挂着的设备 BDF(比如0000:00:1f.2)。如果目标设备的 group 下面只有它自己,直通就顺利;如果还挂了一堆别的设备,那么这些设备要么一起直通到同一台虚拟机,要么想办法拆分 group。
4.2 拆分 group 的思路:ACS 补丁的适用范围
当一个 group 里设备混着太多无关设备时,最常用的处理手段是加pcie_acs_override参数。这个参数告诉内核"我理解 ACS(Access Control Services)没有在硬件层保证隔离,但我依然允许拆开分组"。完整的写法通常是:
pcie_acs_override=downstream,multifunctiondownstream允许拆分下游端口上的设备,multifunction允许拆分同一个 PCI 多功能设备下的不同 Function。这是我处理多 GPU 直通时非常依赖的参数,没有它,很多消费级主板的 x16 插槽和旁边的 USB 控制器根本拆不开。
强调一点:pcie_acs_override是在牺牲安全隔离的前提下换取可用性,生产环境里要谨慎使用。如果你在做的是测试机或者个人工作站,那完全没问题;如果是对多租户隔离有要求的服务器,还是优先考虑购买带 ACS 支持的服务器级主板,或者干脆买多台机器。
4.3 把设备绑定到 vfio-pci 的标准流程
IOMMU group 没问题之后,设备直通的技术栈就比较固定了。第一步是加载 vfio 相关模块:
modprobe vfio-pci modprobe vfio_iommu_type1第二步是确认目标设备的 vendor ID 和 device ID,然后绑定 vfio-pci。可以用driver_override这种方式,也可以直接操作 sysfs:
echo "0000:01:00.0" > /sys/bus/pci/devices/0000:01:00.0/driver/unbind echo "8086 10fb" > /sys/bus/pci/drivers/vfio-pci/new_id这手动的过程适合排查问题,但每次开机都要做一遍。所以生产上我更推荐通过内核模块参数预绑定,在/etc/modprobe.d/vfio.conf里写:
options vfio-pci ids=8086:10fb,10de:1e84 softdep nvidia pre: vfio-pci这样开机时系统会先加载 vfio-pci 并接管指定设备,避免原生驱动抢占。这里有一个常见的坑:预先 blacklist 掉 nvidia、i915 这些驱动的行为有时候反而会引起问题,因为 N 卡驱动需要用到的并非只有 PCI 设备本身。软依赖(softdep)方式比直接 blacklist 更温和可控,推荐优先用。
绑定完成后,可以在 QEMU 命令行里直接把设备挂给虚拟机:
-device vfio-pci,host=01:00.0,multifunction=on如果执行到这一步顺利跑起来,说明 IOMMU、VFIO、IOMMU group 一整条链路都是通的。剩下的问题基本集中在设备固件、中断路由和性能调优上,这就轮到第 5 节了。
5. IOMMU 的代价:性能损耗、pass-through 模式与常见调优
IOMMU 不是免费午餐。每一步 DMA 访问多经过一层地址翻译,必然要付出性能代价。关键是这个代价有多大、能不能接受、怎么降到最低。
5.1 损耗从哪来:页表遍历、TLB 未命中、缓存维护
IOMMU 自己有一套页表,叫做 I/O Page Table,查找这个页表的过程对设备侧来说是额外的指令周期。更关键的是 TLB(这里特指 IOTLB,IOMMU 的页表缓存),当设备频繁访问大量分散的内存页时,IOTLB 会不断 miss,每次 miss 都要回内存里查页表,延迟会明显上升。
高速网卡的 DMA 操作非常密集,小包场景下每秒可能产生数百万次 DMA 请求,IOTLB miss 的比例一旦上去,吞吐量就绷不住。实测中在一些没有开启大页、没有优化映射的机器上,开 IOMMU 后包处理性能下降个百分之二三十并不罕见。这个数字取决于具体网卡、驱动压测方法和内核版本,不必当作绝对标准,但它足以说明问题:IOMMU 的性能损耗不能忽视。
另一个容易被忽视的开销是缓存一致性维护。处理器在修改 IOMMU 页表后,需要发送队列无效化命令(invalidate queue)让 IOMMU 更新 IOTLB。如果页表更新频繁,比如驱动频繁映射/解映射 DMA buffer,这部分开销会挤占正常 DMA 的带宽。所以很多高性能网络场景的调优都会强调:避免频繁的 DMA 映射操作,尽量复用已映射的 buffer。
5.2 pass-through 模式的本质:用隔离换性能
iommu=pt(pass-through 模式)就是在知晓代价之后做出的平衡选择。这个模式下,IOMMU 仍然工作,但它的页表映射退化成 identity mapping,也就是设备 DMA 地址直接等于物理地址,免去了复杂地址翻译的过程。中断重映射、fault 上报这些功能保留,但隔离粒度变粗了,设备之间无法通过 IOMMU 做到严格的内存访问隔离。
到底该用iommu=pt还是完整模式?我的经验如下:
| 场景 | 推荐配置 | 理由 |
|---|---|---|
| 生产服务器、多租户环境 | intel_iommu=on完整模式 | 安全隔离是第一优先级,性能可通过大页等手段缓解 |
| DPDK 高性能转发 | iommu=pt+ 大页 | 转发性能敏感,且通常设备直接绑定用户态驱动 |
| 显卡直通 | intel_iommu=on | 显卡 DMA 模式多为大批量连续传输,损耗影响有限 |
| 内网资源有限的集群 | 完整模式 + 数据面优化 | 避免故障时安全审查风险,后续运维省心 |
一个很常见的误区是:直通 GPU 也非要用iommu=pt。其实对于 GPU 这种大块连续 DMA 的场景,IOMMU 翻译的额外开销占比很小,开完整模式完全够用;反而是iommu=pt会降低设备隔离能力,得不偿失。DPDK 场景用 pt 模式则是合理的,因为小包处理对每一笔 DMA 的延迟都极其敏感。
5.3 直接可抄的调优清单
我在实际环境中反复验证过几组调优手段,按性价比排序:
- 开启 hugepages(2MB 甚至 1GB 大页),减少 IOMMU 页表遍历层级。IOMMU 对大页映射的处理开销远小于 4KB 小页面,这一条收益最直接。
- 把 dmesg 里 IOMMU 域的大小尽量保持稳定。驱动反复 map/unmap 会让页表频繁变更,必要时可以用 DPDK 等用户态驱动来替代传统内核驱动,把 DMA 映射集中管理。
- 确认内核启用了
CONFIG_INTEL_IOMMU_DEFAULT_ON相关的编译选项,部分发行版默认未开启,需要重新编译内核才能使用某些高级特性。 - 对纯网络转发服务器,可以在完整模式基础上单独为 DPDK 网卡开启 VFIO,而不是整个系统切换 pt 模式。这样既能保住安全隔离,又能获得接近零拷贝的性能。
如果你看到 IOMMU 开启后 NVMe 性能也有轻微下降,这同样是页表翻译的代价,正常现象。不要因为 1%-2% 的损失就关掉整个功能,很多系统管理员过于激进地关闭 IOMMU,结果在后续遇到内存隔离问题时反而更难收场。
6. 实战排查实录:DMAR fault、组拆分与内核参数调试链
很多人在 IOMMU 出问题时,第一条反应就是上网搜报错,然后被各种假教程带偏。这一节我分享一套自己的排查链路,几乎能覆盖八成以上的 IOMMU 故障。
6.1 典型日志解读:DMAR fault 到底在说什么
最常见的 IOMMU 报错长这样:
DMAR: DRHD: handling fault status reg 3 DMAR: [DMA Read] Request device [01:00.0] fault addr 0x1234000 [fault reason 0x05] PTE not present逐字段拆解一下。DMA Read表示这是一次读 DMA 请求;[01:00.0]是设备 BDF;fault addr是设备尝试访问的地址;fault reason 0x05是 IOMMU 返回的故障原因码,最常见的是 PTE not present,表示设备访问了一个没有建立映射的地址。
看到这条日志,不要急着怪 IOMMU。先确认这个设备是不是已经被正确绑定到某个驱动(vfio-pci 或原生驱动),驱动是否已经为 DMA 操作申请并映射了内存。如果驱动没有映射就发起 DMA,IOMMU 就会给这个 fault,这在本质上是一个驱动 bug 或设备固件 bug 的暴露。
排查顺序可以是这样:
dmesg | tail -100 ls -l /sys/kernel/iommu_groups/*/devices/ dmesg | grep -i -e vfio -e iommu -e dmar如果设备在 vfio-pci 下依然报 fault,最常见的原因是 QEMU 分配 guest 内存时没有正确传给 VFIO,或者 Linux 的 hugepages 分配不足导致映射归一了内存页。此时可以尝试在 QEMU 里加-object memory-backend-file,prealloc=on这类参数,强制预分配内存,减少运行时页表切换。
6.2 多设备同组拆不开:ACS 参数的合理使用
有一次我给两台 GPU 做直通,lspci 清清楚楚是两个独立设备,但 iommu_groups 里它俩死活在同一个 group 下。反复确认 BIOS 已经没有相关开关,最后用pcie_acs_override=downstream,multifunction解决的。
加参数有一个前提:你必须确认主板 PCIe 拓扑确实不支持 ACS。怎么确认?
lspci -vvv -s 01:00.0 | grep -i acs如果输出空白,说明该端口不支持 ACS。这时加pcie_acs_override是合理的;如果输出里明确写着ACSCap: ...,那也许只是内核没读全,可以用pcie_acs_override=downstream试探。我见过一些工人主板在 BIOS 更新后 ACS 能力被隐藏,需要非常旧的固件才能完整暴露,这类限制就别死磕了,换坑位比刷 BIOS 更实际。
另外,pcie_acs_override参数对直通本身不是必须的,它只是让你能在不合规的拓扑上强行拆分 group。即便不开这个参数,一些支持 ACS 的设备 group 是能自然分开的,所以先查lspci -vvv再决定动不动建议参数,优先级更高。
6.3 两个容易误导的隐藏问题:iGPU 与 AER 报错
IOMMU 常见的"幻影故障"还包括集成 GPU 和 AER(Advanced Error Reporting)。iGPU 在没有正常初始化的情况下一旦尝试 DMA,可能会触发 DMAR fault,导致系统日志刷屏,而实际功能影响不大。此时用igfx_off关闭 iGPU 的 IOMMU 支持即可,它不影响独立显卡直通。
AER 报错则是另一个高频干扰源。dmesg里出现PCIe Bus Error: severity=Corrected且伴随DMAR字样时,很多人会误判成 IOMMU 问题。实际上这是 PCIe 链路层的错误报告,和 IOMMU 没有直接关系。处理方法是先关掉CONFIG_PCIEAER(或者找到对应驱动禁用它),再观察 DMAR fault 是否消失。如果 AER 报错设备本身已经直通给虚拟机,那日志主要作为链路稳定性参考,大可不必为了刷干净日志重装整个系统。
6.4 内核 cmdline 优先级与 grub 链路的最后检查
最后一条经验,写给"改了内核参数但没有效果"的兄弟们。很多新手把参数加在/etc/default/grub的GRUB_CMDLINE_LINUX_DEFAULT里,也执行了update-grub,重启后cat /proc/cmdline一看,参数没进去。
原因多半是 grub 配置文件被覆盖,或者系统使用的是 BCD(Windows 引导管理器 + Linux 的 WSL2 双系统场景),或者主板 UEFI 里同时存在多个引导条目,实际引导的并不是你 update-grub 的那个。解决方法是先手动确认当前引导使用的配置:
cat /proc/cmdline如果参数缺失,检查/boot/grub2/grub.cfg或/boot/efi/EFI/*/grub.cfg里是否真的包含了新参数。部分发行版可能还需要grub2-mkconfig -o /boot/efi/EFI/<distro>/grub.cfg并重启一次才能让 UEFI 引导条目刷新。这里最容易出错的是 UEFI 启动时 grub.cfg 路径和传统 BIOS 启动路径不一致,很多时候你执行的 update-grub 更新了 A 路径的配置,而系统实际读取的是 B 路径。
如果cat /proc/cmdline已经确认有intel_iommu=on,但dmesg仍然看不到 IOMMU enabled,再检查一遍 BIOS 开关。注意部分 OEM 服务器在"启用虚拟化"之后还需要额外确认 I/O 虚拟化开关,不是同一个选项,漏掉任何一个都会导致 IOMMU 不生效。
最后再分享一个小技巧:我排查 IOMMU 问题时习惯性地会先执行find /sys/kernel/iommu_groups/ -type l | wc -l,看看到底有多少个 group。正常情况下这个数字应该和主板 PCIe 拓扑中的独立 Root Port 数量相当。如果 group 数量过少,说明设备都挤在同一组里,直通基本没法做;如果 group 数量异常多,反而要小心驱动的过滤问题。这比单纯看 dmesg 直观得多,至少在虚拟化直通这条赛道上,IOMMU group 的数量和分布就是一切问题的最短路径。