流的编号
做一次转换需要地址、size以及相关属性如读/写/安全域/非安全域/可共享性/可缓存性;如果超过1个client设备使用SMMU流量,那么他们还要有StreamID来区分;StreamID在系统里的构建传送是具体实现决定的,逻辑上讲,一个StreamID就关联到一个发起转换的设备。物理设备到StreamID的映射必须描述给系统软件,ARM推荐StreamID用密集命名空间,从0开始;每个SMMU都是独立的StreamID命名空间;一个设备可以不止使用一个StreamID触发流量,可以用多个StreamID来区分设备不同的状态。
StreamID用于从Stream table选出STE(Stream Table Entry),这个STE包含这个设备的配置;Stream table最大包含2的StreamIDSize条位于内存的配置数据结构。
另一个属性,SubstreamID可能选择性的提供给SMMU做一阶段页表转换;SubstreamID可以是0-20bit,用于区分组织自同一逻辑块但去往不同的程序地址空间,比如说一个有8context的计算加速器,每个context可能映射到不同的用户进程,但是只有一个设备且通用的配置,因此必须将其分配给整个VM;SubstreamID等同于PCIE里的PASID,因为也用于非PCIE系统,所以给了一个更通用的名字;SubstreamID最大值20bit也与PCIE的PASID一致。
这些属性和大小都可以通过SMMU_IDR1寄存器检测到。
StreamID是识别一个transaction所有配置的关键,一个StreamID可以配置成bypass或要转换的项目,而这就决定了要做1或2阶段页表转换。SubstreamID提供了一个修饰符,这个修饰符在StreamID指示的一组1阶段转换间做选择,但对有StreamID选择的2阶段转换没有影响。仅实现2阶段的SMMU不能接受SubstreamID,1阶段的实现不需要支持子流,因此不需要Substream输入。另外SMMU可以选择是否实现安全和非安全两个域,如果实现了,那么SEC_SID标志就是StreamID所属的域的标志;安全域的StreamID从安全域的Stream table中找STE,非安全域的StreamID从非安全域Stream table找STE。
ARM希望对于PCI,StreamID从PCI RequestID生成,这样StreamID[15:0]就是RequestID的[15:0],如果一个SMMU后有超过1个RootComplex,ARM推荐将这16bit的RequestID命名空间编入更大的StreamID命名空间来管理,高bit位来区分连续的RequestID命名空间,这样StreamID[N:16]就能指示哪个RootComplex是stream的源;在PCIE系统里,SubstreamID应该直接从PASID一对一的对应而来。因此,实现与PCI client一起用的SMMU需要支持StreamID最少16bit。
数据结构和转换过程
SMMU使用一组内存中的数据结构来定位翻译数据。寄存器持有初始根数据结构的基地址,也就是Stream Table。一个包含2阶段转换表基指针的STE,同样能定位1阶段配置数据结构,而这数据结构就包含转换表基地址。一个CD(Context Descriptor)就代表1阶段翻译,一个STE就代表2阶段翻译。因此SMMU使用两个不同组的数据结构:配置结构,从一次转换的StreamID映射到转换表基指针、配置和翻译表访问的context;转换表结构,用于表示VA-IPA和IPA-PA翻译。
一次转换的过程首先会定位这次转换的配置,根据StreamID,SubstreamID;然后用这个配置来定位这个地址要用的翻译。第一步是处理转换的STE,STE可以告诉SMMU这次转换需要其他什么配置;概念上,一个STE描述一个client设备的配置,是依据它是1阶段、2阶段还是两阶段转换;多个设备可以关联到一个VM,因此多个STE可以共享同一个2阶段翻译表;同样的多个设备(streams)可能共享同一个1阶段配置,因此多个STEs可能共享同一个CD。
Stream Table查找
根据StreamID可以定位到一个STE,Stream table支持两种格式,由Stream table基寄存器控制;当一次转换发起,StreamID会被检查是否符合设置的表长,如果StreamID超出配置好的Stream table范围,或是超出2级Stream表的跨度,则转换中止;另外安全域相关的SEC_SID标志也会在SMMU_S_IDR1.SECURE_IMPL==1时被检查。
Stream table-线性Stream table
一个线性Stream table是一个连续的STE数组,从0开始,由StreamID索引,占据空间大小可配置位2的n次方再乘上STE的size,最大为SMMU在硬件中支持的StreamID为的最大数目。n就是StreamID[n:0],如下图:
两级Stream table
就像是MMU的页表一般,包含两级,可以不用连续内存空间,并且节省内存空间,具体结构如下图:
1级表有StreamID[n:SPT]索引,n是StreamID的位数,SPT是可配置的切分点,️由SMMU_(S_)STRTAB_BASE_CFG.SPLIT,2级表有StreamID[SPT-1:0]索引,依赖于各个2级表跨度。两级Stream table的支持通过SMMU_IDR0.ST_LEVEL查到,如果支持,SPT(Split Point)可以用6,8,10;StreamID超过6bit的,也就是超过64个StreamID的,必须支持两级Stream table。1级Stream表项包含一个指向2级Stream表的指针,同步的还有这个StreamID下这个表的跨度。每个Stream table项都可以标记为invalid。
上图是一个Split point设为8的1级表项,StreamID有0-1023,4x8bit;StreamID的0-255由STE数组配置在0x1000;StreamID 256-259由STE数组配置在0x2f20;StreamID 512-767都是失效的;Stream_ID 768的STE位于0x4000。
一个split point为8的两级Stream table,可以比与PCIE同时使用的线性Stream table减少内存使用量;如果支持完整的256个PCIE总线号,则RequestID或StreamID空间为16位,但是由于通常每个物理链路只有一条PCIE总线,而每条总线可能只有一个设备,因此在最坏的情况下,有效的StreamID可能没256个StreamID仅出现一次。另外,Split point为6则可提供64个2级STE,从而为每个2级表使用4KB页面。
StreamID到CD(Context Descriptors)
STE包含每个流的配置:来自设备的流量是否使能;是否为1阶段转换;是否为2阶段转换,以及相关的翻译表;哪个数据结构定位1阶段翻译表。如果使用1阶段,STE会指示1到多个内存中的地址。CD将StreamID与1阶段翻译表的基指针、此流的配置、ASID联系起来;如果使用了Substream,就会由多个CD指示多个1阶段翻译,每个CD指向1个substream。提供有SubstreamID的转换在1阶段翻译关闭的情况下会被终止。
如果使用了2阶段转换,STE会包含2阶段翻译表的基指针和VMID;如果多个设备联系到了一个VM,这就意味着他们共享2阶段翻译表,多个STE可能映射到一个2阶段翻译表。ARM期望在有hypervisor时,Stream table和2阶段转换表应该有hypervisor管理,在Guest控制下的与设备相关的CD和1阶段翻译表应该由GuestOS管理,另外,hypervisor可以根据自己的要求使用分离的hypervisor 1阶段翻译。如果没有hypervisor,裸金属OS管理Stream table和CD。
当转换时带了SubstreamID,并且配置也使能了substream,SubstreamID就负责索引1阶段翻译context的CD,在这种配置里,如果转换时没有带SubstreamID,则其转换行为就会根据STE.S1DSS标志位的值:0xb00表示所有流量都应有SubstreamID,没有就会提错误,aborted,然后记录事件;0b01表示没有SubstreamID则bypass掉1阶段翻译;0b10表示没有SubstreamID则用Substream 0的CD,但是当SubstreamID为0时会abort并记录事件。
CD和STE数据结构提供的ASID和VMID值标记TLB条目,这个TLB条目是从CD和STE的配置执行转化查找并创建的;这些标记被用于查找识别不同流的地址空间,或者在接收到TLB维护操作的广播时需要去匹配并失效掉相关条目;具体的实现可能同样使用这些标记来允许识别不同流的翻译表。
上图给出了一个示例配置,StreamID从线性Stream table选出一个STE,这个STE指向一个2阶段的翻译表和一个1阶段配置的CD,然后这个CD只想一个1阶段转换表。下图给出了一个STE只想一个多CD数组的配置,根据输入SubstreamID才会选择一个CD,也就是SubstreamID决定所使用的1阶段翻译表的情况。
下图展示了一个更复杂的层次结构,用上了多级Stream table,其中一个STE指向了CD数组,第二个STE指向了单CD,第三个STE只想了一个多级CD表,基于此多级CD表,可以支持很多流和很多Substream而不需要物理连续的表内存。
一次转换的处理需要一系列逻辑上的步骤:
1. 如果SMMU全局性的关闭了(如SMMU_CR0.SMMUEN==0),转换就会对地址不做处理直通SMMU;全局属性设置如内存类型、共享性可能还用SMMU的SMMU_GBPA寄存器的设置,当然可能SMMU_GBPA寄存器被配置成所有转换都abort;
2. 如果SMMU没关,配置就会由一下情况确定:
a. STE被定位到
b. STE是否使能2阶段翻译,STE还包含2阶段转换表基地址
c. 如果STE使能1阶段翻译,定位到CD,如果STE里2阶段翻译也使能,CD由IPA空间使用2阶段翻译得到,否则CD会从PA地址空间得到。
3. 有转换表,且不是失效的
a. 如果1阶段翻译被配置了,那么CD就有翻译表的基地址,然后去翻译一遍;如果2阶段翻译也在STE里使能了,这可能还需要2阶段翻译。如果1阶段翻译没使能,那就直接bypass掉,然后输入地址直接做2阶段翻译。
b. 如果2阶段翻译被使能了,并且STE使能了1阶段翻译,那么STE就包含一个1阶段转换表地址,然后就会嵌入一个1阶段翻译,然后得到IPA;如果2阶段翻译没使能,2阶段被bypass掉,那么2阶段转换的输入地址就被直接作为输出
4. 一个有效转换意味着没有翻译失败,并且会应用并转发输出地址以及相应的内存属性。
具体的实现可能在上面这些步骤的任意位置做缓存;并且在这些步骤中的很多位置也可能产生转换失败,然后转换就会终止;如果一次转换没有定位到有效的配置或是类型不支持转换都会终止,然后报告事件;根据CD和STE配置,转换在失败时决定终止还是卡住并抛出软件失败结果。
配置和翻译搜索:
下图给出了配置搜索和翻译搜索的概念:
一次转换首先要搜索配置,然后SMMU决定如何开始翻译此次转换,这包含定位到STE,然后如果需要的话,在定位到CD;配置搜索阶段与输入地址无关,与SMMU全局寄存器配置,与要转换的StreamID,与要转换的SubstreamID(如果有的话)相关;配置的搜索结果是特定的stream或substream的配置,能定位翻译相关配置,包含1阶段转换表基指针,ASID和与转换表的解释相关的属性(如翻译粒度);还包含2阶段转换表基指针,VMID和与解释转换表相关属性;流相关的属性,如异常等级、与PE相关翻译域,还是要翻译到PE的内存空间里嘛。翻译搜索阶段与PE的差不多,最后就是输出个物理地址,与输入地址、Stream的安全域一场等级,ASID,VMID等。上图就给出了一个类似PE的使用TLB的翻译搜索步骤,ARM希望SMMU使用TLB来缓存翻译结果,而不是每次转换都要TTW,但是这不是固定的。
一次翻译有StreamWorld属性,表示转换方式,直接等效于PE的一场等级;所有翻译caches都标记了一个StreamWord,好在查找和失效的时候匹配;StreamWorld由插入和查找翻译的配置定义,由STE的安全态:STE.Config、STE.STRW、SMMU_CR2.E2H组合定义;除了插入和查找,StreamWorld/异常等级/翻译方式还会影响不同类型的TLB无效范围。
写到这里,反正我自己已经认为自己对SMMUv3的代码会怎么写有了大致的感觉,本来研究SMMUv3就是要去解掉一个显卡相关的BUG,虽然东西写的乱七八糟,但我又自认为总结完了,是时候去直面这个BUG了!