本文主要分析 linux kernel 中 SMMUv3 的代码 (drivers/iommu/arm-smmu-v3.c)
linux kernel 版本是 linux 5.7, 体系结构是 aarch64
smmu 的位置
SMMU 的作用是把 CPU 提交给设备的 VA 地址,直接作为设备发出的地址,变成正确的物理地址,访问到物理内存上。
和 mmu 不同的是,一个 smmu 可以有多个设备连着,他们的页表不可能复用,SMMU 用 stream id 作区分。
一个设备有多个进程,所以 smmu 单元也要支持多页表,smmu 使用 substream id 区分多进程的页表。
smmu 的设备节点定义
在讨论 smmu 的代码前,先看下 smmu 的设备节点是怎么定义的:
Example:
1 | smmu@2b400000 { |
compatible: 用于匹配 smmu 驱动。
reg:smmu 设备的物理基地址。
interrupts: 描述与中断名称对应的 smmu 中断源,上述分别对应中断类型,中断号以及中断触发方式。
interrupt-names: 中断名称。
eventq,当 event queue 从空变为非空状态时上报中断。
priq, 当 pri queue 从空变为非空状态时上报中断。
cmdq-sync, command queue 中 CMDQ_SYNC 命令完成时产生中断。
gerror,event 记录到 event queue 过程中产生的错误会记录在 SMMU_GERROR 寄存器中,并产生中断。
combined,组合中断,需要硬件支持,如果提供了组合中断,则将优先使用组合中断。
dma-coherent:表示设备通过 smmu 进行的 DMA 访问是否 cache coherent 的,假设 DMA 把外设的数据搬运到内存的某个位置,cpu 去读那段地址,因为 cache 命中了,读到的还是旧的值,这就是 cache 的不 coherent。#iommu-cells
: 一个 cell 代表一个 streamid, smmu-v3 必须定义为 1。
msi-parent:指定 msi 中断控制器。
SMMU 结构体
1 | struct arm_smmu_domain { |
arm_smmu_device: 指定 smmu 设备
io_pgtable_ops: io 页表映射定义的一系列操作
non_strict: smmu non-strict 模式,在该补丁集中引入 add non-strict mode support for arm-smmu-v3,
主要是为了解决开启 smmu 后,频繁的 unmap,需要频繁的 invalid tlb 带来的性能损失, 所以不在每一次 unmap 后都进行 tlb invalidate 操作,而是累计一定次数或者时间后执行 invalid all 操作,但这样是有一定的安全风险(页表虽然释放了但是还是在 tlb 中有残留,可能被利用到)。可以通过启动参数控制。
nr_ats_masters: ats 的设备数量,enable_ats 时数量 + 1, disable ats 时数量减 1
arm_smmu_domain_stage: 代表 smmu 支持的方式,支持 stage1 的转换,stage2 的转换,stage1 + stage2 的转换,以及 bypass 模式。
arm_smmu_s1_cfg: stage1 转换需要的数据结构
arm_smmu_s2_cfg: stage2 转换需要的数据结构
smmu 驱动初始化
从 smmu 驱动的 probe 函数开始分析
1 | +->arm_smmu_device_probe() //smmu设备驱动probe入口函数 |
对 probe 中调用的这些函数进行详细分析
(1)arm_smmu_device_dt_probe
1 | static int arm_smmu_device_dt_probe(struct platform_device *pdev, |
a. 读取设备树,看 smmu 的设备节点定义中 #iommu-cells
是否为 1, 如果不为 1 则直接 bypass 掉 smmu
b. parse_driver_options, 主要解析 smmu 是否有需要规避的硬件 bug
c. 解析 smmu 设备中的 dma-coherent 属性
(2) platform_get_irq_byname
1 | /* Interrupt lines */ |
分别获取 dts 节点中定义的 “combined”, “eventq”, “priq”, “gerror” 中断号
(3) arm_smmu_device_hw_probe
该函数主要探测 smmu 设备的硬件规格,主要是通过读 SMMU 的 IDR0,IDR1,IDR5 寄存器确认
SMMU_IDR0:
域段 | offset | 作用 |
---|---|---|
ST_LEVEL | 28: 27 | 确认 stream table 格式是线性 table 还是 2-level table |
TERM_MODEL | 26 | fault 的处理方式, |
STALL_MODEL | 25: 24 | 确认是否是 stall mode。 该模式下 smmu 会暂停引发 stall 的 transaction, 然后 stall,之后根据软件的 commad 是 resume 还是 stall_term 来决定 stall 命令是 retry 还是 terminate。当前只允许 4 种 fault 类型被 stall: F_TRANSLATION, F_ACCESS, F_ADDR_SIZE. F_PERMISSION.【spec 5.5 Fault configuration (A,R,S bits)】 |
TTENDIAN | 22: 21 | 确认 traslation table 支持的大小端模式 |
CD2L | 19 | 确认是否支持 2-level 的 CD table |
VMW | 17 | 用于控制 vmid wildcards 的功能和范围。 vmid wildcard, vmid 的模糊匹配,是为了方便 tlb 无效化, 两种 tlb 无效化的方式:command 和广播 tlb 无效都会使用到 vmid wildcards |
VMID16 | 18 | 确认是否支持 16bit VMID。 每一个虚拟机都被分配一个 ID 号,这个 ID 号用于标记某个特定的 TLB 项属于哪一个 VM。每一个 VM 有它自己的 ASID 空间。例如两个不同的 VMs 同时使用 ASID 5,但指的是不同的东西。对于虚拟机而言,通常 VMID 会结合 ASID 同时使用。 |
PRI | 16 | 确认是否支持 page request interface。 属于 pcie 硬件特性,PCIe 设备可以发出缺页请求,SMMU 硬件在解析到缺页请求后可以直接将缺页请求写入 PRI queueu, 软件在建立好页表后,可以通过 CMD queue 发送 PRI response 给 PCIe 设备。[ Linux SVA 特性分析] |
SEV | 14 | 确认是否支持 WFE wake-up 事件的产生。 当 SEV == 1 时,command queue 从满到非满状态会触发 WFE wake-up event。 此外,当 CMD_SYNC 完成且要求 SIG_SEV 时也会产生 WFE wake-up event |
MSI | 13 | 确认是否支持 MSI 消息中断 |
ASID16 | 12 | 确认是否支持 16bit ASID.。 在 TLB 的表项中,每个项都有一个 ASID,当切换进程的时候,TLB 中可以存在多个进程的页表项, 不再需要清空 TLB,因为 B 进程用不了里面 A 进程的页表项,可以带来性能提升。[TLB flush 操作] |
ATS | 10 | Address Translation Service, 也是 pcie 的硬件特性,有 ATS 能力的 PCIE,自带地址翻译功能,如果它要发出一个地址,进入 PCIE 总线的时候,一定程度上可以认为就是物理地址。ats 会在设备中缓存 va 对应的 pa, 设备随后使用 pa 做内存访问时无需经过 SMMU 页表转换,可以提高性能。 【PCIe/SMMU ATS analysis note】 |
HTTU | 7:6 | Hardware Translation Table Update,在访问或写入相关页面后,硬件自动更新相关页面的 Access flag、Dirty state。该特性和 armv8.1 的 tthm 特性一样,在没有 tthm 特性之前,软件去更新页表的 young 和 dirty page, 会有一定的开销。 |
S1P | 1 | 确认是否支持 stage1 转换,va->pa |
S2P | 0 | 确认是否支持 stage2 转换,ipa->pa |
SMMU_IDR1:
域段 | offset | 作用 |
---|---|---|
TABLES_PRESET | 30 | 确认 smmu_strtab_base 和 smmu_strtab_base_cfg 的基地址是否固定, smmu_strtab_base 是 stream table 的物理基地址,smmu_strtab_base_cfg 是 stream table 的配置寄存器 |
QUEUES_PRESET | 29 | 确认 smmu queue 的基地址是否固定,queue 指的是 smmu event queue, smmu cmd queue, smmu pri queue |
CMDQS | 25:21 | cmd queue 最大 entry 数目, 等于 log2(entries), 最大是 19 |
EVENTQS | 20:16 | event queue 的最大 entry 数目,等于 log2(entries), 最大是 19 |
PRIQS | 15:11 | pri queue 最大 entry 数目,等于 log2(entries), 最大是 19 |
SSIDSIZE | 10:6 | 确认硬件支持 substreamID 的 bit 数,范围为【0,20】, 0 表示不支持 substreamid |
SIDSIZE | 5:0 | 确认硬件支持 streamID 的 bit 数,范围为【0,32】, 0 表示支持一个 stream |
IDR1 主要用来设置 smmu 各个 queue 的 entry 数量, 设置 ssid 和 sid 的 size.
SMMU_IDR5:
域段 | offset | 作用 |
---|---|---|
STALL_MAX | 31:16 | smmu 支持的最大未完成 stall 事务数 |
VAX | 11:10 | 表示 smmu 支持的 va 地址是 48bit 还是 52bit |
GRAN64K | 6 | 支持 64KB 翻译粒度, Translation Granule 表示 translation table 的 size 大小, 页表粒度是 smmu 管理的最小地址空间 |
GRAN16K | 5 | 支持 16KB 翻译粒度 |
GRAN4K | 4 | 支持 4KB 翻译粒度 |
OAS | 2:0 | 表示 output address size, 32bit ~ 52bit |
IDR5 主要设置 smmu ias(input address size) 和 oas (output address size), ias 代表 IPA,oas 代表 PA。
(4) arm_smmu_init_structures()
smmu 相关的数据结构的内存申请和初始化
1 | static int arm_smmu_init_structures(struct arm_smmu_device *smmu) |
(a) arm_smmu_init_queues() 会初始化三个 queue, 分别为 cmd queue, event queue, pri queue.
SMMU 使用这 3 个队列做基本的事件管理。
event queue 用于记录软件配置错误的状态信息,smmu 将配置错误信息记录到 event queue 中,软件会通过从 event queue 读取配置错误信息,然后进行相应的配置错误处理。
软件使用 command queue 和 smmu 硬件进行交互,软件写命令发送到 command queue 中,smmu 会从 command queue 中读取命令进行处理。
pri queue 需要硬件支持 pri 特性,和 event queue 类似,当有相应硬件事件发生时,硬件把相应的描述符写入 pri queue, 然后上报中断。
(b) arm_smmu_init_strtab
1 | static int arm_smmu_init_strtab(struct arm_smmu_device *smmu) |
首先确认 SMMU 的 stream table 的组织方式是线性 table 还是 2-level table.
如果是 linear table:
使用 STRTAB_BASE + sid * 64(一个 STE 的大小为 64B)找到 STE
1 | +-> arm_smmu_init_strtab_linear |
如果是 2-level table:
先通过 sid 的高位找到 L1_STD(STRTAB_BASE + sid[9:8] * 8, 一个 L1_STD 的大小为 8B), L1_STD 定义了下一级查找的基地址,然后通过 sid 找到具体的 STE(l2ptr + sid[7:0] * 64).
结合代码分析:
1 | +-> arm_smmu_init_strtab_2lvl() |
申请 l1 Stream table 的内存,内存大小为 2 ^ (log2Size - split) * 8
申请 L1 STD 的内存, L1 STD 在 stream table 的索引是 streamID[maxbit: split]
配置完 stream table 的结构和各级大小后,再配置 stream table 的基地址:
1 | "配置stream table(STRTAB_BASE_CFG)的RA和addr, addr对应的是stream table的物理地址ra为read allocate hint " |
至此 stream table 的初始化流程结束
(5) arm_smmu_device_reset
该函数主要是进行 smmu 的硬件配置
主要流程如下:
1 | +-> arm_smmu_device_reset() |
再着重讲下 smmu 的中断注册:arm_smmu_setup_irqs()
1 | +-> arm_smmu_setup_irqs() |
arm_smmu_write_msi_msg() 函数里会去:
配置 MSI 中断的目的地址
配置 MSI 的中断数据
配置 MSI 中断的写地址的属性
配置完成后,当中断产生时,最终会进入中断注册的处理函数, 以 gerror 的中断处理为例:
1 | arm_smmu_gerror_handler() |
对于 PCI 设备,ATS,
PRI 和 PASID 的概念同时存在于 PCIe 和 SMMU 规范中。对于 ATS 的介绍可以参考这里:
https://blog.csdn.net/scarecrow_byr/article/details/74276940
。简单讲,ATS 特性由设备侧的 ATC 和 SMMU 侧的 ATS 同时支持,其目的是在设备中缓存 va 对应的 pa,设备随后使用 pa 做内存访问时无需经过 SMMU 页表转换,可以提高性能。
PRI(page request interface) 也是需要设备和 SMMU 一起工作,PCIe 设备可以发出缺页请求,SMMU 硬件在解析到缺页请求后可以直接将缺页请求写入 PRI queueu, 软件在建立好页表后,可以通过CMD queue 发送 PRI response 给 PCIe 设备。具体的 ATS 和 PRI 的实现是硬件相关的,目前市面上还没有实现这两个硬件特性的 PCIe 设备,但是我们可以设想一下 ATS 和 PRI 的硬件实现,最好的实现应该是软件透明的,也就是软件配置给设备 DMA 的访问地址是 va.
软件控制 DMA 发起后,硬件先发起 ATC 请求,从 SMMU 请求该 va 对应的 pa,如果 SMMU 里已经有 va 到 pa 的映射,那么设备可以得到 pa,然后设备再用 pa 发起一次内存访问,该访问将直接访问对应 pa 地址,不在 SMMU 做地址翻译;
如果 SMMU 没有 va 到 pa 的映射, 那么设备得到这个消息后会继续向 SMMU 发 PRI 请求,设备得到从 SMMU 来的 PRI response 后发送内存访问请求,该请求就可以在 SMMU 中翻译得到 pa, 最终访问到物理内存。
PRI 请求是基于 PCIe 协议的, 平台设备无法用 PRI 发起缺页请求。实际上,平台设备是无法靠自身发起缺页请求的,SMMU 用 stall 模式支持平台设备的缺页,当一个平台设备的内存访问请求到达 SMMU 后,如果 SMMU 里没有为 va 做到 pa 的映射,硬件会给 SMMU 的 event queue 里写一个信息,SMMU 的 event queue 中断处理里可以做缺页处理,然后 SMMU 可以回信息给设备 (fix me: 请求设备重发,还是 smmu 缺页处理后已经把该访问翻译后送到上游总线)。
实际上, SMMU 使用 event queue 来处理各种错误异常,这里的 stall 模式是借用了 event queue 来处理缺页。
(6) iommu_device_register
注册 iommu 设备, 主要设计一个操作,就是将 smmu 设备添加到 iommu_device_list 中
1 | int iommu_device_register(struct iommu_device *iommu) |
着重讲下和 iommu_device 相关的两个重要数据结构 iommu_group 和 iommu_domain
看下 iommu_device 结构体的定义
1 | struct iommu_device { |
iommu_device 中定义了 iommu_ops 以及 struct device,
在 struct device 中,有 iommu_group 的成员,iommu_group 又包含了 iommu_domain。
iommu_device->device->iommu_group->iommu_domain
iommu_domain 的具体定义:
1 | struct iommu_domain { |
每一个 domain 代表一个具体的设备使用 iommu 的详细 spec
在 arm_smmu_domain 结构体中,又将 arm_smmu_domain 和 iommu_domain 关联, 所以 iommu_ops 指向 SMMU 驱动。所以最终 ARM 是用 arm_smmu_domain 来管理驱动和设备之间的关联的。
iommu_group 的具体定义:[drivers/iommu/iommu.c: iommu_group]
1 | struct iommu_group { |
为什么会有一个 iommu_group 的概念,直接将 device 和 iommu_domain 关联不香吗?
假设我们通过 iommu 提供设备的 DMA 能力,当发起 dma_map 的时候,设备设置了 streamid, 但是多个设备的 streamid 有可能是一样的。 那么这时候修改其中一个设备的页表体系,也就相当于修改了另一个设备的页表体系。所以,修改页表的最小单位不是设备,而是 streamid。
因此,为了避免这种情况,增加了一个 iommu_group 的概念,iommu_group 代表共享同一个 streamid 的一组 device(表述在 / sys/kernel/iommu_group 中)。
有了 iommu_group, 设备发起 dma_map 操作时,会定位 streamid 和 iommu_group, group 定位了 iommu_device 和 iommu_domain,iommu_domain 定位了 asid,这样,硬件要求的全部信息都齐了。
(Linux iommu 和 vfio 概念空间解构)
(7) arm_smmu_set_bus_ops
给 smmu 支持的总线设置 bus->iommu_ops, 让总线具有了 iommu attach 的能力。
1 | arm_smmu_set_bus_ops(&arm_smmu_ops) |
arm_smmu_ops 结构体定义如下:
1 | static struct iommu_ops arm_smmu_ops = { |
主要分析 smmu 的两个关键操作:arm_smmu_attach_dev 和 arm_smmu_add_device
arm_smmu_add_device: 将 smmu 设备添加到总线
arm_smmu_add_device()
1 | +-> smmu = arm_smmu_get_by_fwnode(fwspec->iommu_fwnode); |
总线扫描发现了设备,总线的发现流程负责调用 iommu_ops(arm_smmu_ops) 给这个设备加上 iommu_group,然后让 iommu_group 指向对应的 iommu 控制器
arm_smmu_attach_dev, 尝试为设备寻找到驱动
1 | arm_smmu_attach_dev() |
支持了 2-leveli 的 CD 或 linear 格式的 CD, 方式和 SID 查找 ste 类似。
结合代码分析:
1 | static int arm_smmu_alloc_cd_tables(struct arm_smmu_domain *smmu_domain) |
在 CD 的建立过程中,主要涉及到以下几点:
ste.S1Contextptr 中定义了 CD 的基地址, CD 的大小为 64byte
a. 需要配置 ste.S1CDMax, cdmax 为 0 表示这个 ste 只有一个 CD, 不需要使用到 substreamid, 如果 cdmax 不为 0, 那么 CD 的数目是 2 ^ S1CDMax;
b. 需要配置 ste.S1Fmt, 如果是 linear 结构的 CD,CD 的获取方法为 S1ContextPTR + 64 * ssid; 如果是 2-level 结构的 CD, L1CD 的索引为 ssid[s1cdmax - 1: 6], L2CD 的索引为 ssid[5:0]
attach_dev 完成后,如果是 stage1 相关,CD 的结构, 大小和基地址已经成功建立,成功获取 STE 后,可以通过 substreamid 找到 CD(S1ContextPTR + 64 * ssid)。找到的 CD 中包含页表 PTW 需要的 TTBR 寄存器,所以每一个 CD 对应一个页表, 这样一个 SMMU 单元,就可以有多张页表。
总结:
smmu 驱动的初始化流程就是一个探测硬件规格,初始化硬件配置,分配 STD/STE/CD 等空间的过程。
参考资料
Linux iommu 和 vfio 概念空间解构
IHI0070_System_Memory_Management_Unit_Arm_Architecture_Specification