首先放一个社区 iommu patch 的网址:https://lore.kernel.org/linux-iommu/
arm smmu 的原理
smmu 基本知识
如上图所示,smmu 的作用和 mmu 类似,mmu 作用是替 cpu 翻译页表将进程的虚拟地址转换成 cpu 可以识别的物理地址。同理,smmu 的作用就是替设备将 dma 请求的地址,翻译成设备真正能用的物理地址,但是当 smmu bypass 的时候,设备也可以直接使用物理地址来进行 dma;
smmu 的数据结构
smmu 的重要的用来 dma 地址翻译的数据结构都是放在内存中的,由 smmu 的寄存器保存着这些表在内存中的基地址,首先就是 Stream Table(STE),这 ste 表既包含 stage1 的翻译表结构也包含 stage2 的翻译结构,所谓 stage1 负责 VA 到 PA 的转换,stage2 负责 IPA 到 PA 的转换。接下来我们重点看一下这个 STE 的结构,到底在内存中是如何组织的; 对 smmu 来说,一个 smmu 可以给很多个设备服务,所以,在 smmu 里面为了区分的对每个设备进行管理,smmu 给每一个设备一个 ste entry,那设备如何定位这个 ste entry 呢?对于一个 smmu 来说,我们给他所管理的每个设备一个唯一的 device id,这个 device id 又叫 stream id; 对于设备比较少的情况下,我们的 smmu 的 ste 表,很明显只需要是 1 维数组就可以了,如下图:
注意,这里 ste 采用线性表并不是真是由设备的数量来决定的,而是写在 smmu 的 ID0 寄存器中的,也就是配置好了的,对于华为鲲鹏上的 smmu 基本不采用这种结构; 对于设备数量较多的情况下,我们为了 smmu 更加的皮实点,可以采用两层 ste 表的结构,如下图:
这里的结构其实很类似我们的 mmu 的页表了,在 arm smmu v3 我们第一层的目录 desc 的目录结够,大小采用 8(STRTAB_SPLIT)位,也就是 stream id 的高 8 位,stream id 剩下的低位全部用来寻址第二层真正的 ste entry; 介绍完了 smmu 中管理设备的 ste 的表的两种结构后,我们来看看这个 ste 表的具体结构是啥,里面有啥奥秘呢:
如上如所示,红框中就是 smmu 中一个 ste entry 的全貌了,从红框中能看出来,这个 ste entry 同时管理了 stage1 和 stage2 的数据结构其中 config 是表示 ste 有关的配置项,这个不需要理解也不需要记忆,不知道的查一下 smmuv3 的手册即可,里面的 VMID 是指虚拟机 ID, 这里我们重点关注一下 S1ContextPtr 和 S2TTB,首先我们来说 S1ContextPtr: 这个 S1ContextPtr 指向的一个 Context Descriptor 的目录结构,这张图为了好理解只画了一个,在我们 arm 中,如果没有虚拟机参与的话,无论是 cpu 还是 smmu 地址翻译都是从 va->pa/iova->pa,我们称之为 stage1,也就是不涉及虚拟,只是一阶段翻译而已。 重要的 CD 表,读到这里,你是不是会问一个问题,在 smmu 中我们为何要使用 CD 表呢?原因是这样的,一个 smmu 可以管理很多设备,所以用 ste 表来区分每个设备的数据结构,每个设备一个 ste 表。那如果每个设备上跑了多个任务,这些任务又同时使用了不同的 page table 的话,那咋管理呢?对不对?所以 smmu 采用了 CD 表来管理每个 page table; 看一看 cd 表的查找规则:
先说另外一个重要的概念:SubstreamID(pasid),这个叫 substreamid 又称之为 pasid,也是非常简单的概念,既然有表了,那也得有 id 来协助查找啊,所以就出来了这个 id,从这里也可以看出来,道理都一样,用了表了就有 id 啊!
CD 表,在 smmu 中也是可以是线性的或者两级的,这个都是在 smmu 寄存器中配置好了的,由 smmu 驱动来读去,进行按对应的位进行分级,和 ste 表一样的原理;介绍了两个基本的也重要的数据结构后我,smmu 是在支持虚拟化的时候,可以同时进行 stage1 和 stage2 的翻译的,如下图所示:
当我们在虚拟机的 guest 中启用 smmu 的时候,smmu 是需要同时开启 stage1 和 stage2 的,当然了,smmu 也是可以进行 bypass 的;
smmu 的地址翻译流程
如上图,基本可以很明显的概括出了一个外设请求 smmu 的地址翻译的基本流程,当一个外设需要 dma 的物理地址的时候,开始请求 smmu 的地址翻译,这时候外设给 smmu 3 个比较重要的信息,分别是:streamid:协助 smmu 找到管理外设的 ste entry,subsreamid:当找到 ste entry 后,协助 smmu 找到对应的 cd 表,通过这两个 id smmu 就可以找到对应的 iopge table 了,smmu 找到 page table 后结合外设提交过来的最后一个信息 iova,即可开始进行地址翻译; smmu 也有 tlb 的缓存,smmu 首先会根据当前 cd 表中存放的 asid 来查查 tlb 缓存中有没有对应 page table 的缓存,这里其实和 mmu 找页表的原理是一样的,不过多解释了,很简单; 上图中的地址翻译还涉及到了 stage2,这里不解释了,smmu 涉及到虚拟化的过程比较复杂,这个有机会再解释;
smmu 驱动与 iommu 框架
smmu v3 驱动初始化
简单的介绍了上面的两个重要表以及 smmu 内部的基本的查找流程后,我们现在来看看在 linux 内核中,smmu 驱动是如何完成初始化的过程,借着这个分析,我们看看 smmu 里的重要的几种队列: smmuv3 的在内核中的代码路劲:drivers/iommu/arm-smmu-v3.c:
上面是 smmu 驱动中初始化流程的前半部分,从中可以很容易看出来,内核中每个 smmu 都有一个结构体 struct arm_smmu_device 来管理,实际上初始化的流程就是在填充着个结构。看上图,首先就是从 slub/slab 中分配一个对象空间,随后一个比较重要的是函数 arm_smmu_device_dt_probe 和 arm_smmu_device_acpi_probe,这俩函数会从 dts 中的 smmu 节点和 acpi 的 smmu 配置表中读取一些 smmu 中断等等属性; 随后调用函数 platform_get_resource 来从 dts 或者 apci 表中读取 smmu 的寄存器的基地址,这个很重要,后续所有的初始化都是围绕着个配置来的;
继续看剩下的部分,开头很容易看出来,要读取 smmu 的几个中断号,smmu 硬件给软件消息有队列 buffer,smmu 硬件通过中断的方式让 smmu 驱动从队列 buffer 中取消息,我们一一介绍:第一个 eventq 中断,smmu 的一个队列叫 event 队列,这个队列是给挂在 smmu 上的 platform 设备用的,当 platform 设备使用 smmu 翻译 dma 的 iova 的时候,如果发生了一场 smmu 会首先将异常的消息填到 event 队列中,随后上报一个 eventq 的中断给 smmu 驱动,smmu 驱动接到这个中断后,开始执行中断处理程序,从 event 队列中将异常的消息读出来,显示异常;另外一个 priq 中断时给 pri 队列用的,这个队列是专门给挂在 smmu 上的 pcie 类型的设备用的,具体的流程其实是和 event 队列是一样的,这里不多解释了;最后一个是 gerror 中断,如果 smmu 在执行过程中,发生了不可恢复的严重错误,smmu 会报告一个 gerror 中断给 smmu 驱动,就不需要队列了,因为本身严重错误了,直接中断上来处理了; 完成了 3 个中断初始化后(具体的中断初始化映射流程,不在这里介绍,改天单独写个中断章节介绍),smmu 驱动此时已经完成了 smmu 管理结构的分配,以及 smmu 配置的读取,smmu 的寄存器的映射,以及 smmu 中断的初始化,这些都搞完后,smmu 驱动开始读取提前写死在 smmu 寄存器中的各种配置,将配置 bit 位读取出来放到 struct arm_smm_device 的数据结构中,函数 arm_smmu_device_hw_probe 函数就负责读 smmu 的硬件寄存器; 当我们寄存器配置读取完毕后,这时候我们知道了哪些信息呢?会有这个 smmu 支持二级 ste 还是一级的 ste,二级的 cd 还有 1 级的 cd,这个 smmu 支持的物理也大小,iova 和 pa 的地址位数等等;这些头填在 arm_smmu_device 的 features 的字段里面; 基本信息读出来后,我们是不是可开始初始化数据结构了?答案是肯定的啦,看看函数 arm_smmu_init_structures;
从上面的数据结构初始化的函数可以看出来,smmu 驱动主要负责初始化两种数据结构,一个 strtab(stream table 的简写),另外一个种是队列的内存分配和初始化;我们首先来看看队列的:
从上面可以看出来,smmu 驱动主要初始化 3 个队列:cmdq,evtq,priq;这里不再进一步解释了,避免陷入函数细节分析; 最后我们来看看 smmu 的 strtab 的初始化:
从上图可以看出来,首先判断我们需要初始化一级的还是二级的 stream table,这里依据就是上面的硬件寄存器中读取出来的; 我们首先看看函数 arm_smmu_init_strtab_linear 函数:
对于线性的 stream table 表来说 smmu 驱动会将调用 dma alloc 接口将 stream table 需要的所有空间都一把分配完毕了,并且将所有的 ste entry 项都给预先的初始化成 bypass 的模式,具体的就不深入看了,比较简单,设置 bit; 随后我们来看看函数 arm_smmu_init_strtab_2lvl:
我们可以思考一个问题:我们真的需要将所有的 ste entry 都个创造出来吗?很显然,不是的,smmu 驱动的初始化正是基于这种原理,仅仅只会初始化第一级的 ste 目录项,其实这里就是类似页表的初始化了也只是先初始化了目录项;函数中 dma alloc coherent 就是负责分配第一级的目录项的,分配的大小是多大呢?我们可以看一下有一个关键的宏 STRTAB_SPLIT,这个宏目前在 smmu 驱动中是 8 位,也就是预先会分配 2^8 个目录项,每个目录项的大小是固定的; 我们可以看到里面还调用了一个函数 arm_smmu_init_l1_strtab 函数,这里就是我们空间分配完了,总该给这些目录项给初始化一下吧,这里就不深入进去看了; 到此为止,我们已经将基本的数据结构初始化给简要的讲完了;我们接着看 smmu 驱动初始化的剩下的,见下图:
上图是 smmu 驱动初始化的剩下的部分,我们可以看出来里面第一个函数是 arm_smmu_device_reset,这个函数是干嘛的呢,我们前面是不是已经给这个 smmu 在内存中分配了几个队列和 stream table 的目录项?那这些数据结构的基地址总该让 smmu 知道吧?这个函数就是将这些基地址给放到 smmu 的控制寄存器中的;当前我们需要的东西给初始化完后,smmu 驱动接下来就是将 smmu 的基本数据结构注册到上层的 iommu 抽象框架里,让 iommu 结构能够调用到 smmu,这个在后面在说;
smmu 与 iommu 关系
两者的结构关系
smmu 和 iommu 是何种关系呢?在我们的硬件体系中,能够有能力完成设备 iova 到 pa 转换的有很多,例如有 intel iommu, amd 的 iommu ,arm 的 smmu 等等,不一一枚举了;那这些不同的硬件架构不会都作为一个独立的子系统,所以,在 linux 内核中 抽象了一层 iommu 层,由 iommu 层给各个外部设备驱动提供结构,隐藏底层的不同的架构;如图所示:
由上图可以很明显的看出来,各个架构的 smmu 驱动是如何使如何和 iommu 框架对接的,iommu 框架通过不同架构的 ops 来调用到底层 真正的驱动接口; 我们可以问自己一个问题:底层的驱动是如何对接到上层的? 接下来我们来看看进入内核代码来帮我们解开疑惑;
如上图是 smmu 驱动初始化的最后一部分,对于底层的每一个 smmu 结构在 iommu 框架层中都一有一个唯一的一个结构体表示:struct iommu_device,上图中函数 iommu_device_register 所完成的任务就是将我们所初始化好的 iommu 结构体给注册到 iommu 层的链表中,统一管理起来;最后我们根据 smmu 所挂载的是 pcie 外设,还是 platform 外设,将和个 smmu 绑定到不同的总线类型上;
iommu 的重要结构与 ops
iommu 层通过 ops 来调用底层硬件驱动,我们来看看 smmu v3 硬件驱动提供了哪些 ops call:
上图就是 smmu v3 硬件驱动提供的所有的调用函数;
既然到了 iommu 层,那我们也会涉及到两种概念的管理,一种是设备如何管理,另外一种是 smmu 提供的 io page table 如何管理;为了分别管理,这两种概念,iommu 框架提供了两种结构体,一个是 struct iommu_domain 这个结构抽象出了一个 domain 的结构,用来代表底层的 arm_smmu_domain,其实最核心的是管理这个 domian 所拥有的 io page table。另外一个是 sruct iommu_group 这个结构是用来管理设备的,多个设备可以在一个 iommu group 中,以此来共享一个 iopage table; 我们看一个网络上的图即可很明白的表明其中的关系:
这张图来自网络的图中很明显的写出来 smmu domian 和 iommu 的 domain 的关系,以及 iommu group 的作用;不在过多解释;
dma iova 与 iommu
dma 和 iommu 息息相关,iommu 的产生其实很大的原因就是避免 dma 的时候直接使用物理地址而导致的不安全性,所以就产生了 iova, 我们在调用 dma alloc 的时候,首先在 io 的地址空间中分配你一个 iova, 然后在 iommu 所管理的页表中做好 iova 和 dma alloc 时候产生的物理地址进行映射;外设在进行 dma 的时候,只需要使用 iova 即可完成 dma 动作; 那我们如何完成 dma alloc 的时候 iova 到 pa 的映射的呢 dma_alloc -> __iommu_alloc_attrs
在__iommu_alloc_attrs 函数中调用 iommu_dma_alloc 函数来完成 iova 和 pa 的分配与映射; iommu_dma_alloc->__iommu_dma_alloc_pages, 首先会调用者个函数来完成物理页面的分配
函数__iommu_dma_alloc_pages 中完成的任务是页面分配,iommu_dma_alloc_iova 完成的就是 iova 的分配,最后 iommu_map_sg 即可完成 iova 到 pa 的映射;linux 采用 rb tree 来管理每一段的 iova 区间,这其实和我们的虚拟内存的分配是类似的,我们的 vma 的管理也是这样的; 我们接下来在来看看 iova 的释放过程,这个释放的过程,我们是可以看到看到 strict 个 non-strict 模式的最核心的区别的:
老规矩,直接撸代码,我么看到 dma 的释放流程也是很简单的,首先将 iova 和 pa 进行解映射处理,然后将 iova 结构给释放掉;看图中解映射的部分就是在 iommu_unmap_fast 流程中处理的就是调用 iommu 的 unmap 然后通过 ops 调用到 arm smmu v3 驱动的 unmap 函数: __iommu_dma_unmap->iommu_unmap_fast->(ops->unmap:arm_smmu_unmap)->arm_lpae_unmap; 我们进入函数 arm_lpae_unmap 中看看是干啥的,见下图:
这个函数采用递归的方式来查找 io page table 的最后一项,当找到的时候,我们可注意看代码行 613~622 行,其中 613~620 行 是当我们的 iommu 采用默认的 non strict 模式的时候,我们是不用立马对 tlb 进行无效化的;但是当我们采用 strict 模式的时候,我们还是会将 tlb 给刷新一下,调用函数 io_pgtable_tlb_add_flush 给 smmu 写入一个 tlb 无效化的指令; 那我们采用 non-strict 模式的时候是如何刷新 tlb 的呢?秘密就在函数 iommu_dma_free_iova 函数中见下图:
我们可以看到,如果采用 non-strict 的模式的时候,我们是放到一个队列中的,当我们的队列满的时候,会调用函数 iovad->flush_cb,这个函数指针,最终会调用到函数:iommu_dma_flush_iotlb_all,来进行全局的 tlb 的刷新,smmu 无需执行太多的指令了;
smmu 和 iommu 的 bypass
方式一:将 iommu 给彻底给 bypass 掉,linux 提供了 iommu.passthrough command line 的选项,这个选项配置上后,dma 默认不会 走 iommu,而是走传统的 swiotlb 方式的 dma; 方式二:smmu v3 的驱动默认支持驱动参数配置,disable_bypass,在系统中是默认关闭 bypass 的,我们可以通过这个来将某个 smmu 给 bypass 掉 方式三:acpi 或者 dts 中不配置相应的 smmu 节点,比较粗暴的办法
smmu 的 PMCG
ARM 的 SMMU 提供了性能相关的统计寄存器(Performance Monitor Counter Groups - PMCG),首先要确定使用的系统里有 arm_smmuv3_pmu 这个模块,或者它已经被编译进内核。 这个模块的代码在内核目录 kernel/drivers/perf/arm_smmuv3_pmu.c, 内核配置是: CONFIG_ARM_SMMU_V3_PMU;
smmu pmcg 社区的 patch 连接:https://lwn.net/Articles/784040/ 详细用法可以参见 社区 pmcg 的补丁文档,里面内容很简单。
本文转载自阿里云智能基础软件部技术博客。
原文链接:
评论