分类: LINUX
2024-11-13 11:39:15
原文地址:从virtio看iommu和DMA的关系 作者:lvyilong316
——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为例:
点击(此处)折叠或打开
核心逻辑就是先把skb的地址做一下dma map,然后让硬件可以直接dma这段数据,其具体后续调用过程如下图:
之前iommu文章中介绍过在intel环境iommu初始化会把pci bus的iommu ops设置成intel_dma_ops,所以map_page函数{BANNED}最佳终会调用到intel_map_page。
l __intel_map_single
点击(此处)折叠或打开
首先会判断是否为iommu_no_mapping,如果是则直接返回paddr也即物理地址。再来看看iommu_no_mapping这个函数的具体逻辑。
l iommu_no_mapping
点击(此处)折叠或打开
从实现来看,首先会判断iommu_identity_mapping是否为空(如果当iommu=pt的时候这个变量是不会为空的)如果为空则返回false,这里先看一下不为空的逻辑。接着函数走到identity_mapping,这个函数的实现具体如下:
l identity_mapping
点击(此处)折叠或打开
可以看到函数里面首先判断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的初始化:
l si_domain_init
点击(此处)折叠或打开
首先,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。
点击(此处)折叠或打开
这里使用的是“一致性DMA映射”dma_alloc_coherent(解决DMA导致的CPU cache一致性)。
另一处是队列收发包时virtqueue_add -> vring_map_one_sg或vring_map_single,这里使用的是“流式DMA映射”(即DMA的内存区不是驱动分配的,如数据包的buf,每次DMA都要建立一个DMA映射)。
点击(此处)折叠或打开
从这两处的调用我们看到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。