0%

arm smmuv3 代码分析

本文主要分析 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
2
3
4
5
6
7
8
9
10
11
12
smmu@2b400000 {
compatible = "arm,smmu-v3";
reg = <0x0 0x2b400000 0x0 0x20000>;
interrupts = <GIC_SPI 74 IRQ_TYPE_EDGE_RISING>,
<GIC_SPI 75 IRQ_TYPE_EDGE_RISING>,
<GIC_SPI 77 IRQ_TYPE_EDGE_RISING>,
<GIC_SPI 79 IRQ_TYPE_EDGE_RISING>;
interrupt-names = "eventq", "priq", "cmdq-sync", "gerror";
dma-coherent;
#iommu-cells = <1>;
msi-parent = <&its 0xff0000>;
};

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct arm_smmu_domain {
struct arm_smmu_device *smmu;
struct mutex init_mutex; /* Protects smmu pointer */

struct io_pgtable_ops *pgtbl_ops;
bool non_strict;
atomic_t nr_ats_masters;

enum arm_smmu_domain_stage stage;
union {
struct arm_smmu_s1_cfg s1_cfg;
struct arm_smmu_s2_cfg s2_cfg;
};

struct iommu_domain domain;

struct list_head devices;
spinlock_t devices_lock;
};

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
2
3
4
5
6
7
8
+->arm_smmu_device_probe() //smmu设备驱动probe入口函数
+-> arm_smmu_device_dt_probe() //smmu设备树解析
+-> platform_get_irq_byname() // smmu设备中断解析
+-> arm_smmu_device_hw_probe() // smmu硬件规格探测
+-> arm_smmu_init_structures() //smmu 数据结构初始化
+-> arm_smmu_device_reset() // smmu设备复位, 硬件初始化配置
+-> iommu_device_register() // iommu设备注册
+-> arm_smmu_set_bus_ops() // 给支持的总线设置bus->iommu_ops

对 probe 中调用的这些函数进行详细分析

(1)arm_smmu_device_dt_probe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int arm_smmu_device_dt_probe(struct platform_device *pdev,
struct arm_smmu_device *smmu)
{
int ret = -EINVAL;
if (of_property_read_u32(dev->of_node, "#iommu-cells", &cells)) ---- (a)
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); ----- (b)

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

return ret;
}

a. 读取设备树,看 smmu 的设备节点定义中 #iommu-cells 是否为 1, 如果不为 1 则直接 bypass 掉 smmu

b. parse_driver_options, 主要解析 smmu 是否有需要规避的硬件 bug

c. 解析 smmu 设备中的 dma-coherent 属性

(2) platform_get_irq_byname

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* Interrupt lines */

irq = platform_get_irq_byname_optional(pdev, "combined");
if (irq > 0)
smmu->combined_irq = irq;
else {
irq = platform_get_irq_byname_optional(pdev, "eventq");
if (irq > 0)
smmu->evtq.q.irq = irq;

irq = platform_get_irq_byname_optional(pdev, "priq");
if (irq > 0)
smmu->priq.q.irq = irq;

irq = platform_get_irq_byname_optional(pdev, "gerror");
if (irq > 0)
smmu->gerr_irq = irq;
}

分别获取 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
2
3
4
5
6
7
8
9
10
11
static int arm_smmu_init_structures(struct arm_smmu_device *smmu)
{
int ret;

ret = arm_smmu_init_queues(smmu); ----------------- (a)
if (ret)
return ret;

return arm_smmu_init_strtab(smmu); ----------------- (b)

}

(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
}

首先确认 SMMU 的 stream table 的组织方式是线性 table 还是 2-level table.
如果是 linear table:

image-20240416105935403

使用 STRTAB_BASE + sid * 64(一个 STE 的大小为 64B)找到 STE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+-> arm_smmu_init_strtab_linear
// 计算stream table的size, 如果使用linear 查找,stream table的size = sid * 64(sid表示有多少个ste, 一个STE的大小为64B)
+-> size = (1 << smmu->sid_bits) * (STRTAB_STE_DWORDS << 3);
// 申请Stream table的内存
+-> strtab = dmam_alloc_coherent()
// 配置stream table(STRTAB_BASE_CFG)的format, 决定stream table的格式是linear
+-> reg = FIELD_PREP(STRTAB_BASE_CFG_FMT, STRTAB_BASE_CFG_FMT_LINEAR);
// 配置stream table(STRTAB_BASE_CFG)的log2size, ste的entry数目是2 ^ log2size
+-> reg |= FIELD_PREP(STRTAB_BASE_CFG_LOG2SIZE, smmu->sid_bits);
// cfg->num_l1_ents对应的是sid, 对SMMU下的所有sid逐一调用arm_smmu_write_strtab_ent
+-> arm_smmu_init_bypass_stes(strtab, cfg->num_l1_ents)
+-> arm_smmu_write_strtab_ent()
// 发送CMDQ_OP_PREFETCH_CFG
+-> arm_smmu_cmdq_issue_cmd()

如果是 2-level table:

image-20240416105955813

先通过 sid 的高位找到 L1_STD(STRTAB_BASE + sid[9:8] * 8, 一个 L1_STD 的大小为 8B), L1_STD 定义了下一级查找的基地址,然后通过 sid 找到具体的 STE(l2ptr + sid[7:0] * 64).

结合代码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
+-> arm_smmu_init_strtab_2lvl()
"计算l1的大小, 一个l1 std的大小为8byte, 对应的l1_std = sid[maxbit:split], maxbit是log2Size - 1, 所以l1的大小等于2 ^ (log2Size - split) * 8 "
+-> l1size = cfg->num_l1_ents * (STRTAB_L1_DESC_DWORDS << 3);
"申请L1 stream table的空间"
+-> strtab = dmam_alloc_coherent()
"配置stream table(STRTAB_BASE_CFG)的format, 决定stream table的格式是2-level"
+-> reg = FIELD_PREP(STRTAB_BASE_CFG_FMT, STRTAB_BASE_CFG_FMT_2LVL);
"配置stream table(STRTAB_BASE_CFG)的log2size,2级ste的entry是2 ^ log2size, l1 std的 entry大小为2 ^ (log2size - split)"
+-> reg |= FIELD_PREP(STRTAB_BASE_CFG_LOG2SIZE, size);
"配置stream table(STRTAB_BASE_CFG)的split, split的值可以被配置为6/8/10, 分别对应l1 std能够指向的最大二级ste的空间为4k/16k/64k"
+-> reg |= FIELD_PREP(STRTAB_BASE_CFG_SPLIT, STRTAB_SPLIT);
" 分配L1STD的内存, 并配置L1 descriptor的SPAN,SPAN表示L2 table包含多少个STE "
+-> arm_smmu_init_l1_strtab()

申请 l1 Stream table 的内存,内存大小为 2 ^ (log2Size - split) * 8
申请 L1 STD 的内存, L1 STD 在 stream table 的索引是 streamID[maxbit: split]
配置完 stream table 的结构和各级大小后,再配置 stream table 的基地址:

1
2
3
	"配置stream table(STRTAB_BASE_CFG)的RA和addr, addr对应的是stream table的物理地址ra为read allocate hint "
+-> reg = smmu->strtab_cfg.strtab_dma & STRTAB_BASE_ADDR_MASK;
+-> reg |= STRTAB_BASE_RA;

至此 stream table 的初始化流程结束

(5) arm_smmu_device_reset

该函数主要是进行 smmu 的硬件配置
主要流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
+-> arm_smmu_device_reset()
"写SMMU_CR0来disable smmu,并通过SMMU_CR0ACK检查CR0是否被clear"
+-> arm_smmu_device_disable()
"配置读取ste和command queue的属性"
+-> writel_relaxed(ARM_SMMU_CR1);
"random crap"
+-> writel_relaxed(ARM_SMMU_CR2);
"配置 ARM_SMMU_STRTAB_BASE 寄存器,分别对应stream table的物理基地址以及格式,大小等"
+->writeq_relaxed(smmu->strtab_cfg.strtab_base, ARM_SMMU_STRTAB_BASE);
+->writel_relaxed(smmu->strtab_cfg.strtab_base_cfg, ARM_SMMU_STRTAB_BASE);
/* 配置cmd queue相关寄存器
* ARM_SMMU_CMDQ_BASE 是配置command queue的基地址
* ARM_SMMU_CMDQ_PROD, 可以表示取出命令的位置
* ARM_SMMU_CMDQ_CONS, 可以表示输入命令的位置
* ARM_SMMU_CMDQ_PROD和ARM_SMMU_CMDQ_CONS初始化时配置为相同的值,都为0
* 通过CMDQ_PROD和CMDQ_CONS, 可以判断command queue是否还有空间
*/
+-> writeq_relaxed(smmu->cmdq.q.q_base, smmu->base + ARM_SMMU_CMDQ_BASE);
+-> writel_relaxed(smmu->cmdq.q.llq.prod, smmu->base + ARM_SMMU_CMDQ_PROD);
+-> writel_relaxed(smmu->cmdq.q.llq.cons, smmu->base + ARM_SMMU_CMDQ_CONS);
"最后配置command queue的en,对command queue进行使能"
+-> enables = CR0_CMDQEN;
"配置event queue相关寄存器, 流程和command queue类似"
+-> config event queue
"如果支持pri, 则配置pri queue相关寄存器, 流程和上面一致"
+-> config pri queue
"申请并使能smmu支持的相关中断(eventq irq, priq irq, gerror irq)"
+-> arm_smmu_setup_irqs()
"enable smmu, 写SMMU_CR0,并通过SMMU_CR0ACK检查CR0是否被enable"
+-> arm_smmu_write_reg_sync(smmu, enables, ARM_SMMU_CR0, ARM_SMMU_CR0ACK);

再着重讲下 smmu 的中断注册:arm_smmu_setup_irqs()

1
2
3
4
5
+-> arm_smmu_setup_irqs()
+-> arm_smmu_setup_unique_irqs()
+-> arm_smmu_setup_msis(smmu);
+-> arm_smmu_write_msi_msg()
+-> devm_request_irq(smmu->dev, irq, arm_smmu_gerror_handler, 0, "arm-smmu-v3-gerror", smmu);

arm_smmu_write_msi_msg() 函数里会去:

配置 MSI 中断的目的地址
配置 MSI 的中断数据
配置 MSI 中断的写地址的属性
配置完成后,当中断产生时,最终会进入中断注册的处理函数, 以 gerror 的中断处理为例:

1
2
3
4
5
6
arm_smmu_gerror_handler()
"读gerrorgerrorrn寄存器,确认gerror中断发生的错误类型"
+-> gerror = readl_relaxed(smmu->base + ARM_SMMU_GERROR);
+-> gerrorn = readl_relaxed(smmu->base + ARM_SMMU_GERRORN);
"完成中断处理后,写gerror和gerrorn对应的的位一致,global中断处理完成"
+-> writel(gerror, smmu->base + ARM_SMMU_GERRORN);

对于 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
2
3
4
5
6
7
int iommu_device_register(struct iommu_device *iommu)
{
spin_lock(&iommu_device_lock);
list_add_tail(&iommu->list, &iommu_device_list);
spin_unlock(&iommu_device_lock);
return 0;
}

着重讲下和 iommu_device 相关的两个重要数据结构 iommu_group 和 iommu_domain
看下 iommu_device 结构体的定义

1
2
3
4
5
6
struct iommu_device {
struct list_head list;
const struct iommu_ops *ops;
struct fwnode_handle *fwnode;
struct device *dev;
};

iommu_device 中定义了 iommu_ops 以及 struct device,
在 struct device 中,有 iommu_group 的成员,iommu_group 又包含了 iommu_domain。
iommu_device->device->iommu_group->iommu_domain

iommu_domain 的具体定义:

1
2
3
4
5
6
7
8
9
struct iommu_domain {
unsigned type;
const struct iommu_ops *ops;
unsigned long pgsize_bitmap; /* Bitmap of page sizes in use */
iommu_fault_handler_t handler;
void *handler_token;
struct iommu_domain_geometry geometry;
void *iova_cookie;
};

每一个 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
2
3
4
5
6
7
8
9
10
11
12
13
struct iommu_group {
struct kobject kobj;
struct kobject *devices_kobj;
struct list_head devices;
struct mutex mutex;
struct blocking_notifier_head notifier;
void *iommu_data;
void (*iommu_data_release)(void *iommu_data);
char *name;
int id;
struct iommu_domain *default_domain;
struct iommu_domain *domain;
};

为什么会有一个 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
2
3
4
arm_smmu_set_bus_ops(&arm_smmu_ops)
+-> bus_set_iommu(&pci_bus_type, ops);
+-> bus_set_iommu(&amba_bustype, ops);
+-> bus_set_iommu(&platform_bus_type, ops);

arm_smmu_ops 结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 的两个关键操作:arm_smmu_attach_dev 和 arm_smmu_add_device
arm_smmu_add_device: 将 smmu 设备添加到总线

arm_smmu_add_device()

1
2
3
4
5
6
7
8
9
+-> smmu = arm_smmu_get_by_fwnode(fwspec->iommu_fwnode);
/* for each sid, 如果是2-level ste, 为l2 ste分配内存
*在之前的init_l1_strtab, 已经初始化了L1_std, L1_STD定义了下一级查找的基地址,
* 现在可以通过sid 找到具体的STE(l2ptr + sid[7:0] * 64
* 这个函数先为每一个sid分配L2_STE的内存, 分配完成后在为每一个SID进行cfg配置
*/
+-> arm_smmu_init_l2_strtab()
"将device和group关联起来"
+-> iommu_device_link()

总线扫描发现了设备,总线的发现流程负责调用 iommu_ops(arm_smmu_ops) 给这个设备加上 iommu_group,然后让 iommu_group 指向对应的 iommu 控制器

arm_smmu_attach_dev, 尝试为设备寻找到驱动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
arm_smmu_attach_dev()
" 从iommu_domain 中得到arm_smmu_domain"
+-> smmu_domain = to_smmu_domain(iommu_domain );
"一般情况下smmu_domain->smmu = NULL"
"在arm_smmu_add_device中,我们已经为STE项分配了内存"
+-> arm_smmu_domain_finalise(domain, master);
"分配asid"
+-> asids = arm_smmu_bitmap_alloc()
"根据smmu stage是stage1还是stage2, 如果smmu domain是stage1"
+-> arm_smmu_domain_finalise_s1()
"分配CD table的空间"
+-> arm_smmu_alloc_cd_tables(smmu_domain);
"配置CD descriptor的cfg"
+-> cfg->cd.tcr = FIELD_PREP(CTXDESC_CD_0_XXX)...
"如果smmu domain是stage2, STE已经包含了页表的s2ttb基地址和vmid,结束"
+-> arm_smmu_domain_finalise_s2()
+-> finalise_stage_fn(smmu_domain, master, &pgtbl_cfg);

支持了 2-leveli 的 CD 或 linear 格式的 CD, 方式和 SID 查找 ste 类似。

image-20240416110013071

结合代码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
static int arm_smmu_alloc_cd_tables(struct arm_smmu_domain *smmu_domain)
{
int ret;
size_t l1size;
size_t max_contexts;
struct arm_smmu_device *smmu = smmu_domain->smmu;
struct arm_smmu_s1_cfg *cfg = &smmu_domain->s1_cfg;
struct arm_smmu_ctx_desc_cfg *cdcfg = &cfg->cdcfg;

max_contexts = 1 << cfg->s1cdmax; -------------- (a)

if (!(smmu->features & ARM_SMMU_FEAT_2_LVL_CDTAB) ||
max_contexts <= CTXDESC_L2_ENTRIES) {
cfg->s1fmt = STRTAB_STE_0_S1FMT_LINEAR; -------- (b)
cdcfg->num_l1_ents = max_contexts;

l1size = max_contexts * (CTXDESC_CD_DWORDS << 3); ------- (c)
} else {
cfg->s1fmt = STRTAB_STE_0_S1FMT_64K_L2;
cdcfg->num_l1_ents = DIV_ROUND_UP(max_contexts,
CTXDESC_L2_ENTRIES);

cdcfg->l1_desc = devm_kcalloc(smmu->dev, cdcfg->num_l1_ents,
sizeof(*cdcfg->l1_desc),
GFP_KERNEL);
if (!cdcfg->l1_desc)
return -ENOMEM;

l1size = cdcfg->num_l1_ents * (CTXDESC_L1_DESC_DWORDS << 3);
}

cdcfg->cdtab = dmam_alloc_coherent(smmu->dev, l1size, &cdcfg->cdtab_dma,
GFP_KERNEL);
if (!cdcfg->cdtab) {
dev_warn(smmu->dev, "failed to allocate context descriptor\n");
ret = -ENOMEM;
goto err_free_l1;
}

return 0;

err_free_l1:
if (cdcfg->l1_desc) {
devm_kfree(smmu->dev, cdcfg->l1_desc);
cdcfg->l1_desc = NULL;
}
return ret;
}

在 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 等空间的过程。

image-20240416110053027

参考资料
Linux iommu 和 vfio 概念空间解构
IHI0070_System_Memory_Management_Unit_Arm_Architecture_Specification