1.概述
ARM SMMUv3控制器初始化及设备树分析(七)中描述了IOMMU控制器初始化过程。SMMU驱动最后调用iommu_device_register将其注册到内核中,下面分析一下SMMU控制器注册过程中都做了那些工作。
如下图所示,SMMU控制器注册过程中主要做了4部分工作:
- 遍历内核中所有总线,初始化使用SMMU的设备,主要是创建设备的Stream Table,给设备分配或者创建
iommu_group。 - 遍历所有使用SMMU的设备,创建
iommu_domian,并和设备关联起来。主要是初始化设备使用的STE和CD表。 - 遍历所有使用SMMU的设备,初始化DMA映射的
iommu_domian。主要是初始化iova_domain、TLB刷新队列,设置保留的IOVA。 - 释放相关锁,SMMU不需要,这里不多介绍。

如下代码所示,iommu_device_register函数会遍历iommu_buses定义的所有总线,初始化其中使用SMMU的设备。
[drivers/iommu/iommu.c]
static const struct bus_type * const iommu_buses[] = {&platform_bus_type,
#ifdef CONFIG_PCI&pci_bus_type,
#endif
#ifdef CONFIG_ARM_AMBA&amba_bustype,
#endif
#ifdef CONFIG_FSL_MC_BUS&fsl_mc_bus_type,
#endif
#ifdef CONFIG_TEGRA_HOST1X_CONTEXT_BUS&host1x_context_device_bus_type,
#endif
#ifdef CONFIG_CDX_BUS&cdx_bus_type,
#endif
};
2.初始化设备
初始化使用IOMMU的client设备,主要是为其创建Stream Table及分配或者创建iommu_group。
- 调用
arm_smmu_probe_device函数初始化设备。- 由于client设备是smmu的master,因此smmu驱动会给每一个client设备分配一个
arm_smmu_master,然后通过arm_smmu_master将client设备和smmu控制器关联起来。dev_iommu中的私有数据指针priv指向了arm_smmu_master。arm_smmu_master中保存了设备的Stream ID信息。smmu驱动使用红黑树管理Stream ID。 - 如果采用2级Stream table,则需要分配client设备使用的第二级STE表内存,一次性分配256个STE表。然后将STE暂时设置成ABORT。
- 处理PCIe设备ATS相关功能。
- 由于client设备是smmu的master,因此smmu驱动会给每一个client设备分配一个
- 将
iommu_device保存到设备的dev_iommu中。这样可以通过设备的device数据结构找到smmu控制器。 - 在sysfs中关联设备和iommu,这样在sys文件系统中device目录下会有iommu控制器的文件。
- 调用
arm_smmu_device_group函数为设备创建iommu_group。PCI设备可能会共享iommu_group,因此驱动会依据PCI总线拓扑结构、isolation特性或者DMA别名quirks查找或者创建iommu_group。其他设备每个设备分配一个iommu_group。 - 分配
group_device,然后挂到iommu_group的devices链表中。一个iommu_group内可能有多个设备,每个设备使用group_device表示。 iommu_group挂到group_list链表中,稍后统一处理。

3.创建iommu_domian
遍历group_list链表,为每个iommu_group设置默认的iommu_domain,主要的工作内容如下:
- 首先获取
iommu_domain的类型,传入的参数为0,则iommu_domain类型由驱动和系统共同决定。对于PCI untrusted设备,类型为IOMMU_DOMAIN_DMA,其他设备返回0。 - 分配默认的
iommu_domain。如果请求的iommu_domain类型非0,则使用请求的类型,否则使用iommu_def_domain_type表示的类型。iommu_def_domain_type是一个全局变量,由命令行参数和内核配置共同决定,通常情况下为IOMMU_DOMAIN_DMA或IOMMU_DOMAIN_DMA_FQ。如果iommu_domain类型为DMA,则调用arm_smmu_domain_alloc_paging分配iommu_domain,smmu驱动底层会分配一个arm_smmu_domain,内部包含了iommu_domain,然后将arm_smmu_domain和arm_smmu_device(arm_smmu_master)关联在一起,最后初始化domain内页表相关内容。 - 遍历
iommu_group内的每一个设备,如果其保留内存区域类型为IOMMU_RESV_DIRECT或IOMMU_RESV_DIRECT_RELAXABLE,则创建direct mappings。避免这些内存区域被误使用。 - 遍历group中的每一个device,调用
arm_smmu_attach_dev关联对应的iommu_domain。最主要的是初始化arm_smmu_domain中的CD表。

iommu_domain的类型不同,其分配策略也不同。iommu_domain在__iommu_domain_alloc函数中分配,结合smmu驱动,可以总结iommu_domain的分配策略如下:
- 如果分配的
iommu_domain类型是IOMMU_DOMAIN_IDENTITY或IOMMU_DOMAIN_BLOCKED,则直接使用arm_smmu_ops驱动中静态定义的arm_smmu_identity_domain或arm_smmu_blocked_domain。这样就存在多个iommu_group使用一个iommu_domain的情况。 - 如果分配的
iommu_domain类型是IOMMU_DOMAIN_UNMANAGED、IOMMU_DOMAIN_DMA或IOMMU_DOMAIN_DMA_FQ,则smmu驱动会动态分配arm_smmu_domain(内部包含了iommu_domain),这样一个iommu_group对应一个iommu_domain,使用default_domain_ops,smmu驱动需要管理IOVA页表。 - 如果分配的
iommu_domain类型是IOMMU_DOMAIN_SVA,说明DMA共享进程进程地址空间,则smmu驱动会动态分配iommu_domain,并且使用arm_smmu_sva_domain_ops,由于和进程共享,smmu驱动只需要管理iommu_domain和PASID。
3.1.分配iommu_domian
arm_smmu_domain_alloc_paging函数主要分配支持iommu_map/unmap接口的iommu_domain,然后根据不同的地址转换阶段,做不同的初始化。主要的工作如下:
- 分配
arm_smmu_domain,内部包含了iommu_domain。 - 初始化
arm_smmu_domain,ARM_SMMU_DOMAIN_S1和ARM_SMMU_DOMAIN_S2初始化内容有所不同。- 初始化IO页表配置。包括SMMU支持的页表大小掩码、是否支持Cache一致性、刷新TLB回调函数
arm_smmu_flush_ops。 - 如果是第一阶段地址转换。设置输入地址位宽为48位,如果支持虚拟地址扩展(VAX),则设置为52位,设置输出地址位宽等于IPA地址的位宽。设置IO页表格式为
ARM_64_LPAE_S1。 - 如果是第二阶段地址转换。设置输入地址位宽等于IPA地址的位宽,设置输出地址位宽等于PA地址的位宽。设置IO页表格式为
ARM_64_LPAE_S2。 - 根据不同的IO页表格式,创建管理IO页表的接口集合
io_pgtable_ops。- 对于
ARM_64_LPAE_S1页表格式,调用arm_64_lpae_alloc_pgtable_s1创建io_pgtable_ops。设置TCR(类似于MMU中的TCR_ELx)中的参数,如共享属性、页表大小、IPA地址宽度。设置MAIRs。分配保存第0级页表的内存(CD表中TTB0/1指向这块内存)。 - 对于
ARM_64_LPAE_S2页表格式,调用arm_64_lpae_alloc_pgtable_s2创建io_pgtable_ops。设置VTCR(类似于MMU中的VTCR_EL2)中的参数,如共享属性、页表大小、PA地址宽度。分配保存第0级页表的内存(STE表中S2TTB指向这块内存)。
- 对于
- 如果是
ARM_SMMU_DOMAIN_S1,则需要分配ASID。如果是ARM_SMMU_DOMAIN_S2,则需要分配VMID。
- 初始化IO页表配置。包括SMMU支持的页表大小掩码、是否支持Cache一致性、刷新TLB回调函数

arm_smmu_domain_alloc_paging函数分配iommu_domian的过程中涉及到重要的代码定义如下所示:
[drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c]
// TLB刷新回调函数集合
static const struct iommu_flush_ops arm_smmu_flush_ops = {.tlb_flush_all = arm_smmu_tlb_inv_context,.tlb_flush_walk = arm_smmu_tlb_inv_walk,.tlb_add_page = arm_smmu_tlb_inv_page_nosync,
};[drivers/iommu/io-pgtable.c]
// 根据不同的IO页表格式,对应不同的创建io_pgtable_ops的函数
static const struct io_pgtable_init_fns *
io_pgtable_init_table[IO_PGTABLE_NUM_FMTS] = {
#ifdef CONFIG_IOMMU_IO_PGTABLE_LPAE[ARM_32_LPAE_S1] = &io_pgtable_arm_32_lpae_s1_init_fns,[ARM_32_LPAE_S2] = &io_pgtable_arm_32_lpae_s2_init_fns,[ARM_64_LPAE_S1] = &io_pgtable_arm_64_lpae_s1_init_fns,[ARM_64_LPAE_S2] = &io_pgtable_arm_64_lpae_s2_init_fns,[ARM_MALI_LPAE] = &io_pgtable_arm_mali_lpae_init_fns,
#endif......
};// io_pgtable_ops函数集合
[drivers/iommu/io-pgtable-arm.c]
data->iop.ops = (struct io_pgtable_ops) {.map_pages = arm_lpae_map_pages,.unmap_pages = arm_lpae_unmap_pages,.iova_to_phys = arm_lpae_iova_to_phys,.read_and_clear_dirty = arm_lpae_read_and_clear_dirty,
3.2.关联设备
arm_smmu_attach_dev函数的主要作用是将iommu_domain和设备关联起来。如果是第一阶段地址转换的iommu_domain,则需要初始化CD表,如果是第二阶段地址转换的iommu_domain,则需要初始化STE表中S2相关的参数。具体的内容如下:
- 如果是
ARM_SMMU_DOMAIN_S1,则分配CD表。如果是线性CD表(STRTAB_STE_0_S1FMT_LINEAR),则直接分配全部内存。如果是2级CD表(STRTAB_STE_0_S1FMT_64K_L2),先分配第一级L1CD内存,接着分配SSID ==0的第二级CD表数组(可以保存1024个CD),随后将第二级CD表数组的DMA地址写到L1CD内存中(l2.l1tab[0]),最后invalid L1CD。 - 处理和ATS相关的功能。
- 如果是第一阶段地址转换(
ARM_SMMU_DOMAIN_S1)。- 初始化第0个CD表。
- 初始化设备需要的STE表(根据SID初始化),默认SID==0的STE(
STRTAB_STE_1_S1DSS_SSID0)无法使用,如果使用则会ABORT。
- 如果是第二阶段地址转换(
ARM_SMMU_DOMAIN_S2)。初始化设备需要的STE表(根据SID初始化),如果该STE表有对应的CD表,则将第一个CD表清零。 - 按照EATS设置完成STE/CD的配置之后,需要完成对PCIe设备ATC的同步操作。

4.初始化DMA iommu_domian
iommu_setup_dma_ops函数初始化DMA映射的iommu_domian,主要的工作如下:
- 设置
dev->dma_iommu=true。如果启用DMA-API和IOMMU-API的中间层,则设备在分配DMA内存或者进行流式映射内存时,底层将调用DMA-IOMMU接口。 - 初始化
iommu_domain。初始化管理IO虚拟地址的iova_domain及地址缓存、TLB刷新队列和注册保留的IO虚拟地址。

参考资料
- linux 6.12.35 source code.