从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。
l __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这个函数的具体逻辑。
l 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,这个函数的实现具体如下:
l 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的初始化:
l 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。