近年基于arm应用处理器的移动,infrastructure SoC平台都包含SMMUv3 IP(MMU-600,MMU-700),用于DMA mapping,VFIO,Shared Virtual Address(SVA)等场景。
在这些使用场景中,SMMUv3是这样使用的呢?SMMUv3的硬件设计方面比较灵活,但是软件使用方面是有些限制的。本文不是介绍SMMUv3构架或IP,而是站在应用SMMU的角度的十问十答。
本文要求读者对SMMU构架,SMMU driver,VFIO,SVA有一定的理解。
Q1. 多个设备是否可以共用一个SMMU StreamID?
SMMU硬件本身是可以支持多个设备共用一个StreamID的,从而使用同一地址映射关系。
对于PCI(非PCIe)设备,由于一条PCI bus上的PCI设备共享一个requester ID(BDF),而它被用作StreamID,因而这些PCI设备必须共用一个StreamID。
但是Linux SMMU driver的设计不允许多个设备共用一个StreamID (在同一条PCI上的设备除外),SMMU driver会在probe device时,通过device tree或是ACPI table获取该设备的StreamID,
bus_iommu_probe->probe_iommu_group->__iommu_probe_device->arm_smmu_probe_device->arm_smmu_insert_master
https://elixir.bootlin.com/li...
arm_smmu_insert_master将这些StreamID插入这个smmu的全局红黑树中记录。代码可以看出,如果要插入的Stream ID已存在红黑树中,就会出现错误,driver报告
stream n already in tree
利用arm_smmu_find_master(struct arm_smmu_device *smmu, u32 sid)来查找这个红黑树得到一个StreamID对应的master,处理SMMU event时可以得到这个event对应的device.
因此对应同一SMMU上的platform device和PCIe device,smmu driver不允许它们共用一个StreamID。这点SoC构架师需要特别注意。
Q2. 多个设备(多个StreamID)是否可以使用一个iommu group?
smmu driver为每个platform device创建一个单独的iommu group。
对于传统PCI/PCI-X设备,因为它们没有单独的PCIe Requester ID, 因此一条传统PCI总线或者PCI-X总线上的PCI设备都划分到同一个device group。对于PCIe设备,在设备通往PCIe树根节点的路径上,所有的PCIe downstream port和multi-function device都需要通过ACS特性,将peer-to-peer转发的功能关闭。若某个PCIe downstream port或multi-function未通过ACS特性关闭peer-to-peer特性,则下面的所有设备都必须归到同一个iommu group,否则该PCIe设备就可以独立成一个iommu group。
在DMA mapping使用场景里,为每个iommu group分配一个default iommu domain。
VFIO场景下,可以通过vifo_iommu_attach_group->iommu_attach_group来更改为一个新创建的iommu domain。
Q3. SMMU上的device使用的地址映射是否需要和CPU使用的地址映射一样?如何给使用SMMU的device分配ASID和VMID?
SMMU的页表格式和使用方式与CPU的页表非常类似,这是为了可以让device的IOMMU可以共享CPU的页表,因而SMMU的页表中也可以设置ASID和VMID。但是在实际软件使用中,DMA mapping和VFIO的使用场景,设备的IOMMU的页表并不与CPU共享,设备的IOVA-PA的映射是由smmu driver独立建立,与CPU使用的VA-PA映射本不相同。为设备地址转换设置的ASID和VMID主要是为了在IOMMU的TLB里区分不同设备对应的TLB。
一个SMMU上ASID分配是在attach device时,在Xaary里找到一个没使用的ASID,而VMID是通过一个bitmap找到一个没有使用的VMID,与CPU使用ASID和VMID空间无关。
但在shared virtual address (SVA)使用场景中,设备的页表会被设置为共享一个CPU进程的VA-PA映射,因而共享该CPU进程的ASID和VMID。
Q4. SMMU如何处理TLBI Broadcast?
在硬件设计上,一般会SMMU和CPU放在同一个inner shareable domain,比如MMU-700(基于SMMUv3的实现)的TCU总线接口时ACE-Lite+DVM,它通过coherent interconnct与CPU连接。这样的设计让SMP系统更加高效:一个CPU上执行的TLBI操作,可以被硬件broadcast到其他CPU(TLBI broadcast),也可以broadcast到SMMU,让这些共享页表的master 更高效地维护它们TLB里面的内容一致性。
但如上所述,DMA mapping和VFIO使用场景中,设备的ASID和VMID与CPU并不共享。设备的SMMU TLB中具有同样ASID,VMID的TLB项不应该受到CPU发过来的TLBI broadcast的影响。实际上,SMMU driver会设置CD.ASET,
[47] ASET
ASID Set.
Selects type for ASID, between sets shared and non-shared with PE ASIDs. This flag affects broadcast TLB invalidation participation and the scope within which Global TLB entries are matched:
• 0b0: ASID in shared set: This ASID, and address space described by TTB0 and TTB1, are shared with that of a process on the PE. All matching broadcast invalidation messages will invalidate TLB entries created from this context (where supported and globally enabled), keeping SMMU and PE address spaces synchronized.
• 0b1: ASID in non-shared set: TLB entries created from this context are not expected to be invalidated by some broadcast invalidations
因而从CPU发来的带ASID值的TLBI广播(DVM)不会invalidate SMMU TLB中带同样ASID的TLB项。
https://elixir.bootlin.com/li...
在arm_smmu_write_ctx_desc中,
val = cd->tcr |
#ifdef __BIG_ENDIAN
CTXDESC_CD_0_ENDI |
#endif
CTXDESC_CD_0_R | CTXDESC_CD_0_A |
(cd->mm ? 0 : CTXDESC_CD_0_ASET) |
CTXDESC_CD_0_AA64 |
FIELD_PREP(CTXDESC_CD_0_ASID, cd->asid) |
CTXDESC_CD_0_V;
由代码可知,只有在SVA场景下(cd->mm不为空),才会清除CD.ASET, 共享CPU的ASID,从CPU发来的带ASID值的TLBI广播(DVM)会invalidate SMMU TLB中带同样ASID的TLB项。
这点对验证DVM是否可以在系统中是否正常工作需要注意。
Q5. SMMU上device使用stage 1还是stage 2页表?
DMA mapping场景下,使用IOMMU_DOMAIN_DMA domain type,它对应到SMMU的smmu_domain->stage。
https://elixir.bootlin.com/li...
if (domain->type == IOMMU_DOMAIN_IDENTITY) {
smmu_domain->stage = ARM_SMMU_DOMAIN_BYPASS;
return 0;
}
/* Restrict the stage to what we can actually support */
if (!(smmu->features & ARM_SMMU_FEAT_TRANS_S1))
smmu_domain->stage = ARM_SMMU_DOMAIN_S2;
if (!(smmu->features & ARM_SMMU_FEAT_TRANS_S2))
smmu_domain->stage = ARM_SMMU_DOMAIN_S1;
https://elixir.bootlin.com/li...
if (smmu_domain) {
switch (smmu_domain->stage) {
case ARM_SMMU_DOMAIN_S1:
s1_cfg = &smmu_domain->s1_cfg;
break;
case ARM_SMMU_DOMAIN_S2:
case ARM_SMMU_DOMAIN_NESTED:
s2_cfg = &smmu_domain->s2_cfg;
break;
default:
break;
}
在SMMU硬件实现同时支持stage 1和stage 2的情况下,将使用stage 1。
在VFIO使用场景中,如果设置ioctl(container, VFIO_SET_IOMMU, VFIO_TYPE1_IOMMU)则使用stage1;如果设置ioctl(container, VFIO_SET_IOMMU, VFIO_TYPE1_NESTING_IOMMU)则使用stage 2, 这样可以让出stage 1给虚拟机的软件用。
Q6. 一个设备有多个Stream ID会如何呢?
如果一个device在device tree, ACPI table中指定了多个StreamID, 那么arm SMMU driver为这些Stream ID使用一样的STE和CD值,即使用同样的translation。
https://elixir.bootlin.com/li...
static void arm_smmu_install_ste_for_dev(struct arm_smmu_master *master)
{
int i, j;
struct arm_smmu_device *smmu = master->smmu;
for (i = 0; i < master->num_streams; ++i) {
u32 sid = master->streams[i].id;
__le64 *step = arm_smmu_get_step_for_sid(smmu, sid);
/* Bridged PCI devices may end up with duplicated IDs */
for (j = 0; j < i; j++)
if (master->streams[j].id == sid)
break;
if (j < i)
continue;
arm_smmu_write_strtab_ent(master, sid, step);
}
}
Q7. 如何告知smmu driver设备使用的StreamID?
以device tree为例,smmu node信息如下
smmu: iommu@2b400000 {
compatible = "arm,smmu-v3";
reg = <0x0 0x2b400000 0x0 0x100000>;
interrupts = <GIC_SPI 74 IRQ_TYPE_EDGE_RISING>,
<GIC_SPI 79 IRQ_TYPE_EDGE_RISING>,
<GIC_SPI 75 IRQ_TYPE_EDGE_RISING>,
<GIC_SPI 77 IRQ_TYPE_EDGE_RISING>;
interrupt-names = "eventq", "gerror", "priq", "cmdq-sync";
dma-coherent;
#iommu-cells = <1>;
msi-parent = <&its 0x10000>;
};
Platform device 的StreamID设置方式为
master@1 {
/* device has Stream ID 42 in the IOMMU */
iommus = <&{/smmu} 42>;
};
master@2 {
/* device has Stream IDs 23 and 24 in the IOMMU */
iommus = <&{/smmu} 23>, <&{/smmu} 24>;
};
of_iommu_configure->of_iommu_configure_device->of_iommu_configure_dev从iommus,#iommu-cells中获得stream ID。
https://elixir.bootlin.com/li...
while (!of_parse_phandle_with_args(master_np, "iommus",
"#iommu-cells",
idx, &iommu_spec)) {
err = of_iommu_xlate(dev, &iommu_spec);
对于PCIe设备,硬件设计上会将PCIe的requester ID(Bus:Device:Fucntion, BDF)用作产生StreamID。在PCIe Root Complex的device tree node上的iommu-map,iommu-map-mask指定硬件设计PCIe requester ID 与StreamID的对应关系:
iommu-map用于指定requester ID与StreamID的映射关系:
iommu-map = <rid-base,iommu,iommu-base,length>
rid-base是PCIe Requester ID base, iommu是指定用哪个IOMMU,iommu-base是对应的Stream ID base,Length是Requester ID/StreamID 的个数。对应关系是:
StreamID = RequesterID - rid-base + iommu-base
iommu-map-mask用于mask那些RequesterID bit:
iommu-map-mask=<mask>
iommu-map-mask是一个可选的属性,如有iommu-map-mask,对应关系为:
StreamID = (RequesterID &mask) - rid-base + iommu-base
但如上所述,在arm SMMUv3 driver上,如果使用iommu-map-mask可能导致多个PCIe Device Requester ID 得到同一个StreamID,会出现错误,SMMU driver报告stream ‘num’ already in tree
https://elixir.bootlin.com/li...
一个例子:
PCIe Root Complex:
pci: pci@40000000 {
#address-cells = <0x3>;
#size-cells = <0x2>;
#interrupt-cells = <0x1>;
compatible = "pci-host-ecam-generic";
device_type = "pci";
bus-range = <0x0 0x1>;
reg = <0x0 0x40000000 0x0 0x10000000>;
ranges = <0x2000000 0x0 0x50000000 0x0 0x50000000 0x0 0x10000000>;
interrupt-map-mask = <0x0 0x0 0x0 0x7>;
msi-map = <0x0 &its 0x0 0x10000>;
iommu-map = <0x0 &smmu 0x0 0x10000>;
dma-coherent;
};
这个过程是pci_for_each_dma_alias( .., of_pci_iommu_init, …) ->of_pci_iommu_init ->of_iommu_configure_dev_id 从iommu-map, iommu-map-mask中得到BDF对应的streamid
https://elixir.bootlin.com/li...
err = of_map_id(master_np, *id, "iommu-map",
"iommu-map-mask", &iommu_spec.np,
iommu_spec.args);
if (err)
return err == -ENODEV ? NO_IOMMU : err;
err = of_iommu_xlate(dev, &iommu_spec);
Q8. 如何处理软件上没有设置iommu但硬件上连接smmu的情况?设置了iommu的设备使用什么default iommu domain?
系统硬件设计上,为了系统安全或是可能的软件使用场景,除CPU之外的其他很多master都会接在SMMU上。但是如果没有在device tree中指定”iommus”或是“iommu-map”,那么arm SMMU driver会如何设置SMMU处理来自这个device的传输地址转换呢?是bypass SMMU还是被SMMU abort这个传输呢?
它取决于“arm-smmu.disable_bypass” kernel boot cmdline和smmu kernel module的“disable_bypass” 参数,如果disable_bypass被设置,那么smmu abort这些传输,否则bypass这些传输。
对于那些在device tree中指定”iommus”或是“iommu-map”的device,default iommu domain到底是IOMMU_DOMAIN_DMA(需要经过stage1转换),IOMMU_DOMAIN_IDENTITY(bypass SMMU)取决于CONFIG_IOMMU_DEFAULT_PASSTHROUGH和“iommu.passthrough” kernel boot cmdline。“iommu.passthrough=1”让device bypass SMMU地址转换,也就是identity-mapped,让Device可以访问DMA-mapped 页。iommu.passthrough可以override CONFIG_IOMMU_DEFAULT_PASSTHROUGH。
Q9. SMMU使用什么page size和页表格式?
为了方便页表管理和可能的与CPU共享页表的情形,SMMU driver尽量采用与Linux kernel一样的page size。当SMMU硬件不支持CPU使用的page size时,
https://elixir.bootlin.com/li...
static void arm_lpae_restrict_pgsizes(struct io_pgtable_cfg *cfg)
{
unsigned long granule, page_sizes;
unsigned int max_addr_bits = 48;
/*
* We need to restrict the supported page sizes to match the
* translation regime for a particular granule. Aim to match
* the CPU page size if possible, otherwise prefer smaller sizes.
* While we're at it, restrict the block sizes to match the
* chosen granule.
*/
if (cfg->pgsize_bitmap & PAGE_SIZE)
granule = PAGE_SIZE;
Q10. 使用2 level STE/CD table还是linear table?
SMMU driver会检查SMMU硬件是否支持2 level的STE和CD。如不支持,则只能使用linear STE和CD table。如支持,则STE table使用2 level table, split bit被设置为,
#define STRTAB_SPLIT 8
但如果SMMU硬件支持的StreamID bit <=8, 那么使用linear table。
CD table 使用2 level table, split bit被设置为,
#define CTXDESC_SPLIT 10
但如果SMMU硬件支持的SubstreamID bit <=10, 那么使用linear table。