ARM SMMUv3控制器注册过程分析(八)
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.