Chinaunix首页 | 论坛 | 博客
  • 博客访问: 329
  • 博文数量: 3
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 14
  • 用 户 组: 普通用户
  • 注册时间: 2024-11-13 11:08
文章分类
文章存档

2024年(3)

我的朋友
最近访客

分类: LINUX

2024-11-13 11:39:15

virtioiommuDMA的关系

 ——lvyilong316

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

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

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

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

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

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

 

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

点击(此处)折叠或打开

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

  9.         while (unlikely(size > IXGBE_MAX_DATA_PER_TXD)) {
  10.             i++;
  11.             tx_desc++;
  12.             tx_desc->read.buffer_addr = cpu_to_le64(dma);
  13.         }
  14. //...
  15. }

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

 

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

 __intel_map_single

点击(此处)折叠或打开

  1. static dma_addr_t __intel_map_single(struct device *dev, phys_addr_t paddr,
  2.                  size_t size, int dir, u64 dma_mask)
  3. {
  4.     struct dmar_domain *domain;
  5.     phys_addr_t start_paddr;
  6.     unsigned long iova_pfn;
  7.     int prot = 0;
  8.     int ret;
  9.     struct intel_iommu *iommu;
  10.     unsigned long paddr_pfn = paddr >> PAGE_SHIFT;

  11.     BUG_ON(dir == DMA_NONE);

  12.     if (iommu_no_mapping(dev))
  13.         return paddr;

  14.     domain = get_valid_domain_for_dev(dev);
  15.     if (!domain)
  16.         return 0;

  17.     iommu = domain_get_iommu(domain);
  18.     size = aligned_nrpages(paddr, size);

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

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

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

  48.     start_paddr = (phys_addr_t)iova_pfn << PAGE_SHIFT;
  49.     start_paddr += paddr & ~PAGE_MASK;
  50.     return start_paddr;

  51.     return 0;
  52. }

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

iommu_no_mapping

点击(此处)折叠或打开

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

  5.     if (iommu_dummy(dev))
  6.         return 1;

  7.     if (!iommu_identity_mapping)
  8.         return 0;

  9.     found = identity_mapping(dev);
  10.     if (found) {
  11.         if (iommu_should_identity_map(dev, 0))
  12.             return 1;
  13.         else {
  14.             /*
  15.              * 32 bit DMA is removed from si_domain and fall back
  16.              * to non-identity mapping.
  17.              */
  18.             dmar_remove_one_dev_info(si_domain, dev);
  19.             pr_info("32bit %s uses non-identity mapping\n",
  20.                 dev_name(dev));
  21.             return 0;
  22.         }
  23.     } else {
  24.         /*
  25.          * In case of a detached 64 bit DMA device from vm, the device
  26.          * is put into si_domain for identity mapping.
  27.          */
  28.         if (iommu_should_identity_map(dev, 0)) {
  29.             int ret;
  30.             ret = domain_add_dev_info(si_domain, dev);
  31.             if (!ret) {
  32.                 pr_info("64bit %s uses identity mapping\n",
  33.                     dev_name(dev));
  34.                 return 1;
  35.             }
  36.         }
  37.     }

  38.     return 0;
  39. }

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

identity_mapping

点击(此处)折叠或打开

  1. static int identity_mapping(struct device *dev)
  2. {
  3.     struct device_domain_info *info;

  4.     if (likely(!iommu_identity_mapping))
  5.         return 0;

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

  9.     return 0;
  10. }

可以看到函数里面首先判断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

2iommu_identity_mapping 的值不是IDENTMAP_ALL

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

4pcie设备且pcie 设备是pcie bridge

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

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

si_domain_init

点击(此处)折叠或打开

  1. static int __init si_domain_init(int hw)
  2. {
  3.     int nid, ret = 0;

  4.     si_domain = alloc_domain(DOMAIN_FLAG_STATIC_IDENTITY);
  5.     if (!si_domain)
  6.         return -EFAULT;

  7.     if (md_domain_init(si_domain, DEFAULT_DOMAIN_ADDRESS_WIDTH)) {
  8.         domain_exit(si_domain);
  9.         return -EFAULT;
  10.     }

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

  12.     if (hw)
  13.         return 0;

  14.     for_each_online_node(nid) {
  15.         unsigned long start_pfn, end_pfn;
  16.         int i;

  17.         for_each_mem_pfn_range(i, nid, &start_pfn, &end_pfn, NULL) {
  18.             ret = iommu_domain_identity_map(si_domain,
  19.                     PFN_PHYS(start_pfn), PFN_PHYS(end_pfn));
  20.             if (ret)
  21.                 return ret;
  22.         }
  23.     }

  24.     return 0;
  25. }

首先,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=pthwtrue的情况;如果hwfalse的情况又会怎么样呢?具体的逻辑还是要从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支持viommuVIRTIO_F_ACCESS_PLATFORM,后端vhost-user每次访问内存都要经过qemuviommu转换。正常情况下虚拟机是不需要支持iommu的。尤其目前大多数云厂商采用了smartnic方案使用了设备直通,这样就不需要虚拟机支持了iommu了。所以我们一般在虚拟机看/proc/cmdline,是没有iommu相关选项的,默认是disable的。

 

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

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

 

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

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

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

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

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

点击(此处)折叠或打开

  1. static void *vring_alloc_queue(struct virtio_device *vdev, size_t size,
  2.              dma_addr_t *dma_handle, gfp_t flag)
  3. {
  4.     if (vring_use_dma_api(vdev)) {
  5.         return dma_alloc_coherent(vdev->dev.parent, size,
  6.                      dma_handle, flag);
  7.     } else {
  8.         void *queue = alloc_pages_exact(PAGE_ALIGN(size), flag);
  9.         if (queue) {
  10.             phys_addr_t phys_addr = virt_to_phys(queue);
  11.             *dma_handle = (dma_addr_t)phys_addr;
  12.         }
  13.         return queue;
  14.     }
  15. }

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

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

点击(此处)折叠或打开

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

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

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

  22.     return dma_map_single(vring_dma_dev(vq),
  23.              cpu_addr, size, direction);
  24. }

从这两处的调用我们看到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_domainIOVA空间,也叫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 domaindma操作也会失败。不过其实较新的内核(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_PLATFORMforce DMA API

 

问题6iommuVIRTIO_F_ACCESS_PLATFORM feature的关系?

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

阅读(37) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~