抖音技术能力大揭密!钜惠大礼、深度体验,尽在火山引擎增长沙龙,就等你来! 立即报名>> 了解详情
写点什么

ARM SMMU 的原理与 IOMMU

2020 年 9 月 04 日

ARM SMMU的原理与IOMMU

首先放一个社区 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 的补丁文档,里面内容很简单。


本文转载自阿里云智能基础软件部技术博客。


原文链接


ARM SMMU的原理与IOMMU


2020 年 9 月 04 日 14:031662

评论

发布
暂无评论
发现更多内容

【写作群星榜】6.27~7.10 写作平台优秀作者 & 文章排名

InfoQ写作平台官方

写作平台 排行榜

亚马逊:让创新科技成为重启世界的新动能

爱极客侠

啃碎并发(八):深入分析wait&notify原理 猿码架构

猿灯塔

区块链+高考,让世界再无冒名顶替

CECBC区块链专委会

SpringBoot入门:01 - 配置数据源

封不羁

Java spring springboot

编程能力 —— 异步编程

wendraw

Java 前端进阶训练营 编程能力

领域驱动设计(DDD)实践之路(一)

vivo互联网技术

架构 领域驱动设计 DDD

编程能力 —— 解析表达式

wendraw

Java 前端进阶训练营 编程能力

编程能力 —— 寻路问题

wendraw

Java 前端进阶训练营 编程能力

积极支持EdgeX发展,英特尔为2020 EdgeX中国挑战赛获奖队伍创造广阔合作空间

最新动态

利用 Python 爬取了 13966 条运维招聘信息,我得出了哪些结论?

JackTian

Python Linux 运维 数据分析 招聘

最大的 String 字符长度是多少?

武培轩

Java 源码 后端 JVM

Java 后端博客系统文章系统——No2

猿灯塔

521我发誓读完本文,再也不会担心Spring配置类问题了

YourBatman

spring springboot @Configuration Spring配置类

Docker基础修炼3--Docker容器及常用命令

黑马腾云

Docker Linux 命令 容器技术

【Java虚拟机】垃圾收集器与内存分配

烫烫烫个喵啊

Java Java虚拟机

30 张图带你分分钟看懂进程和线程基础知识全家桶

爱嘤嘤嘤斯坦

Java 线程 进程 进程线程区别

Java集合总结,从源码到并发一路狂飙

给你买橘子

Java 编程 算法 集合

漫画通信:一图看懂通信发展史

阿里云Edge Plus

微服务架构下分布式事务解决方案

Arthur

DDD实施过程中的点滴思考

Winfield

领域驱动设计 DDD

5分钟上手部署!!!

清风

Java Spring Boot

实验室里的AI激情:腾讯优图的升级修炼之路

脑极体

创业使人成长系列 (2)- 散伙协议

石云升

创业 股权 合伙人 散伙协议

数据结构与算法知识点总结

围绕工作的务实学习

流水账

zack

肖风:数据要素市场与分布式AI平台

CECBC区块链专委会

终于有人把Elasticsearch架构原理讲明白了,感觉之前看的都是渣

爱嘤嘤嘤斯坦

Java elasticsearch 编程 架构

LR.Net平台研发轶事,每一个点都很难,但我们不将就

力软.net/java开发平台

C# .net 跨平台 框架开发

HTTP/2 总结

guoguo 👻

16种设计思想 - Design for failure

Man

Java 微服务 设计原则

Study Go: From Zero to Hero

Study Go: From Zero to Hero

ARM SMMU的原理与IOMMU-InfoQ