27

修志龙_ZenonXiu · 2022年11月05日 · 上海市浦东新区

SMMUv3实际使用十问十答

近年基于arm应用处理器的移动,infrastructure SoC平台都包含SMMUv3 IP(MMU-600,MMU-700),用于DMA mapping,VFIO,Shared Virtual Address(SVA)等场景。
v2-2fd7afc1e1898053f4671cd423bba909_hd.jpg
在这些使用场景中,SMMUv3是这样使用的呢?SMMUv3的硬件设计方面比较灵活,但是软件使用方面是有些限制的。本文不是介绍SMMUv3构架或IP,而是站在应用SMMU的角度的十问十答。
本文要求读者对SMMU构架,SMMU driver,VFIO,SVA有一定的理解。

1.jpg

2.jpg

Q1. 多个设备是否可以共用一个SMMU StreamID?

图片.png
0861980accb343328835f4e359cc63b0.png
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。

推荐阅读
关注数
8647
内容数
61
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息