本文属于第二册“IOMMU:地址翻译与隔离的硬件机制及内核管理”,回答第 0 篇提出的问题 6:IOMMU 如何同时解决物理连续性、寻址范围与设备隔离三个问题?其内部的地址翻译过程是怎样的?
第 5 篇第 6 节指出,物理连续性与地址位宽两类约束,以及设备访问范围不受限制的问题,都源于设备地址直接对应物理地址这一前提,并提出了在设备与内存之间加入一层地址翻译的思路。本文首先说明 IOMMU 在系统中的位置,然后以一次 DMA 写入为例说明其翻译过程,接着结合内核 DMA 映射层的实现说明三个问题如何得到统一解决,最后讨论隔离的粒度,即 domain 与 group 两个概念。
1. 问题回顾
第 5 篇第 6 节将现有手段的局限归结为一点:设备发出的地址就是物理地址,内核只能让物理内存的布局迁就设备。IOMMU 的思路是令设备发出的地址成为 I/O 虚拟地址(IOVA),由 IOMMU 依据内核建立的 I/O 页表翻译为物理地址。
这一思路要在硬件上实现,需要回答以下几个具体问题:
- IOMMU 应位于何处,才能检查设备发出的每一个请求;
- 不同设备应使用不同的 I/O 页表,IOMMU 如何判断请求来自哪一个设备;
- 翻译失败时如何处理;
- 内核如何利用这一机制满足第 5 篇所述的各项约束;
- 隔离能否精确到单个设备。
以下各节依次讨论。
2. IOMMU 在系统中的位置
2.1 位于设备访问内存的必经路径上
IOMMU 只有位于设备访问内存的必经路径上,才能对每一个请求进行翻译与检查。在 PCIe 系统中,设备发往内存的请求经由交换机、根端口向上传递,进入 Root Complex 后再转发至内存控制器(第 2 篇第 2 节)。因此 IOMMU 通常集成在 Root Complex 或主桥中,对进入的上游请求进行处理。在 SoC 中,IOMMU 一般位于设备(或一组设备)与片上互连之间。图 1 对比了这两种结构:无论哪一种,设备访问内存的请求都必须先经过 IOMMU,而 CPU 访问内存的路径不经过 IOMMU。
一个系统中通常存在多个 IOMMU 实例,每个实例负责一部分设备。设备与 IOMMU 实例的对应关系由固件描述,内核在启动时据此建立关联。主要的实现如下:
| 实现 | 硬件单元 | 固件描述 | 内核驱动 |
|---|---|---|---|
| Intel VT-d | DMA Remapping 硬件单元 | ACPI DMAR 表 | drivers/iommu/intel/ |
| AMD-Vi | AMD IOMMU | ACPI IVRS 表 | drivers/iommu/amd/ |
| ARM SMMU | SMMUv2、SMMUv3 | ACPI IORT 表,或设备树的iommus、iommu-map属性 | drivers/iommu/arm/ |
| RISC-V IOMMU | RISC-V IOMMU | 设备树等 | drivers/iommu/riscv/ |
各实现的术语与数据结构不同,但基本职能一致。以下以 Intel VT-d 为主线,并在必要处对照 AMD 与 ARM 的实现。
2.2 与 CPU MMU 的比较
IOMMU 与 CPU 的 MMU 都以多级页表完成地址翻译,但二者所处的位置不同,由此产生以下差异:
| 方面 | CPU MMU | IOMMU |
|---|---|---|
| 输入地址 | 进程的虚拟地址 | 设备发出的 IOVA |
| 页表的选择 | 由进程切换时写入的页表基址寄存器决定,如 x86 的 CR3 | 按每个请求的来源设备查表选择,无须切换 |
| 翻译缓存 | TLB | IOTLB(第 7 篇) |
| 翻译失败 | 触发缺页异常,内核处理后重新执行访存指令 | 通常直接拦截请求并记录故障;可恢复的设备缺页需要设备与 IOMMU 的额外支持(第 21 篇) |
其中最关键的差异在于页表的选择。CPU 在同一时刻只执行一个上下文,页表基址随上下文切换而更新;IOMMU 则同时接收来自多个设备的请求,必须依据每个请求本身携带的信息确定应使用的页表。
3. 地址翻译过程
3.1 请求的来源标识
第 2 篇第 2.2 节介绍了 PCIe 事务层报文(TLP)的字段,其中 Requester ID 由总线号、设备号与功能号组成(即 BDF),共 16 位,标识发出请求的设备。第 2 篇第 2.3 节提到,Root Complex 依据 Requester ID 选择 I/O 页表。IOMMU 正是以 Requester ID 作为查表的索引。
ARM SMMU 使用 StreamID 标识请求来源。对于 PCIe 设备,StreamID 由 Requester ID 按固件描述的规则换算得到;对于 SoC 内的设备,StreamID 由互连在请求上附加。此外,PCIe 还允许请求携带 PASID,用于在同一设备内区分不同的地址空间,这一机制将在第 20 篇讨论。
3.2 设备表查找
IOMMU 首先以来源标识查找设备表,取得该设备的翻译配置。各实现的设备表组织如下:
| 实现 | 设备表结构 | 表项内容 |
|---|---|---|
| Intel VT-d | 两级:根表以总线号索引,每项指向一个上下文表;上下文表以设备号与功能号索引 | 翻译类型、页表根地址、地址宽度、domain ID |
| AMD-Vi | 单级设备表,以 16 位设备 ID 索引,每项 256 位 | 有效位、页表级数、页表根地址、读写权限、domain ID |
| ARM SMMUv3 | 流表,以 StreamID 索引,表项称为 STE | 配置类型(中止、旁路、第一阶段翻译、第二阶段翻译)、页表或上下文描述符地址、VMID |
Intel VT-d 上下文表项的格式(drivers/iommu/intel/iommu.h):
/* * low 64 bits: * 0: present * 1: fault processing disable * 2-3: translation type * 12-63: address space root * high 64 bits: * 0-2: address width * 3-6: aval * 8-23: domain id */structcontext_entry{u64 lo;u64 hi;};其中,翻译类型决定请求是经过多级页表翻译(CONTEXT_TT_MULTI_LEVEL),还是直接以输入地址作为物理地址(CONTEXT_TT_PASS_THROUGH);address space root 给出 I/O 页表的根地址;domain id 为 16 位,用于标识该设备所属的地址空间,其含义将在第 5.1 节讨论。
内核查找某一设备的上下文表项时,按照硬件的两级结构进行(drivers/iommu/intel/iommu.c,节选):
structcontext_entry*iommu_context_addr(structintel_iommu*iommu,u8 bus,u8 devfn,intalloc){structroot_entry*root=&iommu->root_entry[bus];structcontext_entry*context;u64*entry;...entry=&root->lo;...if(*entry&1)context=phys_to_virt(*entry&VTD_PAGE_MASK);else{...}return&context[devfn];}根表以总线号bus为下标,表项的最低位表示该总线的上下文表是否存在;上下文表再以devfn为下标取得设备的表项。省略部分处理表项不存在时的分配,以及可扩展模式(scalable mode)下的不同布局。
ARM SMMUv3 的 STE 以配置字段区分处理方式(drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.h):
#defineSTRTAB_STE_0_CFGGENMASK_ULL(3,1)#defineSTRTAB_STE_0_CFG_ABORT0#defineSTRTAB_STE_0_CFG_BYPASS4#defineSTRTAB_STE_0_CFG_S1_TRANS5#defineSTRTAB_STE_0_CFG_S2_TRANS6ABORT表示拒绝该设备的全部请求,BYPASS表示不经翻译直接访问,其余两项表示经由第一阶段或第二阶段页表翻译,其中第二阶段翻译主要用于虚拟化场景。
3.3 I/O 页表遍历
取得页表根地址后,IOMMU 以 IOVA 为输入遍历多级页表。以 4 KB 页、48 位 IOVA 为例,IOVA 的低 12 位为页内偏移,其余 36 位按每级 9 位拆分为 4 级索引,每级页表包含 512 个表项。VT-d 与 AMD-Vi 还支持更多级数,以覆盖更宽的 IOVA。
VT-d 页表项的格式(drivers/iommu/intel/iommu.h):
/* * 0: readable * 1: writable * 2-6: reserved * 7: super page * 8-10: available * 11: snoop behavior * 12-63: Host physical address */structdma_pte{u64 val;};表项中的读、写两位决定设备对该页的访问权限;super page 位表示该表项直接映射一个大页(如 2 MB),遍历在此提前结束;snoop behavior 位与第 4 篇第 3.3 节讨论的 No Snoop 属性有关,硬件支持时,该位可使经由此映射的请求强制侦听 CPU cache。
ARM SMMU 使用与 CPU 相同的 LPAE 页表格式,内核在drivers/iommu/io-pgtable-arm.c中实现,CPU 与 SMMU 的页表格式因此可以共用。
图 2 以网卡的一次 DMA 写入为例,展示上述过程。
每次翻译都需要多次读取内存中的页表,IOMMU 因此以 IOTLB 缓存翻译结果。IOTLB 的结构以及页表更新时的失效操作,将在第 7 篇讨论。
3.4 翻译失败的处理
若设备表项无效、页表中不存在对应映射,或请求的读写类型与表项的权限不符,翻译即告失败。此时 IOMMU 不会将请求转发至内存:写请求被丢弃,读请求则返回表示失败的完成报文。同时,IOMMU 将故障信息记录在故障寄存器或事件队列中,并以中断通知内核。
Intel VT-d 驱动的故障日志格式(drivers/iommu/intel/dmar.c,节选):
pr_err("[%s NO_PASID] Request device [%02x:%02x.%d] fault addr 0x%llx [fault reason 0x%02x] %s\n",type?"DMA Read":"DMA Write",source_id>>8,PCI_SLOT(source_id&0xFF),PCI_FUNC(source_id&0xFF),addr,fault_reason,reason);...dmar_fault_dump_ptes(iommu,source_id,addr,pasid);日志给出了请求类型、来源设备的 BDF、出错的 IOVA 与故障原因,随后转储该地址对应的页表项。AMD-Vi 的对应日志为IO_PAGE_FAULT事件,ARM SMMUv3 则通过事件队列报告翻译故障(EVT_ID_TRANSLATION_FAULT)与权限故障(EVT_ID_PERMISSION_FAULT)。
与 CPU 缺页不同,上述故障一般不可恢复:被拦截的请求不会被重新发出,设备通常也无法得知需要等待内核补充映射。这类故障在多数情况下意味着驱动或设备存在缺陷,第 6.3 节将进一步讨论。
4. 三个问题的统一解决
IOMMU 提供了地址翻译的硬件能力,但翻译的内容,即哪些 IOVA 映射到哪些物理页,由内核决定。内核在 DMA 映射层完成这一工作,驱动使用的接口与未启用 IOMMU 时完全相同。dma_map_single()、dma_map_page()等流式映射接口最终进入dma_map_phys(),后者根据设备的配置选择实现路径(kernel/dma/mapping.c,节选):
if(dma_map_direct(dev,ops)||(!is_mmio&&!is_cc_shared&&arch_dma_map_phys_direct(dev,phys+size)))addr=dma_direct_map_phys(dev,phys,size,dir,attrs,true);elseif(is_cc_shared)returnDMA_MAPPING_ERROR;elseif(use_dma_iommu(dev))addr=iommu_dma_map_phys(dev,phys,size,dir,attrs);未启用 IOMMU 时,映射走dma_direct_map_phys(),即第 5 篇讨论的直接映射路径,必要时经由 SWIOTLB 反弹;设备挂接到由 DMA 映射层管理的 IOMMU 地址空间时,use_dma_iommu()为真,映射交由drivers/iommu/dma-iommu.c中的iommu_dma_map_phys()处理。DMA 映射层的分层结构将在第 12 篇讨论,以下仅关注 IOMMU 路径如何满足三项约束。
图 3 概括了 IOMMU 路径下 IOVA 与物理内存的映射关系,以下三小节分别说明其中的寻址范围、物理连续性与设备隔离。
4.1 寻址范围
iommu_dma_map_phys()的核心步骤由__iommu_dma_map()完成(drivers/iommu/dma-iommu.c,节选):
size=iova_align(iovad,size+iova_off);iova=iommu_dma_alloc_iova(domain,size,dma_mask,dev);if(!iova)returnDMA_MAPPING_ERROR;if(iommu_map(domain,iova,phys-iova_off,size,prot,GFP_ATOMIC)){iommu_dma_free_iova(domain,iova,size,NULL);returnDMA_MAPPING_ERROR;}returniova+iova_off;映射分为两步:先在设备的寻址范围dma_mask之内分配一段 IOVA,再将其映射到缓冲区所在的物理页。返回给驱动的 DMA 地址是 IOVA,而不是物理地址。
IOVA 的分配范围由设备的寻址能力决定,与物理页的实际位置无关。即使缓冲区位于 4 GB 以上,只要分配到的 IOVA 在 32 位以内,32 位设备即可访问,无须经由 SWIOTLB 中转。IOVA 的分配过程考虑了设备掩码、总线限制与 IOMMU 自身的地址宽度(drivers/iommu/dma-iommu.c,节选):
dma_limit=min_not_zero(dma_limit,dev->bus_dma_limit);if(domain->geometry.force_aperture)dma_limit=min(dma_limit,(u64)domain->geometry.aperture_end);/* * Try to use all the 32-bit PCI addresses first. The original SAC vs. * DAC reasoning loses relevance with PCIe, but enough hardware and * firmware bugs are still lurking out there that it's safest not to * venture into the 64-bit space until necessary. * * If your device goes wrong after seeing the notice then likely either * its driver is not setting DMA masks accurately, the hardware has * some inherent bug in handling >32-bit addresses, or not all the * expected address bits are wired up between the device and the IOMMU. */if(dma_limit>DMA_BIT_MASK(32)&&dev->iommu->pci_32bit_workaround){iova=alloc_iova_fast(iovad,iova_len,DMA_BIT_MASK(32)>>shift,false);if(iova)gotodone;dev->iommu->pci_32bit_workaround=false;dev_notice(dev,"Using %d-bit DMA addresses\n",bits_per(dma_limit));}bus_dma_limit即第 5 篇第 3.2 节所述的总线限制,aperture_end为 IOMMU 硬件支持的最大 IOVA。对于 PCI 设备,即使其掩码超过 32 位,内核也优先在 32 位以内分配 IOVA,只有在该范围耗尽后才使用更高的地址。注释说明了这一做法的原因:部分硬件与固件在处理 32 位以上的地址时仍存在缺陷。
4.2 物理连续性
物理连续性在 IOMMU 路径下转化为 IOVA 的连续性。对于 SG 映射,iommu_dma_map_sg()先计算整个列表所需的 IOVA 长度,一次分配一段连续的 IOVA,再将各段物理内存依次映射到其中。函数开头的注释说明了该函数的作用(drivers/iommu/dma-iommu.c):
/* * The DMA API client is passing in a scatterlist which could describe * any old buffer layout, but the IOMMU API requires everything to be * aligned to IOMMU pages. Hence the need for this complicated bit of * impedance-matching, to be able to hand off a suitably-aligned list, * but still preserve the original offsets and sizes for the caller. */映射完成后,物理上分散的多个段在设备看来可以合并为一段或少数几段连续的 IOVA。即使设备不支持 SG,或支持的段数有限,也能处理物理分散的缓冲区。合并的规则以及段长度、段边界约束的处理,将在第 10 篇讨论。
一致性分配同样受益。dma_alloc_coherent()在 IOMMU 路径下,对于允许睡眠的调用,由iommu_dma_alloc_remap()逐页或以较小的阶数分配物理页,再将其映射到一段连续的 IOVA,并为 CPU 建立一段连续的内核虚拟地址。分配物理页的函数中有如下注释(drivers/iommu/dma-iommu.c,节选):
/* IOMMU can map any pages, so himem can also be used here */gfp|=__GFP_NOWARN|__GFP_HIGHMEM;.../* * Higher-order allocations are a convenience rather * than a necessity, hence using __GFP_NORETRY until * falling back to minimum-order allocations. */第一条注释说明物理页不必位于设备可直接访问的区域,第二条说明高阶分配只是为了提高效率,并非必要条件。第 5 篇第 2.4 节所述的大块连续内存分配问题,在此不再依赖 CMA 或高阶伙伴分配解决。
4.3 设备隔离
在 IOMMU 路径下,设备只能访问其 I/O 页表中已建立映射的 IOVA。驱动调用映射接口时建立映射,调用解除映射接口时撤销映射,设备在任一时刻能够访问的内存,仅限于驱动当前交给它的缓冲区。访问其他地址的请求将按第 3.4 节所述被拦截。
映射的权限由 DMA 方向决定(drivers/iommu/dma-iommu.c,节选):
switch(dir){caseDMA_BIDIRECTIONAL:returnprot|IOMMU_READ|IOMMU_WRITE;caseDMA_TO_DEVICE:returnprot|IOMMU_READ;caseDMA_FROM_DEVICE:returnprot|IOMMU_WRITE;以DMA_TO_DEVICE映射的发送缓冲区只允许设备读取,设备对其写入将引发权限故障。DMA 方向的含义将在第 9 篇讨论。
这一隔离以页为粒度。若驱动映射的缓冲区不足一页,例如kmalloc()分配的一个小对象,设备实际可访问的是该对象所在的整页,其中可能包含其他内核数据。对于一般设备,这一残余风险可以接受;对于可能发起 DMA 攻击的设备,内核另行处理。PCI 子系统将此类设备标记为不可信(include/linux/pci.h):
/* * Devices marked being untrusted are the ones that can potentially * execute DMA attacks and similar. They are typically connected * through external ports such as Thunderbolt but not limited to * that. When an IOMMU is enabled they should be getting full * mappings to make sure they cannot access arbitrary memory. */unsignedintuntrusted:1;对于不可信设备,若缓冲区的起始地址或长度未按 IOMMU 页对齐,iommu_dma_map_phys()先将数据复制到 SWIOTLB 的中转缓冲区,再映射中转缓冲区所在的页(drivers/iommu/dma-iommu.c,节选):
staticbooldev_use_swiotlb(structdevice*dev,size_tsize,enumdma_data_directiondir){returnIS_ENABLED(CONFIG_SWIOTLB)&&(dev_is_untrusted(dev)||dma_kmalloc_needs_bounce(dev,size,dir));}...if(dev_use_swiotlb(dev,size,dir)&&iova_unaligned(iovad,phys,size)){...phys=iommu_dma_map_swiotlb(dev,phys,size,dir,attrs);与第 5 篇第 4.3 节不同,此处使用 SWIOTLB 的目的不是解决寻址范围问题,而是使设备可访问的页中不含其他数据。
5. 隔离的粒度:domain 与 group
第 4 节的讨论隐含了一个前提:每个设备拥有独立的 I/O 页表,且设备的所有请求都经过 IOMMU 检查。实际系统中,这两点并非总能满足。内核以 domain 与 group 两个概念描述隔离的单位。
5.1 domain:I/O 地址空间
domain 表示一个 I/O 地址空间,即一套 I/O 页表及其映射。设备挂接(attach)到某个 domain 后,其请求按该 domain 的页表翻译。多个设备可以挂接到同一个 domain,从而共享同一个 IOVA 空间。在硬件上,domain 对应设备表项中的 domain ID(VT-d 上下文表项的第 8–23 位、AMD-Vi 设备表项中的 domain ID),IOTLB 中的缓存项也以 domain ID 标记,以便按地址空间失效。
内核定义了以下几类 domain(include/linux/iommu.h):
/* * This are the possible domain-types * * IOMMU_DOMAIN_BLOCKED - All DMA is blocked, can be used to isolate * devices * IOMMU_DOMAIN_IDENTITY - DMA addresses are system physical addresses * IOMMU_DOMAIN_UNMANAGED - DMA mappings managed by IOMMU-API user, used * for VMs * IOMMU_DOMAIN_DMA - Internally used for DMA-API implementations. * This flag allows IOMMU drivers to implement * certain optimizations for these domains * IOMMU_DOMAIN_DMA_FQ - As above, but definitely using batched TLB * invalidation. * IOMMU_DOMAIN_SVA - DMA addresses are shared process addresses * represented by mm_struct's. * IOMMU_DOMAIN_PLATFORM - Legacy domain for drivers that do their own * dma_api stuff. Do not use in new drivers. */各类型的用途如下:
- BLOCKED:拒绝设备的全部 DMA 请求,用于在设备尚未就绪或切换地址空间的过程中保持隔离。
- IDENTITY:IOVA 即物理地址,相当于旁路翻译。设备挂接到此类 domain 时,
use_dma_iommu()为假,DMA 映射走第 4 节所述的直接映射路径,不再获得 IOMMU 提供的隔离。 - DMA 与 DMA_FQ:由 DMA 映射层管理的地址空间,即第 4 节所述的路径。二者的区别在于 IOTLB 失效的方式,将在第 7 篇讨论。
- UNMANAGED:映射由 DMA 映射层以外的使用者管理,典型的使用者是 VFIO,将在第 14 篇讨论。
- SVA:设备直接使用进程的虚拟地址,与进程共享页表,将在第 20 篇讨论。
内核启动时为每个设备(确切地说,为第 5.2 节所述的每个 group)选择一个默认 domain,其类型为 DMA、DMA_FQ 或 IDENTITY 之一,可通过内核参数iommu.passthrough与iommu.strict调整,也可在运行时通过 sysfs 查看。对于第 4.3 节所述的不可信设备,内核强制使用 DMA 类型的默认 domain,若设备驱动要求使用其他类型,将拒绝加载该驱动。
5.2 group:可隔离的最小单位
IOMMU 依据请求的来源标识区分设备,并且只能检查经过它的请求。若两个设备的请求无法被 IOMMU 区分,或二者之间的访问不经过 IOMMU,IOMMU 就无法隔离这两个设备。内核将这样一组设备称为一个 IOMMU group,即可隔离的最小单位。以下三种情形会使多个设备落入同一个 group。
其一,Requester ID 别名。传统 PCI 总线的事务不携带 Requester ID,PCIe 至 PCI 桥在转发下游设备的请求时,以自身的标识代替原设备的标识,桥下所有设备的请求在 IOMMU 看来来自同一来源。内核在pci_for_each_dma_alias()中枚举设备可能使用的全部来源标识(drivers/pci/search.c,节选):
/* * PCIe-to-PCI/X bridges alias transactions from downstream * devices using the subordinate bus number (PCI Express to * PCI/PCI-X Bridge Spec, rev 1.0, sec 2.3). For all cases * where the upstream bus is PCI/X we alias to the bridge * (there are various conditions in the previous reference * where the bridge may take ownership of transactions, even * when the secondary interface is PCI-X). */此外,部分设备存在硬件缺陷,以其他功能号的 Requester ID 发出 DMA 请求。例如drivers/pci/quirks.c记载,Marvell 88SE9123 以功能 1 作为 DMA 的 Requester ID。内核以 quirk 将这类别名记录在dma_alias_mask中。
其二,绕过 IOMMU 的点对点访问。第 2 篇第 2.3 节提到,同一交换机下的两个设备之间的请求可由交换机直接转发,不经过 Root Complex,自然也不经过 IOMMU。PCIe 的访问控制服务(ACS)用于约束这一行为,其控制位定义如下(include/uapi/linux/pci_regs.h):
#definePCI_ACS_SV0x0001/* Source Validation */#definePCI_ACS_TB0x0002/* Translation Blocking */#definePCI_ACS_RR0x0004/* P2P Request Redirect */#definePCI_ACS_CR0x0008/* P2P Completion Redirect */#definePCI_ACS_UF0x0010/* Upstream Forwarding */#definePCI_ACS_EC0x0020/* P2P Egress Control */#definePCI_ACS_DT0x0040/* Direct Translated P2P */内核判断一个端口能否隔离其下游设备时,要求以下四项均已启用(drivers/iommu/iommu.c):
#defineREQ_ACS_FLAGS(PCI_ACS_SV|PCI_ACS_RR|PCI_ACS_CR|PCI_ACS_UF)其中,源验证(SV)检查请求中的 Requester ID 是否属于该端口下游的总线范围,防止设备冒用其他设备的标识;请求重定向(RR)与完成报文重定向(CR)将点对点的请求与完成报文转发至上游,使其经过 Root Complex 与 IOMMU;上游转发(UF)使经重定向的报文能够继续向上传递。若交换机的下游端口未实现或未启用这些功能,其下的设备之间可以绕过 IOMMU 互相访问,必须归入同一个 group。
其三,多功能设备。同一设备的多个功能之间可能在设备内部直接访问,不产生经过 IOMMU 的总线事务。除非设备通过 ACS 声明各功能之间相互隔离,内核将同一插槽上的多个功能归入同一个 group。
pci_device_group()综合上述三种情形,为 PCI 设备确定所属的 group(drivers/iommu/iommu.c,节选):
/* * Find the upstream DMA alias for the device. A device must not * be aliased due to topology in order to have its own IOMMU group. * If we find an alias along the way that already belongs to a * group, use it. */if(pci_for_each_dma_alias(pdev,get_pci_alias_or_group,&data))returndata.group;pdev=data.pdev;/* * Continue upstream from the point of minimum IOMMU granularity * due to aliases to the point where devices are protected from * peer-to-peer DMA by PCI ACS. Again, if we find an existing * group, use it. */for(bus=pdev->bus;!pci_is_root_bus(bus);bus=bus->parent){if(!bus->self)continue;if(pci_acs_path_enabled(bus->self,NULL,REQ_ACS_FLAGS))break;pdev=bus->self;group=iommu_group_get(&pdev->dev);if(group)returngroup;}...group=get_pci_function_alias_group(pdev,(unsignedlong*)devfns);if(group)returngroup;/* No shared group found, allocate new */returniommu_group_alloc();函数首先沿别名关系找到设备在 IOMMU 看来的来源,然后从该位置向上遍历,直至遇到启用了所需 ACS 功能的端口为止,途经的桥若已属于某个 group,则设备加入该 group;最后检查同一插槽上的其他功能。只有在以上检查均未发现共享关系时,设备才获得独立的 group。图 4 展示了三种典型拓扑下的 group 划分。
group 的划分直接约束了 domain 的使用:同一 group 内的设备必须挂接到同一个 domain,因为 IOMMU 无法为它们分别施加不同的翻译与权限。系统中的 group 划分可在/sys/kernel/iommu_groups/下查看,每个 group 目录下的devices列出其中的设备,type给出当前默认 domain 的类型。group 的概念在将设备交给用户态或虚拟机时尤为重要:VFIO 以 group 为单位分配设备,这一点将在第 14 篇讨论。
6. 常见误区
6.1 认为启用 IOMMU 后无须正确设置 DMA 掩码
IOMMU 消除了物理内存位置对设备的影响,但 IOVA 仍须位于设备的寻址范围之内。第 4.1 节所示的代码中,dma_mask是 IOVA 分配的上限;若驱动声明的掩码超过设备的实际能力,分配到的 IOVA 可能超出设备能够发出的地址。此外,若设备挂接到 IDENTITY 类型的 domain,映射走直接映射路径,掩码的作用与未启用 IOMMU 时相同。
这类错误往往不会立即暴露。如第 4.1 节所述,内核对 PCI 设备优先分配 32 位以内的 IOVA,掩码设置错误的设备在负载较低时可以正常工作,直到 32 位以内的 IOVA 耗尽、内核打印Using %d-bit DMA addresses之后才出现故障。该处注释已指出,此时出现问题的常见原因是驱动未准确设置 DMA 掩码。DMA 掩码的协商将在第 11 篇讨论。
6.2 认为启用 IOMMU 后设备的所有访问都受到检查
以下情形中,设备的访问不受 IOMMU 的完整约束:
- 设备挂接到 IDENTITY 类型的 domain 时,IOMMU 不进行翻译与权限检查;
- 同一 group 内的设备之间可能存在不经过 IOMMU 的点对点访问;
- 隔离以页为粒度,映射一个小缓冲区会使设备获得整页的访问权限;
- 解除映射后,IOTLB 中的缓存项在失效之前仍可能被设备使用,失效的时机将在第 7 篇讨论。
因此,判断一个系统中的 DMA 隔离程度,需要同时检查默认 domain 的类型、group 的划分以及设备是否被标记为不可信。
6.3 认为 IOMMU 故障可以像 CPU 缺页一样恢复
CPU 缺页可能是正常的执行流程,例如按需分配与换入;而在 DMA 映射层管理的地址空间中,IOMMU 故障一般表明设备访问了驱动未交给它的地址。常见原因包括:解除映射后设备仍在访问缓冲区、驱动将错误的地址写入描述符、设备复位后沿用了旧的地址,以及对只读映射执行写入等。遇到此类故障时,应从故障日志中的设备标识与 IOVA 入手排查驱动,而不应试图通过放宽隔离来规避。排查方法将在第 24 篇讨论。设备缺页的可恢复处理需要专门的硬件支持,属于另一类机制,将在第 21 篇讨论。
7. 小结
本文回答了问题 6,要点如下:
- IOMMU 位于设备访问内存的必经路径上。在 PCIe 系统中通常集成于 Root Complex,每个实例负责一部分设备,对应关系由 ACPI DMAR、IVRS、IORT 表或设备树描述。
- 翻译过程分为设备表查找与页表遍历两步。IOMMU 以请求中的 Requester ID(或 StreamID)查找设备表,取得翻译类型、页表根地址与 domain ID,再以 IOVA 遍历多级 I/O 页表得到物理地址,同时检查读写权限。翻译失败时请求被拦截,故障被记录并上报,一般不可恢复。
- 三个问题统一转化为 IOVA 的分配与映射。内核在设备的寻址范围内分配 IOVA,解决寻址范围问题;将分散的物理页映射到连续的 IOVA,解决物理连续性问题;设备只能访问已映射的 IOVA,且权限由 DMA 方向决定,实现设备隔离。驱动使用的 DMA 映射接口保持不变。
- 隔离以 domain 与 group 为单位。domain 是一个 I/O 地址空间,多个设备可以共享;group 是 IOMMU 能够区分并隔离的最小设备集合,由 Requester ID 别名、ACS 配置与多功能设备的隔离能力共同决定,同一 group 内的设备必须使用同一个 domain。
本文的讨论集中于 IOMMU 能够做什么。每次映射都需要分配 IOVA、更新 I/O 页表,每次解除映射都需要使 IOTLB 中的缓存项失效,设备的每次访问也可能因 IOTLB 未命中而遍历页表。这些操作的开销如何产生,频繁的映射与解除映射为何会成为性能瓶颈,即为问题 7。
系列导航:
- 上一篇《第 5 篇 DMA 与内存布局:物理连续性、寻址位宽与反弹缓冲》
- 下一篇《第 7 篇 IOMMU 的内部机制:I/O 页表、IOVA 分配与 IOTLB》将回答问题 7:IOMMU 地址翻译存在哪些隐性开销。