Linux之IOMMU 涉及DMA-API(alloc 和free) 调用流程分析(9)

  • 时间:2025-11-07 14:53 作者: 来源: 阅读:0
  • 扫一扫,手机访问
摘要:1.概述 当使用IOMMU且使能DMA-IOMMU中间层时,使用DMA API接口alloc、free、map、unmap内存时,底层都会调用到IOMMU API,最终调用到SMMUv3驱动,完成内存的map和unmap。调用流程如下图所示。 2.dma_alloc_coherent 从下图可以看出,dma_alloc_coherent有四种方式分配内存。具体如下: 优先调用从设备的c

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区域分配内存。主要的流程如下:

内存按页对齐,长度不足一页,则至少分配一页内存分配过程不能睡眠,即指定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内存池中分配内存。
 

2.2.iommu_dma_alloc

下面重点分析DMA-IOMMU接口分配内存的流程。 iommu_dma_alloc两种分配策略,第一种需要分配连续的物理内存,第二种不需要分配连续的物理内存。不管是分配连续或者不连续物理内存,都是以页为单位,最小分配一页物理内存。主要的工作如下:

若支持阻塞(gfp定义了GFP_KERNEL标志,当内存不足时,会进行内存回收,内存回收时当前线程会被阻塞)且不需要强制物理内存连续。  分配物理内存。底层调用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中分配物理内存。 若不满足1,则调用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函数,主要的流程如下:

备注: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进行映射。 
 

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运行在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。
 

  • 全部评论(0)
手机二维码手机访问领取大礼包
返回顶部