当前位置: 首页 > news >正文

DMA-API(alloc和free)调用流程分析(十)

1.概述

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

2.dma_alloc_coherent

从下图可以看出,dma_alloc_coherent有四种方式分配内存。具体如下:

  1. 优先调用从设备的coherent pool中分配。设备的coherent pool有两种方式注册。第一是在设备树中配置保留内存,然后在该设备的设备树节点中使用memory-region引用,最后调用of_reserved_mem_device_init函数将这段内存注册为设备的coherent pool。第二种是直接调用dma_decleare_coherent_memory函数注册coherent pool。
  2. 设备没有coherent pool,没有使用iommu且没有定义dma_map_ops,则使用direct方式分配内存,底层从内核的coherent pool或者CMA区域分配内存。
  3. 设备没有coherent pool,但使用了iommu,则调用DMA-IOMMU接口分配内存。
  4. 设备没有coherent pool,没有使用iommu且定义dma_map_ops了,则使用dma_map_ops定义的alloc回调函数分配内存。
    dma_alloc_coherent
    下面重点分析direct方式和DMA-IOMMU接口分配内存。

2.1.dma_direct_alloc

direct方式主要从内核的coherent pool或者CMA区域分配内存。主要的流程如下:

  1. 内存按页对齐,长度不足一页,则至少分配一页
  2. 若定义CONFIG_DMA_DIRECT_REMAP,且不能阻塞和不使用swiotlb分配内存,则从coherent pool分配内存。coherent pool在内核初始化的时候分配好,内存不足时会动态扩展。
  3. 将物理内存清零,然后将page转换成DMA地址并返回。
  4. 若不满足第2点,则从swiotlb、CMA区域或系统内存分配。
  5. 如果定义CONFIG_DMA_RESTRICTED_POOL,且设备需要从swiotlb分配,则从swiotlb分配内存。
  6. 如果不满足a,则从CMA区域分配内存。
  7. 如果CMA分配失败,则从系统内存分配,如果从系统中分配的内存不满足coherent的要求,则会调整gfp重新分配。
  8. 将物理内存清零,然后将page转换成DMA地址并返回。

direct方式分配内存
Linux内核中有三种coherent pool,分别是atomic_pool_dma32atomic_pool_dmaatomic_pool_kernel,支持原子性的分配内存,分配过程不会阻塞且不会睡眠。这三个coherent pool在系统启动的时候调用dma_atomic_pool_init初始化,分别从ZONE_DMA32ZONE_DMAZONE_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两种分配策略,第一种需要分配连续的物理内存,第二种不需要分配连续的物理内存。不管是分配连续或者不连续物理内存,都是以页为单位,最小分配一页物理内存。主要的工作如下:

  1. 若支持阻塞(gfp定义了__GFP_DIRECT_RECLAIM标志,当内存不足时,会进行内存回收,内存回收时当前线程会被阻塞,通常情况下不指定该标志)且不需要强制物理内存连续。
    1. 分配物理内存。底层调用alloc_pages_node从系统分配内存,分配出来的内存可能连续,也可能不连续。
    2. 分配IOVA。IOVA从iommu_domain中的iova_domain中分配。从这也可以看出,一个iommu_domain对应一个IO虚拟地址空间。
    3. 将分配的物理内存pages转换成sg table,便于下一步进行映射。
    4. 若设备不是coherent设备,则需要对分配的每一页物理内存,做clean cache。
    5. 调用iommu_map_sg函数,映射IOVA和PA。底层调用smmu驱动arm_smmu_map_pages映射。
    6. 将分配的物理内存映射成连续的内核虚拟地址,便于CPU访问。
  2. 需要分配连续的物理内存。
    1. 若定义了CONFIG_DMA_DIRECT_REMAP、不允许阻塞且不是coherent设备,则直接从内核的DMA_ZONE中分配物理内存。
    2. 若不满足a,则调用iommu_dma_alloc_pages分配内存。首先尝试从CMA中分配连续内存,若不成功,则从系统内存分配,然后将物理内存映射为内核的虚拟地址,若不是coherent设备,则还需要clean cache,最后将所有物理内存清零。
    3. 映射IOVA和PA。首先分配IOVA,然后调用iommu_map进行映射。底层调用smmu驱动arm_smmu_map_pages映射。

iommu_dma_alloc

2.2.1.arm_smmu_map_pages

arm_smmu_map_pages函数最终调用SMMU实现的io_pgtable_ops完成地址映射,即arm_lpae_map_pages函数,主要的流程如下:

  1. 获取页表属性。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;
}
  1. 递归调用__arm_lpae_map完成各级页表的创建。具体的工作流程如下:
    1. 如果映射的内存大小和当前Block descriptor或者Page descriptor页表描述符映射的内存大小一样,则调用arm_lpae_init_pte设置页表。主要是设置页表的类型和物理内存地址。如果IOMMU不支持coherent_walk,还需要clean dcache。
    2. 如果不满足a,说明要使用下一级页表进行映射。首先读取下一级页表的地址,根据是否存在下一级页表,有两种处理方式:
      1. 如果下一级页表不存在,则需要分配一页内存,然后将分配的页表内存地址设置到上一级页表当中。设置页表地址使用原子指令(使能FEAT_LSE,则使用CAS指令,否则使用LDXRSTXR指令)。如果原来的页表地址为0,则设置新的页表地址,并返回老的页表地址。如果原来的页表地址为非0,说明在分配页表的时候,有进程设置了下一级页表,则直接返回老的页表地址,然后释放分配的页表内存。
      2. 如果存在下一级页表,无需分配。如果IOMMU不支持coherent_walk且页表没有设置ARM_LPAE_PTE_SW_SYNC,则需要clean dcache。然后判断下一级页表是不是叶子页表(Block descriptor或者Page descriptor),如果不是,则通过iopte_deref宏获取下一级页表的虚拟地址,否则报错,返回错误,因为当前页表是最后一级页表,已经配置了映射,需要先unmap,才能再配置映射。
    3. 递归调用__arm_lpae_map进行映射。
      arm_smmu_map_pages

3.dma_free_coherent

dma_alloc_coherent一样,dma_free_coherent有四种方式释放内存。具体如下:

  1. 如果分配的内存位于设备的coherent pool中,则走设备的coherent pool释放流程。设备的coherent pool通过bitmap管理,释放时只需要清除对应的bitmap即可。
  2. 如果是direct方式分配内存,则走coherent_pool或者CMA内存的释放流程。
  3. 如果使用了iommu,则调用DMA-IOMMU接口释放内存。
  4. 如果使用dma_map_ops定义的alloc回调函数分配内存,则使用dma_map_ops定义的free回调函数释放内存。

dma_free_coherent

3.1.dma_direct_free

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

dma_direct_free

3.2.iommu_dma_free

下面重点分析DMA-IOMMU接口释放内存的流程。主要的工作如下:

  1. 调用iommu_unmap_fast解除IOVA和PHY地址的映射。底层调用smmu驱动arm_smmu_unmap_pages解除映射。
  2. 如果iommu_domain的类型不是IOMMU_DOMAIN_DMA_FQ,则需要主动刷新IOTLB。底层调用smmu驱动arm_smmu_iotlb_sync刷新IOTLB。如果iommu_domain的类型是IOMMU_DOMAIN_DMA_FQ,内核有fq_domain队列异步刷新,释放内存的时候不需要主动刷新。
  3. 释放IOVA。通过iommu_domain中的iova_domain释放。
  4. 释放物理内存。
  5. 如果是从DMA_ZONE中分配的内存,则走DMA_ZONE的释放流程。
  6. 如果物理内存不连续,即虚拟内存位于vmalloc区域,则调用vunmap解除物理内存和虚拟内存之间的映射。最后调用__free_page释放物理内存。
  7. 如果物理内存连续,则走CMA的内存释放流程。

iommu_dma_free

3.2.1.arm_smmu_unmap_pages

arm_smmu_unmap_pages函数最终调用SMMU实现的io_pgtable_ops完成地址映射,即arm_lpae_unmap_pages函数。最终通过调用__arm_lpae_unmap函数递归unmap内存。主要的流程如下:

  1. 如果unmap的内存大小和当前Block descriptor或者Page descriptor页表描述符映射的内存大小一样,则调用__arm_lpae_clear_pte清除页表,即将页表项清零。同时将IOVA合并并且记录到iommu_iotlb_gather中,便于后面根据IOVA刷TLB。这里面还有一个处理非叶子页表的逻辑,图中没有画出。如果当前页表项不是叶子页表,则需要清理其下一级页表,然后释放页表内存。
  2. 如果不满足1且当前页表项是叶子页表,说明当前页表项是Block descriptor,unmap的内存长度小于Block descriptor映射的长度,需要将Block descriptor映射的地址切分,去掉unmap部分的长度,剩余的重新建立页表映射。
  3. 获取下一级页表页表虚拟地址,然后递归调用__arm_lpae_unmap函数unmap内存。

arm_smmu_unmap_pages

3.2.2.arm_smmu_iotlb_sync

unmap内存以后,IOTLB缓存的这部分页表失效,需要主动invalidate IOTLB,避免访问到已经释放的内存,以及影响数据安全。SMMUv3驱动提供了arm_smmu_iotlb_sync函数用于invalidate IOTLB,只invalidate叶子页表的IOTLB,工作流程如下:

  1. 构造invalidate IOTLB的命令。如下代码所示。如果是第一阶段地址转换,支持ARM_SMMU_FEAT_E2H特性,则opcodeCMDQ_OP_TLBI_EL2_VA,否则opcodeCMDQ_OP_TLBI_NH_VA,invalidate IOTLB命令需要提供leaf、asid、iova三个参数,leaf表示是否只invalidate叶子页表项TLB缓存,asid表示进程id,用于区分不同的iova地址空间,iova表示io虚拟地址。如果是第二阶段地址转换,opcodeCMDQ_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;}......
}
  1. 初始化临时命令队列。
  2. 如果SMMU支持ARM_SMMU_FEAT_RANGE_INV特性,则将IOVA按照num * 2^scale * pgsize的长度切分成数个inv_rangenum最大为31,否则IOVA按照page size切分,每个inv_range创建一个命令(命令使用arm_smmu_cmdq_ent描述),然后添加到临时命令队列(使用arm_smmu_cmdq_batch描述)里面。IOVA保存在iommu_iotlb_gather数据结构中。
  3. 提交临时命令队列,最终临时命令队列中的命令会写入到SMMU的命令队列中,启动SMMU执行命令,最后等待命令执行完毕。
  4. 如果使能了ATS功能,还需要invalidate PCIe设备的ATC。

arm_smmu_iotlb_sync

参考资料

  1. linux6.12 source code.
  2. -Arm ® System Memory Management Unit Architecture Specification version 3.
http://www.dtcms.com/a/349250.html

相关文章:

  • 胸部X光片数据集:健康及肺炎2类,14k+图像
  • 【网络运维】Shell脚本编程:函数
  • 大件垃圾识别精准度↑90%!陌讯多尺度融合模型在智慧环卫的落地实践
  • 鸿蒙ArkTS 基础篇-03-对象
  • 【黑色星期五输出当年有几个】2022-10-23
  • 单词搜索+回溯法
  • Windows客户端部署和管理
  • Week 13: 深度学习补遗:RNN的训练
  • 青少年软件编程(python五级)等级考试试卷-客观题(2023年12月)
  • 2024年09月 Python(一级)真题解析#中国电子学会#全国青少年软件编程等级考试
  • 使用 LangGraph + Zep 打造一款有记忆的心理健康关怀机器人
  • 【LLIE专题】一种用于低光图像增强的空间自适应光照引导 Transformer(SAIGFormer)框架
  • 超级助理:百度智能云发布的AI助理应用
  • JUC之并发容器
  • 2025最新酷狗kgm格式转mp3,kgma格式转mp3,kgg格式转mp3
  • 《程序员修炼之道》第五六章读书笔记
  • 【云馨AI-大模型】AI热潮持续升温:2025年8月第三周全球动态
  • 复杂场景横幅识别准确率↑91%!陌讯多模态融合算法在智慧园区的实战解析
  • 删掉一个元素以后全为1的最长子数组-滑动窗口
  • 【Luogu】P4317 花神的数论题 (数位DP)
  • 深度学习周报(8.18~8.24)
  • ASCII码值,可打印的字符有
  • 文档目录索引
  • 详解无监督学习的核心原理
  • 基于实例教学的软件工程专业教学系统
  • Livedata:感知生命周期变化,如何更新数据?避免内存泄漏,黏性事件
  • TCP--执行Linux命令(虚拟xshell)
  • 苍穹外卖项目实战(日记十)-记录实战教程及问题的解决方法-(day3-2)新增菜品功能完整版
  • 不再让Windows更新!Edge游戏助手卸载及关闭自动更新
  • Leetcode 3661. Maximum Walls Destroyed by Robots