Home Kernel Virtual Function I/O
Post
Cancel

Kernel Virtual Function I/O

一、vfio

Virtual Function I/O (VFIO) 是一种现代化的设备直通方案,它充分利用了VT-d/AMD-Vi技术提供的DMA Remapping和Interrupt Remapping特性, 在保证直通设备的DMA安全性同时可以达到接近物理设备的I/O的性能。 用户态进程可以直接使用VFIO驱动直接访问硬件,并且由于整个过程是在IOMMU的保护下进行因此十分安全, 而且非特权用户也是可以直接使用。 换句话说,VFIO是一套完整的用户态驱动(userspace driver)方案,因为它可以安全地把设备I/O、中断、DMA等能力呈现给用户空间。

为了达到最高的IO性能,虚拟机就需要VFIO这种设备直通方式,因为它具有低延时、高带宽的特点,并且guest也能够直接使用设备的原生驱动。 这些优异的特点得益于VFIO对VT-d/AMD-Vi所提供的DMA Remapping和Interrupt Remapping机制的应用。 VFIO使用DMA Remapping为每个Domain建立独立的IOMMU Page Table将直通设备的DMA访问限制在Domain的地址空间之内保证了用户态DMA的安全性, 使用Interrupt Remapping来完成中断重映射和Interrupt Posting来达到中断隔离和中断直接投递的目的。

image-20221208102036619

在了解VFIO之前需要了解3个基本概念:device, group, container,它们在逻辑上的关系如上图所示。

  • Group 是IOMMU能够进行DMA隔离的最小硬件单元,一个group内可能只有一个device,也可能有多个device,这取决于物理平台上硬件的IOMMU拓扑结构。 设备直通的时候一个group里面的设备必须都直通给一个虚拟机。 不能够让一个group里的多个device分别从属于2个不同的VM,也不允许部分device在host上而另一部分被分配到guest里, 因为就这样一个guest中的device可以利用DMA攻击获取另外一个guest里的数据,就无法做到物理上的DMA隔离。 另外,VFIO中的group和iommu group可以认为是同一个概念。
  • Device 指的是我们要操作的硬件设备,不过这里的“设备”需要从IOMMU拓扑的角度去理解。如果该设备是一个硬件拓扑上独立的设备,那么它自己就构成一个iommu group。 如果这里是一个multi-function设备,那么它和其他的function一起组成一个iommu group,因为多个function设备在物理硬件上就是互联的, 他们可以互相访问对方的数据,所以必须放到一个group里隔离起来。值得一提的是,对于支持PCIe ACS特性的硬件设备,我们可以认为他们在物理上是互相隔离的。
  • Container 是一个和地址空间相关联的概念,这里可以简单把它理解为一个VM Domain的物理内存空间。

从上图可以看出,一个或多个device从属于某个group,而一个或多个group又从属于一个container。 如果要将一个device直通给VM,那么先要找到这个设备从属的iommu group,然后将整个group加入到container中即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
+-------------------------------------------+
|                                           |
|             VFIO Interface                |
|                                           |
+---------------------+---------------------+
|                     |                     |
|     vfio_iommu      |      vfio_pci       |
|                     |                     |
+---------------------+---------------------+
|                     |                     |
|    iommu driver     |    pci_bus driver   |
|                     |                     |
+---------------------+---------------------+

例子:

vfio的样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/vfio.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <string.h>
#include <stdlib.h>

#define VFIO_PATH "/dev/vfio/vfio"
#define DEVICE_ID "0000:ba:00.0" // BDF of the device taken over by vfio.

static int vfio_get_group_num(const char *linkname, int *iommu_group_num)
{
	char filename[256];
	char *group_tok, *end;
	int ret;

	memset(filename, 0, sizeof(filename));

	ret = readlink(linkname, filename, sizeof(filename));

	/* if the link doesn't exist, no VFIO for us */
	if (ret < 0)
		return 0;

	printf("%s\n", filename);

	end = strtok(filename, "/");

	while (end != NULL) {
		group_tok = end;
		end = strtok(NULL, "/");
	}

	/* IOMMU group is always the last token */
	*iommu_group_num = strtol(group_tok, NULL, 10);
	return 1;
}

int main()
{
	int container, group, device, i;
	struct vfio_group_status group_status =
	    {.argsz = sizeof(group_status) };
	struct vfio_iommu_type1_info iommu_info = {.argsz = sizeof(iommu_info)
	};
	struct vfio_iommu_type1_dma_map dma_map = {.argsz = sizeof(dma_map) };
	struct vfio_device_info device_info = {.argsz = sizeof(device_info) };
	int ret;

	/* Create a new container */
	container = open("/dev/vfio/vfio", O_RDWR);
	if (container < 0) {
		printf("Error container.\n");
		return -1;
	}

	/* Unknown API version */
	if (ioctl(container, VFIO_GET_API_VERSION) != VFIO_API_VERSION) {
		printf("Error: Unknown VFIO API version.\n");
		return -1;
	}

	/* Doesn't support the IOMMU driver we want. */
	if (!ioctl(container, VFIO_CHECK_EXTENSION, VFIO_TYPE1_IOMMU)) {
		printf("Error: Doesn't support the IOMMU driver we want.\n");
		return -1;
	}

	/* Get the device's group index *sys/bus/pci/devices/0000\:60\:00.0/iommu_group*/
	char *sys_path = "/sys/bus/pci/devices/" DEVICE_ID "/iommu_group";
	int group_num;
	char group_name[20];
	memset(group_name, 0, sizeof(group_name));
	vfio_get_group_num(sys_path, &group_num);
	printf("group_num=%d\n", group_num);
	snprintf(group_name, sizeof(group_name), "/dev/vfio/%d", group_num);

	/* Open the group */
	group = open(group_name, O_RDWR);

	/* Test the group is viable and available */
	ioctl(group, VFIO_GROUP_GET_STATUS, &group_status);

	/* Group is not viable (ie, not all devices bound for vfio) */
	if (!(group_status.flags & VFIO_GROUP_FLAGS_VIABLE)) {
		printf("Error: Group is not viable.\n");
		return -1;
	}

	/* Add the group to the container */
	ret = ioctl(group, VFIO_GROUP_SET_CONTAINER, &container);
	if (ret < 0) {
		printf("Error: VFIO_GROUP_SET_CONTAINER.\n");
		return -1;
	}

	/* Enable the IOMMU model we want */
	ret = ioctl(container, VFIO_SET_IOMMU, VFIO_TYPE1_IOMMU);
	if (ret < 0) {
		printf("Error: VFIO_SET_IOMMU.\n");
		return -1;
	}

	/* Get addition IOMMU info */
	ret = ioctl(container, VFIO_IOMMU_GET_INFO, &iommu_info);
	if (ret < 0) {
		printf("Error: VFIO_IOMMU_GET_INFO.\n");
		return -1;
	}
	printf
	    ("iommu_info.flags=0x%x, iommu_info.iova_pgsizes=0x%lx\n",
	     iommu_info.flags, iommu_info.iova_pgsizes);

	/* Allocate some space and setup a DMA mapping */
	dma_map.vaddr =
	    (unsigned long long)mmap(0, 1024 * 1024, PROT_READ | PROT_WRITE,
				     MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
	dma_map.size = 1024 * 1024;
	dma_map.iova = 0;	/* 1MB starting at 0x0 from device view */
	dma_map.flags = VFIO_DMA_MAP_FLAG_READ | VFIO_DMA_MAP_FLAG_WRITE;

	ret = ioctl(container, VFIO_IOMMU_MAP_DMA, &dma_map);
	if (ret < 0) {
		printf("Error: VFIO_IOMMU_MAP_DMA.\n");
		return -1;
	}

	/* Get a file descriptor for the device */
	device = ioctl(group, VFIO_GROUP_GET_DEVICE_FD, DEVICE_ID);
	if (device < 0) {
		printf("Error: VFIO_GROUP_GET_DEVICE_FD.\n");
		return -1;
	}

	/* Test and setup the device */
	ret = ioctl(device, VFIO_DEVICE_GET_INFO, &device_info);
	if (ret < 0) {
		printf("Error: VFIO_DEVICE_GET_INFO.\n");
		return -1;
	}
	printf
	    ("device_info.flags=0x%x\t, device_info.num_regions=0x%x\t, device_info.num_irqs=0x%x\n",
	     device_info.flags, device_info.num_regions, device_info.num_irqs);

	/* index 0~5: bar space; can read/write/mmap
	 * index 6  : rom space; 
	 * index 7  : config space; can read/write */
	for (i = 0; i < device_info.num_regions; i++) {
		struct vfio_region_info reg = {.argsz = sizeof(reg) };

		reg.index = i;

		/* Setup mappings... read/write offsets, mmaps
		 * For PCI devices, config space is a region */
		ret = ioctl(device, VFIO_DEVICE_GET_REGION_INFO, &reg);
#if 0
		if (ret < 0) {
			printf("Error: VFIO_DEVICE_GET_REGION_INFO.\n");
			return -1;
		}
#endif
		printf
		    ("index=%u, reg.flags=0x%x, cap_offset=0x%x, size=0x%lx\n",
		     reg.index, reg.flags, reg.cap_offset, reg.size);
	}

	for (i = 0; i < device_info.num_irqs; i++) {
		struct vfio_irq_info irq = {.argsz = sizeof(irq) };

		irq.index = i;

		ret = ioctl(device, VFIO_DEVICE_GET_IRQ_INFO, &irq);

		/* Setup IRQs... eventfds, VFIO_DEVICE_SET_IRQS */
		if (ret < 0) {
			printf("Error: VFIO_DEVICE_GET_IRQ_INFO.\n");
			return -1;
		}
		printf("irq.flags=0x%x,irq.index=%u\t, irq.count=%u\n", irq.flags, irq.index, irq.count);
	}

	/* Gratuitous device reset and go... */
	ioctl(device, VFIO_DEVICE_RESET);
}


1、/dev/vfio/vfio

  1. container
1
insmod vfio.ko
1
2
3
4
5
6
7
static struct miscdevice vfio_dev = {
	.minor = VFIO_MINOR,
	.name = "vfio",
	.fops = &vfio_fops,
	.nodename = "vfio/vfio",
	.mode = S_IRUGO | S_IWUGO,
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static const struct file_operations vfio_fops = {
	.owner		= THIS_MODULE,
	.open		= vfio_fops_open,
	.release	= vfio_fops_release,
	.read		= vfio_fops_read,
	.write		= vfio_fops_write,
	.unlocked_ioctl	= vfio_fops_unl_ioctl,
	.compat_ioctl	= compat_ptr_ioctl,
	.mmap		= vfio_fops_mmap,
};

.open: struct vfio_container *container; filep->private_data = container; 申请个container;
.read: container->iommu_driver->ops->read() vfio_iommu_driverread, 需要VFIO_SET_IOMMU之后才有效
.write: container->iommu_driver->ops->write() vfio_iommu_driverwrite
.ioctl: | VFIO_GET_API_VERSION 查看版本信息
        | VFIO_CHECK_EXTENSION check是否支持VFIO_TYPE1_IOMMU
        | VFIO_SET_IOMMU container设置iommu操作
        | default driver->ops->ioctl(data, cmd, arg); 转到IOMMUcmd操作
1
2
3
4
5
6
7
8
list_for_each_entry(driver, &vfio.iommu_drivers_list, vfio_next) {
    if (driver->ops->ioctl(NULL, VFIO_CHECK_EXTENSION, arg) <= 0) { //从注册到的iommu_drivers_list中的所有iommu驱动中,找到支持arg的类型的iommu
			module_put(driver->ops->owner);
			continue;
		}

		data = driver->ops->open(arg);
}
  1. vfio iommu driver
1
2
vfio_iommu_type1_init
	->vfio_register_iommu_driver(&(vfio_iommu_driver_ops_type1.ops)); //为了VFIO_SET_IOMMU
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static const struct vfio_iommu_driver_compat_ops vfio_iommu_driver_ops_type1 = {
	.ops = {
		.name			= "vfio-iommu-type1",
		.owner			= THIS_MODULE,
		.open			= vfio_iommu_type1_open,
		.release		= vfio_iommu_type1_release,
		.ioctl			= vfio_iommu_type1_ioctl,
		.attach_group		= vfio_iommu_type1_attach_group,
		.detach_group		= vfio_iommu_type1_detach_group,
		.pin_pages		= vfio_iommu_type1_pin_pages,
		.unpin_pages		= vfio_iommu_type1_unpin_pages,
		.register_notifier	= vfio_iommu_type1_register_notifier,
		.unregister_notifier	= vfio_iommu_type1_unregister_notifier,
		.dma_rw			= vfio_iommu_type1_dma_rw,
	},
	.notify				= vfio_iommu_type1_notify,
};
ioctl:
	switch (cmd) { //除了VFIO_CHECK_EXTENSION之外,都能对应上iommu的ops
	case VFIO_CHECK_EXTENSION: //支持哪些iommu driver type, 为了VFIO_SET_IOMMU来查看
	case VFIO_IOMMU_GET_INFO: //获取iommu的信息
	case VFIO_IOMMU_MAP_DMA: //dma映射
	case VFIO_IOMMU_UNMAP_DMA://dma解映射
	case VFIO_IOMMU_DIRTY_PAGES://支持iommu dirty
	case VFIO_IOMMU_BIND: //支持iommu sva的bind
	case VFIO_IOMMU_UNBIND: //支持iommu sva的unbind
	case VFIO_IOMMU_SET_PASID_TABLE: //attach a pasid table
	case VFIO_IOMMU_CACHE_INVALIDATE://invalidate translation caches
	case VFIO_IOMMU_SET_MSI_BINDING: //provides a stage1 giova/gpa MSI doorbell mapping
	default:
		return -ENOTTY;
	}

2、/dev/vfio/$(iommu_group_id)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//vfio_pci.c
static const struct file_operations vfio_group_fops = {
	.owner		= THIS_MODULE,
	.unlocked_ioctl	= vfio_group_fops_unl_ioctl,
	.compat_ioctl	= compat_ptr_ioctl,
	.open		= vfio_group_fops_open,
	.release	= vfio_group_fops_release,
};

vfio_group_fops_unl_ioctl:
	VFIO_GROUP_GET_STATUS: //获取group的状态,是否visable,有没有绑定container
    VFIO_GROUP_SET_CONTAINER //group绑定vfio container
    VFIO_GROUP_UNSET_CONTAINER //group解绑定vfio container
	VFIO_GROUP_GET_DEVICE_FD //根据device id获取struct vfio_device的fd,可以操作device

3、device fd

1
2
3
4
5
vfio_pci_probe
    ->| vfio device->ops = ops; //vfio_pci_ops
       vfio device->dev = dev; //pci struct device
	   vfio device->drvdata = vdev;
        pci device->drvdata = vfio device;
1
2
3
4
VFIO_GROUP_GET_DEVICE_FD //根据设备id match获得struct vfio_device
    ->|struct vfio_device
    ->|ret = vfio_device->ops->open(device->device_data); if (ret) return ret
    ->| if device->ops->open失败了,分配个不使用的fd,创建anon_inode文件,挂上vfio_device_fopsops, return fd.	
1
2
3
//vfio_device_fops vs vfio_pci_ops
    
vfio_device_fops 执行vfio_device->ops->xxx = vfio_pci_ops
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
vfio_pci_ops
static const struct vfio_device_ops vfio_pci_ops = {
	.name		= "vfio-pci",
	.open		= vfio_pci_open,
	.release	= vfio_pci_release,
	.ioctl		= vfio_pci_ioctl,
	.read		= vfio_pci_read,
	.write		= vfio_pci_write,
	.mmap		= vfio_pci_mmap,
	.request	= vfio_pci_request,
	.match		= vfio_pci_match,
};

ioctl:
    VFIO_DEVICE_GET_INFO:
    VFIO_DEVICE_GET_REGION_INFO: {
        VFIO_PCI_CONFIG_REGION_INDEX
        VFIO_PCI_BAR0_REGION_INDEX ... VFIO_PCI_BAR5_REGION_INDEX
        VFIO_PCI_ROM_REGION_INDEX
        VFIO_PCI_VGA_REGION_INDEX    
    }
	VFIO_DEVICE_GET_IRQ_INFO: {
       	VFIO_PCI_INTX_IRQ_INDEX,
        VFIO_PCI_MSI_IRQ_INDEX,
        VFIO_PCI_MSIX_IRQ_INDEX,
        VFIO_PCI_ERR_IRQ_INDEX,
        VFIO_PCI_REQ_IRQ_INDEX, 
    }
	VFIO_DEVICE_SET_IRQS:
	VFIO_DEVICE_RESET:
	VFIO_DEVICE_GET_PCI_HOT_RESET_INFO:
        
mmap:
    
request:

二、设备透传实现

在前面介绍VFIO的使用实例时,核心思想就是IOVA经过IOMMU映射出的物理地址与HVA经过MMU映射出的物理地址是同一个。对于设备透传的情况,先上图,然后看图说话。

img

这段话可以慢慢理解一下

先来分析一下设备的DMA透传的工作流程,一旦设备透传给了虚机,虚机在配置设备DMA时直接使用GPA。此时GPA经由EPT会映射成HPA1,GPA经由IOMMU映射的地址为HPA2,此时的HPA1和HPA2必须相等,设备的透传才有意义。下面介绍在配置IOMMU时如何保证HPA1和HPA2相等,在VFIO章节讲到了VFIO_IOMMU_MAP_DMA这个命令就是将iova通过IOMMU映射到vaddr对应的物理地址上去。对于IOMMU来讲,此时的GPA就是iova,我们知道GPA经由EPT会映射为HPA1,对于VMM来讲,这个HPA1对应的虚机地址为HVA,那样的话在传入VFIO_IOMMU_MAP_DMA命令时讲hva作为vaddr,IOMMU就会将GPA映射为HVA对应的物理地址及HPA1,即HPA1和HPA2相等。上述流程帮助理清整个映射关系,实际映射IOMMU的操作很简单,前面提到了qemu维护了GPA和HVA的关系,在映射IOMMU的时候也可以派上用场。 注:IOMMU的映射在虚机启动时就已经建立好了,映射要涵盖整个GPA地址范围,同时虚机的HPA对应的物理页都不会交换出去(设备DMA交换是异步的)。

reference:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
vfio概述(vfio/iommu/device passthrough)
https://www.cnblogs.com/yi-mu-xi/p/12370626.html

vfio直通介绍
https://luohao-brian.gitbooks.io/interrupt-virtualization/content/vfio-she-bei-zhi-tong-jian-jie.html


VFIO demo
https://blog.csdn.net/qq_42138566/article/details/127573967


VFIO概述
https://blog.csdn.net/hx_op/article/details/104029622

VFIO(Virtual Function IO)研究
https://blog.csdn.net/21cnbao/article/details/115683410

Linux内核:VFIO 内核文档 (实例,APIbus驱动API)
https://rtoax.blog.csdn.net/article/details/110845720

内核文档: Documentation/driver-api/vfio.rst
https://elixir.bootlin.com/linux/latest/source/Documentation/driver-api/vfio.rst

MSI: message signal interrupt
This post is licensed under CC BY 4.0 by the author.