时间:2024-05-26 04:20:52
首先请安装社区iommu
1: arm smmu原理1.1: smmu基础知识如上图所示,smmu的功能和mmu类似,转换CPU的页表,将进程的虚拟地址转换成如下格式: CPU可以识别的物理地址。同样,smmu的作用就是将一个设备的DMA请求的地址翻译成设备实际可以使用的物理地址,但是如果smmu被绕过,设备可以直接使用物理地址来进行DMA,也可以运行
1.2: smmu数据结构。 SMMU 中用于DMA 地址转换的所有重要数据结构都位于内存中。 smmu 寄存器在内存中存储这些表的基地址。第一个是流。
接下来,我们将重点关注这个STE 的结构以及它在内存中的组织方式。
通过smmu,一个smmu可以服务很多设备。因此,为了让smmu能够单独管理每个设备,smmu给每个设备一个ste条目。设备如何找到此ste 条目?对于smmu,您为管理的每个设备提供唯一的设备ID。该设备ID 也称为流ID。显然,如果设备数量比较少,一维smmu ste表就足够了。一个数组就足够了。如下图所示:
注意,这里ste使用的线性表实际上并不是由设备数量决定的,而是写入到配置的smmu的ID0寄存器中。华为鲲鹏的smmu基本不使用这种结构。
为了使smmu 对大量设备更加鲁棒,可以采用两层ste 表结构,如下所示。
这里的结构其实和mmu页表非常相似。在arm smmu v3中,第一级目录desc的目录结构大小为8(STRTAB_S)。
介绍完用smmu管理设备的ste表的两种结构之后,我们就来看看这个ste表的具体结构以及它的奥秘。
如上图所示,整个smmu ste条目用红色勾勒出来,说明这个ste条目同时管理stage1和stage2数据结构;config管理与stage 2相关的配置项,这个不需要理解或记住。如果您不确定,请查看smmuv3 手册。其中VMID指的是虚拟机ID。这里我们将重点关注S1ContextPtr 和S2TTB。
首先,我们来谈谈S1ContextPtr。
这个S1ContextPtr指向上下文描述符目录结构。此图仅显示一个以帮助理解。在我们手中,如果不涉及虚拟机,无论是CPU地址转换还是smmu地址转换,都是来自va-pa/iova。 - 爸,我们称之为第一阶段。这意味着不涉及虚拟性,它只是一个翻译阶段。
读完这里重要的CD表,你不问为什么我们在smmu中使用CD表吗?原因是一个smmu可以管理大量的设备,所以我们需要为每个设备创建一个数据结构。用表来区分,每个设备都有一个状态表。那么,如果每个设备上运行多个任务,并且这些任务同时使用不同的页表,那么如何管理它们呢?对吧?因此,smmu使用CD表来管理各个页表。
我们来看看cd表的查找规则。
我们先来说另一个重要的概念:SubstreamID(pasid)。这称为substreamid 或pasid。这也是一个非常简单的概念。因为我们有一张表,所以我们需要一个ID 来帮助我们找到它。于是这个ID就出来了。可以看到这里原理是一样的,只不过可以用table来获取id!
smmu 中的CD 表也可以是线性的或两级的。这是在smmu 寄存器中设置的。这是由smmu 驱动程序读取的,并根据相应的位进行分级。这和ste表的原理是一样的。
介绍完两个基本且重要的数据结构后,smmu 如果支持虚拟化的话,可以同时转换stage1 和stage2,如下图所示。
在虚拟机guest 上启用smmu 时,必须同时在stage1 和stage2 上启用smmu。当然,你也可以绕过smmu。
1、3: smmu地址转换流程如上图所示,可以清晰的概括出一个外设需要smmu地址转换的基本流程,当一个外设需要DMA物理地址时,该外设必须开始请求地址转换,这个此时,外设向smmu提供了三个重要的信息:streamid:帮助smmu找到管理外设的ste条目,subsreamid:当找到ste条目时,帮助smmu找到对应的cd表,通过这两个ID,smmu找到对应的iopge表,然后找到页表并用外设iova最后发送的信息填充它并Combine开始地址转换。
smmu 还有一个tlb 缓存。 smmu首先根据当前cd表中存储的asid检查tlb缓存中是否有该页表对应的缓存。其实这里的原理和mmu搜索是一样的。关于页表有太多需要解释的地方,但它们很简单。
上图中的地址转换还包括阶段2,这里不讨论。 smmu加虚拟化的过程比较复杂。当我有机会时我会解释这一点。
2 smmu 驱动和iommu 框架2.1: 初始化smmu v3 驱动在简单介绍了上面两个重要的表以及smmu 内部的基本搜索过程之后,我们现在来解释一下smmu 驱动如何在Linux 内核中执行初始化过程,让我们看看如何完成它。这次分析我们先来看看smmu中重要的队列。
内核中smmuv3的代码路径:drivers/iommu/arm-smmu-v3.c:
上面是smmu驱动初始化过程的前半部分,但是可以看到内核中的每个smmu都管理着一个叫做struct arm_smmu_device的结构体,初始化过程就填充了这个结构体。看上图,我们可以看到,首先我们从slab/slab中分配对象空间,更重要的是,分配函数。
arm_smmu_device_dt_probe 和arm_smmu_device_a
接下来,调用函数platform_get_resource从dts或apci表中读取smmu寄存器的基地址。这个非常重要。所有后续初始化都围绕配置进行。
继续阅读以查看其余内容。首先你可以很容易地看到你需要读取smmu中的一些中断号。 smmu 硬件有一个用于软件消息的队列缓冲区。 smmu 硬件使用smmu 驱动程序的中断。从队列缓冲区获取消息。首先:
smmu中的第一个eventq中断队列称为事件队列。该队列用于挂在smmu 上的平台设备。当平台设备使用smmu将IOVA转换为DMA时,smmu发生时会出现异常。消息将被填写。事件队列中上报事件q中断,smmu驱动收到中断后开始执行中断处理程序,从事件队列中读取异常消息,并显示异常。
另一个priq 用于中断期间的pri 队列。该队列专门用于挂在smmu 上的PCIe 类型设备。具体过程其实和事件队列是一样的。我这里就不解释了。
最后一个错误是gerror 中断。如果运行smmu 时发生不可恢复的严重错误,则smmu 会向smmu 驱动程序报告gerror 中断。严重错误本身由中断直接处理,因此不需要队列。
三个中断初始化完成后(这里不再赘述具体的中断初始化映射过程,改天单独写一章中断),smmu驱动就会分配smmu管理结构体和寄存器,映射完成后smmu中断初始化完成后,smmu驱动程序开始读取之前写入smmu寄存器的各种配置,并将配置位读入struct arm_smm_device数据结构中。功能arm_smmu_device_hw_probe 函数负责读取smmu 硬件寄存器。
一旦我们读完寄存器配置,我们现在知道什么信息?这个smmu 支持level 2 ste 还是level 1 ste、level 2 CD 和level 1 CD?Arm_smmu_device 功能嵌入在这些标头中,例如物理尺寸使用,iova 和pa 等的地址数字。场内。
看完了基本信息,我们是不是可以开始初始化数据结构了呢?答案是可以的。看一下函数arm_smmu_init_structs。
从上面的数据结构初始化函数可以看出,smmu驱动主要负责初始化两个数据结构。一是strtab(流表的缩写),二是内存分配和队列初始化。首先我们看一下队列:
从上面可以看出,smmu驱动主要初始化了三个队列:cmdq、evtq和priq,但为了避免进行详细的功能分析,我们在这里不再进一步讨论它们。
最后我们看一下初始化smmu的strtab。
从上图可以看出,首先决定是需要初始化一级流表还是二级流表。这里的基础是从上面的硬件寄存器中读取的内容。
首先,我们看一下arm_smmu_init_strtab_linear函数。
对于线性流表,smmu 驱动程序调用dma alloc 接口为流表分配所有所需的空间,并将所有ste 条目预初始化为旁路模式。只需设置位即可。
接下来我们看一下功能。
arm_smmu_init_strtab_2lvl;
考虑这个问题:“我真的需要创建每个ste 条目吗?”显然不是,smmu驱动初始化就是基于这个原理的。仅初始化第一级ste 目录条目。其实这和页表初始化类似,只是先初始化目录项。连贯函数dma alloc 负责分配第一级目录项。分配的大小是多少呢?我们看一下关键的宏STRTAB_SPLIT。该宏当前在smmu 驱动程序中为8 位。这意味着预先分配了2^8 个目录项,并且每个目录项都有固定的大小。
还有一个函数叫arm_smmu_init_l1_strtab函数。空间分配现已完成。这些目录项必须被初始化。我这里就不详细说了。
现在我们已经简要介绍了基本的数据结构初始化,让我们看一下smmu 驱动程序初始化的其余部分,如下图所示。
上图显示了smmu 驱动程序初始化的其余部分。可以看到它的第一个函数是arm_smmu_device_reset。这个函数的作用是什么?我们是否在内存中为这个smmu 分配了一些队列和流?表目录项?所以smmu 必须始终知道这些数据结构的基地址对吗?这个函数的作用是将这些基地址置于smmu 的控制之下登记。当前需求初始化后,smmu 驱动程序将基本smmu 数据结构注册到更高级别的IOMMU 抽象框架。这样就可以调用iommu结构体了。 smmu稍后会解释。
2.2 smmu 和iommu 的关系2.2.1 它们之间的结构关系smmu 和iommu 是什么关系?我们的硬件系统包括intel iommu、amd iommu、arm smmu 等设备,可以转换成的设备有很多,所以我就不一一列举了。所有这些不同的硬件架构都被视为独立的子系统。因此,iommu层在Linux内核中被抽象出来。 iommu层为每个外部设备驱动提供了结构,并隐藏了各种底层架构,如图所示。
从上图可以清楚的看到各个架构的smmu驱动是如何连接到iommu框架的。 iommu框架通过各种架构的ops调用实际的底层驱动接口。
您可以问自己这个问题:“底层驱动程序如何连接到上层?”
接下来我们看一下内核代码输入来解答大家的疑问。
上图显示了smmu 驱动程序初始化的最后部分。每个底层smmu 结构都有一个特定于iommu 框架层的结构表示:struct iommu_device。上图中函数iommu_device_register完成的任务就是初始化我们所做的事情。合适的iommu结构被注册到iommu层的链表中并进行统一管理。最后,根据smmu 是作为PCie 外设还是平台外设实现,将每个smmu 绑定到不同的总线类型。
2.2.2 iommu和opsiommu层中的一个关键结构通过ops调用底层硬件驱动。我们来看看smmu v3 硬件驱动提供了哪些ops 调用。
上图显示了smmu v3 硬件驱动程序提供的所有调用函数。
现在我们已经到达了iommu层,两个管理概念也相关了。一是如何管理设备,二是如何管理smmu提供的io页表。
为了分别管理这两个概念,iommu 框架提供了两种结构。一种是struct iommu_domain。该结构体抽象了domain结构,用于表示底层的arm_smmu_domain。其实核心就是管理这个域所拥有的域。io页表。另一个是sstruct iommu_group。该结构用于设备管理。多个设备可以包含在一个iommu 组中以共享iopage 表。看看网络上的图片就可以清楚地看出这种关系。
这张图清楚地展示了smmu domian和iommu域之间的关系以及iommu组的作用,但是并没有解释太多。
2.3 dma iova与iommudma和iommu密切相关。事实上,创建IOMMU的一个很大原因就是为了避免在使用DMA时直接使用物理地址带来的安全问题,这也是IOVA产生的原因。 io在调用dma alloc时,首先在地址空间中分配iova,然后将iova和dma分配时生成的物理地址映射到iommu管理的页表中。当外设进行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-trees 来管理每个段的iova 间距。这其实和虚拟内存分配类似,管理VMA也是一样的。
我们先来看看iova的发布流程。这个发布过程突出了严格模式和非严格模式之间的核心区别。
旧规则只是开始代码。可以看到DMA释放过程也非常简单。首先对iova 和pa 进行解映射,然后释放iova 结构。
查看该图的解映射部分。 iommu_unmap_fast进程所做的就是调用iommu的unmap,通过ops调用arm smmu v3驱动的unmap函数。
我们进入函数arm_lpae_unmap看看它是如何工作的,如下图所示。
该函数使用递归方法查找io页表中的最后一项。找到后,您可以重点关注第613-622 行代码。第613-620行是iommu采用默认的非严格模式时。虽然不需要立即禁用TLB,但即使使用严格模式,也要更新TLB并调用函数io_pgtable_tlb_add_flush向smmu写入TLB禁用指令。
那么使用非严格模式时我们如何更新tlb呢?秘密就在于iommu_dma_free_iova函数,如下所示。
当使用非严格模式时,它会被排队,当队列满时,调用iovad-flush_cb 函数。
该函数指针最终调用函数iommu_dma_flush_iotlb_all来更新全局tlb。 SMMU不需要执行太多指令。
2.4 绕过smmu和iommu 方法一:完全绕过iommu。 Linux 提供了iommu.passthrough 命令行选项。设置此选项后,DMA 默认情况下不使用iommu,而是使用DMA 的旧版swiotlb 方法。
方法二:smmu v3驱动默认支持驱动参数配置。 disable_bypass,系统默认关闭bypass。您可以使用它来绕过某些smmu。
方法三:acpi或dts中不要配置对应的smmu节点。这是一个粗略的方法。
3. smmu 中的PMCG ARM SMMU 提供与性能相关的统计寄存器(Performance Monitor Counter Group - PMCG)。首先,确保模块arm_smmuv3_pmu 包含在您的系统中或编译到您的内核中。
该模块的代码位于内核目录kernel/drivers/perf/arm_smmuv3_pmu.c中,内核配置为: CONFIG_ARM_SMMU_V3_PMU。
smmu pmcg 社区补丁链接:
https://lwn.net/article/784040/
详细使用说明请参考社区pmcg 补丁文档。这很简单。