当使用IOMMU且使能DMA-IOMMU中间层时,使用DMA API接口alloc、free、map、unmap内存时,底层都会调用到IOMMU API,最终调用到SMMUv3驱动,完成内存的map和unmap。调用流程如下图所示。

从下图可以看出,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接口分配内存。
direct方式主要从内核的coherent pool或者CMA区域分配内存。主要的流程如下:
内存按页对齐,长度不足一页,则至少分配一页内存分配过程不能睡眠,即指定GFP_ATOMIC标志 若定义CONFIG_DMA_DIRECT_REMAP(可以将分配的物理内存重新映射到别的虚拟地址,有mmu即可使能该选项,arm架构默认打开该选项,但重映射过程可能会睡眠),且不能阻塞和不使用swiotlb分配内存,则从coherent pool分配内存。coherent pool在内核初始化的时候分配好,内存不足时会动态扩展。从coherent pool分配内存,分配出来的是虚拟内存,需要进一步转换得到物理地址。将物理内存清零,然后将page转换成DMA地址。分配成功后直接返回,不走后面的流程。 内存分配过程可能睡眠,即指定GFP_KERNEL标志。 如果定义CONFIG_DMA_RESTRICTED_POOL,且设备需要从swiotlb分配,则从swiotlb分配内存。如果不满足1,则从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内存池中分配内存。
下面重点分析DMA-IOMMU接口分配内存的流程。
iommu_dma_alloc两种分配策略,第一种需要分配连续的物理内存,第二种不需要分配连续的物理内存。不管是分配连续或者不连续物理内存,都是以页为单位,最小分配一页物理内存。主要的工作如下:

arm_smmu_map_pages函数最终调用SMMU实现的
io_pgtable_ops完成地址映射,即
arm_lpae_map_pages函数,主要的流程如下:
备注:LPAE 是指 Large Physical Address Extension,这是ARM架构中用于支持大于4GB物理地址空间的扩展技术。
获取页表属性。
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_S1
if (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匹配的IOVA
pte = 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, UnprivWrite
if (!(prot & IOMMU_PRIV))
pte |= ARM_LPAE_PTE_AP_UNPRIV;
......
}
// 第二阶段地址转换,这里只关注ARM_64_LPAE_S2
if (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-nGnRE
else if (prot & IOMMU_CACHE)
// Normal: Outer Write-Back Cacheable和Inner Write-Back Cacheable
pte |= ARM_LPAE_PTE_MEMATTR_OIWB;
else
// Normal: Outer Non-cacheable和Inner Non-cacheable
pte |= 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 Shareable
else
pte |= 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。 如果不满足1,说明要使用下一级页表进行映射。首先读取下一级页表的地址,根据是否存在下一级页表,有两种处理方式:
如果下一级页表不存在,则需要分配一页内存,然后将分配的页表内存地址设置到上一级页表当中。设置页表地址使用原子指令(使能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进行映射。 
和
dma_alloc_coherent一样,
dma_free_coherent有四种方式释放内存。具体如下:

direct方式从coherent pool、CMA区域或者系统内存分配内存,则释放内存也从分配内存的区域释放内存。

下面重点分析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的内存释放流程。
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内存。
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运行在EL2
cmd.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。