名词缩写

  1. ASID:Address Space ID   地址空间标识符
  2. CD:Context Descriptor;  上下文描述符;
  3. CTP:Context-table pointer   上下文表指针
  4. EPT:Extended Page Table 扩展页表
  5. GPA:Guest Phyical Address   客人的实际地址
  6. GVA:Guest Virtual Address   访客虚拟地址
  7. HPA:Host Phyical Address    主机物理地址
  8. IOVA:IO Virtual Address space   IO虚拟地址空间
  9. IPA:Intermediate Phyical Address    中间物理地址
  10. NPT:Nested Page Table   嵌套页表
  11. PCID:Process context identifier 进程上下文标识符
  12. PMCG:Performance Monitor Counter Groups 性能监控计数器组
  13. S2TTB:Stage 2 Translate Table Base  第二阶段翻译表库
  14. SMMU:System MMU 系统MMU
  15. VT-d:Virtualization Technology for Direct I/O 直接I/O虚拟化技术
     

关键要点

  1. MMU地址翻译是将进程的虚拟地址(HVA)翻译成物理地址(HPA);
  2. IOMMU地址翻译则是将虚拟机物理地址空间内的GPA翻译成HPA;
  3. IOMMU页表和MMU页表一样,都采用了多级页表的方式来进行翻译;
  4. 专门转换I/O地址的MMU在x86的阵营里就是IOMMU;
  5. Intel把IOMMU技术叫做VT-d(Virtualization Technology for Direct I/O);
  6. EPT/NPT MMU作为传统MMU的扩展,也是有TLB;
  7. ARM公司主要是依靠出售core的license来赚钱的;
  8. 能够有能力完成设备iova 到 pa转换的有很多,例如有intel iommu, amd的iommu ,arm的smmu等等;

目录

VT-d DMA Remapping

1 DMA Remapping 简介

2 DMA隔离和地址翻译

Related Posts

虚拟化技术 - I/O虚拟化

透传 - Device Passthrough

虚拟化技术 - 内存虚拟化

软件实现 - 影子页表

硬件辅助 - EPT/NPT

EPT/NPT MMU优化

SMMU和IOMMU技术

ARM的SMMU

ARM SMMU的原理与IOMMU

1 arm smmu的原理

1.1 smmu 基本知识

1.2 smmu 的数据结构

1.3 smmu的地址翻译流程

2 smmu驱动与iommu框架

2.1 smmu v3驱动初始化

2.2 smmu 与 iommu关系

2.3 dma iova 与iommu

2.4 smmu和iommu的bypass

3.smmu 的PMCG


VT-d DMA Remapping

https://kernelgo/dma-remapping.html


《搞懂Linux零拷贝,DMA》https://rtoax.blog.csdn/article/details/108825666

本文主要探讨一下VT-d DMA Remapping机制。在分析DMA Remapping之前回顾下什么是DMA,DMA是指在不经过CPU干预的情况下外设直接访问(Read/Write)主存(System Memroy)的能力。 DMA带来的最大好处是:CPU不再需要干预外设对内存的访问过程,而是可以去做其他的事情,这样就大大提高了CPU的利用率。

在设备直通(Device Passthough)的虚拟化场景下,直通设备在工作的时候同样要使用DMA技术来访问虚拟机的主存以提升IO性能。那么问题来了,直接分配给某个特定的虚拟机的,我们必须要保证直通设备DMA的安全性一个VM的直通设备不能通过DMA访问到其他VM的内存,同时也不能直接访问Host的内存,否则会造成极其严重的后果。因此,必须对直通设备进行“DMA隔离”和“DMA地址翻译”,隔离将直通设备的DMA访问限制在其所在VM的物理地址空间内保证不发生访问越界,地址翻译则保证了直通设备的DMA能够被正确重定向到虚拟机的物理地址空间内。

为什么直通设备会存在DMA访问的安全性问题呢?

原因也很简单:由于直通设备进行DMA操作的时候guest驱动直接使用gpa来访问内存的,这就导致如果不加以隔离和地址翻译必然会访问到其他VM的物理内存或者破坏Host内存,因此必须有一套机制能够将gpa转换为对应的hpa这样直通设备的DMA操作才能够顺利完成。

VT-d DMA Remapping的引入就是为了解决直通设备DMA隔离和DMA地址翻译的问题,下面我们将对其原理进行分析,主要参考资料是Intel VT-d SPEC Chapter 3。

1 DMA Remapping 简介


VT-d DMA Remapping的硬件能力主要是由IOMMU来提供,通过引入根Context Entry和IOMMU Domain Page Table等机制来实现直通设备隔离和DMA地址转换的目的。那么具体是怎样实现的呢?下面将对其进行介绍。

根据DMA Request是否包含地址空间标志(address-space-identifier)我们将DMA Request分为2类

  • Requests without address-space-identifier: 不含地址空间标志的DMA Request,这种一般是endpoint devices的普通请求,请求内容仅包含请求的类型(read/write/atomics),DMA请求的address/size以及请求设备的标志符等。
  • Requests with address-space-identifier: 包含地址空间描述标志的DMA Request,此类请求需要包含额外信息以提供目标进程的地址空间标志符(PASID),以及Execute-Requested (ER) flag和 Privileged-mode-Requested 等细节信息。

为了简单,通常称上面两类DMA请求简称为:Requests-without-PASID和Requests-with-PASID。本节我们只讨论Requests-without-PASID,后面我们会在讨论Shared Virtual Memory的文中单独讨论Requests-with-PASID。

首先要明确的是DMA Isolation是以Domain为单位进行隔离的,在虚拟化环境下可以认为每个VM的地址空间为一个Domain,直通给这个VM的设备只能访问这个VM的地址空间这就称之为“隔离”。根据软件的使用模型不同,直通设备的DMA Address Space可能是某个VM的Guest Physical Address Space或某个进程的虚拟地址空间(由分配给进程的PASID定义)或是由软件定义的一段抽象的IO Virtual Address space (IOVA),总之DMA Remapping就是要能够将设备发起的DMA Request进行DMA Translation重映射到对应的HPA上。

下面的图描述了DMA Translation的原理,这和MMU将虚拟地址翻译成物理地址的过程非常的类似。

值得一提的是,Host平台上可能会存在一个或者多个DMA Remapping硬件单元,而每个硬件单元支持在它管理的设备范围内的所有设备的DMA Remapping。例如,你的台式机CPU Core i7 7700k在MCH中只集成一个DMA Remapping硬件单元(IOMMU),但在多路服务器上可能集成有多个DMA Remapping硬件单元。每个硬件单元负责管理挂载到它所在的PCIe Root Port下所有设备的DMA请求。BIOS会将平台上的DMA Remapping硬件信息通过ACPI协议报告给操作系统,再由操作系统来初始化和管理这些硬件设备。

为了实现DMA隔离,我们需要对直通设备进行标志,而这是通过PCIe的Request ID来完成的。根据PCIe的SPEC,每个PCIe设备的请求都包含了PCI Bus/Device/Function信息,通过BDF号我们可以唯一确定一个PCIe设备。

同时为了能够记录直通设备和每个Domain的关系,VT-d引入了root-entry/context-entry的概念,通过查询root-entry/context-entry表就可以获得直通设备和Domain之间的映射关系。

Root-table是一个4K页,共包含了256项root-entry,分别覆盖了PCI的Bus0-255,每个root-entry占16-Byte,记录了当前PCI Bus上的设备映射关系,通过PCI Bus Number进行索引。 Root-table的基地址存放在Root Table Address Register当中。Root-entry中记录的关键信息有:

  • Present Flag:代表着该Bus号对应的Root-Entry是否呈现,CTP域是否初始化;
  • Context-table pointer (CTP):CTP记录了当前Bus号对应点Context Table的地址。

同样每个context-table也是一个4K页,记录一个特定的PCI设备和它被分配的Domain的映射关系,即对应Domain的DMA地址翻译结构信息的地址。 每个root-entry包含了该Bus号对应的context-table指针,指向一个context-table,而每张context-table包又含256个context-entry, 其中每个entry对应了一个Device Function号所确认的设备的信息。通过2级表项的查询我们就能够获得指定PCI被分配的Domain的地址翻译结构信息。Context-entry中记录的信息有:

  • Present Flag:表示该设备对应的context-entry是否被初始化,如果当前平台上没有该设备Preset域为0,索引到该设备的请求也会被block掉。
  • Translation Type:表示哪种请求将被允许;
  • Address Width:表示该设备被分配的Domain的地址宽度;
  • Second-level Page-table Pointer:二阶页表指针提供了DMA地址翻译结构的HPA地址(这里仅针对Requests-without-PASID而言);
  • Domain Identifier: Domain标志符表示当前设备的被分配到的Domain的标志,硬件会利用此域来标记context-entry cache,这里有点类似VPID的意思;
  • Fault Processing Disable Flag:此域表示是否需要选择性的disable此entry相关的remapping faults reporting。

因为多个设备有可能被分配到同一个Domain,这时只需要将其中每个设备context-entry项的 Second-level Page-table Pointer 设置为对同一个Domain的引用, 并将Domain ID赋值为同一个Domian的就行了。

2 DMA隔离和地址翻译


VT-d中引入root-table和context-table的目的比较明显,这些额外的table的存在就是为了记录每个直通设备和其被分配的Domain之间的映射关系。 有了这个映射关系后,DMA隔离的实现就变得非常简单。 IOMMU硬件会截获直通设备发出的请求,然后根据其Request ID查表找到对应的Address Translation Structure即该Domain的IOMMU页表基地址, 这样一来该设备的DMA地址翻译就只会按这个Domain的IOMMU页表的方式进行翻译,翻译后的HPA必然落在此Domain的地址空间内(这个过程由IOMMU硬件中自动完成), 而不会访问到其他Domain的地址空间,这样就达到了DMA隔离的目的。

DMA地址翻译的过程和虚拟地址翻译的过程是完全一致的唯一不同的地方在于MMU地址翻译是将进程的虚拟地址(HVA)翻译成物理地址(HPA),而IOMMU地址翻译则是将虚拟机物理地址空间内的GPA翻译成HPA。IOMMU页表和MMU页表一样,都采用了多级页表的方式来进行翻译。例如,对于一个48bit的GPA地址空间的Domain而言,其IOMMU Page Table共分4级,每一级都是一个4KB页含有512个8-Byte的目录项。和MMU页表一样,IOMMU页表页支持2M/1G大页内存,同时硬件上还提供了IO-TLB来缓存最近翻译过的地址来提升地址翻译的速度。

  • IOVA:IO Virtual Address space   IO虚拟地址空间
  • CTP:Context-table pointer   上下文表指针
  • GPA:Guest Phyical Address   客人的实际地址
  • GVA:Guest Virtual Address   访客虚拟地址
  • HPA:Host Phyical Address    主机物理地址
  • IPA:Intermediate Phyical Address    中间物理地址

Related Posts

  • VT-d Interrupt Remapping
  • VT-d Posted Interrupt
  • VFIO Introduction
  • VT-d Interrupt Remapping Code Analysis
  • VIM8 Customized Configuration
  • Article Archive 2019 Reading Plan
  • Virtio Spec Overview

虚拟化技术 - I/O虚拟化

https://zhuanlan.zhihu/p/69627614


在虚拟化系统中,I/O外设只有一套,需要被多个guest VMs共享。VMM/hypervisor提供了两种机制来实现对I/O设备的访问,一种是透传(passthrough),一种是模拟(emulation)

透传 - Device Passthrough


所谓passthrough,就是指guest VM可以透过VMM,直接访问I/O硬件,这样guest VM的I/O操作路径几乎和无虚拟化环境下的I/O路径相同,性能自然是非常高的。

在虚拟化环境下,guest VM使用的物理地址是GPA(参考这篇文章《虚拟化技术 - 内存虚拟化 [一]》),如果直接用guest OS中的驱动程序去操作I/O设备的话(这里的I/O限定于和内存统一编址的MMIO),那么设备使用的地址也是GPA。这倒不难办,使用CPU的EPT/NPT MMU查询对应guest VM的nPT页表,进行一下GPA->HPA的转换就可以了。

注释

  • IOVA:IO Virtual Address space   IO虚拟地址空间
  • CTP:Context-table pointer   上下文表指针
  • GPA:Guest Phyical Address   客人的实际地址
  • GVA:Guest Virtual Address   访客虚拟地址
  • HPA:Host Phyical Address    主机物理地址
  • IPA:Intermediate Phyical Address    中间物理地址

可是别忘了,有一些I/O设备是具备DMA(Direct Memory Access)功能的。由于DMA是直接在设备和物理内存之间传输数据,必须使用实际的物理地址(也就是HPA),但DMA本身是为了减轻CPU的处理负担而存在的,其传输过程并不经过CPU。对于一个支持DMA传输的设备,当它拿着GPA去发起DMA操作时,由于没有真实的物理内存地址,传输势必会失败。

那如何实现对进行DMA传输的设备的GPA->HPA转换呢?再来一个类似于EPT/NPT的MMU?没错,这种专门转换I/O地址的MMU在x86的阵营里就是IOMMU

然而,不和AMD使用相同的名字是Intel一贯的路数,所以Intel通常更愿意把这种硬件辅助的I/O虚拟化技术叫做VT-d(Virtualization Technology for Direct I/O)。作为后起之秀的ARM自然也不甘示弱,推出了对应的SMMU(System MMU)。

IOMMU查找的页表通常是专门的I/O page tables。既然都是进行GPA->HPA的转换,为什么不和EPT/NPT MMU共享nPT页表呢?这个问题将在接下来的文章中给出解答。为了加速查找过程,IOMMU中也有类似于EPT/NPT TLB的IOTLB硬件单元。

以Intel的VT-d为例,它规定了一个domain对应一个IO页表。在具体的实现中,通常是一个guest VM作为一个domain,因此分配给同一个guest VM的设备将共享同一个IO页表。

这里为了支持device passthrough(透传)模式下的DMA传输,IOMMU进行的是GPA->HPA的转换。既然EPT/NPT MMU都可以同时支持GVA->GPA和GPA->HPA的转换,那IOMMU是否也可以呢?这个问题也将留在后续的文章中讨论。

Device passthrough(透传)机制要求VMM为guest VM分配好设备,并提供隔离。假设系统中现在有三个guest VMs,编号分别是0, 1, 2,如果VM 0分配到了网卡A,就要阻止VM 1和VM 2对网卡A的访问。

可以采用的方法是在拥有设备的guest VM加载驱动程序前,先给要分配出去的设备加载一个伪驱动作为占位符,由于没有真正的驱动程序,这个设备对于其他的guest VM来说就相当于是“隐藏”了。

这同时也暴露了使用device passthrough存在的一个问题,就是同一个I/O设备通常无法在不同的guest VM之间实现共享和动态迁移(比如PCI设备的热插拔)。下文将介绍的device emulation机制将可以解决设备共享和迁移的问题。

虚拟化技术 - 内存虚拟化

https://zhuanlan.zhihu/p/69828213


  • IOVA:IO Virtual Address space   IO虚拟地址空间
  • CTP:Context-table pointer   上下文表指针
  • GPA:Guest Phyical Address   客人的实际地址
  • GVA:Guest Virtual Address   访客虚拟地址
  • HPA:Host Phyical Address    主机物理地址
  • IPA:Intermediate Phyical Address    中间物理地址
  • VT-d:Virtualization Technology for Direct I/O
  • SMMU:System MMU

大型操作系统(比如Linux)的内存管理的内容是很丰富的,而内存的虚拟化技术在OS内存管理的基础上又叠加了一层复杂性,比如我们常说的虚拟内存(virtual memory),如果使用虚拟内存的OS是运行在虚拟机中的,那么需要对虚拟内存再进行虚拟化,也就是vitualizing virtualized memory。本文将仅从“内存地址转换”和“内存回收”两个方面探讨内存虚拟化技术。

【虚拟机内存地址转换

在Linux这种使用虚拟地址的OS中,虚拟地址经过page table转换可得到物理地址(参考这篇文章,当然,我们可以在用空间完成这种在转化《Linux用户空间将虚拟地址转化为物理地址》):

如果这个操作系统是运行在虚拟机上的,那么这只是一个中间的物理地址(Intermediate Phyical Address - IPA),需要经过VMM/hypervisor的转换,才能得到最终的物理地址(Host Phyical Address - HPA)。从VMM的角度,guest VM中的虚拟地址就成了GVA(Guest Virtual Address),IPA就成了GPA(Guest Phyical Address)。

可见,如果使用VMM,并且guest VM中的程序使用虚拟地址(如果guest VM中运行的是不支持虚拟地址的RTOS,则在虚拟机层面不需要地址转换),那么就需要两次地址转换。

但是传统的IA32架构从硬件上只支持一次地址转换,即由CR3寄存器指向进程第一级页表的首地址,通过MMU查询进程的各级页表,获得物理地址。

软件实现 - 影子页表


为了支持GVA->GPA->HPA的两次转换,可以计算出GVA->HPA的映射关系,将其写入一个单独的影子页表(sPT - shadow Page Table)。在一个运行Linux的guest VM中,每个进程有一个由内核维护的页表,用于GVA->GPA的转换,这里我们把它称作gPT(guest Page Table)。

VMM层的软件会将gPT本身使用的物理页面设为write protected的,那么每当gPT有变动的时候(比如添加或删除了一个页表项),就会产生被VMM截获的page fault异常,之后VMM需要重新计算GVA->HPA的映射,更改sPT中对应的页表项。可见,这种纯软件的方法虽然能够解决问题,但是其存在两个缺点:

  • 实现较为复杂,需要为每个guest VM中的每个进程的gPT都维护一个对应的sPT,增加了内存的开销。
  • VMM使用的截获方法增多了page fault和trap/vm-exit的数量,加重了CPU的负担。

在一些场景下,这种影子页表机制造成的开销可以占到整个VMM软件负载的75%。

硬件辅助 - EPT/NPT


为此,各大CPU厂商相继推出了硬件辅助的内存虚拟化技术,比如Intel的EPT(Extended Page Table)和AMD的NPT(Nested Page Table),它们都能够从硬件上同时支持GVA->GPA和GPA->HPA的地址转换的技术。

GVA->GPA的转换依然是通过查找gPT页表完成的,而GPA->HPA的转换则通过查找nPT页表来实现,每个guest VM有一个由VMM维护的nPT。其实,EPT/NPT就是一种扩展的MMU(以下称EPT/NPT MMU),它可以交叉地查找gPT和nPT两个页表:

假设gPT和nPT都是4级页表,那么EPT/NPT MMU完成一次地址转换的过程是这样的(不考虑TLB):

首先它会查找guest VM中CR3寄存器(gCR3)指向的PML4页表,由于gCR3中存储的地址是GPA,因此CPU需要查找nPT来获取gCR3的GPA对应的HPA。nPT的查找和前面文章讲的页表查找方法是一样的,这里我们称一次nPT的查找过程为一次nested walk

  • IOVA:IO Virtual Address space   IO虚拟地址空间
  • CTP:Context-table pointer   上下文表指针
  • GPA:Guest Phyical Address   客人的实际地址
  • GVA:Guest Virtual Address   访客虚拟地址
  • HPA:Host Phyical Address    主机物理地址
  • IPA:Intermediate Phyical Address    中间物理地址

如果在nPT中没有找到,则产生EPT violation异常(可理解为VMM层的page fault)。如果找到了,也就是获得了PML4页表的物理地址后,就可以用GVA中的bit位子集作为PML4页表的索引,得到PDPE页表的GPA。接下来又是通过一次nested walk进行PDPE页表的GPA->HPA转换,然后重复上述过程,依次查找PD和PE页表,最终获得该GVA对应的HPA。

不同于影子页表是一个进程需要一个sPT,EPT/NPT MMU解耦了GVA->GPA转换和GPA->HPA转换之间的依赖关系,一个VM只需要一个nPT,减少了内存开销。如果guest VM中发生了page fault,可直接由guest OS处理,不会产生vm-exit,减少了CPU的开销。可以说,EPT/NPT MMU这种硬件辅助的内存虚拟化技术解决了纯软件实现存在的两个问题。

EPT/NPT MMU优化


事实上,EPT/NPT MMU作为传统MMU的扩展,自然也是有TLB(《Linux内存管理:转换后备缓冲区(TLB)原理》)的,它在查找gPT和nPT之前,会先去查找自己的TLB(前面为了描述的方便省略了这一步)。但这里的TLB存储的并不是一个GVA->GPA的映射关系,也不是一个GPA->HPA的映射关系,而是最终的转换结果,也就是GVA->HPA的映射。

不同的进程可能会有相同的虚拟地址,为了避免进程切换的时候flush所有的TLB,可通过给TLB entry加上一个标识进程的PCID/ASID(ASID(Address Space ID))的tag来区分(参考这篇文章,或者这篇《Linux内存管理:转换后备缓冲区(TLB)原理》如何管理ASID小节)。同样地,不同的guest VM也会有相同的GVA,为了flush的时候有所区分,需要再加上一个标识虚拟机的tag,这个tag在ARM体系中被叫做VMID,在Intel体系中则被叫做VPID。

PS:

ASID:Address Space ID   地址空间标识符
PCID:Process context identifier 进程上下文标识符

PCID(进程上下文标识符)是在Westmere架构引入的新特性。简单来说,在此之前,TLB是单纯的VA到PA的转换表,进程1和进程2的VA对应的PA不同,不能放在一起。加上PCID后,转换变成VA + 进程上下文ID到PA的转换表,放在一起完全没有问题了。这样进程1和进程2的页表可以和谐的在TLB中共处,进程在它们之前切换完全不需要预热了!

在最坏的情况下(也就是TLB完全没有命中),gPT中的每一级转换都需要一次nested walk【1】,而每次nested walk需要4次内存访问,因此5次nested walk总共需要 (4+1)*5-1=24 次内存访问(就像一个5x5的二维矩阵一样):

虽然这24次内存访问都是由硬件自动完成的,不需要软件的参与,但是内存访问的速度毕竟不能与CPU的运行速度同日而语,而且内存访问还涉及到对总线的争夺,次数自然是越少越好。

要想减少内存访问次数,要么是增大EPT/NPT TLB的容量,增加TLB的命中率,要么是减少gPT和nPT的级数。gPT是为guest VM中的进程服务的,通常采用4KB粒度的页,那么在64位系统下使用4级页表是非常合适的(参考这篇文章)。

而nPT是为guset VM服务的,对于划分给一个VM的内存,粒度不用太小。64位的x86_64支持2MB和1GB的large page,假设创建一个VM的时候申请的是2G物理内存,那么只需要给这个VM分配2个1G的large pages就可以了(这2个large pages不用相邻,但large page内部的物理内存是连续的),这样nPT只需要2级(nPML4和nPDPE)。

如果现在物理内存中确实找不到2个连续的1G内存区域,那么就退而求其次,使用2MB的large page,这样nPT就是3级(nPML4, nPDPE和nPD)。

下文将介绍从guest VM回收内存的技术。

【1】:这里区分一个英文表达,stagelevel,查找gPT的转换过程被称作stage 1,查找nPT的转换过程被称作stage 2,而gPT和nPT自身都是由multi-level的页表组成。

参考:

AMD-V™ Nested Paging

Performance Evaluation of Intel EPT Hardware Assist

SMMU和IOMMU技术

https://zhuanlan.zhihu/p/75978422

https://zhuanlan.zhihu/p/76643300


前面的文章讲到了为支持I/O透传机制中的DMA设备传输而引入的IOMMU/SMMU技术,同时留了一个问题:IOMMU/SMMU是否可以同时支持GVA->GPA和GPA->HPA的转换?答案是Yes。

既然在虚拟化的环境中,DMA设备可以借助GPA->HPA的转换,绕过VMM,实现与guest VM中OS的直接数据传递。那在非虚拟化的环境中,DMA设备也可以借助GVA->GPA的转换,绕过OS kernel,实现与userspace(用户空间)的进程的直接交互,这种用法就是用户空间的DMA传输。

两者其实是统一的,不管是GVA还是GPA,说到底都是虚拟地址,有了IOMMU/SMMU之后,DMA就可以使用虚拟地址作为传输的目标地址了。

ARM的SMMU


那IOMMU/SMMU具体应该如何使用呢?本文将主要以ARM的SMMU为例,讲解其转换地址的过程。

  • MMU地址翻译是将进程的虚拟地址(HVA)翻译成物理地址(HPA);
  • IOMMU地址翻译则是将虚拟机物理地址空间内的GPA翻译成HPA;
  • IOMMU页表和MMU页表一样,都采用了多级页表的方式来进行翻译;
  • 专门转换I/O地址的MMU在x86的阵营里就是IOMMU;
  • Intel把IOMMU技术叫做VT-d(Virtualization Technology for Direct I/O);
  • EPT/NPT MMU作为传统MMU的扩展,也是有TLB;

在Linux的实现中,一个进程有一个对应的页表,而SMMU是为设备服务的,几个设备可能同属于一个guest VM,因此多个设备可能会共用一个GPA->HPA的转换页表。同一个guest VM的设备可能属于或者不属于某一个特定的进程,因此也可能共用或者不共用GVA->GPA的转换页表。

在SMMU中,一个发起DMA传输(transaction)的设备的信息由一个Stream Table Entry(STE)来描述。所有的STEs共同构成了Stream Table,可由StreamID作为Stream Table数组的索引,查找得到对应的STE,因此StreamID也就成了设备唯一性的标识。Stream table可以是1-level的:

也可以是2-level的,比如一个10位的StreamID使用[9:8]的2个bits作为第一级Stream table的索引,[7:0]的8个bits作为第二级Stream table的索引,这和使用虚拟地址作为索引查找页表是一样的(参考这篇文章)。

不同的是,多级页表中每级页表的大小是相同的,而第二级Stream table的大小可以是不同的,比如下图中A就是占满了的,有255个entries,而B和C分别只有4个entries和1个entry。用户可以根据实际的需要灵活配置,以节约内存空间。

大家应该都知道,ARM公司主要是依靠出售core的license来赚钱的,它给予了具体的芯片厂商一些自由发挥的空间,比如SMMUv3只规定了StreamID的大小是0-32个bits,但具体采用多少个bits,以及StreamID是怎样构成的,则都属于implementation defined。但SMMUv3同时也做出了限制:如果StreamID的数目超过了64(也就是超过了6个bits),那么必须使用2-level的Stream table。

如果一个STE对应的设备要进行的是GPA->HPA的stage 2转换(关于stage请参考这篇文章的注[1]),那么该STE包含的S2TTB(Stage 2 Translate Table Base)将直接指向设备所属的guest VM对应的stage 2页表(下图中的D)。设备所属的VM由STE中的VMID标识,属于同一个VM的设备对应的STEs将指向同一个stage 2页表。

在同一个VM内,一个设备可能被VM内的多个进程共享,而这些进程会有不同的页表,因此一个设备在和不同进程交互时,也会使用不同的页表。为了区分,需要一个设备对应进程的描述信息,这个描述信息被称为CD(Context Descriptor)。

如果该设备需要进行的是GVA->GPA的stage 1转换(比如前面提到的用户空间的DMA传输),那么描述该设备的STE将指向当前进程对应的这个CD(下图中的B)。和同一个进程交互的不同设备将共用这个进程的stage 1页表,因此这些设备的STEs将指向同一个CD。

每个CD包含了一个标识进程的ASID,由TTB0和TTB1指向进程对应的stage 1页表(上图中的C)。熟悉ARM架构的同学应该知道,TTBR0和TTBR1是ARM中分别用来存储进程页表和内核页表的基地址的寄存器,由于这里不再是Register,而是内存中的Descriptor,但用途类似,所以就叫TTB0和TTB1。

同一个设备对应的所有CDs构成了一个CD table。CD table使用的查找索引被称为SubstreamID。SubstreamID最多占20个bits,这个规定是有出处的。熟悉PCIe的同学可能知道,PCIe中有个PASID,而PASID最大就是20个bits。SubstreamID基本就是和PASID对等的概念,虽然SMMU并不限于只支持PCIe的设备,但这里SubstreamID还是和PASID保持了统一的步调。

同Stream table一样,CD table可以是1-level的(下图中的D),也可以是2-level的(下图中的E和F)。

如果是熟悉x86的segmentation机制或者是看过这个系列的文章的同学,会不会发现SMMU中的这个Stream table/CD table,其实和x86中的GDT/LDT是很相似的。都是通过索引查找得到对应的entry,每一个entry都是记录的一些描述信息,而且后面还都是跟的page tables。

为什么IOMMU要使用专门的I/O页表?

以AMD的IOMMU为例,它和内置在一个CPU内的MMU(以下称作CPU MMU,ARM的CoreLink MMU,还有前面介绍的EPT/NPT,都属于CPU MMU)是存在一些区别的:

  • 64位的x86_64系列的CPU MMU只支持4KB, 2MB和1GB的页大小(参考这篇文章),而AMD的IOMMU除了支持这些默认的页大小,还支持8KB, 16KB, 1MB, 4MB, 4GB的页大小。

  • CPU MMU需要按照页表结构一级一级的遍历,而当虚拟地址包含一长串的0的时候,AMD的IOMMU在查找时可以跳过其中的一级页表。

正因为有了IOMMU设计上的这些差异,使用专门的I/O页表可以获得更快的查找速度。如果非要用普通的页表也不是不可以,但需要舍弃IOMMU的一些特性,以适配普通页表的查找规则。然而,ARM的SMMU的设计就不是这样的,它使用普通的页表依然可以工作的很好,不需要专门的I/O页表。


参考:

  • ARM System Memory Management Unit Architecture Specification, version 3.1:https://static.docs.arm/ihi0070/ca/IHI_0070_C_a_System_Memory_Management_Unit_Arm_Architecture_Specification.pdf
  • Intel Virtualization Technology for Directed I/O Architecture Specification, Rev. 3.1:https://software.intel/content/dam/develop/external/us/en/documents/vt-directed-io-spec.pdf
  • AMD I/O Virtualization Technology (IOMMU) Specification, Rev. 3.0.0:http://developer.amd/wordpress/media/2013/12/48882_IOMMU.pdf


 

ARM SMMU的原理与IOMMU


首先放一个社区iommupatch的网址:https://lore.kernel/linux-iommu/

1 arm smmu的原理

1.1 smmu 基本知识


如上图所示,smmu 的作用和mmu 类似,mmu作用是替cpu翻译页表将进程的虚拟地址转换成cpu可以识别的物理地址。同理,smmu的作用就是替设备将dma请求的地址,翻译成设备真正能用的物理地址,但是当smmu bypass的时候,设备也可以直接使用物理地址来进行dma;

1.2 smmu 的数据结构


smmu的重要的用来dma地址翻译的数据结构都是放在内存中的,由smmu的寄存器保存着这些表在内存中的基地址,首先就是StreamTable(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(Context Descriptor) 表来管理每个page table;

看一看cd(Context Descriptor) 表的查找规则:

先说另外一个重要的概念:SubstreamID(pasid),这个叫substreamid又称之为pasid,也是非常简单的概念,既然有表了,那也得有id来协助查找啊,所以就出来了这个id,从这里也可以看出来,道理都一样,用了表了就有id 啊!

CD表,在smmu中也是可以是线性的或者两级的,这个都是在smmu 寄存器中配置好了的,由smmu驱动来读去,进行按对应的位进行分级,和ste表一样的原理;

介绍了两个基本的也重要的数据结构后我,smmu是在支持虚拟化的时候,可以同时进行stage1 和 stage2的翻译的,如下图所示:

当我们在虚拟机的guest中启用smmu的时候,smmu是需要同时开启stage1 和 stage2的,当然了,smmu 也是可以进行bypass的;

1.3 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涉及到虚拟化的过程比较复杂,这个有机会再解释;

2 smmu驱动与iommu框


2.1 smmu v3驱动初始化


简单的介绍了上面的两个重要表以及smmu内部的基本的查找流程后,我们现在来看看在linux内核中,smmu驱动是如何完成初始化的过程,借着这个分析,我们看看smmu里的重要的几种队列:

smmuv3的在内核中的代码路径:drivers/iommu/arm-smmu-v3.c:

static int arm_smmu_device_probe(struct platform_device *pdev)
{
	struct resource *res;
	resource_size_t ioaddr;
	struct arm_smmu_device *smmu;
	struct device *dev = &pdev->dev;
	int num_irqs, i, err;

	smmu = devm_kzalloc(dev, sizeof(*smmu), GFP_KERNEL);
	if (!smmu) {
		dev_err(dev, "failed to allocate arm_smmu_device\n");
		return -ENOMEM;
	}
	smmu->dev = dev;

	if (dev->of_node)
		err = arm_smmu_device_dt_probe(pdev, smmu);
	else
		err = arm_smmu_device_acpi_probe(pdev, smmu);

	if (err)
		return err;

	smmu = arm_smmu_impl_init(smmu);
	if (IS_ERR(smmu))
		return PTR_ERR(smmu);

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	ioaddr = res->start;
    ...
}

上面是smmu驱动中初始化流程的前半部分,从中可以很容易看出来,内核中每个smmu都有一个结构体struct arm_smmu_device来管理,实际上初始化的流程就是在填充着个结构。看上图,首先就是从slub/slab中分配一个对象空间,随后一个比较重要的是函数 arm_smmu_device_dt_probe 和 arm_smmu_device_acpi_probe,这俩函数会从dts中的smmu节点和acpi的smmu配置表中读取一些smmu中断等等属性;

在5.6.5内核中(drivers/iommu/arm-smmu-v3.c


static int arm_smmu_device_dt_probe(struct platform_device *pdev,
				    struct arm_smmu_device *smmu)
{
	struct device *dev = &pdev->dev;
	u32 cells;
	int ret = -EINVAL;

	if (of_property_read_u32(dev->of_node, "#iommu-cells", &cells))
		dev_err(dev, "missing #iommu-cells property\n");
	else if (cells != 1)
		dev_err(dev, "invalid #iommu-cells value (%d)\n", cells);
	else
		ret = 0;

	parse_driver_options(smmu);

	if (of_dma_is_coherent(dev->of_node))
		smmu->features |= ARM_SMMU_FEAT_COHERENCY;

	return ret;
}

...

static int arm_smmu_device_acpi_probe(struct platform_device *pdev,
				      struct arm_smmu_device *smmu)
{
	struct device *dev = smmu->dev;
	struct acpi_iort_node *node =
		*(struct acpi_iort_node **)dev_get_platdata(dev);
	struct acpi_iort_smmu *iort_smmu;
	int ret;

	/* Retrieve SMMU1/2 specific data */
	iort_smmu = (struct acpi_iort_smmu *)node->node_data;

	ret = acpi_smmu_get_data(iort_smmu->model, smmu);
	if (ret < 0)
		return ret;

	/* Ignore the configuration access interrupt */
	smmu->num_global_irqs = 1;

	if (iort_smmu->flags & ACPI_IORT_SMMU_COHERENT_WALK)
		smmu->features |= ARM_SMMU_FEAT_COHERENT_WALK;

	return 0;
}

随后调用函数 platform_get_resource 来从dts或者apci表中读取smmu的寄存器的基地址,这个很重要,后续所有的初始化都是围绕着个配置来的;

在5.6.5内核中(drivers/iommu/arm-smmu-v3.c

/**
 * platform_get_resource - get a resource for a device
 * @dev: platform device
 * @type: resource type
 * @num: resource index
 */
struct resource *platform_get_resource(struct platform_device *dev,
				       unsigned int type, unsigned int num)
{
	u32 i;

	for (i = 0; i < dev->num_resources; i++) {
		struct resource *r = &dev->resource[i];

		if (type == resource_type(r) && num-- == 0)
			return r;
	}
	return NULL;
}
EXPORT_SYMBOL_GPL(platform_get_resource);

继续看剩下的部分,开头很容易看出来,要读取smmu的几个中断号,smmu 硬件给软件消息有队列buffer,smmu硬件通过中断的方式让smmu驱动从队列buffer中取消息,我们一一介绍:

  1. 第一个eventq中断,smmu的一个队列叫event队列,这个队列是给挂在smmu上的platform设备用的,当platform设备使用smmu翻译dma 的iova的时候,如果发生了一场smmu会首先将异常的消息填到event队列中,随后上报一个eventq的中断给 smmu 驱动,smmu驱动接到这个中断后,开始执行中断处理程序,从event队列中将异常的消息读出来,显示异常;

  2. 另外一个priq中断时给pri队列用的,这个队列是专门给挂在smmu上的pcie类型的设备用的,具体的流程其实是和event队列是一样的,这里不多解释了;

  3. 最后一个是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(Context Descriptor),这个smmu支持的物理也大小,iova和pa的地址位数等等;这些头填在arm_smmu_device的features的字段里面;

基本信息读出来后,我们是不是可开始初始化数据结构了?

答案是肯定的啦,看看函数arm_smmu_init_structures;

从上面的数据结构初始化的函数可以看出来,smmu驱动主要负责初始化两种数据结构,一个strtab(stream table的简写),另外一个种是队列的内存分配和初始化;

我们首先来看看队列的:

在5.6.5内核中


static int arm_smmu_init_queues(struct arm_smmu_device *smmu)
{
	int ret;

	/* cmdq */
	ret = arm_smmu_init_one_queue(smmu, &smmu->cmdq.q, ARM_SMMU_CMDQ_PROD,
				      ARM_SMMU_CMDQ_CONS, CMDQ_ENT_DWORDS,
				      "cmdq");
	if (ret)
		return ret;

	ret = arm_smmu_cmdq_init(smmu);
	if (ret)
		return ret;

	/* evtq */
	ret = arm_smmu_init_one_queue(smmu, &smmu->evtq.q, ARM_SMMU_EVTQ_PROD,
				      ARM_SMMU_EVTQ_CONS, EVTQ_ENT_DWORDS,
				      "evtq");
	if (ret)
		return ret;

	/* priq */
	if (!(smmu->features & ARM_SMMU_FEAT_PRI))
		return 0;

	return arm_smmu_init_one_queue(smmu, &smmu->priq.q, ARM_SMMU_PRIQ_PROD,
				       ARM_SMMU_PRIQ_CONS, PRIQ_ENT_DWORDS,
				       "priq");
}

从上面可以看出来,smmu驱动主要初始化3个队列:cmdq,evtq,priq;这里不再进一步解释了,避免陷入函数细节分析;

最后我们来看看smmu 的strtab的初始化:

在5.6.5内核中


static int arm_smmu_init_strtab(struct arm_smmu_device *smmu)
{
	u64 reg;
	int ret;

	if (smmu->features & ARM_SMMU_FEAT_2_LVL_STRTAB)
		ret = arm_smmu_init_strtab_2lvl(smmu);
	else
		ret = arm_smmu_init_strtab_linear(smmu);

	if (ret)
		return ret;

	/* Set the strtab base address */
	reg  = smmu->strtab_cfg.strtab_dma & STRTAB_BASE_ADDR_MASK;
	reg |= STRTAB_BASE_RA;
	smmu->strtab_cfg.strtab_base = reg;

	/* Allocate the first VMID for stage-2 bypass STEs */
	set_bit(0, smmu->vmid_map);
	return 0;
}

从上图可以看出来,首先判断我们需要初始化一级的还是二级的stream table,这里依据就是上面的硬件寄存器中读取出来的;

我们首先看看函数arm_smmu_init_strtab_linear 函数:

5.6.5代码


static int arm_smmu_init_strtab_linear(struct arm_smmu_device *smmu)
{
	void *strtab;
	u64 reg;
	u32 size;
	struct arm_smmu_strtab_cfg *cfg = &smmu->strtab_cfg;

	size = (1 << smmu->sid_bits) * (STRTAB_STE_DWORDS << 3);
	strtab = dmam_alloc_coherent(smmu->dev, size, &cfg->strtab_dma,
				     GFP_KERNEL);
	if (!strtab) {
		dev_err(smmu->dev,
			"failed to allocate linear stream table (%u bytes)\n",
			size);
		return -ENOMEM;
	}
	cfg->strtab = strtab;
	cfg->num_l1_ents = 1 << smmu->sid_bits;

	/* Configure strtab_base_cfg for a linear table covering all SIDs */
	reg  = FIELD_PREP(STRTAB_BASE_CFG_FMT, STRTAB_BASE_CFG_FMT_LINEAR);
	reg |= FIELD_PREP(STRTAB_BASE_CFG_LOG2SIZE, smmu->sid_bits);
	cfg->strtab_base_cfg = reg;

	arm_smmu_init_bypass_stes(strtab, cfg->num_l1_ents);
	return 0;
}

对于线性的stream table表来说smmu 驱动会将调用dma alloc接口将stream table 需要的所有空间都一把分配完毕了,并且将所有的ste entry项都给预先的初始化成bypass的模式,具体的就不深入看了,比较简单,设置bit;

随后我们来看看函数:

arm_smmu_init_strtab_2lvl;

5.6.5代码


static int arm_smmu_init_strtab_2lvl(struct arm_smmu_device *smmu)
{
	void *strtab;
	u64 reg;
	u32 size, l1size;
	struct arm_smmu_strtab_cfg *cfg = &smmu->strtab_cfg;

	/* Calculate the L1 size, capped to the SIDSIZE. */
	size = STRTAB_L1_SZ_SHIFT - (ilog2(STRTAB_L1_DESC_DWORDS) + 3);
	size = min(size, smmu->sid_bits - STRTAB_SPLIT);
	cfg->num_l1_ents = 1 << size;

	size += STRTAB_SPLIT;
	if (size < smmu->sid_bits)
		dev_warn(smmu->dev,
			 "2-level strtab only covers %u/%u bits of SID\n",
			 size, smmu->sid_bits);

	l1size = cfg->num_l1_ents * (STRTAB_L1_DESC_DWORDS << 3);
	strtab = dmam_alloc_coherent(smmu->dev, l1size, &cfg->strtab_dma,
				     GFP_KERNEL);
	if (!strtab) {
		dev_err(smmu->dev,
			"failed to allocate l1 stream table (%u bytes)\n",
			size);
		return -ENOMEM;
	}
	cfg->strtab = strtab;

	/* Configure strtab_base_cfg for 2 levels */
	reg  = FIELD_PREP(STRTAB_BASE_CFG_FMT, STRTAB_BASE_CFG_FMT_2LVL);
	reg |= FIELD_PREP(STRTAB_BASE_CFG_LOG2SIZE, size);
	reg |= FIELD_PREP(STRTAB_BASE_CFG_SPLIT, STRTAB_SPLIT);
	cfg->strtab_base_cfg = reg;

	return arm_smmu_init_l1_strtab(smmu);
}

我们可以思考一个问题:我们真的需要将所有的ste entry都个创造出来吗?

很显然,不是的,smmu驱动的初始化正是基于这种原理,仅仅只会初始化第一级的ste目录项,其实这里就是类似页表的初始化了也只是先初始化了目录项;函数中dma alloc coherent就是负责分配第一级的目录项的,分配的大小是多大呢?我们可以看一下有一个关键的宏STRTAB_SPLIT,这个宏目前在smmu驱动中是8位,也就是预先会分配2^8个目录项,每个目录项的大小是固定的;

#define STRTAB_SPLIT			8

我们可以看到里面还调用了一个函数arm_smmu_init_l1_strtab函数,这里就是我们空间分配完了,总该给这些目录项给初始化一下吧,这里就不深入进去看了;


static int arm_smmu_init_l1_strtab(struct arm_smmu_device *smmu)
{
	unsigned int i;
	struct arm_smmu_strtab_cfg *cfg = &smmu->strtab_cfg;
	size_t size = sizeof(*cfg->l1_desc) * cfg->num_l1_ents;
	void *strtab = smmu->strtab_cfg.strtab;

	cfg->l1_desc = devm_kzalloc(smmu->dev, size, GFP_KERNEL);
	if (!cfg->l1_desc) {
		dev_err(smmu->dev, "failed to allocate l1 stream table desc\n");
		return -ENOMEM;
	}

	for (i = 0; i < cfg->num_l1_ents; ++i) {
		arm_smmu_write_strtab_l1_desc(strtab, &cfg->l1_desc[i]);
		strtab += STRTAB_L1_DESC_DWORDS << 3;
	}

	return 0;
}

到此为止,我们已经将基本的数据结构初始化给简要的讲完了;我们接着看smmu驱动初始化的剩下的,见下图:

上图是smmu 驱动初始化的剩下的部分,我们可以看出来里面第一个函数是arm_smmu_device_reset,这个函数是干嘛的呢,我们前面是不是已经给这个smmu在内存中分配了几个队列和stream table的目录项?那这些数据结构的基地址总该让smmu知道吧?这个函数就是将这些基地址给放到smmu的控制寄存器中的;当前我们需要的东西给初始化完后,smmu驱动接下来就是将smmu的基本数据结构注册到上层的iommu抽象框架里,让iommu结构能够调用到smmu,这个在后面再说。

2.2 smmu 与 iommu关系


2.2.1 两者的结构关系

smmu 和 iommu 是何种关系呢?

在我们的硬件体系中,能够有能力完成设备iova 到 pa转换的有很多,例如有intel iommu, amd的iommu ,arm的smmu等等,不一一枚举了;那这些不同的硬件架构不会都作为一个独立的子系统,所以,在linux 内核中 抽象了一层 iommu 层,由iommu层给各个外部设备驱动提供结构,隐藏底层的不同的架构;如图所示:

由上图可以很明显的看出来,各个架构的smmu驱动是如何使如何和iommu框架对接的,iommu框架通过不同架构的ops来调用到底层真正的驱动接口;

我们可以问自己一个问题:底层的驱动是如何对接到上层的?

接下来我们来看看进入内核代码来帮我们解开疑惑;

5.6.5:virtio-iommu.c    drivers\iommu    29447    2020/4/17    628


	ret = iommu_device_sysfs_add(&viommu->iommu, dev, NULL, "%s",
				     virtio_bus_name(vdev));
	if (ret)
		goto err_free_vqs;

	iommu_device_set_ops(&viommu->iommu, &viommu_ops);
	iommu_device_set_fwnode(&viommu->iommu, parent_dev->fwnode);

	iommu_device_register(&viommu->iommu);

#ifdef CONFIG_PCI
	if (pci_bus_type.iommu_ops != &viommu_ops) {
		pci_request_acs();
		ret = bus_set_iommu(&pci_bus_type, &viommu_ops);
		if (ret)
			goto err_unregister;
	}
#endif
#ifdef CONFIG_ARM_AMBA
	if (amba_bustype.iommu_ops != &viommu_ops) {
		ret = bus_set_iommu(&amba_bustype, &viommu_ops);
		if (ret)
			goto err_unregister;
	}
#endif

如上图是smmu 驱动初始化的最后一部分,对于底层的每一个smmu结构在iommu框架层中都一有一个唯一的一个结构体表示:struct iommu_device,上图中函数iommu_device_register所完成的任务就是将我们所初始化好的iommu结构体给注册到iommu层的链表中,统一管理起来;最后我们根据smmu所挂载的是pcie外设,还是platform外设,将和个smmu绑定到不同的总线类型上;

2.2.2 iommu的重要结构与ops

iommu 层通过ops来调用底层硬件驱动,我们来看看smmu v3硬件驱动提供了哪些ops call:

5.6.5:arm-smmu.c    drivers\iommu    64751    2020/4/17    1513


static struct iommu_ops arm_smmu_ops = {
	.capable		= arm_smmu_capable,
	.domain_alloc		= arm_smmu_domain_alloc,
	.domain_free		= arm_smmu_domain_free,
	.attach_dev		= arm_smmu_attach_dev,
	.map			= arm_smmu_map,
	.unmap			= arm_smmu_unmap,
	.flush_iotlb_all	= arm_smmu_flush_iotlb_all,
	.iotlb_sync		= arm_smmu_iotlb_sync,
	.iova_to_phys		= arm_smmu_iova_to_phys,
	.add_device		= arm_smmu_add_device,
	.remove_device		= arm_smmu_remove_device,
	.device_group		= arm_smmu_device_group,
	.domain_get_attr	= arm_smmu_domain_get_attr,
	.domain_set_attr	= arm_smmu_domain_set_attr,
	.of_xlate		= arm_smmu_of_xlate,
	.get_resv_regions	= arm_smmu_get_resv_regions,
	.put_resv_regions	= generic_iommu_put_resv_regions,
	.pgsize_bitmap		= -1UL, /* Restricted during device attach */
};

上图就是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的作用;不再过多解释。

2.3 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无需执行太多的指令了;

2.4 smmu和iommu的bypass


  1. 方式一:将iommu 给彻底给bypass掉,linux 提供了iommu.passthrough command line的选项,这个选项配置上后,dma 默认不会走iommu,而是走传统的swiotlb方式的dma;
  2. 方式二:smmu v3的驱动默认支持驱动参数配置,disable_bypass,在系统中是默认关闭bypass的,我们可以通过这个来将某个smmu给bypass掉;
  3. 方式三:acpi 或者dts中不配置相应的smmu节点,比较粗暴的办法。

3.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/Articles/784040/

详细用法可以参见 社区pmcg的补丁文档,里面内容很简单。

更多推荐

ARM SMMU原理与IOMMU技术(“VT-d” DMA、I/O虚拟化、内存虚拟化)

本文发布于:2023-03-28 00:43:00,感谢您对本站的认可!

本文链接:ARM SMMU原理与IOMMU技术(“VT-d” DMA、I/O虚拟化、内存虚拟化) - 行业资讯 - 电子产品设计开发与电子技术学习交流!

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

Logo

鸿蒙生态一站式服务平台。

更多推荐