DMA-API(alloc和free)调用流程分析(十)
1.概述
当使用IOMMU且使能DMA-IOMMU中间层时,使用DMA API接口alloc、free、map、unmap内存时,底层都会调用到IOMMU API,最终调用到SMMUv3驱动,完成内存的map和unmap。调用流程如下图所示。
2.dma_alloc_coherent
从下图可以看出,dma_alloc_coherent有四种方式分配内存。具体如下:
- 优先调用从设备的coherent pool中分配。设备的coherent pool有两种方式注册。第一是在设备树中配置保留内存,然后在该设备的设备树节点中使用memory-region引用,最后调用
of_reserved_mem_device_init
函数将这段内存注册为设备的coherent pool。第二种是直接调用dma_decleare_coherent_memory
函数注册coherent pool。 - 设备没有coherent pool,没有使用iommu且没有定义
dma_map_ops
,则使用direct方式分配内存,底层从内核的coherent pool或者CMA区域分配内存。 - 设备没有coherent pool,但使用了iommu,则调用DMA-IOMMU接口分配内存。
- 设备没有coherent pool,没有使用iommu且定义
dma_map_ops
了,则使用dma_map_ops
定义的alloc回调函数分配内存。
下面重点分析direct方式和DMA-IOMMU接口分配内存。
2.1.dma_direct_alloc
direct方式主要从内核的coherent pool或者CMA区域分配内存。主要的流程如下:
- 内存按页对齐,长度不足一页,则至少分配一页
- 若定义
CONFIG_DMA_DIRECT_REMAP
,且不能阻塞和不使用swiotlb分配内存,则从coherent pool分配内存。coherent pool在内核初始化的时候分配好,内存不足时会动态扩展。 - 将物理内存清零,然后将page转换成DMA地址并返回。
- 若不满足第2点,则从swiotlb、CMA区域或系统内存分配。
- 如果定义
CONFIG_DMA_RESTRICTED_POOL
,且设备需要从swiotlb分配,则从swiotlb分配内存。 - 如果不满足a,则从CMA区域分配内存。
- 如果CMA分配失败,则从系统内存分配,如果从系统中分配的内存不满足coherent的要求,则会调整gfp重新分配。
- 将物理内存清零,然后将page转换成DMA地址并返回。
Linux内核中有三种coherent pool,分别是atomic_pool_dma32
、atomic_pool_dma
和atomic_pool_kernel
,支持原子性的分配内存,分配过程不会阻塞且不会睡眠。这三个coherent pool在系统启动的时候调用dma_atomic_pool_init
初始化,分别从ZONE_DMA32
、ZONE_DMA
和ZONE_NORMAL
区域分配内存,默认大小为1GB分配128KB内存。如果在使用的过程中,coherent pool中可用的内存小于初始化时分配的大小,则会通过工作队列动态扩展内存,每次扩展一倍。
[kernel/dma/pool.c]
static int __init dma_atomic_pool_init(void)
{/** If coherent_pool was not used on the command line, default the pool* sizes to 128KB per 1GB of memory, min 128KB, max MAX_PAGE_ORDER.*/if (!atomic_pool_size) {unsigned long pages = totalram_pages() / (SZ_1G / SZ_128K);pages = min_t(unsigned long, pages, MAX_ORDER_NR_PAGES);atomic_pool_size = max_t(size_t, pages << PAGE_SHIFT, SZ_128K);}// 初始化动态扩展内存池的工作队列INIT_WORK(&atomic_pool_work, atomic_pool_work_fn);atomic_pool_kernel = __dma_atomic_pool_init(atomic_pool_size, GFP_KERNEL);if (has_managed_dma()) {atomic_pool_dma = __dma_atomic_pool_init(atomic_pool_size,GFP_KERNEL | GFP_DMA)}if (IS_ENABLED(CONFIG_ZONE_DMA32)) {atomic_pool_dma32 = __dma_atomic_pool_init(atomic_pool_size,GFP_KERNEL | GFP_DMA32);}
}
postcore_initcall(dma_atomic_pool_init);
系统启动后,可以通过/sys下面的节点查看三种coherent pool的大小。
/sys/kernel/debug/dma_pools/pool_size_dma
/sys/kernel/debug/dma_pools/pool_size_dma32
/sys/kernel/debug/dma_pools/pool_size_kernel
dma_alloc_coherent
分配内存时,如果gfp==GFP_DMA32
,则从atomic_pool_dma32
内存池中分配内存,如果gfp==GFP_DMA
,则从atomic_pool_dma
内存池中分配内存,如果gfp==GFP_KERNEL
或者其他,则从atomic_pool_kernel
内存池中分配内存。
2.2.iommu_dma_alloc
下面重点分析DMA-IOMMU接口分配内存的流程。iommu_dma_alloc
两种分配策略,第一种需要分配连续的物理内存,第二种不需要分配连续的物理内存。不管是分配连续或者不连续物理内存,都是以页为单位,最小分配一页物理内存。主要的工作如下:
- 若支持阻塞(gfp定义了
__GFP_DIRECT_RECLAIM
标志,当内存不足时,会进行内存回收,内存回收时当前线程会被阻塞,通常情况下不指定该标志)且不需要强制物理内存连续。- 分配物理内存。底层调用
alloc_pages_node
从系统分配内存,分配出来的内存可能连续,也可能不连续。 - 分配IOVA。IOVA从
iommu_domain
中的iova_domain
中分配。从这也可以看出,一个iommu_domain
对应一个IO虚拟地址空间。 - 将分配的物理内存pages转换成sg table,便于下一步进行映射。
- 若设备不是coherent设备,则需要对分配的每一页物理内存,做clean cache。
- 调用
iommu_map_sg
函数,映射IOVA和PA。底层调用smmu驱动arm_smmu_map_pages
映射。 - 将分配的物理内存映射成连续的内核虚拟地址,便于CPU访问。
- 分配物理内存。底层调用
- 需要分配连续的物理内存。
- 若定义了
CONFIG_DMA_DIRECT_REMAP
、不允许阻塞且不是coherent设备,则直接从内核的DMA_ZONE
中分配物理内存。 - 若不满足a,则调用
iommu_dma_alloc_pages
分配内存。首先尝试从CMA中分配连续内存,若不成功,则从系统内存分配,然后将物理内存映射为内核的虚拟地址,若不是coherent设备,则还需要clean cache,最后将所有物理内存清零。 - 映射IOVA和PA。首先分配IOVA,然后调用
iommu_map
进行映射。底层调用smmu驱动arm_smmu_map_pages
映射。
- 若定义了
2.2.1.arm_smmu_map_pages
arm_smmu_map_pages
函数最终调用SMMU实现的io_pgtable_ops
完成地址映射,即arm_lpae_map_pages
函数,主要的流程如下:
- 获取页表属性。
iommu_prot
只能是IOMMU_READ
或者IOMMU_WRITE
,或者两者都包含。最终页表的属性如下代码所示。
[drivers/iommu/io-pgtable-arm.c]
// port必须包含IOMMU_READ和IOMMU_WRITE其中之一或者两者都包含
static arm_lpae_iopte arm_lpae_prot_to_pte(struct arm_lpae_io_pgtable *data,int prot)
{arm_lpae_iopte pte;// 第一阶段地址转换,这里只关注ARM_64_LPAE_S1if (data->iop.fmt == ARM_64_LPAE_S1 ||data->iop.fmt == ARM_32_LPAE_S1) {// ARM_LPAE_PTE_nG表示地址转换是non-global的,TLB entry和特定的// ASID或者ASID和VMID关联起来,只转换和ASID或者ASID和VMID匹配的IOVApte = ARM_LPAE_PTE_nG;if (!(prot & IOMMU_WRITE) && (prot & IOMMU_READ))pte |= ARM_LPAE_PTE_AP_RDONLY; // 只读-// Enables dirty tracking in stage 1 pagetable.else if (data->iop.cfg.quirks & IO_PGTABLE_QUIRK_ARM_HD)pte |= ARM_LPAE_PTE_DBM;// 非特权,UnprivRead, UnprivWriteif (!(prot & IOMMU_PRIV))pte |= ARM_LPAE_PTE_AP_UNPRIV;......}// 第二阶段地址转换,这里只关注ARM_64_LPAE_S2if (data->iop.fmt == ARM_64_LPAE_S2 ||data->iop.fmt == ARM_32_LPAE_S2) {if (prot & IOMMU_MMIO)pte |= ARM_LPAE_PTE_MEMATTR_DEV; // Device-nGnREelse if (prot & IOMMU_CACHE)// Normal: Outer Write-Back Cacheable和Inner Write-Back Cacheablepte |= ARM_LPAE_PTE_MEMATTR_OIWB;else// Normal: Outer Non-cacheable和Inner Non-cacheablepte |= ARM_LPAE_PTE_MEMATTR_NC;......}/** Also Mali has its own notions of shareability wherein its Inner* domain covers the cores within the GPU, and its Outer domain is* "outside the GPU" (i.e. either the Inner or System domain in CPU* terms, depending on coherency).*/if (prot & IOMMU_CACHE && data->iop.fmt != ARM_MALI_LPAE)pte |= ARM_LPAE_PTE_SH_IS; // Inner Shareableelsepte |= ARM_LPAE_PTE_SH_OS; // Outer Shareable......return pte;
}
- 递归调用
__arm_lpae_map
完成各级页表的创建。具体的工作流程如下:- 如果映射的内存大小和当前Block descriptor或者Page descriptor页表描述符映射的内存大小一样,则调用
arm_lpae_init_pte
设置页表。主要是设置页表的类型和物理内存地址。如果IOMMU不支持coherent_walk
,还需要clean dcache。 - 如果不满足a,说明要使用下一级页表进行映射。首先读取下一级页表的地址,根据是否存在下一级页表,有两种处理方式:
- 如果下一级页表不存在,则需要分配一页内存,然后将分配的页表内存地址设置到上一级页表当中。设置页表地址使用原子指令(使能
FEAT_LSE
,则使用CAS
指令,否则使用LDXR
和STXR
指令)。如果原来的页表地址为0,则设置新的页表地址,并返回老的页表地址。如果原来的页表地址为非0,说明在分配页表的时候,有进程设置了下一级页表,则直接返回老的页表地址,然后释放分配的页表内存。 - 如果存在下一级页表,无需分配。如果IOMMU不支持
coherent_walk
且页表没有设置ARM_LPAE_PTE_SW_SYNC
,则需要clean dcache。然后判断下一级页表是不是叶子页表(Block descriptor或者Page descriptor),如果不是,则通过iopte_deref
宏获取下一级页表的虚拟地址,否则报错,返回错误,因为当前页表是最后一级页表,已经配置了映射,需要先unmap,才能再配置映射。
- 如果下一级页表不存在,则需要分配一页内存,然后将分配的页表内存地址设置到上一级页表当中。设置页表地址使用原子指令(使能
- 递归调用
__arm_lpae_map
进行映射。
- 如果映射的内存大小和当前Block descriptor或者Page descriptor页表描述符映射的内存大小一样,则调用
3.dma_free_coherent
和dma_alloc_coherent
一样,dma_free_coherent
有四种方式释放内存。具体如下:
- 如果分配的内存位于设备的coherent pool中,则走设备的coherent pool释放流程。设备的coherent pool通过bitmap管理,释放时只需要清除对应的bitmap即可。
- 如果是direct方式分配内存,则走
coherent_pool
或者CMA内存的释放流程。 - 如果使用了iommu,则调用DMA-IOMMU接口释放内存。
- 如果使用
dma_map_ops
定义的alloc
回调函数分配内存,则使用dma_map_ops
定义的free
回调函数释放内存。
3.1.dma_direct_free
direct方式从coherent pool、CMA区域或者系统内存分配内存,则释放内存也从分配内存的区域释放内存。
3.2.iommu_dma_free
下面重点分析DMA-IOMMU接口释放内存的流程。主要的工作如下:
- 调用
iommu_unmap_fast
解除IOVA和PHY地址的映射。底层调用smmu驱动arm_smmu_unmap_pages
解除映射。 - 如果
iommu_domain
的类型不是IOMMU_DOMAIN_DMA_FQ
,则需要主动刷新IOTLB。底层调用smmu驱动arm_smmu_iotlb_sync
刷新IOTLB。如果iommu_domain
的类型是IOMMU_DOMAIN_DMA_FQ
,内核有fq_domain
队列异步刷新,释放内存的时候不需要主动刷新。 - 释放IOVA。通过
iommu_domain
中的iova_domain
释放。 - 释放物理内存。
- 如果是从
DMA_ZONE
中分配的内存,则走DMA_ZONE
的释放流程。 - 如果物理内存不连续,即虚拟内存位于
vmalloc
区域,则调用vunmap
解除物理内存和虚拟内存之间的映射。最后调用__free_page
释放物理内存。 - 如果物理内存连续,则走CMA的内存释放流程。
3.2.1.arm_smmu_unmap_pages
arm_smmu_unmap_pages
函数最终调用SMMU实现的io_pgtable_ops
完成地址映射,即arm_lpae_unmap_pages
函数。最终通过调用__arm_lpae_unmap
函数递归unmap内存。主要的流程如下:
- 如果unmap的内存大小和当前Block descriptor或者Page descriptor页表描述符映射的内存大小一样,则调用
__arm_lpae_clear_pte
清除页表,即将页表项清零。同时将IOVA合并并且记录到iommu_iotlb_gather
中,便于后面根据IOVA刷TLB。这里面还有一个处理非叶子页表的逻辑,图中没有画出。如果当前页表项不是叶子页表,则需要清理其下一级页表,然后释放页表内存。 - 如果不满足1且当前页表项是叶子页表,说明当前页表项是Block descriptor,unmap的内存长度小于Block descriptor映射的长度,需要将Block descriptor映射的地址切分,去掉unmap部分的长度,剩余的重新建立页表映射。
- 获取下一级页表页表虚拟地址,然后递归调用
__arm_lpae_unmap
函数unmap内存。
3.2.2.arm_smmu_iotlb_sync
unmap内存以后,IOTLB缓存的这部分页表失效,需要主动invalidate IOTLB,避免访问到已经释放的内存,以及影响数据安全。SMMUv3驱动提供了arm_smmu_iotlb_sync
函数用于invalidate IOTLB,只invalidate叶子页表的IOTLB,工作流程如下:
- 构造invalidate IOTLB的命令。如下代码所示。如果是第一阶段地址转换,支持
ARM_SMMU_FEAT_E2H
特性,则opcode
为CMDQ_OP_TLBI_EL2_VA
,否则opcode
为CMDQ_OP_TLBI_NH_VA
,invalidate IOTLB命令需要提供leaf、asid、iova三个参数,leaf表示是否只invalidate叶子页表项TLB缓存,asid表示进程id,用于区分不同的iova地址空间,iova表示io虚拟地址。如果是第二阶段地址转换,opcode
为CMDQ_OP_TLBI_S2_IPA
,invalidate IOTLB命令需要提供leaf、vmid、iova三个参数,vmid表示虚拟机id。
[drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c]
static void arm_smmu_tlb_inv_range_domain(unsigned long iova,size_t size, size_t granule, bool leaf,struct arm_smmu_domain *smmu_domain)
{struct arm_smmu_cmdq_ent cmd = {.tlbi = {.leaf = leaf, // 是否只invalidate叶子页表项TLB缓存},};// 第一阶段地址转换if (smmu_domain->stage == ARM_SMMU_DOMAIN_S1) {// ARM_SMMU_FEAT_E2H - 虚拟机主机扩展,整个Host OS运行在EL2cmd.opcode = smmu_domain->smmu->features & ARM_SMMU_FEAT_E2H ?CMDQ_OP_TLBI_EL2_VA : CMDQ_OP_TLBI_NH_VA;cmd.tlbi.asid = smmu_domain->cd.asid;} else {cmd.opcode = CMDQ_OP_TLBI_S2_IPA;cmd.tlbi.vmid = smmu_domain->s2_cfg.vmid;}......
}
- 初始化临时命令队列。
- 如果SMMU支持
ARM_SMMU_FEAT_RANGE_INV
特性,则将IOVA按照num * 2^scale * pgsize
的长度切分成数个inv_range
,num
最大为31,否则IOVA按照page size切分,每个inv_range
创建一个命令(命令使用arm_smmu_cmdq_ent
描述),然后添加到临时命令队列(使用arm_smmu_cmdq_batch
描述)里面。IOVA保存在iommu_iotlb_gather
数据结构中。 - 提交临时命令队列,最终临时命令队列中的命令会写入到SMMU的命令队列中,启动SMMU执行命令,最后等待命令执行完毕。
- 如果使能了ATS功能,还需要invalidate PCIe设备的ATC。
参考资料
- linux6.12 source code.
- -Arm ® System Memory Management Unit Architecture Specification version 3.