
1. 从一次网卡掉线说起IOMMU映射故障到底卡在哪手上有一台跑虚拟化的服务器板载的Realtek 2.5G网卡在直通给虚拟机之后每隔几小时就会莫名其妙掉线一次。dmesg里刷出一串DMAR: DRHD: handling fault status reg 3紧接着就是DMAR: [DMA Write] Request device [03:00.0] fault addr ffff...。虚拟机里的网卡直接变成NO-CARRIER重启虚拟机才能恢复。这个问题折腾了我整整两天最后定位到根因是IOMMU的DMA重映射表项没有正确建立导致设备发起的DMA请求被硬件直接拦截。IOMMUInput-Output Memory Management Unit这个硬件单元本质上就是给外设做地址翻译和访问隔离的。CPU访问内存有MMU做虚拟地址到物理地址的转换外设通过PCIe发起DMA请求时也需要一个类似的翻译层这就是IOMMU。它把设备看到的IOVAI/O Virtual Address翻译成真实的物理地址同时做权限检查。一旦映射关系没建好或者设备用了错误的地址发起请求IOMMU就会报fault轻则设备功能异常重则整个PCIe链路挂死。这篇文章适合谁看如果你正在做PCIe设备驱动开发、虚拟化直通配置、或者嵌入式平台上调试DMA相关问题尤其是遇到IOMMU fault、设备DMA异常、PCIe枚举失败这类现象那这篇排查记录应该能帮你少走弯路。我会从内核日志的解读开始一步步深入到硬件寄存器的读取把整个排查链路讲清楚。涉及的具体命令和寄存器操作都基于x86_64平台和Linux内核但排查思路在ARM64平台上同样适用。2. 排查前的知识储备IOMMU、PCIe与DMA的三角关系2.1 IOMMU在PCIe体系中的位置PCIe设备要访问系统内存走的是DMA路径。设备驱动在初始化时会向内核申请DMA缓冲区拿到一个总线地址bus address然后把这个地址写进设备的DMA寄存器。设备发起DMA请求时用的是这个总线地址。在没有IOMMU的系统中总线地址就是物理地址设备直接访问物理内存。有了IOMMU之后总线地址变成了IOVAIOMMU负责把IOVA翻译成物理地址。这个翻译过程依赖IOMMU的页表也就是DMAR表中的映射关系。每个PCIe设备更准确地说每个Requester ID在IOMMU中都有一个对应的页表。当设备发起DMA请求时IOMMU根据Requester ID找到对应的页表再用IOVA查表得到物理地址。如果查不到或者权限不匹配就产生fault。注意Requester ID通常由PCIe的Bus号、Device号、Function号组成也就是常说的BDF。排查IOMMU问题时BDF是贯穿始终的关键标识。2.2 内核日志中IOMMU相关信息的解读Linux内核在启动阶段会解析ACPI表中的DMARDMA Remapping表把IOMMU的硬件信息注册到系统中。dmesg里能看到类似这样的输出DMAR: IOMMU enabled DMAR: Host address width 39 DMAR: DRHD base: 0x000000fed90000 flags: 0x1 DMAR: dmar0: reg_base_addr fed90000 ver 1:0 cap 1c0000c40660462 ecap 7e3ff0505e DMAR: RMRR base: 0x0000000000000000 end: 0x00000000000fffffDRHD是DMA Remapping Hardware Unit Definition每个DRHD对应一个IOMMU硬件单元。base是它的寄存器基地址后面调试硬件寄存器时会用到。cap和ecap是能力寄存器描述了这块IOMMU支持哪些特性。当发生fault时内核会打印类似这样的信息DMAR: DRHD: handling fault status reg 3 DMAR: [DMA Write] Request device [03:00.0] fault addr 0xfffff000 [fault reason 05] PTE Write access is not set这里的关键信息有几个Request device [03:00.0]是发起DMA的设备BDFfault addr是出错的IOVAfault reason是具体的错误原因。常见的fault reason包括Fault Reason含义典型原因01Present bit not set页表项不存在映射未建立02Non-present PTE同上但发生在多级页表中间层05PTE Write access is not set页表项存在但无写权限06PTE Read access is not set页表项存在但无读权限07PTE Execute access is not set执行权限缺失2.3 排查工具链准备在开始深入排查之前需要准备几个工具。dmesg是最基础的但要注意内核日志的环形缓冲区可能会覆盖旧信息建议用dmesg -s 2097152增大缓冲区或者直接查看/var/log/kern.log。lspci用来确认设备的BDF和PCIe配置空间信息setpci可以直接读写PCIe配置空间寄存器。对于IOMMU硬件寄存器的读取需要用到devmem或者自己写内核模块因为IOMMU的寄存器在MMIO空间不在PCIe配置空间里。另外如果系统支持可以打开IOMMU的调试输出。在内核启动参数里加上iommudebug或者在/sys/kernel/debug/iommu/下查看相关信息。不过不同内核版本的调试接口差异较大最可靠的方式还是直接读硬件寄存器。3. 从内核日志到硬件寄存器完整排查流程拆解3.1 第一步确认IOMMU是否启用以及工作模式拿到一台机器先确认IOMMU的状态。查看/sys/class/iommu/目录下有没有设备节点或者直接看dmesg | grep -i iommu。如果看到DMAR: IOMMU enabled说明IOMMU已经在硬件层面启用并且内核也识别到了。如果只看到DMAR: IOMMU disabled或者完全没有DMAR相关输出那可能是BIOS里没开VT-d或者内核启动参数里加了intel_iommuoff。IOMMU的工作模式有两种strict和lazy。strict模式下每次DMA请求都会查页表性能开销大但安全性高lazy模式下会缓存翻译结果性能好但fault检测会有延迟。可以通过/sys/kernel/debug/iommu/intel/iommu_regs查看当前模式不过这个接口不一定存在。更直接的方式是看内核启动参数里有没有iommu.strict0。实操心得在调试阶段建议先用iommustrict模式这样fault会立即触发方便定位。等排查完成后再切回lazy模式提升性能。3.2 第二步解读fault日志定位出错的设备与地址假设dmesg里出现了这样的faultDMAR: DRHD: handling fault status reg 3 DMAR: [DMA Read] Request device [03:00.0] fault addr 0xfffff000 [fault reason 05] PTE Write access is not set先看Request device [03:00.0]这是BDF。用lspci -s 03:00.0 -vv确认这个设备是什么。假设是一块网卡那问题就聚焦在这块网卡的DMA操作上。fault addr 0xfffff000是出错的IOVA这个地址看起来像是页对齐的说明设备在访问某个页的边界时出了问题。fault reason 05表示页表项存在但没有写权限说明IOMMU页表里这个IOVA对应的条目是只读的但设备发起了写操作。接下来要确认这个IOVA是谁分配的。在Linux中DMA映射是通过dma_map_single或dma_map_page建立的。如果是驱动直接映射的那可能是驱动在映射时用了DMA_TO_DEVICE方向但设备实际做了写操作。如果是通过IOMMU的默认域default domain映射的那可能是内核的DMA API在某个环节出了问题。3.3 第三步读取IOMMU硬件寄存器确认页表状态内核日志只能告诉我们fault发生了但页表的具体内容需要读硬件寄存器才能确认。IOMMU的寄存器基地址在dmesg里已经打印了比如DRHD base: 0x000000fed90000。这个基地址是MMIO地址需要用devmem或者内核模块来读。IOMMU的寄存器布局在Intel的VT-d规范里有详细定义。关键寄存器包括VER0x00版本寄存器CAP0x08能力寄存器ECAP0x10扩展能力寄存器GCMD0x18全局命令寄存器FSTS0x34fault状态寄存器FECTL0x38fault事件控制寄存器FEADDR0x3Cfault事件地址寄存器RTADDR0x20根表地址寄存器先读FSTS确认fault状态devmem 0xfed90034 32如果返回值不为0说明有未处理的fault。然后读FEADDR获取fault事件在内存中的地址再根据这个地址去读fault事件的详细信息。fault事件的格式在VT-d规范里有定义包含fault reason、Requester ID、fault address等字段。注意直接读MMIO寄存器需要root权限而且不同平台的IOMMU寄存器基地址可能不同。如果devmem读出来全是0xFFFFFFFF可能是地址不对或者IOMMU被禁用了。3.4 第四步检查IOMMU页表映射关系IOMMU的页表是分级的根表Root Table指向上下文表Context Table上下文表指向二级页表。每个设备在根表中有一个条目根据BDF计算索引。根表地址在RTADDR寄存器里。假设RTADDR的值是0x00000000bf7fe000那根表的基地址就是0xbf7fe000。对于BDF03:00.0Bus号是3Device号是0Function号是0。根表索引的计算方式是(Bus 8) | (Device 3) | Function所以索引是0x300。每个根表条目是16字节所以偏移是0x300 * 16 0x3000。读这个地址的内容devmem 0xbf7fe000 64如果低位的Present bit是1说明这个设备有上下文表。上下文表的地址在根表条目的高52位。拿到上下文表地址后再根据Device和Function计算上下文表索引读取对应的上下文条目。上下文条目里包含了二级页表的地址和地址空间标识符。二级页表的遍历和x86的MMU页表类似都是4级或5级页表。根据fault addr逐级查表最终找到对应的页表项。如果页表项的Present bit是1但Write bit是0那就和fault reason 05对上了。3.5 第五步修复映射并验证定位到问题后修复方式取决于根因。如果是驱动映射方向错误需要修改驱动代码确保dma_map_single的方向参数和设备实际操作一致。如果是IOMMU页表被意外修改需要检查是否有其他代码在操作IOMMU页表。如果是硬件问题比如IOMMU的TLB缓存了旧的翻译结果可以尝试刷新TLB。刷新IOMMU TLB可以通过写GCMD寄存器的IOTLB_INV位来实现# 先读GCMD devmem 0xfed90018 32 # 设置IOTLB_INV位bit 3然后写回 devmem 0xfed90018 32 0x8修复后重新触发设备的DMA操作观察dmesg是否还有fault。如果fault消失且设备功能正常说明问题解决。4. 常见故障场景与排查速查表4.1 场景一设备直通后DMA失败这是最常见的场景。虚拟机直通一块PCIe设备设备在虚拟机里工作不正常dmesg里刷IOMMU fault。根因通常是VFIO驱动在建立IOMMU映射时没有正确设置权限位。比如设备需要写内存但映射时只给了读权限。排查步骤确认vfio-pci驱动已经绑定到设备检查/sys/kernel/debug/iommu/下该设备的映射信息用dmesg确认fault reason如果是05或06说明权限位不对检查QEMU的启动参数确认没有错误的iommupt配置实操心得VFIO直通时建议先用iommupt模式passthrough让设备直接使用物理地址绕过IOMMU翻译。如果iommupt下设备正常说明问题出在IOMMU映射上如果仍然异常那可能是PCIe链路或设备本身的问题。4.2 场景二PCIe枚举阶段就报IOMMU fault有些设备在BIOS枚举阶段就会触发IOMMU fault表现为系统启动时卡住或者设备无法识别。这种情况通常是设备的Option ROM或者固件在初始化时发起了DMA但此时IOMMU页表还没建立。排查步骤在BIOS里关闭IOMMU确认设备能否正常枚举如果关闭IOMMU后正常说明是IOMMU映射建立时机的问题检查内核启动参数尝试加上iommupt或者intel_iommuon,igfx_off对于特定设备可以在IOMMU的RMRRReserved Memory Region Report中预留内存区域4.3 场景三IOMMU fault导致系统挂死严重的IOMMU fault会导致PCIe链路挂死系统直接panic或者无响应。这种情况通常是设备发起了越界DMA访问了未映射的地址IOMMU无法处理触发了不可恢复的错误。排查步骤查看dmesg中是否有DMAR: DRHD: handling fault status reg 3之后的AERAdvanced Error Reporting信息用lspci -vv查看设备的AER能力确认是否有Uncorrectable Error检查设备的DMA寄存器配置确认没有越界访问如果设备支持启用ATSAddress Translation Service来减少IOMMU fault4.4 常见问题速查表现象可能原因排查方法解决方式DMA Read fault页表项无读权限读IOMMU页表确认权限位修改映射权限DMA Write fault页表项无写权限同上同上Present bit not set映射未建立检查驱动DMA映射代码补全映射设备直通后faultVFIO映射错误检查VFIO配置调整QEMU参数启动阶段fault固件DMA越界关闭IOMMU对比配置RMRR系统挂死不可恢复fault查看AER信息修复设备DMA5. 硬件寄存器调试的实操细节与避坑指南5.1 如何安全地读写IOMMU寄存器IOMMU寄存器在MMIO空间直接用devmem读写有风险因为写错寄存器可能导致系统崩溃。建议先读后写确认地址和值都正确。对于只读寄存器比如CAP和ECAP只读不写。对于GCMD这种控制寄存器写之前先读当前值修改特定位后再写回避免误改其他位。# 读GCMD当前值 current$(devmem 0xfed90018 32) # 设置IOTLB_INV位bit 3 new$((current | 0x8)) # 写回 devmem 0xfed90018 32 $new注意devmem的写操作是直接写硬件没有缓存和同步机制。写完之后最好再读一次确认写入成功。5.2 页表遍历的常见陷阱IOMMU页表遍历和MMU页表遍历类似但有几个坑。第一IOMMU页表的地址是物理地址不能直接用虚拟地址去读。第二IOMMU页表的层级可能和MMU不同VT-d规范里定义的是多级页表具体级数取决于IOMMU的能力。第三页表项的格式和MMU不同比如Present bit的位置可能不一样。在遍历页表时建议先用dmesg里的Host address width确认物理地址宽度然后根据VT-d规范里的页表格式逐级解析。如果读出来的页表项全是0可能是地址算错了或者IOMMU的根表地址不对。5.3 内核调试接口的局限性/sys/kernel/debug/iommu/下的接口在不同内核版本里差异很大。有些版本只提供只读的统计信息有些版本可以动态修改映射。在排查时不要完全依赖这些接口它们可能不完整或者有延迟。最可靠的方式还是直接读硬件寄存器虽然麻烦但信息最准确。另外内核的IOMMU驱动可能会缓存页表信息导致dmesg里的fault日志和实际硬件状态不一致。如果怀疑有缓存问题可以尝试刷新IOMMU TLB或者重启系统后再复现。5.4 实操心得从日志到寄存器的闭环验证我在排查网卡掉线问题时最初只看了dmesg里的fault日志以为是驱动映射方向错了。改了驱动代码后问题依旧后来读IOMMU寄存器才发现页表项确实存在但权限位不对。进一步排查发现是VFIO在建立映射时用了错误的权限标志。这个问题的教训是内核日志只能告诉你“发生了什么”但“为什么发生”需要读硬件寄存器才能确认。另一个心得是IOMMU fault的日志可能会被限流。如果fault频繁发生内核会限制打印频率导致你看到的日志不完整。可以通过/sys/module/intel_iommu/parameters/下的参数调整日志级别或者直接读FSTS寄存器确认fault计数。6. 从故障排查到性能调优IOMMU的进阶用法6.1 IOMMU对PCIe带宽的影响IOMMU的地址翻译会引入额外的延迟尤其是在strict模式下。对于高带宽设备比如NVMe SSD或者25G网卡IOMMU的翻译开销可能成为瓶颈。实测数据显示在strict模式下PCIe设备的DMA带宽可能下降5%到15%具体取决于IOMMU的TLB命中率。如果性能敏感可以切换到lazy模式让IOMMU缓存翻译结果。但lazy模式下fault检测会有延迟可能掩盖一些间歇性问题。折中方案是先用strict模式排查问题确认稳定后再切到lazy模式。6.2 使用ATS减少IOMMU翻译开销PCIe的ATSAddress Translation Service允许设备缓存IOMMU的翻译结果减少每次DMA都查IOMMU页表的开销。支持ATS的设备会在PCIe配置空间里声明ATCAddress Translation Cache能力。启用ATS后设备第一次DMA时查IOMMU页表后续DMA直接用ATC里的缓存。启用ATS需要在IOMMU的ECAP寄存器里确认支持然后在设备的PCIe配置空间里使能ATS能力。具体操作涉及写设备的ATC控制寄存器不同设备实现不同需要参考设备的数据手册。6.3 IOMMU与PCIe枚举的交互PCIe枚举过程中BIOS或者内核会扫描总线读取设备的配置空间。如果IOMMU在枚举阶段就启用了设备的Option ROM或者固件发起的DMA可能会触发fault。为了避免这个问题可以在枚举阶段暂时禁用IOMMU等枚举完成后再启用。在Linux内核里可以通过iommupt参数让IOMMU在启动阶段使用passthrough模式等系统起来后再切换到翻译模式。不过这个切换需要内核支持不是所有版本都能动态切换。6.4 排查工具推荐除了devmem和lspci还有一些工具可以辅助排查。iommu-utils包里的iommuview可以图形化展示IOMMU的映射关系不过需要内核支持debugfs。perf可以用来分析IOMMU翻译的性能开销trace-cmd可以跟踪IOMMU驱动的函数调用。对于硬件寄存器调试如果devmem不够用可以写一个简单的内核模块用ioremap映射IOMMU的MMIO区域然后通过/proc接口读写。实操心得写内核模块调试IOMMU时注意不要在中断上下文里操作因为IOMMU寄存器的读写可能需要睡眠。另外模块卸载时要确保没有残留的映射否则可能导致系统不稳定。7. 写在最后几个容易踩的坑IOMMU的fault日志里fault addr有时候看起来像是物理地址但实际上是IOVA。不要被地址的格式迷惑一定要用IOMMU页表去翻译。另外不同平台的IOMMU寄存器基地址不同不要硬编码地址要从dmesg或者ACPI表里动态获取。还有一个坑是IOMMU的TLB刷新。修改页表后一定要刷新TLB否则IOMMU可能还在用旧的翻译结果。刷新TLB的时机也很重要要在修改页表之后、设备发起DMA之前刷新。最后如果排查了很久还是找不到根因可以尝试在BIOS里关闭IOMMU确认问题是否消失。如果关闭IOMMU后问题依旧那可能不是IOMMU的问题而是PCIe链路或者设备本身的问题。这种二分法虽然简单但能快速缩小排查范围。