从virtio看iommu和DMA的关系

从virtio看iommu和DMA的关系

 ——lvyilong316

上一篇iommu的文章主要介绍了 passthough:http://blog.chinaunix.net/uid-28541347-id-5868588.html,这篇文章主要从virtio和ADMA的角度讲述一下iommu中的一些逻辑。

做虚拟化或者网络的人对virtio,iommu或者DMA这些概念都不陌生,但是其中的关联却又有很多人不是很明白,比如在裸金属或物理机上支持虚拟机或安全容器需要开启iommu,那虚拟机前端不支持VIRTIO_F_ACCESS_PLATFORM是否有影响呢?这里就是把iommu和viommu搞混了。又比如物理机开启或关闭iommu对应virtio设备的处理逻辑有什么影响?这篇文章主要就是把这些问题讨论清楚。

问题1:如果使用设备直通方式在物理机上启动虚拟机,为什么需要物理机开启iommu?

这个问题比较简单,物理机开启iommu主要是为了避免直通给虚拟机A的外设DMA到虚拟机B的内存,所以不直接使用hpa,而使用iova(gpa)进行DMA,这样每个虚拟机用自己的iova,iommu确保其转换后之会访问自己对应的内存。

问题2:iommu是否开启对前端驱动的处理逻辑有什么影响?

我们知道开启iommu后,设备发起DMA操作会经过如下图流程,根据TLP中的bdf找到应对的context entry进行地址转换。那么对于前端的驱动软件处理行为有什么差异呢?

 

其实,iommu对于驱动软硬的影响主要是在进行DMA操作或者说进行DMA地址映射时使用的地址差异(直接使用物理地址还是iova)。 我们以ixgbe的发送函数ixgbe_tx_map为例:

点击(此处)折叠或打开

    static void ixgbe_tx_map(struct ixgbe_ring *tx_ring,
                 struct ixgbe_tx_buffer *first,
                 const u8 hdr_len)
    {
        //...
        dma = dma_map_single(tx_ring->dev, skb->data, size, DMA_TO_DEVICE);
        for (frag = &skb_shinfo(skb)->frags[0];; frag++) {
            tx_desc->read.buffer_addr = cpu_to_le64(dma);

            while (unlikely(size > IXGBE_MAX_DATA_PER_TXD)) {
                i++;
                tx_desc++;
                tx_desc->read.buffer_addr = cpu_to_le64(dma);
            }
    //...
    }

核心逻辑就是先把skb的地址做一下dma map,然后让硬件可以直接dma这段数据,其具体后续调用过程如下图:

 

之前iommu文章中介绍过在intel环境iommu初始化会把pci bus的iommu ops设置成intel_dma_ops,所以map_page函数{BANNED}最佳终会调用到intel_map_page。

 __intel_map_single

点击(此处)折叠或打开

    static dma_addr_t __intel_map_single(struct device *dev, phys_addr_t paddr,
                     size_t size, int dir, u64 dma_mask)
    {
        struct dmar_domain *domain;
        phys_addr_t start_paddr;
        unsigned long iova_pfn;
        int prot = 0;
        int ret;
        struct intel_iommu *iommu;
        unsigned long paddr_pfn = paddr >> PAGE_SHIFT;

        BUG_ON(dir == DMA_NONE);

        if (iommu_no_mapping(dev))
            return paddr;

        domain = get_valid_domain_for_dev(dev);
        if (!domain)
            return 0;

        iommu = domain_get_iommu(domain);
        size = aligned_nrpages(paddr, size);

        iova_pfn = intel_alloc_iova(dev, domain, dma_to_mm_pfn(size), dma_mask);
        if (!iova_pfn)
            goto error;

        /*
         * Check if DMAR supports zero-length reads on write only
         * mappings..
         */
        if (dir == DMA_TO_DEVICE || dir == DMA_BIDIRECTIONAL || \
                !cap_zlr(iommu->cap))
            prot |= DMA_PTE_READ;
        if (dir == DMA_FROM_DEVICE || dir == DMA_BIDIRECTIONAL)
            prot |= DMA_PTE_WRITE;
        /*
         * paddr - (paddr + size) might be partial page, we should map the whole
         * page. Note: if two part of one page are separately mapped, we
         * might have two guest_addr mapping to the same host paddr, but this
         * is not a big problem
         */
        ret = domain_pfn_mapping(domain, mm_to_dma_pfn(iova_pfn),
                     mm_to_dma_pfn(paddr_pfn), size, prot);
        if (ret)
            goto error;

        /* it's a non-present to present mapping. Only flush if caching mode */
        if (cap_caching_mode(iommu->cap))
            iommu_flush_iotlb_psi(iommu, domain,
                     mm_to_dma_pfn(iova_pfn),
                     size, 0, 1);
        else
            iommu_flush_write_buffer(iommu);

        start_paddr = (phys_addr_t)iova_pfn << PAGE_SHIFT;
        start_paddr += paddr & ~PAGE_MASK;
        return start_paddr;

        return 0;
    }

首先会判断是否为iommu_no_mapping,如果是则直接返回paddr也即物理地址。再来看看iommu_no_mapping这个函数的具体逻辑。

iommu_no_mapping

点击(此处)折叠或打开

    /* Check if the dev needs to go through non-identity map and unmap process.*/
    static int iommu_no_mapping(struct device *dev)
    {
        int found;

        if (iommu_dummy(dev))
            return 1;

        if (!iommu_identity_mapping)
            return 0;

        found = identity_mapping(dev);
        if (found) {
            if (iommu_should_identity_map(dev, 0))
                return 1;
            else {
                /*
                 * 32 bit DMA is removed from si_domain and fall back
                 * to non-identity mapping.
                 */
                dmar_remove_one_dev_info(si_domain, dev);
                pr_info("32bit %s uses non-identity mapping\n",
                    dev_name(dev));
                return 0;
            }
        } else {
            /*
             * In case of a detached 64 bit DMA device from vm, the device
             * is put into si_domain for identity mapping.
             */
            if (iommu_should_identity_map(dev, 0)) {
                int ret;
                ret = domain_add_dev_info(si_domain, dev);
                if (!ret) {
                    pr_info("64bit %s uses identity mapping\n",
                        dev_name(dev));
                    return 1;
                }
            }
        }

        return 0;
    }

从实现来看,首先会判断iommu_identity_mapping是否为空(如果当iommu=pt的时候这个变量是不会为空的)如果为空则返回false,这里先看一下不为空的逻辑。接着函数走到identity_mapping,这个函数的实现具体如下:

identity_mapping

点击(此处)折叠或打开

    static int identity_mapping(struct device *dev)
    {
        struct device_domain_info *info;

        if (likely(!iommu_identity_mapping))
            return 0;

        info = dev->archdata.iommu;
        if (info && info != DUMMY_DEVICE_DOMAIN_INFO)
            return (info->domain == si_domain);

        return 0;
    }

可以看到函数里面首先判断iommu_identity_mapping是否为空,那么在iommut=pt的情况下这个是不为空的,然后判断设备的domain是否为si_domain,当然这个答案也是肯定的。因此这个函数返回值为true,接着函数走到iommu_should_identity_map(dev, 0),那么这个函数主要的判断如下:

1. 如果这个设备不是pci设备且这个设备有RMRR,则返回False.

2. 如果这个设备是pci设备,则下面几种情况会返回False:

(1)这个pci设备有rmrr

(2)iommu_identity_mapping 的值不是IDENTMAP_ALL

(3)是pci设备但不是pcie设备,则如果设备不是 root bus 或者说pci设备的种类是pci bridge

(4)是pcie设备且pcie 设备是pcie bridge

3. 如果这个设备是32bit的设备则返回false

如果这个函数返回false则需要从si_domain里面把这个设备的mapping删除掉,如果返回True则直接返回物理地址。所以总结一下在iommu=pt的场景下,由于静态映射的存在所以直接返回paddr。为什么能够直接返回物理地址而不是iova呢?这里我们再详细地介绍一下,我们先来看一下si_domain的初始化:

si_domain_init

点击(此处)折叠或打开

    static int __init si_domain_init(int hw)
    {
        int nid, ret = 0;

        si_domain = alloc_domain(DOMAIN_FLAG_STATIC_IDENTITY);
        if (!si_domain)
            return -EFAULT;

        if (md_domain_init(si_domain, DEFAULT_DOMAIN_ADDRESS_WIDTH)) {
            domain_exit(si_domain);
            return -EFAULT;
        }

        pr_debug("Identity mapping domain allocated\n");

        if (hw)
            return 0;

        for_each_online_node(nid) {
            unsigned long start_pfn, end_pfn;
            int i;

            for_each_mem_pfn_range(i, nid, &start_pfn, &end_pfn, NULL) {
                ret = iommu_domain_identity_map(si_domain,
                        PFN_PHYS(start_pfn), PFN_PHYS(end_pfn));
                if (ret)
                    return ret;
            }
        }

        return 0;
    }

首先,hw这个参数输入为hw_pass_through,它指的是iommu硬件上是否支持paas through翻译模式即iova就是真实的物理地址不需要再走一遍从iova转换到hpa的流程。那么从上面的函数实现也能看到如果hw为true则si_domain不会再去做相关内存mapping(关于hw为false的情况后面我们再分析),也就是说如果iommu硬件支持hw且iommu配置了pt则这种场景下硬件的DMA到达iommu之后不需要走页表翻译直接跟memory controller进行交互就可以了。但是iommu硬件是如何知道哪些设备的dma要走页表进行转换,哪些设备的dma不需要进行地址转换呢?答案在iommu硬件单元的contex_entry中,设备在通过bus号在root table里面找到相应的root_entry,然后再通过devfn在context table里面找到对应的context_entry,然后才能找到真正的页表。而从vt-d的spec来看,contex_entry的format里面有一个标志位(TT)来表明这个设备的DMA是否是paasthroug。而这个TT位是在设备添加到iommu_domain中,即domain_add_dev_info 这个函数并{BANNED}最佳终走到domain_context_mapping_one设置的,这里不再展开。

上面主要理了一下在iommu=pt,hw为true的情况;如果hw为false的情况又会怎么样呢?具体的逻辑还是要从init_dmars这个函数开始看起,通过分析可以看到因为hw=false也就是说iommu硬件不支持paas through 的translation type,所以必须要是创建页表的,但是因为是静态映射即iova就等于hpa,所以在这种情况下也是可以直接返回paddr的,但是效率肯定是没法跟hw=true相比的。

聊完iommu=pt的各种情况之后,我们再看一下iommu为默认设置的情况下设备是如何进行dma操作的。还是先要从intel_iommu_init这个函数里面的init_dmars看起,从这相关的逻辑来看区别在于不会提前创建si_domain(即提前做好iova的映射),那它是在什么时候创建的呢?答案是在dma_map的时候而且dma map相关的api返回的iova,如果大家感兴趣可以去仔细读一下__intel_map_single这个函数。

问题3:什么情况虚拟机支持需要支持iommu?

首先是虚拟机支持iommu,这种方式一般是通过qemu模拟viommu,并且前后端协商VIRTIO_F_ACCESS_PLATFORM这个feature。不过虚拟机一般是不需要支持iommu,除非在类似vhost-user场景的安全考虑,防止后端被攻陷,比如vswitch被控制,由于后端vhost-user map了所有虚拟机的内存,所以可以进行内存攻击,这种情况前端通过qemu支持viommu和VIRTIO_F_ACCESS_PLATFORM,后端vhost-user每次访问内存都要经过qemu的viommu转换。正常情况下虚拟机是不需要支持iommu的。尤其目前大多数云厂商采用了smartnic方案使用了设备直通,这样就不需要虚拟机支持了iommu了。所以我们一般在虚拟机看/proc/cmdline,是没有iommu相关选项的,默认是disable的。

 

问题4:vfio一般需要绑定iommu group,那在虚拟机里面如果跑DPDK程序,并且使用vfio驱动,是不是一定要虚拟机支持iommu(即viommu)呢?

答案显然不是,vfio可以支持iommu,并不是一定要iommu,当iommu disable时vfio就不使用iova,而是直接使用pa的方式,当然这种情况对VM中的DPDK程序的大页连续性有要求。

 

问题5:裸金属场景下,如果要在裸金属中再启动虚拟机,iommu应该如何配置?

首先对于裸金属上启动的虚拟机不需要特殊配置,无需支持viommu,保持默认的iommu disable即可。

其次对于裸金属(host)系统,需要保证直通给不同虚拟机的网卡设备DMA隔离,因此host需要开启iommu。即硬件支持iommu,并且/proc/cmdline中配置intel_iommu=on。

{BANNED}最佳后host有了iommu能力,还需要把直通给虚拟机的网卡设备进行iommu domain和iommu group的设置,以及context的关联,只有这样后端iommu硬件上才会存在对应的转换页表。这是通过启动虚拟机的时候qemu进程将设备绑定到vfio驱动,并且配置vfio创建独立的iommu_group并绑定设备实现的,详细过程以后有时间再展开,这里不再赘述。

这样就完全可以了吗?还没有,我们先看一下virtio_net和DMA相关的API调用。virtio-net和DMA相关的操作主要由两处。一处是virtio-net初始化分配队列时vring_alloc_queue。

点击(此处)折叠或打开

    static void *vring_alloc_queue(struct virtio_device *vdev, size_t size,
                 dma_addr_t *dma_handle, gfp_t flag)
    {
        if (vring_use_dma_api(vdev)) {
            return dma_alloc_coherent(vdev->dev.parent, size,
                         dma_handle, flag);
        } else {
            void *queue = alloc_pages_exact(PAGE_ALIGN(size), flag);
            if (queue) {
                phys_addr_t phys_addr = virt_to_phys(queue);
                *dma_handle = (dma_addr_t)phys_addr;
            }
            return queue;
        }
    }

这里使用的是“一致性DMA映射”dma_alloc_coherent(解决DMA导致的CPU cache一致性)。

另一处是队列收发包时virtqueue_add -> vring_map_one_sg或vring_map_single,这里使用的是“流式DMA映射”(即DMA的内存区不是驱动分配的,如数据包的buf,每次DMA都要建立一个DMA映射)。

点击(此处)折叠或打开

    static dma_addr_t vring_map_one_sg(const struct vring_virtqueue *vq,
                     struct scatterlist *sg,
                     enum dma_data_direction direction)
    {
        if (!vring_use_dma_api(vq->vq.vdev))
            return (dma_addr_t)sg_phys(sg);

        /*
         * We can't use dma_map_sg, because we don't use scatterlists in
         * the way it expects (we don't guarantee that the scatterlist
         * will exist for the lifetime of the mapping).
         */
        return dma_map_page(vring_dma_dev(vq),
                 sg_page(sg), sg->offset, sg->length,
                 direction);
    }

    static dma_addr_t vring_map_single(const struct vring_virtqueue *vq,
                     void *cpu_addr, size_t size,
                     enum dma_data_direction direction)
    {
        if (!vring_use_dma_api(vq->vq.vdev))
            return (dma_addr_t)virt_to_phys(cpu_addr);

        return dma_map_single(vring_dma_dev(vq),
                 cpu_addr, size, direction);
    }

从这两处的调用我们看到DMA API只有在vring_use_dma_api返回true的情况下才可以,否则就只能直接使用virt_to_phys返回物理地址(gpa)。而查看代码要想vring_use_dma_api返回true,需要virtio协商支持VIRTIO_F_ACCESS_PLATFORM这个feature。

而如果不支持VIRTIO_F_ACCESS_PLATFORM,如virtio0.95的情况,这种情况virtio不会使用DMA API,直接返回物理地址。这在虚拟机场景是没有问题的(虚拟机没有开启iommu),但是裸金属host上就有问题了,因为裸金属host上开启了iommu,后端硬件开启了iommu,而驱动却没有使用DMA API从对应的iommu_domain(IOVA空间,也叫DMA空间)中分配地址,直接使用HPA是有问题的。所以要想裸金属上启动虚拟机必须支持VIRTIO_F_ACCESS_PLATFORM这个feature,来强制virtio-net使用DMA API。这一点从VIRTIO_F_ACCESS_PLATFORM的作用也能看出。

Virtio froce DMA API后还有一个问题,就是在实际应用中我们发现裸金属host eni的性能相对虚拟机比较差。原因是每次virtio数据路径使用DMA API存在iommu的地址转换开销,其实对于裸金属本身的网卡是不需要iommu隔离的(只是虚拟机需要隔离)。但是为了支持虚拟机裸金属host又不得不开启iommu。如何解决这个问题呢?这就用到了我们上一篇文章中讲到的iommu=pt选项。通过passthrough来直接使用静态映射,从而减少性能开销。那么虚拟机为什么没有问题呢?因为虚拟机没有开启iommu,直接使用的物理地址,不存在iommu地址转换。

  而上述virtio force DMA API还有另一个作用,既然裸金属host开启了iommu,那么网卡设备就需要进行相关的iommu配置(如绑定iommu_domain等),而网卡设备分为两大类:直通给虚拟机的和裸金属host自己用的。前者我们说了是qemu通过vfio进行设置绑定的,而后者又分为两类:开机就存在的网卡和运行中热插拔的网卡。开机存在的网卡我们在上篇iommu初始化中已经有分析过,在iommu初始化中会对挂在其下的设备分配iommu group和绑定domain。而热插拔的设备就比较特殊了。它依赖上面驱动加载过程中的如下调用路径添加的,可以看到其依赖DMA API的调用:virtio_dev_probe->virtnet_probe->virtnet_find_vqs->vp_find_vqs->vp_try_fo_find_vqs->setup_vq->vring_create_virtqueue->vring_alloc_queue->dma_alloc_coherent->intel_alloc_coherent->domain_add_dev_info.

如果不force DMA API,热插拔的网卡也无法关联对应的iommu domain,dma操作也会失败。不过其实较新的内核(5.3)对iommu做了较大重构,热插拔的设备在iommu的通用层通过pci bus注册的回调函数就做了iommu domain的关联。不再依赖DMA API关联,但是既然开启了iommu,还是要依赖DMA API。

{BANNED}最佳佳佳后总结一下,裸金属上支持启动虚拟机需要:

1.  /proc/cmdline,配置intel_iommu=on iommu=pt;

2. 裸金属需要支持VIRTIO_F_ACCESS_PLATFORM来force DMA API;

 

问题6:iommu和VIRTIO_F_ACCESS_PLATFORM feature的关系?

如果host支持iommu,那么host就需要支持VIRTIO_F_ACCESS_PLATFORM,来force virtio DMA API,使其DMA地址落在对应IOVA空间;如果vm支持iommu(viommu),则虚拟机内部需要支持VIRTIO_F_ACCESS_PLATFORM,来使虚拟机内部virtio 的DMA地址落在对应的guest IOVA空间。当前如果虚拟机不开启iommu,或者物理机也不开启iommu(不需要启动虚拟机)则无需依赖次feature。