理解PCIE设备透传

19 篇文章 4 订阅
15 篇文章 1 订阅

PCIE设备透传解决的是使虚拟机直接访问PCIE设备的技术,通常情况下,为了使虚拟机能够访问Hypervisor上的资源,QEMU,KVMTOOL等虚拟机工具提供了"trap and emulate", Virtio半虚拟化等机制实现。但是这些实现都需要软件的参与,性能较低。

trap and emulate情况下,虚拟机每次访问硬件资源都要进行VMExit退出虚拟机执行相应的设备模拟或者访问设备的操作,完成后再执行VMEnter进入虚拟机。频繁的模式切换导致IO访问的低效。

而Virtio则是一种半虚拟化机制,要求虚拟机中运行的操作系统需要加载特殊的virtio前端驱动(Virtio-xxx),虚拟机通过循环命令队列和Hypervisor上运行的Virtio后端驱动进行通信,后端驱动负责适配不同的物理硬件设备,再收到命令后,后端驱动执行命令。

PCIE设备透传到底"透"了什么?

参考如下两篇文章搭建PCIE设备PASS-THROUGH的环境:

KVM虚拟化之小型虚拟机kvmtool的使用-CSDN博客

ubuntu18.04下pass-through直通realteck PCI设备到qemu-kvm虚拟机实践_kvm网卡直通-CSDN博客

透了HOST MEMORY

设备透传解决了让虚拟机中的驱动使用IOVA访问物理内存的问题,在KVMTOOL中,它是通过调用VFIO的VFIO_IOMMU_MAP_DMA 命令来实现的,用来将IOVA映射到具体的物理页面上(通过HVA 得到HVA对应的物理页面,再进行映射)。下图说明了一切问题:

0.映射SIZE为整个GPA大小,也就是虚拟机的整个物理内存。

1.kvm->ram_start和bank->host_addr相同,表示被映射的区域,VFIO驱动会通过bank->host_addr找到对应的PAGE页面。

2.iova为bank->guest_phys_addr,也就是虚拟机内的GPA。也就是说,IOMMU页表建立后,透传的设备驱动可以通过和CPU一致的物理地址,访问到真实的物理页面上(HPA),这样,从CPU和涉笔的角度,可以做大IOVA==GPA。

3.映射完成后,从虚拟机的角度来看,CPU看到的物理地址(GPA)和硬件看到的物理地址(IOVA)都通过各自的路径(前者通过EPT,后者通过IOMMU)访问同一个存储单元。

4. IOVA到HPA的映射通过HOST主机的VFIO驱动完成,VFIO驱动代码规模比较小,VFIO驱动的一个重要功能之一通过设备节点的方式,使用户态应用能够进行IOMMU映射,从这个角度来讲,VFIO是一个精简的IOMMU驱动和管理框架。

GPA和IOVA建立后的效果如下,设备和CPU通过相同的地址,就可以访问到同一个物理单元,这样虚拟机系统不需通过VMM就可以直接访问到设备,这就是设备“透传”的本质吧。

下面是一个演示设备透传的程序:

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
#include <errno.h>
#include <linux/vfio.h>

#define IOVA_DMA_MAPSZ  (1*1024UL*1024UL)
#define IOVA_START      (0UL)
#define VADDR           0x400000000000
// refer https://www.cnblogs.com/dream397/p/13546968.html
// container fd: the container provides little functionality, with all but a couple vrson and extension query interfaces.
// 1. first identify the group associated wth the desired device.
// 2. unbinding the device from the host driver and binding it to a vfio driver, then a new group would appear for the group as /dev/vfio/$group.
//    make sure all the devices belongs to the group are all need to do the unbind and binding operations or error will got for next group ioctl.
// 3. group is ready, then add to the caontainer by opening the vfio group character device and use VFIO_GROUP_SET_CONTAINER ioctl to add the group
//    fd to container.depending the iommu, multi group can be set to one container.
// 4. after group adding to container, the remaning ioctls became available. enable the iommu device access.now you can get each device belongs the
//    iommu group and get the fd.
// 5. the vfio device ioctls includes for describing the device, the IO regions, and their read/write/mmap operations, and others such as describing
//    and registering interrupt notificactions.

/*
 * #1:echo vfio-pci > /sys/bus/pci/devices/0000:02:00.0/driver_override
 * #2:echo 10de 1d13 > /sys/bus/pci/drivers/vfio-pci/new_id
 *root@zlcao-RedmiBook-14:~# ls -l /dev/vfio/
 *总用量 0
 *crw------- 1 root root 243,   0 11月  8 12:40 12
 *crw-rw-rw- 1 root root  10, 196 11月  8 12:31 vfio
 */

int main(void)
{
	int container, group, device, i;
	void *maddr = NULL;
	struct vfio_group_status group_status = { .argsz = sizeof(group_status) };
	struct vfio_iommu_type1_info *iommu_info = NULL;
	size_t iommu_info_size = sizeof(*iommu_info);
	struct vfio_device_info device_info = { .argsz = sizeof(device_info) };
	struct vfio_iommu_type1_dma_map dma_map;
	struct vfio_iommu_type1_dma_unmap dma_unmap;

	container = open("/dev/vfio/vfio", O_RDWR);
	if (container < 0) {
		printf("%s line %d, open vfio container error.\n", __func__, __LINE__);
		return 0;
	}

	if (ioctl(container, VFIO_GET_API_VERSION) != VFIO_API_VERSION) {
		printf("%s line %d, vfio api version check failure.\n", __func__, __LINE__);
		return 0;
	}

	if (ioctl(container, VFIO_CHECK_EXTENSION, VFIO_TYPE1_IOMMU) == 0) {
		printf("%s line %d, vfio check extensin failure.\n", __func__, __LINE__);
		return 0;
	}

	group = open("/dev/vfio/9", O_RDWR);
	if (group < 0) {
		printf("%s line %d, open vfio group error.\n", __func__, __LINE__);
		return 0;
	}

	if (ioctl(group, VFIO_GROUP_GET_STATUS, &group_status)) {
		printf("%s line %d, failed to get vfio group status.\n", __func__, __LINE__);
		return 0;
	}

	if ((group_status.flags & VFIO_GROUP_FLAGS_VIABLE) == 0) {
		printf("%s line %d, vfio group is not viable.\n", __func__, __LINE__);
		return 0;
	}

	if (ioctl(group, VFIO_GROUP_SET_CONTAINER, &container)) {
		printf("%s line %d, vfio group set conatiner failure.\n", __func__, __LINE__);
		return 0;
	}

	if (ioctl(container, VFIO_SET_IOMMU, VFIO_TYPE1_IOMMU) != 0) {
		printf("%s line %d, vfio set type1 mode failure %s.\n", __func__, __LINE__, strerror(errno));
		return 0;
	}

	iommu_info = malloc(iommu_info_size);
	if (iommu_info == NULL) {
		printf("%s line %d, vfio alloc iommu info failure %s.\n", __func__, __LINE__, strerror(errno));
		return 0;
	}

	memset(iommu_info, 0x00, iommu_info_size);

	iommu_info->argsz = iommu_info_size;

	if (ioctl(container, VFIO_IOMMU_GET_INFO, iommu_info)) {
		printf("%s line %d, vfio failed to get iomu info, %s.\n", __func__, __LINE__, strerror(errno));
		return 0;
	}

	// todo
	// collect available iova regions from VFIO_IOMMU_GET_INFO.

	// 0000:02:00.0 must in this group.
	device = ioctl(group, VFIO_GROUP_GET_DEVICE_FD, "0000:02:00.0");
	if (device < 0) {
		printf("%s line %d, get vfio group device error.\n", __func__, __LINE__);
		return 0;
	}

	ioctl(device, VFIO_DEVICE_RESET);

	if (ioctl(device, VFIO_DEVICE_GET_INFO, &device_info)) {
		printf("%s line %d, get vfio group device info error.\n", __func__, __LINE__);
		return 0;
	}

	{
		struct vfio_region_info region = {
			.index = VFIO_PCI_CONFIG_REGION_INDEX,
			.argsz = sizeof(struct vfio_region_info),
		};

		if (ioctl(device, VFIO_DEVICE_GET_REGION_INFO, &region)) {
			printf("%s line %d, get vfio group device region info error.\n", __func__, __LINE__);
			return 0;
		}
	}

	maddr = mmap((void *)VADDR, IOVA_DMA_MAPSZ, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
	if (maddr == MAP_FAILED) {
		printf("%s line %d, faild to map buffer, error %s.\n", __func__, __LINE__, strerror(errno));
		return -1;
	}

	memset(&dma_map, 0x00, sizeof(dma_map));

	dma_map.argsz = sizeof(dma_map);
	dma_map.flags = VFIO_DMA_MAP_FLAG_READ | VFIO_DMA_MAP_FLAG_WRITE;
	dma_map.iova = IOVA_START;
	dma_map.vaddr = (unsigned long)maddr;
	dma_map.size = IOVA_DMA_MAPSZ;

	if (ioctl(container, VFIO_IOMMU_MAP_DMA, &dma_map)) {
		printf("%s line %d, faild to do dma map on this conatainer.\n", __func__, __LINE__);
		return -1;
	}

	printf("%s line %d, do vfio dma mamp 1M memory buffer success, the iova is 0x%llx, dmaavddr 0x%llx, userptr %p.\n",
	       __func__, __LINE__, dma_map.iova, dma_map.vaddr, maddr);

	memset(&dma_unmap, 0x00, sizeof(dma_unmap));
	dma_unmap.argsz = sizeof(dma_unmap);
	dma_unmap.iova = IOVA_START;
	dma_unmap.size = IOVA_DMA_MAPSZ;

	if (ioctl(container, VFIO_IOMMU_UNMAP_DMA, &dma_unmap)) {
		printf("%s line %d, faild to do dma unmap on this conatainer.\n", __func__, __LINE__);
		return -1;
	}

	munmap((void *)maddr, IOVA_DMA_MAPSZ);
	close(device);
	close(group);
	close(container);

	return 0;
}

测试程序在IOMMU上影射了1M的空间,IOVA范围为[0, 0x100000],我们DUMP PCIE连接IOMMU其页表为:

然后DUMP HOST HVA[0x400000000000,0x400000100000]范围的页表映射:

仔细对比两张截图,会发现他们的物理页框顺序完全一致,这样,在虚拟机中CPU通过GPA访问得到的数据和设备通过同样的IOVA访问的数据会保持一致,就像在真实的硬件上执行时的情况一样,这就是透传的效果,IOMMU功不可没,它让GUEST OS中具备了越过VMM直接访问设备的能力。

PCIE BAR空间的透传

PCI设备上可能会有板上的存储空间,比如PCIE显卡上的独立显存,或者PCIE网卡上的发送和接收缓冲队列,处理器需要将这些板上的内存映射到地址空间进行访问,但是与标准中预先定义好的内存不同,不同的机器上插的PCIE设备不同,这些都是变化的,处理器不可能为所有的PCIE设备预先定义一个地址空间的映射方案,因此,PCI标准提出了一个灵活的办法,各个PCIE设备自己提出需要占据的地址空间大小,以及映射方式(MMIO还是PIO),然后将这些诉求信息记录在配置空间的BAR字段,每个PCIE设备最多可以映射六个区域,对应六个BAR,至于映射到地址空间的什么位置,由BIOS在系统初始化时,查询PCIE设备的诉求信息,统一为PCI设备划分地址空间。

注意,前面提到的地址空间是PA(HPA 或者GPA)。

那么VCPU是如何访问透传到虚拟机的PCIE设备的BAR空间,从而达到访问PCIE设备上的存储的目的的呢?

先看一个PCIE设备透传前后,BAR空间映射的例子:

Realtek的一块PCIE有线网卡的信息如下,可以看到,它有三个BAR空间, BAR0是PIO模式访问的IO空间,BAR2是一块MMIO映射的Memory,大小为4K,启动时BIOS分配的地址是0xdf104000,最后一块BAR是Region4,它的起始地址为0xdf100000,大小为16K。当前设备使用的kernel-driver是vfio-pci,说明当前设备已经处于透传状态。

在虚拟机中的设备状态如下,虚拟机中的lspci工具基于BB,比较简陋,但是我们仍然能够通过vendor id/product id确认设备00:00.0就是透传到虚拟机的网卡设备:

lspci无法得到设备BAR信息,可以通过/proc/iomem以及/proc/ioport获取,如下图,我们找到了透传到虚拟机后的网卡的三个BAR空间信息,从每个BAR的大小来看,是和主机端一致的,这也侧面说明我们找对了。

BAR资源的透传是说,从VM中访问资源地址0xd2004000, 和在HOST中访问0xdf100000访问到的内容是一样的,它的过程和在KVM中添加一个内存条的步骤是一致的,都是将一个HVA区域映射到 VM中一段指定的GPA,只不过,在映射内存的时候,HVA是mmap映射的一段主存,而GPA是0(VM的物理地址从0开始),而在映射BAR空间的时候,HVA则是用户态MMAP HOST机上的BAR空间得到的用户态地址,GPA则成为了一段VM中的一段空闲的物理空间,这里是0xd2004000。

整个映射的逻辑如下图所示:

经过软硬件的层层映射,最终达到了GUEST OS中的应用程序通过VCPU访问PCIE BAR空间的目的。

KVMTOOL定义了PCIE MMIO GPA的范围:

下图是验证BAR内存从HPA(0xdf104000)到HVA(0x7f7e59eb5000)再到GPA(0xd2004000)的映射过程,利用内核EXPORT接口follow_pfn反查页表,发现GPA对应的HVA,反查得到的页表项恰好就是HPA BAR地址,证明了上图分析的正确性。

注意,在VFIO的视线中,HOST MMAP的是通过PAGE FAULT填充BAR空间的资源页的,由于BAR资源没有PAGE结构对应,所以vfio_pci_mmap_fault直接调用内核接口io_remap_pfn_range建立映射。

#
#                                _-----=> irqs-off
#                               / _----=> need-resched
#                              | / _---=> hardirq/softirq
#                              || / _--=> preempt-depth
#                              ||| /     delay
#           TASK-PID     CPU#  ||||   TIMESTAMP  FUNCTION
#              | |         |   ||||      |         |
             cat-29366   [000] .... 62928.901313: vfio_pci_mmap_fault <-__do_fault
             cat-29366   [000] .... 62928.901327: <stack trace>
 => vfio_pci_mmap_fault
 => __do_fault
 => __handle_mm_fault
 => handle_mm_fault
 => fixup_user_fault
 => iopfn_map_get
 => my_seq_ops_show
 => seq_read
 => proc_seq_read
 => proc_reg_read
 => __vfs_read
 => vfs_read
 => ksys_read
 => __x64_sys_read
 => do_syscall_64
 => entry_SYSCALL_64_after_hwframe

总结

1.透传场景下,设备内存映射给VCPU,不会通过DMAR映射给设备自身,设备内存本身就在设备上,设备可以直接访问(通过设备内部的IOMMU),不需要映射给DMAR。资源内存映射给DMAR的情况一般是三方外设通过自身IOMMU DOMAIN(自身设备绑定)访问其他设备上的存储资源。

2.透传的设备访问主存仅仅需要一次映射,由DMAR完成IOVA到HPA的翻译。而VCPU访问无论访问主存还是访问设备内存,都需要走两次映射,第一次在虚拟机内部,实现GVA到GPA翻译,由MMU完成,第二次由EPT完成,实现从GPA到HPA的翻译。

3.由于资源内存没有struct page管理,所以EPT在建表做映射时,获取HPFN的方式会有所区别,对于主内存可以得到其struct page,但是对于设备内存来说,由于没有struct page对应,需要手搓查页表获取HPFN(资源存储映射的VMA会设置VM_IO | VM_PFNMAP标志),具体可以分析hva_to_pfn调用hva_to_pfn_remapped的分支实现.

4.IOMMU和EPT中的页表项填充的地址都是HPA地址,这个HPA地址并不是直接分配获取的,而是透过进程HVA地址空间分配->查页表方式获取的。这样既利用了DMAR的REMAP实现透传到不同虚拟机的设备之间的隔离,又将这种隔离透过进程间通信等方式,实现HVA之间的页面共享,隔离和共享均控制在用户手中。避免了设备直接访问内存带来的不确定性。

5.GPA==IOVA, 也就是说,IO function看到的物理地址空间和CPU看到的物理地址空间完全一致了。

更新:

透传设备访问自身资源和其他设备访问本设备路径应该是不同的,本设备资源地址从自身地址域开始,而其他设备映射本设备时,是从系统物理映射的物理地址资源域作为物理地址映射的。

更新:

DMAR既可以映射设备内存,也可以映射主存,本设备通过本地SMMU访问,其他设备通过本地SMMU+DMAR映射总线地址访问。


参考文章

https://www.kernel.org/doc/ols/2007/ols2007v1-pages-225-230.pdf

VIRTIO后端框架QEMU与VHOST分析_qemu vhost-CSDN博客

基于virtio的半虚拟化概述 - 知乎

结束

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

papaofdoudou

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值