Linux虚拟化之VFIO框架
引言
VFIO为多用户在用户态访问设备提供了支持,这为用户态驱动和设备虚拟化奠定了基础。VFIO抽象了三个层次:device, group, container:
- container : 一个container对应一个虚拟机的域、或者一个用户;
- group : 相对于intel iommu中的group,是设备直通的最小单位;
- device :我们访问的设备。
注:device属于group,每个group可以关联到一个container。
1 用户接口
1.1 container
首先,container负责管理内存资源,和IOMMU、DMA及地址空间相关。用户空间通过文件/dev/vfio/vfio获取对应的文件描述符。具体操作如下:
//打开container文件
container = open("/dev/vfio/vfio", O_RDWR);
//通过ioctl设置IOMMU。此外,还可以通过ioctl做诸如获取container信息或其他设置
ioctl(container, VFIO_SET_IOMMU, VFIO_TYPE1_IOMMU);
//让iommu建立 “dma_map.iova -> dma_map.vaddr对应的物理地址” 的映射
dma_map.vaddr = 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;
ioctl(container, VFIO_IOMMU_MAP_DMA, &dma_map);
1.2 group
group是透传的最小单位。假设lspci获取需要透传的设备id为05:00.0,操作group前需要做如下准备:
- 通过readlink /sys/bus/pci/devices/0000:05:00.0/iommu_group可以获取设备的group id,假设为29;
- 通过lspci -n -s 0000:05:00.0输出05:00.0 0200: 10ec:8125 (rev 05)获取到vendor和dev id为10ec:8125;
- 通过echo 0000:05:00.0 > /sys/bus/pci/devices/0000:05:00.0/driver/unbind解除绑定到pci bus;
- 通过echo 10ec:8125 > /sys/bus/pci/drivers/vfio-pci/new_id将设备交vfio-pci管理。
然后,用户空间就可以通过打开文件/dev/vfio/29访问该group,具体操作如下:
// 打开group文件
group = open("/dev/vfio/29", O_RDWR);
// 通过ioctl将group关联到container,另外还可以作信息获取或其他设置
ioctl(group, VFIO_GROUP_SET_CONTAINER, &container);
1.3 device
通过group可以打开device文件,然后可以对文件进行读写和mmap。
// 通过group打开设备
device = ioctl(group, VFIO_GROUP_GET_DEVICE_FD, "0000:05:00.0");
ioctl(device, VFIO_DEVICE_GET_INFO, &device_info);
for (int i = 0; i < device_info.num_regions; i++) {
reg.index = i;
... ...
ioctl(device, VFIO_DEVICE_GET_REGION_INFO, ®);
... ...
}
for (i = 0; i < device_info.num_irqs; i++) {
irq.index = i;
... ...
ioctl(device, VFIO_DEVICE_GET_IRQ_INFO, &irq);
... ...
}
ioctl(device, VFIO_DEVICE_RESET);
2 内核实现
2.1 container
在vfio_init函数中,将vfio_dev注册为misc设备,本质上是一个字符设备。vfio_dev的fops回调为:
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,
#ifdef CONFIG_COMPAT
.compat_ioctl = vfio_fops_compat_ioctl,
#endif
.mmap = vfio_fops_mmap,
};
vfio_fops_open
在vfio_fops_open中,通过kzalloc申请一片内存将其作为struct vfio_container。最后将filep->private_data设置为新申请的struct vfio_container内存指针。
vfio_fops_release
在vfio_fops_release中,释放filep->private_data指向的内存、并将filep->private_data置NULL。
vfio_fops_unl_ioctl / vfio_fops_compat_ioctl
后者是对前者的封装。在vfio_fops_unl_ioctl中,实现了如下的ioctl :
- VFIO_GET_API_VERSION : 获取VFIO版本信息
- VFIO_CHECK_EXTENSION : 检测是否支持特定扩展,例如VFIO_TYPE1_IOMMU;
- VFIO_SET_IOMMU : 设置指定的IOMMU类型, 例如VFIO_TYPE1_IOMMU;
注:当设置了IOMMU类型后,container的iommu_driver和iommu_data也就被设置。然后其他ioctl命令则由container->iommu_driver->ops->ioctl处理。
目前有三种iommu实现,vfio_noiommu_ops、tce_iommu_driver_ops和vfio_iommu_driver_ops_type1。
- tce_iommu_driver_ops :
- vfio_iommu_driver_ops_type1 :
- vfio_noiommu_ops :
2.2 group
在vfio_init函数中,为group字符设备注册了vfio_group_fops文件操作回调:
static const struct file_operations vfio_group_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = vfio_group_fops_unl_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = vfio_group_fops_compat_ioctl,
#endif
.open = vfio_group_fops_open,
.release = vfio_group_fops_release,
};
vfio_create_group
在vfio_add_group_dev中,如果dev的group不存在,则会通过vfio_create_group创建一个group。vfio_create_group逻辑如下:
- 分配struct vfio_group内存,对其初始化;
- 通过vfio_alloc_group_minor将group添加到vfio.group_idr中,返回一个id号。这个id号就是group文件的次设备号;
- 通过device_create创建主设备号为vfio.group_devt,次设备号为上面返回的id号的字符设备。
vfio_group_release
该函数执行了vfio_create_group的反向释放操作,当group引用计数为0时释放。
vfio_group_fops_open
当打开group文件的时候,会调用该回调。其逻辑如下:
- 通过inode节点获取次设备号,然后通过次设备号找到group指针;
- 如果group打开原子计数大于0、或者group已经关联到container,则返回失败;
- 通过系列检查后,将group赋值给filep->private_data;
vfio_group_fops_release
该函数是文件关闭时的释放操作。
vfio_group_fops_unl_ioctl / vfio_group_fops_compat_ioctl
后者是对前者的封装。在vfio_group_fops_unl_ioctl中,实现了如下的ioctl :
- VFIO_GROUP_GET_STATUS:获取group 的状态信息;
- VFIO_GROUP_SET_CONTAINER:将group绑定到container;
- VFIO_GROUP_UNSET_CONTAINER:将group解除绑定到container;
- VFIO_GROUP_GET_DEVICE_FD:获取group中某个设备的文件ID。
2.3 device
注:vfio可以实现多种不同功能的设备,目前主要实现了三类:mdev、pci、platform设备。后面以vfio-pci设备为例。
首先,在vfio-pci驱动模块初始化的时候,注册了一个pci驱动vfio_pci_driver:
static struct pci_driver vfio_pci_driver = {
.name = "vfio-pci",
.id_table = NULL, /* only dynamic ids */
.probe = vfio_pci_probe,
.remove = vfio_pci_remove,
.err_handler = &vfio_err_handlers,
};
- 可以看到id_table字段为NULL,所以通过echo VendorID:DevID> /sys/bus/pci/drivers/vfio-pci/new_id实现设备匹配。
- 在vfio_pci_probe中,创建了struct vfio_pci_device数据结构,通过vfio_add_group_dev将vfio-pci设备对象添加到vfio框架中,并设置了vfio_pci_ops作为设备文件的操作回调。
- vfio_add_group_dev函数最终会调用vfio_group_create_device创建一个struct vfio_device对象,并将vfio_pci_ops赋值给struct vfio_device.ops;struct vfio_pci_device指针赋值给struct vfio_device.device_data和其他相关初始化赋值。最后添加到struct vfio_group.device_list链表中。
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,
};
对group文件调用ioctl执行VFIO_GROUP_GET_DEVICE_FD命令,将调用vfio_group_get_device_fd从struct vfio_group.device_list链表中查询设备,并创建一个文件对象,将struct vfio_device赋值给file->private_data;然后调用vfio_pci_ops.open方法;同时设置file的操作函数集合为vfio_device_fops:
static const struct file_operations vfio_device_fops = {
.owner = THIS_MODULE,
.release = vfio_device_fops_release,
.read = vfio_device_fops_read,
.write = vfio_device_fops_write,
.unlocked_ioctl = vfio_device_fops_unl_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = vfio_device_fops_compat_ioctl,
#endif
.mmap = vfio_device_fops_mmap,
};
注:上述vfio_device_fops_*函数集合是对vfio_pci_*的包装,或者说代理。