资讯动态

Linux 6.6 IOMMU DMA重映射原理与故障排查

发布时间:2026/10/23 5:36:02 来源:尧图企业网站定制
设备直通跑不起来、驱动 probe 阶段报DMAR: DRHD相关错误、虚拟机里网卡一开大包就崩这些现场我见得太多了最后追下去十有八九落在一个东西上——IOMMU 的 DMA 重映射。Linux 6.6 这一版把 IOMMU 子系统里几处历史遗留的接口做了收敛iommu_ops上的map/unmap回调彻底退场统一走map_pages/unmap_pagesiommufd也已经站稳脚跟。这意味着你手上那些几年前照着老接口写的驱动代码、调试笔记可能需要重新过一遍。这篇就聊 DMA 重映射这件事本身IOMMU 在总线上到底插在哪、一次 DMA 请求从设备发出到内存落盘经历了什么、内核里dma_map_single最后是怎么变成一个页表项的。适合已经能看懂基本 PCI 枚举、写过字符设备驱动、想往虚拟化或设备直通方向深入的同学也适合日常被iommu相关 dmesg 刷屏、想搞清楚那些报错到底在说什么的运维同学。1. 先看清问题没有 IOMMU 的 DMA 世界是什么样讨论重映射之前得先把不重映射的状态描述清楚。很多概念之所以难懂是因为大家直接从IOMMU 做什么入手跳过了没有它的时候会发生什么。把缺省状态想明白后面所有设计取舍基本都能自己推出来。1.1 物理地址 DMA 的三个硬伤在完全没有 IOMMU 的机器上驱动调用dma_map_single()返回的那个dma_addr_t本质上就是物理地址中间可能经过swiotlb的 bounce buffer但那是另一回事。设备拿到这个地址直接往总线上发写请求内存控制器收到就写进去了。这个过程没有任何中间人检查。第一个硬伤是越界无感知。设备固件有 bug、描述符环算错、指针被踩设备就可能往任意物理地址写数据。CPU 侧的 MMU 保护不了这条路径因为 DMA 根本不经过 CPU。一个网卡的环形缓冲区索引算错一位理论上可以覆盖内核代码段。第二个硬伤是地址空间受限。32 位设备只能发 32 位地址可机器有几十上百 GB 内存设备根本够不着高地址。传统解法是ZONE_DMA32加 bounce buffer把数据先拷到低 4G 区间再让设备读。拷贝是有成本的万兆网卡跑满带宽时这个开销很可观。第三个硬伤是隔离无法实现。把一块物理网卡直接分配给虚拟机等于把这个设备能访问的全部物理内存都交给了虚拟机。虚拟机里的驱动写错一个地址宿主机的内存就没了。没有 IOMMU 的直通只能靠信任客户机工程上不可接受。1.2 IOMMU 在总线上的位置一次请求的完整路径IOMMU 的位置可以用一句话概括它卡在设备与内存控制器之间所有来自设备的 DMA 请求都必须先经过它。注意是设备发起的方向CPU 访问内存不经过 IOMMU。一次完整的 DMA 写请求大致是这样走的设备驱动把数据准备好把缓冲区的 DMA 地址写进设备的描述符或寄存器设备发起 PCIe 写请求TLP请求头里带上Requester ID也就是 BDFBus/Device/Function请求到达 IOMMUIOMMU 用 Requester ID 去查自己的根表Root Table找到这个总线号对应的上下文表Context Table用 BDF 的低位索引上下文表拿到该设备对应的页表基址和翻译配置用请求里携带的地址对设备来说就是 IOVAI/O Virtual Address去查页表得到物理地址如果配了权限位还要检查读/写权限越权直接拦截翻译结果写入IOTLB缓存然后请求被转发到内存控制器用物理地址完成访问。这套流程里最关键的一点是设备完全不知道自己发的是虚拟地址。它以为自己访问的是从 0 开始的一段连续地址空间实际上背后被映射到了物理内存的任意碎片上。驱动拿到的dma_addr_t就是这个设备视角地址。提示不同平台把这套硬件的叫法不一样x86 上叫 DMA 重映射单元ARM64 上习惯叫系统内存管理单元SMMU。名字不同模型是一致的都是根据设备标识查表把设备地址翻译成物理地址。后面统一用 IOMMU 指代。1.3 重映射到底重了什么重映射这三个字容易被误解成加个偏移。它实际做的是两件更本质的事。第一件是地址翻译。设备看到的地址空间和物理地址空间解耦了。驱动可以拿到一段从 0x1000 开始的连续 IOVA背后可以是散落在物理内存各处的 4K 页。这意味着设备不再需要物理连续的大块内存——散列/聚集scatter-gather从设备能力变成了软件能力。对老式只能做单块 DMA 的设备来说这是质变。第二件是访问控制。页表项里有读/写/执行权限位有 supervisor 之类的属性。IOMMU 会拿 Requester ID 找到对应设备的页表一个设备只能访问自己那张表里映射过的区域。设备越权访问会被硬件拦截产生一个DMA remapping fault内核通常在 dmesg 里打出一条包含源设备 BDF、访问地址、错误类型的记录。这里有个理解上的关键点映射表的粒度是设备不是驱动也不是进程。同一块物理网卡同一时刻只会挂在一张页表上。这也解释了后面要讲的iommu_group概念——如果两个设备共享同一张表那它们之间就没有隔离可言了。2. Linux 6.6 的 IOMMU 软件栈四层抽象内核里 IOMMU 相关的代码分布在drivers/iommu/下从硬件驱动到 DMA API 的胶水层大概可以分四层看。把层次理清楚看代码时就不会在函数调用链里迷路。2.1 iommu_device、iommu_ops、iommu_domain 各管什么这三个结构是理解整个子系统的钥匙职责边界非常清晰。struct iommu_device代表一个 IOMMU 硬件实例。一块主板可能有好几个 IOMMU比如多路机器上每个 CPU 插槽对应一个每个实例注册一个iommu_device并出现在/sys/class/iommu/下面。它管的是硬件寄存器基址、中断、全局能力位这类信息。struct iommu_ops是驱动实现的回调集合本质上是一张操作表。厂商驱动在初始化时把自己实现的那组函数挂进去往上注册。6.6 里这张表的核心成员大致是这些struct iommu_ops { bool (*capable)(struct device *dev, enum iommu_cap cap); struct iommu_domain *(*domain_alloc)(unsigned int type); int (*probe_device)(struct device *dev); void (*release_device)(struct device *dev); int (*dev_enable_feat)(struct device *dev, enum iommu_dev_features f); int (*dev_disable_feat)(struct device *dev, enum iommu_dev_features f); int (*attach_dev)(struct iommu_domain *domain, struct device *dev); void (*detach_dev)(struct iommu_domain *domain, struct device *dev); int (*map_pages)(struct iommu_domain *domain, unsigned long iova, phys_addr_t paddr, size_t pgsize, size_t pgcount, int prot, gfp_t gfp, size_t *mapped); size_t (*unmap_pages)(struct iommu_domain *domain, unsigned long iova, size_t pgsize, size_t pgcount, struct iommu_iotlb_gather *gather); phys_addr_t (*iova_to_phys)(struct iommu_domain *domain, dma_addr_t iova); };注意map_pages的签名里有pgcount。这是 6.6 相对早期版本最明显的变化之一一次回调可以映射连续的多页而不是一页一回调。老接口-map(domain, iova, paddr, size)虽然也能一次传大 size但语义上驱动自己决定怎么拆新接口把页大小和页数量拆成两个参数让上层能精确表达用多大的页、映射多少页。struct iommu_domain代表一个地址空间也就是一套页表。它有三个核心字段值得记字段含义实际调试中的用处type域的类型DMA、DMA_FQ、IDENTITY、BLOCKED、UNMANAGED判断这块设备走的是 DMA API 还是直通映射是排查映射为什么没生效的第一入口pgsize_bitmap该域支持的页大小位图大页是否生效、映射是否被打散全看这个geometry地址窗口aperture_start、aperture_end、force_apertureIOVA 分配器的可用范围窗口外的地址根本不会分给你type的取值直接决定行为。IOMMU_DOMAIN_DMA表示这个域由内核 DMA API 托管设备驱动正常调dma_map_*就行IOMMU_DOMAIN_IDENTITY表示直通IOVA 等于物理地址等于绕过翻译IOMMU_DOMAIN_BLOCKED表示全部拦截用于设备还没被驱动接管时的保护状态IOMMU_DOMAIN_UNMANAGED是给 VFIO 这类自己管页表的使用者准备的。注意设备在probe之前通常处于BLOCKED域此时它发任何 DMA 都会被拦。所以你在驱动probe里看到设备能正常工作、但更早的阶段一访问就 fault这往往是正常行为不是故障。2.2 iommu_group 为什么按组隔离这是新手最容易卡住的地方。直觉上隔离应该是每设备一张页表但现实里硬件做不到于是内核引入了iommu_group。原因在于 PCIe 交换机和多功能设备的结构。一个 PCIe 交换机下的多个下游端口如果交换机本身不支持 ACSAccess Control Services那么下游设备之间的点对点 DMA 是可以绕过上游的地址翻译的。换句话说硬件层面这几个设备互相看得见对方的内存访问软件再怎么分页表也没用。内核的处理办法是把这些分不开的设备塞进同一个 group整个 group 共享一个 domain。你只能整组 attach、整组 detach。想看当前机器的分组情况for g in /sys/kernel/iommu_groups/*; do echo ${g##*/} for d in $g/devices/*; do [ -e $d ] echo $(basename $(readlink -f $d)) done done如果输出里某个 group 挂了两个设备而你只想把其中一个直通给虚拟机那就会失败。解法有两个换一个支持 ACS 的插槽/交换机或者在内核命令行加pciacs_maskdownstream之类的参数强制打开 ACS前提是硬件实际支持只是 BIOS 没开。后者有风险因为强制打开可能引入不可预期的路由行为我曾经见过开了之后某块卡直接不识别的情况所以只在实验环境用。还有一个相关概念叫别名alias。有些平台上一个物理设备会以多个 BDF 出现比如某些集显和它的音频功能固件通过iommu_fwspec声明它们其实是一个设备。内核会把它们归到同一个 group并且共用一套翻译配置。如果你手动往 sysfs 里写 BDF 去绑定驱动绕过了这套别名关系很容易出问题。2.3 6.6 里接口收敛的几处关键变化6.6 这个版本在 IOMMU 子系统上不算激进但前面几个版本积累的接口收敛在这里已经全面落地了读老代码时要有心理准备。第一处是map/unmap回调的退场。早期iommu_ops里同时有-map、-map_pages、-unmap、-unmap_pages驱动实现哪个都行上层要写兼容分支。现在统一到map_pages/unmap_pagesiommu_map()内部按页大小切分后调ops-map_pages()iommu_map_sg()也建立在同一套原语上。对驱动作者来说实现负担轻了对读代码的人来说调用链短了。第二处是 IOTLB 失效的批量收集。struct iommu_iotlb_gather贯穿 map/unmap 流程把改页表和刷 TLB解耦。以前每 unmap 一页就要刷一次现在可以先攒着最后统一iommu_iotlb_sync()。这在 unmapping 大块内存时能省掉大量 MMIO 写操作实测在映射几十万页的场景下差别非常明显。第三处是iommufd的成熟。用户态现在可以通过/dev/iommu直接管理 IOMMU 页表不必再走 VFIO 的老接口。这对做用户态虚拟化方案的团队意义很大——页表管理从内核里挪到了用户态灵活度高得多。不过这条线展开要另写一篇这里先记住它的存在。3. DMA 重映射的内核路径从 dma_map_single 一路走到页表项这一段是全篇的核心。很多人能背出IOMMU 把 IOVA 翻译成物理地址但具体到内核里哪一行代码干了什么就说不上来了。我们顺着调用链走一遍。3.1 dma-iommu.c 这层胶水做了什么驱动的入口永远是标准的 DMA APIdma_addr_t dma_handle dma_map_single(dev, buf, len, DMA_TO_DEVICE); /* ... 把 dma_handle 交给硬件 ... */ dma_unmap_single(dev, dma_handle, len, DMA_TO_DEVICE);dma_map_single()会走到dma_map_page_attrs()然后根据dev-dma_ops分派。当设备挂在 IOMMU 上时dma_ops指向iommu_dma_ops进入iommu_dma_map_page()。这层胶水的职责可以概括成三件事找域通过设备找到它当前 attach 的iommu_domain。找不到就退化成直接映射等于没有 IOMMU。分地址从该域的 IOVA 分配器里要一段长度合适的虚拟地址。分配失败要考虑重试和回收。建映射调iommu_map()把(iova, paddr, size)写进页表并处理页对齐、权限位、原子上下文等细节。这里有个容易忽略的点iommu_dma_map_page()拿到的物理地址可能不是页对齐的。驱动的 buffer 往往有偏移比如skb-data指向页中间。IOMMU 页表只能按页粒度映射所以胶水层要把起始地址向下对齐到页边界映射范围相应扩大最后返回给驱动的 IOVA 再加上偏移。这就是为什么你算出来的 DMA 地址和物理地址看起来差了一点。3.2 IOVA 分配地址空间不是随便挑的IOVA 分配器iova_domain管理一段虚拟地址窗口窗口范围来自domain-geometry。分配核心是alloc_iova()按大小和对齐要求找空闲区间。这里有三个参数经常被忽略但出问题时全是它们在作祟。第一个是dma_mask。驱动的dma_mask决定设备能寻址多高。如果设备只有 32 位能力IOVA 分配器必须把地址限制在 4G 以下。分配器内部会用dma_limit做上界同时考虑force_aperture是否生效。第二个是 IOVA 与物理地址的伪别名问题。有些设备或中间桥片在做地址转换时会截断高位导致 IOVA 和物理地址的低位相同就被误认为命中缓存。内核的处理是在分配时避开这种重叠通过iova_offset()判断并调整。这个逻辑平时看不见但一旦碰上特定硬件表现就是数据莫名其妙串了。第三个是strict模式。内核参数iommu.strict0会启用延迟刷新flush queueunmap 时不立刻刷 IOTLB而是攒一批再刷。性能提升明显但语义上变成了最终一致。如果设备在 unmap 之后还残留对旧地址的访问strict0 下这个访问可能仍然成功。做安全敏感场景时应该显式设iommu.strict1。分配成功后的映射建立看这段简化后的逻辑省掉了错误处理和几何范围检查static dma_addr_t iommu_dma_map_page(struct device *dev, struct page *page, unsigned long offset, size_t size, enum dma_data_direction dir, unsigned long attrs) { phys_addr_t phys page_to_phys(page) offset; bool coherent dev_is_dma_coherent(dev); int prot dma_info_to_prot(dir, coherent, attrs); struct iommu_domain *domain iommu_get_dma_domain(dev); struct iommu_dma_cookie *cookie domain-iova_cookie; /* 向下对齐到页边界映射范围相应扩展 */ size_t iova_off iova_offset(cookie-iovad, phys); size ALIGN(size iova_off, cookie-iovad-granule); iova iommu_dma_alloc_iova(domain, size, dma_get_mask(dev), dev); if (!iova) return DMA_MAPPING_ERROR; if (iommu_map_atomic(domain, iova, phys - iova_off, size, prot)) { iommu_dma_free_iova(cookie, iova, size, NULL); return DMA_MAPPING_ERROR; } return iova iova_off; }注意里面phys - iova_off和最后返回iova iova_off这一对操作——对齐进去偏移回来这是保证驱动拿到的地址和它请求的缓冲区起始位置一致的关键。3.3 __iommu_map 的页大小选择与循环iommu_map()进来之后真正的活由__iommu_map()干。它的核心逻辑是一个循环每次尽量挑一个够大且对齐的页调一次map_pages然后推进指针。页大小的选择规则是从大到小尝试要求 IOVA 和物理地址都按该大小对齐且剩余长度足够。这段判断的简化版size_t iommu_pgsize(struct iommu_domain *domain, unsigned long iova, phys_addr_t paddr, size_t size) { unsigned int pgsize_idx, pgsize_idx_next; unsigned long pgsizes domain-pgsize_bitmap; size_t pgsize; /* 按对齐要求筛掉当前地址不适合的页大小 */ pgsize_idx __fls(pgsizes); if (iova ((1UL pgsize_idx) - 1) || paddr ((1UL pgsize_idx) - 1)) pgsizes (1UL pgsize_idx) - 1; /* 排除超过剩余长度的页大小 */ while ((pgsizes ((1UL pgsize_idx) - 1)) size (1UL pgsize_idx)) pgsizes (1UL pgsize_idx) - 1; pgsize 1UL __fls(pgsizes); return pgsize; }举例来说一个 2M 对齐、长度 2M 的缓冲区IOVA 分配器又恰好给了个 2M 对齐的地址那这次映射就只产生一个页表项走一次map_pages(pgsize2M, pgcount1)。如果物理地址只 4K 对齐那就要拆成 512 个 4K 页。大页能不能用上取决于物理地址、IOVA、长度三者同时满足对齐条件缺一不可。这也是为什么驱动里预分配一大块对齐内存往往能换到明显性能提升的原因。DMA 过程中 TLB miss 的代价不小页表项少了命中率自然上去。3.4 解除映射与 IOTLB 失效的时机dma_unmap_single()对应iommu_dma_unmap_page()最终调iommu_unmap(domain, iova, size)走ops-unmap_pages()。关键问题是什么时候必须刷 IOTLB。设备的地址翻译结果缓存在 IOTLB 里页表改了但 IOTLB 没刷设备可能继续用旧结果访问已经释放的物理页那就是内存踩踏。内核的处理是分两步unmap_pages内部把要失效的范围记录进iommu_iotlb_gather可能顺带做一些页表级的清理流程末尾调iommu_iotlb_sync()真正发出失效命令。在 strict 模式下每个 unmap 都会立刻 sync。在 flush queue 模式下多个 unmap 攒到一起达到阈值或遇到不连续地址时才刷。队列的长度和超时可以通过内核参数调整但一般不需要动。提示如果你在做设备直通且客户机里跑的是不信任的负载建议在宿主机上强制iommu.strict1。延迟刷新带来的窗口期虽然很短但在安全边界上不留余地更好。4. 硬件侧机制两级翻译、IOTLB 与 ATS软件栈走通之后还得回头看看硬件提供了什么能力否则遇到映射明明建了却还是 fault这种问题就只能干瞪眼。4.1 Stage-1 与 Stage-2 的分工虚拟化场景下 IOMMU 要处理两级翻译这是理解 VFIO 直通的关键。Stage-1是设备眼里看到的地址IOVA到客户机物理地址GPA的翻译由客户机操作系统控制客户机的驱动建映射时改的就是这一层。Stage-2是客户机物理地址到宿主机物理地址HPA的翻译由 hypervisor 控制。一次 DMA 访问要依次过两级IOVA → GPA → HPA。两级页表都由硬件遍历中间结果缓存在 IOTLB 里。这带来一个管理上的要求任何一级页表变更都要让对方知道。客户机改了 stage-1宿主机要感知并刷对应缓存宿主机改了 stage-2也要让客户机的 TLB 失效。这套协同靠 hypercall 完成。裸机场景通常只有 stage-1或者说 stage-2 被配置成恒等映射。所以你在裸机上调试的结论直接搬到虚拟化环境里可能不成立——两级翻译会引入额外的 fault 类型和延迟。4.2 IOTLB 与 flush 语义IOTLB 的组织形式各家不同有的是统一缓存有的是分层一层全局、一层按设备。软件能操作的是失效命令常见几种粒度全局失效把整个 IOTLB 清空代价最大按域失效只清某个页表对应项按地址范围失效给一个页对齐的范围和页大小按设备失效带上 Requester ID只影响某设备。内核尽量用范围失效因为粒度细抖动小。但这里有个坑范围失效要求起始地址和长度按页大小对齐如果 unmap 的范围跨了不同页大小内核会拆成多次失效命令。你在 trace 里看到一串连续的iommu_iotlb_sync调用多半就是这个原因。这里还牵扯到设备侧 TLB下面马上讲和影子页表的一致性如果只刷了 IOMMU 侧的 IOTLB而设备自己缓存了旧翻译结果问题依然存在。4.3 ATS/PRI 带来的新问题支持 ATSAddress Translation Services的设备会自己缓存翻译结果到ATCAddress Translation Cache里。好处是设备不用每次 DMA 都问 IOMMU延迟低坏处是软件改页表之后必须通知设备失效 ATC否则设备继续用旧结果。通知的方式是发一个Invalidation Request设备回应后才算完成。这个过程是异步的要等完成消息。内核里对应iommu_dev_enable_feat(dev, IOMMU_DEV_FEAT_ATS)和相关的失效回调。实际踩过的坑是某些设备对 Invalidation Request 的响应很慢或者干脆不回导致 unmap 路径卡住表现为进程 D 状态。遇到这种情况先看/proc/interrupts里对应 IOMMU 的中断计数有没有增长再看 trace 里是否卡在等待完成队列。PRIPage Request Interface是另一半设备遇到没有映射的地址时不直接 fault而是发一个页请求让软件按需换入页面。这套机制在用户态驱动和共享虚拟地址SVA场景里用得多NVMe 和部分加速卡支持。如果设备支持 PRI 但软件没配好表现是设备静默丢请求非常难查——建议在不确定的时候先用iommu_dev_disable_feat()把相关特性关掉确认基础路径能跑通再逐个打开。5. 动手验证把映射关系看清楚理论讲完得能对着机器验证。这部分给出可以直接抄的检查步骤。5.1 内核配置与启动参数先确认基础配置。抓一份当前内核的配置grep -E CONFIG_(IOMMU|INTEL_IOMMU|AMD_IOMMU|ARM_SMMU|IOMMU_DEFAULT|IOMMU_SUPPORT|IOMMUFD) /boot/config-$(uname -r)几个关键项的含义配置项作用缺失时的表现CONFIG_IOMMU_SUPPORTIOMMU 子系统总开关全盘不可用设备只能走直接映射CONFIG_IOMMU_DEFAULT_DMA_LAZY/_STRICT默认刷新策略决定 strict 的默认值可被命令行覆盖CONFIG_INTEL_IOMMU或CONFIG_ARM_SMMU_V3平台驱动没有对应驱动硬件再多也白搭CONFIG_IOMMUFD用户态页表管理无法在用户态建 IOMMU 域CONFIG_IOMMU_DEBUGFSdebugfs 导出无法直接看域和映射启动参数最常用的是这三个iommupt # 保留 IOMMU 但默认走直通性能优先 iommu.strict1 # 强制同步刷新 iommu.passthrough0 # 强制翻译不给直通iommupt是个很实用的折中IOMMU 硬件仍然启用设备隔离能力在但 DMA API 走 identity 域翻译等于恒等没有页表开销。适合排查性能下降是不是 IOMMU 翻译引起的。要验证的话分别用iommupt和默认配置跑一次同样的网络吞吐测试差值就是翻译成本。5.2 用 sysfs 和 debugfs 观察域与分组先看硬件实例是否被识别ls /sys/class/iommu/ # 输出类似: dmar0 dmar1 dmesg | grep -iE iommu|DMAR|SMMU | head -30正常的启动日志里会看到硬件能力、支持的页大小、启用的特性。看到DMAR: DRHD base: ... flags: 0x1这种行说明硬件表项被解析了。再看设备归属# 某设备属于哪个组 basename $(readlink -f /sys/bus/pci/devices/0000:3b:00.0/iommu_group) # 该组的域类型需要 debugfs 挂载 mount -t debugfs none /sys/kernel/debug cat /sys/kernel/debug/iommu/0000:3b:00.0/domain 2/dev/nulldebugfs 下的路径随版本有变化6.6 里通常在/sys/kernel/debug/iommu/device/下面能看到域类型、页表基址、映射条目之类的信息。如果目录是空的检查CONFIG_IOMMU_DEBUGFS是否打开——这个选项在部分发行版里被关掉因为它的调试接口本身有一定风险。5.3 一个最小验证思路想在自己写的模块里观察映射变化最省事的办法是在dma_map_single()前后打印地址和帧号static int __init dmatest_init(void) { struct page *pg; void *buf; dma_addr_t dma; phys_addr_t pa; buf (void *)__get_free_page(GFP_KERNEL); pg virt_to_page(buf); pa page_to_phys(pg); dma dma_map_single(probe_dev, buf, PAGE_SIZE, DMA_BIDIRECTIONAL); if (dma_mapping_error(probe_dev, dma)) { pr_err(dma map failed\n); return -EIO; } pr_info(paddr%pa dma%pad offset%ld\n, pa, dma, (long)(dma - pa)); dma_unmap_single(probe_dev, dma, PAGE_SIZE, DMA_BIDIRECTIONAL); free_page((unsigned long)buf); return 0; }几个观察要点有 IOMMU 时dma和pa一般不相等差值就是 IOVA 与物理地址的偏移连续多次映射同一块内存偏移大概率变化分配器是第一次适配如果你把驱动改成iommupt启动dma和pa应该相等这能反向验证翻译路径确实生效。这个实验简单但非常有效。很多人怀疑IOMMU 到底有没有在工作跑一次这个对比就有答案了。5.4 dmesg 常见输出逐条解读现场最常见的一类报错长这样DMAR: [DMA Write] Request device [3b:00.0] PASID ffffffff fault addr 7f3a0000 [fault reason 05] PTE Write access is not set逐字段拆[DMA Write]是访问方向device [3b:00.0]是发起者 BDFfault addr是设备发出的地址这是 IOVA不是物理地址fault reason是硬件给的错误码不同平台编码不同需要查对应硬件的规范最后那句文字描述通常是驱动翻译后的可读说明。类似的还有日志片段含义排查方向no DMAR table found固件没提供表检查 BIOS 里 VT-d/DMA Remapping 是否开启PTE Read access is not set读权限没给检查dma_info_to_prot()是否因方向参数给错而只映射了写non-present PTE该地址没有映射设备用了没经过dma_map_*的地址或映射被提前 unmapIOMMU: Default domain type: Translated默认走翻译正常信息可作为基线确认Failed to allocate iovaIOVA 空间耗尽有映射泄漏检查 unmap 是否配对fault addr那一列特别值得注意它是 IOVA。很多人拿着这个地址去/proc/kallsyms里找当然找不到。正确做法是找到对应设备的域用iova_to_phys()去反查或者对照日志时间点前后驱动的映射记录。6. 踩坑速查表与几条硬经验前面几节讲的是应该怎样这一节讲实际会怎样。下面这些坑都是我在不同平台上反复撞见的。6.1 五类高频故障的定位套路第一类设备 probe 成功但一发 DMA 就 fault。先看 fault 日志里的地址范围和驱动申请缓冲区的大小对不对得上。对不上说明设备用了错误的地址——常见原因是驱动里做了 32 位截断或者描述符里的地址字段是自己算的而不是dma_map_*返回的。对得上但依然 fault检查方向参数。DMA_TO_DEVICE只需要读权限如果驱动误写成DMA_FROM_DEVICE设备侧读会被拦。第二类映射建立成功设备读到的数据是旧的。典型的缓存一致性问题不是 IOMMU 的锅但经常被误判。检查设备的 DMA 一致性能力dev_is_dma_coherent()非一致性设备需要显式dma_sync_single_for_device()。另一种可能是 IOTLB 没刷用iommu.strict1跑一次对比如果问题消失就是刷新时机问题。第三类直通给虚拟机后虚拟机里设备工作但宿主机内存被踩。几乎可以确定是 group 划分有问题两个本该隔离的设备被塞进了一个域。用 5.2 的方法检查 group 内容确认目标设备独占一个组。如果确实分不开考虑换插槽或加 ACS 覆盖。第四类系统启动后可用内存明显减少。IOMMU 的页表本身要占内存大型机器上几十 GB 内存配 4K 页表开销可能到几百 MB。可以用大页减少页表项数量或者在iommupt下运行。另外swiotlb的预留也会占一块swiotlb0可以关掉但要看设备是否需要。第五类unmap 卡住进程进 D 状态。大概率是等设备的 ATC 失效完成。查对应 IOMMU 的中断计数是否增长看是否卡在 completion 等待。短期绕过办法是关掉 ATS 特性长期要看是不是设备固件的问题。6.2 速查表症状首选检查次要检查常见根因DMA fault地址在预期范围内映射方向与权限位pgsize_bitmap是否支持该页大小权限位给错DMA fault地址不在预期范围驱动是否用了裸物理地址是否有 32 位截断未走 DMA API数据陈旧dma_sync_*是否配对iommu.strict取值缓存/刷新时机IOVA 分配失败unmap 是否配对域窗口大小geometry映射泄漏直通后宿主机异常iommu_group内容ACS 是否开启隔离不彻底性能明显下降大页是否生效strict 模式页表项过多几条硬经验值得单独记一下。经验一先用iommupt建立基线。遇到性能问题第一步永远是用直通模式跑一遍确认差异是翻译引起的还是别的。这个对比能在十分钟内排除掉一半的怀疑方向。经验二不要相信应该对齐这种假设。物理地址、IOVA、长度三者任一不对齐大页就用不上。写驱动时如果有性能要求预分配对齐缓冲区比事后调优有效得多。经验三group 是硬边界不是建议。试图通过改内核参数强行把设备拆出组短期可能能用长期一定出问题。遇到分不开的硬件正确的解法是换硬件或换拓扑。经验四iova_to_phys()是调试利器但别在热路径用。它要遍历页表开销不小。但在故障现场拿它反查一个 fault 地址比翻代码快得多。经验五dmesg 里的地址一律先当 IOVA 看。想找物理地址先过一遍翻译。这个习惯能省掉大量地址对不上的困惑。我在实际使用中感受最深的一点是IOMMU 的问题很少是IOMMU 本身坏了绝大多数是软件对地址空间的假设和硬件的实际行为不一致。驱动以为自己拿到的是物理地址设备以为自己能访问全部内存管理程序以为两个设备是隔离的——每一个以为都可能在某个特定拓扑下被打破。把本文第 1 节那套没有 IOMMU 会怎样的思维模型建起来再配合第 3 节的调用链遇到新问题时基本都能自己推出该往哪查。下一篇会接着聊 IOMMU 域的生命周期和iommufd那条用户态路径那部分和虚拟化方案的耦合更深坑也更集中。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑