IOMMU 介绍
在计算机体系结构中IOMMU(Input Output Memory Management Unit)是将设备直接内存访问(DMA)的IO总线和物理内存连接的内存管理单元,和传统的MMU一样,IOMMU将设备可见的虚拟地址(IOVA)映射到物理地址。不同的平台有不同的IOMMU,如Intel的IOMMU,PCIE图形卡使用的图形重映射表(GART),Arm平台的IOMMU是SMMU(System Memory Management),它们主要功能都是完成设备可见的IOVA到物理地址的映射。
CPU和外设访问物理内存:
+---------------------+ | Main Memory | +---------------------+ | pa | ------------- | | +--------+ +--------+ | IOMMU | | MMU | +--------+ +--------+ | | iova va | | +--------+ +--------+ | Device | | CPU | +--------+ +--------+
相对于设备DMA直接访问内存,使用IOMMU有很明显的优点:
- 可以在内存中分配比较大的非连续区域,IOMMU可以将连续的虚拟地址映射到零散的物理地址
- 即使设备不支持足够长的内存寻址来访问整个物理内存,也可以通过IOMMU来完成整个内存的寻址。比如x86计算机可以额通过物理地址扩展(PAE)功能寻址超过4GB的内存,但是普通的32位PCI设备则无法寻址超过4GB的内存,有IOMMU则可以让设备访问整个物理内存。
- 由于设备无法直接读取或写入映射的物理内存,可以保护内存免受恶意设备进行DMA攻击和尝试错误的内存传输。
- 在虚拟化场景中,Guest OS可以使用非专门位虚拟化设计的硬件,比如将高性能硬件如显卡直通给虚拟机,通过DMA直接访问内存。在虚拟化环境中,所有的内存地址都由虚拟化程序(如Qemu)进行重映射,Guest OS使用DMA直接访问内存时会发生故障,只有IOMMU完成了重映射之后,Guest OS才可以驱动设备正确进行DMA访存。
- 在有些体系结构中,IOMMU还以类似地址重映射的方式进行中断重映射。
- IOMMU还可以支持外围设备内存分页,使用PCI-SIG PCIe地址转换服务(ATS)PRI扩展的外围设备可以检测并通知内存管理服务。
相对于DMA直接访问物理内存,使用IOMMU的缺点主要体现在额外的性能和内存开销,地址翻译和缺页处理会增加额外的性能开销,并且IOMMU需要在内存中为IO页表分配空间,在有些情况下IOMMU和CPU共享页表可以避免这一内存开销,比如设备和CPU共享虚拟地址SVA。
Arm SMMU 数据结构
SMMU(System Memory Management Unit)是Arm平台的IOMMU,
SMMU为设备提供用设备可见的IOVA地址来访问物理内存的能力,体系结构中可能存在多个设备使用IOVA经过IOMMU来访问物理内存,IOMMU需要能够区分不同的设备,从而为每个设备引入了一个Stream ID,指向对应的STE(Stream Table Entry),所有的STE在内存中以数组的形式存在,SMMU记录STE数组的首地址。在操作系统扫描设备的时候会为其分配独有的Stream ID简称sid,设备通过IOMMU进行访存的所有配置都写在对应sid的STE中。
Stream Table:
+-------+ strtab_base ----- | STE 0 | | STE 1 | StreamID[n:0] -> | STE 2 | | STE 3 | +-------+
STE表项中保存了从IOVA到PA的地址翻译过程,为了适应虚拟化场景下的访存需求,SMMU设计了类似EPT页表的两级地址翻译过程。Stage 1 完成从虚拟地址VA到中间地址IPA的翻译过程,stage 2 完成从IPA到实际物理地址的翻译过程。
Stream Table Entry:
Stream Table Entry (STE) +-----------------------+ | Config | S1ContextPtr | -> CD -> Stage 1 translation tables +-----------------------+ | VMID | S2TTB | -> Stage 2 translation tables +-----------------------+ | Other attributes, | | configuration | +-----------------------+
对于非虚拟化场景,设备使用IOVA经过IOMMU进行DMA只需要经过Stage 1的地址转换,因为多个设备可能使用一个设备,所以每个设备的STE中还记录了CD(Context Descriptor)表的信息,由S1ContextPtr指向内存中CD表的基地址,CD表也是一个数组,使用SubstreamID来进行访存,简称ssid,也叫做pasid,pasid是与进程关联的id,用于区分不同进程的虚拟地址空间。在使用pasid找到对应的CD表项之后,也就找到了Stage 1地址翻译的IO页表,保存在TTB0和TTB1中。
Context Descriptor:
+-------+ S1ContextPtr ---- | CD 0 | | CD 1 | SubStreamID -> | CD 2 | | CD 3 | +-------+ Context Desctriptor (CD) +-----------------------+ | Configuration | TTB0 | +-----------------------+ | ASID | TTB1 | +-----------------------+
值得一提的是,在一般情况下,进程通过设备驱动让设备进行DMA时使用的IOVA由内核态驱动分配,当存在多个进程时,将该内核态的IOVA映射到进程的虚拟地址空间即可,也就是说不同进程在DMA时其实使用的是相同的IOVA地址空间,所以这时只需要第0项CD即可,一般只需要在CD0中保存IO页表的基地址。然而,当需要统一设备的IO地址空间和进程的虚拟地址空间,如共享虚拟地址访问(SVA)时,则会用到多个CD项分别绑定不同进程的虚拟地址空间。
设备经过SMMU进行地址翻译是一个很复杂的过程,首先会根据设备的sid(Stream ID)找到对应的STE项,STE项中的配置信息记录了是否需要Bypass Stage 1 的地址翻译,Bypass意味着直接使用PA(或IPA),如果没有Bypass会根据sid(Substream ID)找到对应CD项,CD项中记录了Stage 1地址翻译的页表,会将VA翻译成IPA,然后如果STE中还配置了Stage 2 的页表翻译,会根据Stage 2地址翻译的页表,将IPA翻译成最终的PA地址,如果没有配置Stage 2地址翻译,则之前获取到的IPA就是最终的PA地址。
SMMU地址翻译过程:
VA | ----------- | | +---------------------+ | | Stage 1 translation | Bypass | VA->IPA | | +---------------------+ | | | ----------- | IPA | ----------- | | +---------------------+ | | Stage 2 translation | Bypass | IPA->VA | | +---------------------+ | | | ----------- | PA
Arm SMMU V3 初始化
所有IOMMU相关的驱动都在内核 drivers/iommu 目录下面,arm平台的最新的架构SMMU-v3的驱动为arm-smmu-v3.c,SMMU本身是一个平台设备,struct arm_smmu_device
结构体在内存中管理SMMU设备的关键信息,内核对SMMU设备本身的初始化过程主要就是在填充这个结构体。
/* An SMMUv3 instance */ struct arm_smmu_device { struct device *dev; void __iomem *base; u32 features; u32 options; struct arm_smmu_cmdq cmdq; struct arm_smmu_evtq evtq; struct arm_smmu_priq priq; int gerr_irq; int combined_irq; u32 sync_nr; unsigned long ias; /* IPA */ unsigned long oas; /* PA */ unsigned long pgsize_bitmap; #define ARM_SMMU_MAX_ASIDS (1 < < 16) unsigned int asid_bits; DECLARE_BITMAP(asid_map, ARM_SMMU_MAX_ASIDS); #define ARM_SMMU_MAX_VMIDS (1 < < 16) unsigned int vmid_bits; DECLARE_BITMAP(vmid_map, ARM_SMMU_MAX_VMIDS); unsigned int ssid_bits; unsigned int sid_bits; struct arm_smmu_strtab_cfg strtab_cfg; /* IOMMU core code handle */ struct iommu_device iommu; };
驱动加载的入口为 arm_smmu_device_probe 函数,其主要做了如下几件事情:
1. 从dts的SMMU节点或ACPI的SMMU配置表中读取SMMU中断等属性
2. 用struct resource 来从设备获取到其资源信息,并IO重映射
3. probe SMMU的硬件特性
4. 中断和事件队列初始化
5. 建立STE表
6. 设备reset
7. 将SMMU注册到IOMMU
1. 读取dts的SMMU节点信息
函数 arm_smmu_device_dt_probe 读取节点信息主要是从 smmu->dev->of_node中读取对应的属性,并记录到 smmu->options 中,此外还会检查 DMA 是否支持 coherent, 是的话会设置COHERENCY特性。
if (of_dma_is_coherent(dev->of_node)) smmu->features |= ARM_SMMU_FEAT_COHERENCY;
2. 获取设备资源信息并IO重映射
获取到SMMU设备的资源信息保存在 struct resource
结构体中,记录IO基地址,并用 smmu->base 记录完成IO重映射之后的基地址。之后就可以通过 smmu->base 加偏移读写SMMU的硬件寄存器。
/* Base address */ res = platform_get_resource(pdev, IORESOURCE_MEM, 0); ioaddr = res->start; smmu->base = devm_ioremap_resource(dev, res);
3. probe SMMU硬件特性
函数 arm_smmu_device_hw_probe 通过读取 SMMU 的寄存器获取SMMU的硬件特性,
IDR0寄存器:
reg = readl_relaxed(smmu->base + ARM_SMMU_IDR0);
- 是否支持:两级STE表、两级CD表
- 是否支持:PRI、ATS、SEV、MSI、HYP、STALL、Stage 1、Stage 2
- 获取:ias长度、asid_bits、vmid_bits
IDR1寄存器:
reg = readl_relaxed(smmu->base + ARM_SMMU_IDR1);
- 获取:evtq和priq的队列长度、ssid_bits、sid_bits、
IDR5寄存器:
reg = readl_relaxed(smmu->base + ARM_SMMU_IDR5);
- 获取:evtq最多能stall的的数量
- 是否支持:VAX、oas长度、pgsize_bitmap
4. 中断和事件队列初始化
函数 arm_smmu_init_structures 会完成内存中的数据结构的初始化,包括三条队列evtq、priq、cmdq,cmdq用于SMMU驱动向硬件发送命令,比如刷新TLB、写CD内容等,event队列用于SMMU上挂载的platform设备向驱动发送异常消息,priq队列功能类似只不过用于挂载的PCI设备。event队列和pri队列分别有各自的中断号完成异常事件的通知,此外还有一个gerror的中断号用于上报不可恢复(unrecoverable)的严重错误,其直接中断处理不需要队列。
irq = platform_get_irq_byname(pdev, "combined"); if (irq > 0) smmu->combined_irq = irq; else { irq = platform_get_irq_byname(pdev, "eventq"); if (irq > 0) smmu->evtq.q.irq = irq; irq = platform_get_irq_byname(pdev, "priq"); if (irq > 0) smmu->priq.q.irq = irq; irq = platform_get_irq_byname(pdev, "gerror"); if (irq > 0) smmu->gerr_irq = irq; } /* Initialise in-memory data structures */ ret = arm_smmu_init_structures(smmu);
在驱动进行SMMU设备reset的时候,arm_smmu_setup_unique_irqs 会注册相应的事件处理,eventq和priq会注册内核线程完成事件处理,而对于不可恢复错误gerror则直接注册函数完成中断处理。
5. 建立STE表
根据SMMU的配置不同,可以建立两级或者线性STE表,相对于线性STE表,两级STE表不需要一开始创建所有的STE项,只需要先分配第一级的目录项即可。对于STE线性表,根据sid_bits和STE项的大小,在内存中用dma分配一块连续的内存,在配置中记录其基地址,然后将所有的STE项配置成默认情况下的bypass模式。
static int arm_smmu_init_strtab_linear(struct arm_smmu_device *smmu) { void *strtab; u64 reg; u32 size; struct arm_smmu_strtab_cfg *cfg = & smmu->strtab_cfg; size = (1 < < smmu->sid_bits) * (STRTAB_STE_DWORDS < < 3); strtab = dmam_alloc_coherent(smmu->dev, size, & cfg->strtab_dma, GFP_KERNEL | __GFP_ZERO); cfg->strtab = strtab; cfg->num_l1_ents = 1 < < smmu->sid_bits; /* Configure strtab_base_cfg for a linear table covering all SIDs */ reg = FIELD_PREP(STRTAB_BASE_CFG_FMT, STRTAB_BASE_CFG_FMT_LINEAR); reg |= FIELD_PREP(STRTAB_BASE_CFG_LOG2SIZE, smmu->sid_bits); cfg->strtab_base_cfg = reg; arm_smmu_init_bypass_stes(strtab, cfg->num_l1_ents); return 0; }
6. 设备 reset
函数 arm_smmu_device_reset 会进行设备的复位操作,通过前面获取到的设备寄存器,对控制寄存器CR1和CR2写入队列内存属性等信息,对STRTAB_BASE寄存器写入STE表的基地址和配置信息,将三条队列在内存中的基地址、队首和队尾信息分别写入对应的寄存器中,之后 reset 会调用arm_smmu_setup_irqs注册中断事件的处理操作。
7. 将SMMU注册到IOMMU
不同平台的IOMMU设备在Linux内核中抽象出了统一的IOMMU接口,SMMU的初始化会在sys目录下面注册一个smmu->iommu设备节点,并且将arm_smmu_ops注册给该设备以及系统PCI总线和平台设备总线。这样当使用IOMMU公共接口时,会调用smmu提供的功能,具体可以查看arm_smmu_ops中提供的各种IOMMU接口实现。
iommu_device_sysfs_add(& smmu->iommu, dev, NULL, "smmu3.%pa", & ioaddr); iommu_device_set_ops(& smmu->iommu, & arm_smmu_ops); iommu_device_set_fwnode(& smmu->iommu, dev->fwnode); iommu_device_register(& smmu->iommu); bus_set_iommu(& pci_bus_type, & arm_smmu_ops); bus_set_iommu(& platform_bus_type, & arm_smmu_ops);
IOMMU与DMA
IOMMU的主要功能之一就是避免设备在进行DMA访问内存的时候直接使用物理地址不安全,所以就产生了IOVA地址,在dma_alloc分配内存的时候,首先在IO地址空间分配一个IOVA地址,然后在IOMMU管理的页表中建立IOVA和dma_alloc分配的物理地址的映射关系,外设在进行dma的时候,只需要使用IOVA地址即可。
调用 dma alloc 系列函数分配内存最终会调到 iommu_dma_alloc 函数,其会分配iova和实际的物理内存,并用iommu_map建立iova到物理内存的映射关系,也就是找到设备对应的STE,并找到CD项(一般是第0项),然后找到内存中对应的页表,将iova到物理地址的映射写入页表中,arm SMMU 相关的页表操作在 io-pgtable.c中完成。
iommu_dma_alloc pages = __iommu_dma_alloc_pages(count, alloc_sizes >> PAGE_SHIFT, gfp); iova = iommu_dma_alloc_iova(domain, size, dev->coherent_dma_mask, dev); iommu_map_sg(domain, iova, sgt.sgl, sgt.orig_nents, prot
可以有多种方式bypass掉IOMMU,首先Linux提供了iommu.passthrough的方式,可以配置dma默认不走iommu,通过软件的地址映射技术swiotlb来访存;其次,SMMU v3驱动中提供了参数可以bypass掉某个SMMU;第三十可以在ACPI或者DTS中不配置对应的SMMU节点,这样系统加载的时候就不会probe对应的SMMU了。
小结
IOMMU的主要功能就是为设备访问物理内存提供IOVA到PA的映射,使得设备不会直接使用物理地址来访存比较安全。Arm SMMUv3 作为IOMMU的一种具体实现,为其提供相应的接口。与IOMMU相关的包括设备进行DMA操作,VFIO直通将设备硬件能力安全的暴露给用户态等,本质上都是为了建立设备能够识别的IOVA到实际物理地址的映射关系。
Reference:
- https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit
- https://developer.arm.com/documentation/ihi0070/latest
- https://kernel.taobao.org/2020/06/ARM-SMMU-and-IOMMU/