- IOVA
- VFIO
- 因为IOMMU和EPT共用页表项,所以也需要判断下是否支持内存虚拟化
- 判断CPU是否支持虚拟化,Intel系列CPU支持虚拟化的标志为“vmx”,AMD系列CPU的标志为“svm”
- 判断是否开启了基础虚拟化功能
- 判断是否开启VT-D,这个只支持Intel机器
- 假如设备如下
- 确定这个设备所属group,因为group 是IOMMU 进行DMA隔离的最小单元
- 如果设备对应的RC以及switch设备一旦不支持PCIE ACS路由功能,那么凉凉,
- 因为IOMMU会将整个RC都挂到同一个group中, 后边分离后会将整个RC桥都分离。
- 解除绑定
- 将设备挂到vfio上
- 直通或者SR-IOV设备
- VFIO-MDEV
IOVA
IO 其实都知道,就是输入输出设备,VA也听说过,就是虚拟地址。IOVA?对,就是IO的虚拟地址。 个人理解就是: 总线地址, dma_addr_t 见过吧。
LDD3 P392: 一个 DMA 映射是 分配一个 设备的DMA 能访问到的内存。
它试图使用一个简单的对 virt_to_bus 的调用来获得这个地址, 但是有充分的理由来避免那个方法,它们中的第一个是合理的硬件带有一个 IOMMU 来为总线提供一套映射寄存器.
IOMMU 可为任何物理内存安排来出现在设备可存取的地址范围内, 并且它可使物理上散布的缓冲对设备看来是连续的. ===》 IOMMU可以解决 外设与CPU地址总线位数不一致问题; 也能对物理不连续的内存进行DMA传输
使用 IOMMU 需要使用通用的 DMA 层; virt_to_bus 不负责这个任务
DMA
在学习 VIDI-DMA学习 的时候,有部分这样描述的:
Dynamic DMA mapping Guide(翻译: DMA-API-HOWTO.txt)中 描述了虚拟地址和总线地址
驱动在调用dma_map_single/dma_alloc_coherent 这样的接口函数的时候会传递一个虚拟地址X,在这个函数中会设定IOMMU的页表,将地址X映射到Z,并且将返回z这个总线地址
CPU CPU Bus
Virtual Physical Address
Address Address Space
Space Space
+-------+ +------+ +------+
| | |MMIO | Offset | |
| | Virtual |Space | applied | |
C +-------+ --------> B +------+ ----------> +------+ A
| | mapping | | by host | |
+-----+ | | | | bridge | | +--------+
| | | | +------+ | | | |
| CPU | | | | RAM | | | | Device |
| | | | | | | | | |
+-----+ +-------+ +------+ +------+ +--------+
| | Virtual |Buffer| Mapping | |
X +-------+ --------> Y +------+ <---------- +------+ Z
| | mapping | RAM | by IOMMU
| | | |
| | | |
+-------+ +------+
IOMMU这个部分是怎么转换的? 什么是IOVA?
这里要理解两个关键点:
- 外设访问主存,只能通过DMA方式。
- DMA设备使用的是总线地址,在有IOMMU的情况,总线地址和物理地址不一定相等。
IOVA
IOVA- io virtual address 文章中描述了几个部分:
- 总线地址的由来?因为给设备上带了个页表,也就是 IOMMU的诞生,使得 从DMA 去看内存 用的是虚拟地址。
- 有了IOMMU,总线地址不一定等于物理地址咯,所以 dma_alloc_coherent / dma_map_single 等 对于buffer的管理都需要先转到总线地址上。
- IOMMU也解决了 设备端 和 主机端 总线位数不一致情况。
- IOMMU可以使DMA支持虚拟地址, 因此可以将DMA导入到用户空间可用。
IOMMU下的DMA buffer申请
dma_alloc_coherent 一致性DMA
先简单看下DMA的代码流程
// 一致性dma buffer的申请流程
dmam_alloc_coherent // drivers/base/dma-mapping.c
dma_alloc_coherent // include/linux/dma-mapping.h
dma_alloc_attrs // 重点接口
const struct dma_map_ops *ops = get_dma_ops(dev);
// dma_alloc_from_dev_coherent(.....) // 不用管,没实现
// arch_dma_alloc_attrs(&dev, &flag) // 也不用管,不支持CONFIG_HAVE_GENERIC_DMA_COHERENT
cpu_addr = ops->alloc(dev, size, dma_handle, flag, attrs);
// debug_dma_alloc_coherent(dev, size, *dma_handle, cpu_addr); // 调试的接口
// 先看下intel的 get_dma_ops(dev),也就是ops接口
// 没开IOMMU
const struct dma_map_ops nommu_dma_ops = {
.alloc = dma_generic_alloc_coherent,
.free = dma_generic_free_coherent,
.map_sg = nommu_map_sg,
.map_page = nommu_map_page,
.sync_single_for_device = nommu_sync_single_for_device,
.sync_sg_for_device = nommu_sync_sg_for_device,
.is_phys = 1,
.mapping_error = nommu_mapping_error,
.dma_supported = x86_dma_supported,
};
// 开了IOMMU 在intel_iommu_init中有: dma_ops = &intel_dma_ops;
dma_ops = &intel_dma_ops;
const struct dma_map_ops intel_dma_ops = {
.alloc = intel_alloc_coherent,
.free = intel_free_coherent,
.map_sg = intel_map_sg,
.unmap_sg = intel_unmap_sg,
.map_page = intel_map_page,
.unmap_page = intel_unmap_page,
.mapping_error = intel_mapping_error,
#ifdef CONFIG_X86
.dma_supported = x86_dma_supported,
#endif
};
所以,在开启IOMMU之后,调用的dma接口是
intel_alloc_coherent(dev,size,dma_addr_t, gfp_t, 0)
iommu_no_mapping // ???
if (gfpflags_allow_blocking(flags)) { // 申请内存允许阻塞的话(GFP带阻塞标记)
// 尝试从CMA去分配内存
page = dma_alloc_from_contiguous(dev, count, order, flags);
}
// 如果是非阻塞方式,或者CMA分配内存失败,通过alloc_pages
page = alloc_pages(flags, order);
// 管理虚拟地址到总线地址的映射 (重点)
__intel_map_single(dev, page_to_phys(page), size,
DMA_BIDIRECTIONAL,
dev->coherent_dma_mask);
注释: 非PCIE 设备 使用 dma buffer时,会分配一个domain
https://patchwork.kernel.org/project/platform-driver-x86/patch/20190123230131.42094-1-sfruhwald@google.com/
> + #if IS_ENABLED(CONFIG_IOMMU_API) &&
> defined(CONFIG_INTEL_IOMMU)
> + dev->dev.archdata.iommu = INTEL_IOMMU_DUMMY_DOMAIN;
> + #endif
dma_map_single 流式DMA
dma_map_single_attrs
addr = ops->map_page(dev, virt_to_page(ptr),...)
// 在开启IOMMU情况 ops->map_page == intel_map_page
intel_map_page
__intel_map_single(dev, page_to_phys(page) + offset, size,
dir, *dev->dma_mask);
注: 关于__intel_map_single 的实现,后边分析intel iommu代码时在考虑。
个人理解:
在用IOMMU之前, dma接口申请的内存地址,其实就是物理地址HPA,所以存在 HPA==总线地址dma_addr_t。(当然不同架构可能有不同设计,好像PowerPC是加了个offset的线性映射)。
在使用IOMMU后,所有的DMA内存分配 经过 __intel_map_single 之后, 给的不再是 HPA, 吧对应 客户机的物理地址(GPA) 传给DMA就行。(在只开启stage2 的情况,如果开启了stage1,甚至可能将用户空间的虚拟地址给DMA都可以,牛逼不)。
IOVA是怎么解决物理地址不连续的问题?
LLDMA 是如何实现的
还记得LDD3中有一章 特别生涩难懂的 发散/汇聚映射 ? 一听都不想看,其实就是 scatter/gather 好像就是LLDMA (Link List DMA)
PCIe实践之路:DMA机制 中描述的很好(虽然图画的不怎么形象): 将每一个DMA传输 用链表连起来,一次发送多个DMA传输。
这样似乎解决了DMA需求的:内存物理地址连续。 但 实质上 是 将DMA化成了多次传输。 Linux下DMA驱动框架分析 的图片画的不错:
IOMMU是如何实现的?
可以看到,DMA加入了IOMMU后,只要给总线域的地址(IOVA-在开启stage2 MMU情况下,IOVA就是GPA,在开启stage1+2 MMU情况下,IOVA是GVA)保持连续就好,也就是虚拟地址连续就行。DMA发出地址信号后,由IOMMU去关联到对应的存储域地址。
VFIO
VFIO基础知识
参考:
- An Introduction to PCI Device Assignment with VFIO - Williamson
- Introduction to VFIO
- QEMU/KVM源码解析及应用-李强 7.7章节
- Linux内核VFIO
VFIO的作用
VFIO的目的是把设备的DMA能力直接暴露到用户态,也就是说:基于IOMMU功能,用户层可以直接用设备的DMA功能。
vfio的基本思想和原理
vfio是一个用户态驱动框架,利用硬件I/O虚拟化技术,将设备直通给虚拟机。
vfio的基本思想:
- 分解:将设备资源分解,并将资源接口导出到用户空间。
- 聚合:将硬件设备资源聚合,对虚拟化展示一个完整的设备接口。
IOMMU需要关注的事:
难点1:地址隔离,因为DMA可以指定任意地址。 IOMMU需要将主机保护起来,只允许DMA给自己当前 Guest 内存空间写入。
难点2:MSI的中断重定向, 使设备能根据自己所属domain,而只给当前Guest产生中断。
VFIO工作的机制:
IOVA- io virtual address 文章中对VFIO有了部分描述比较通俗易懂:
人们需要支持虚拟化,提出了VFIO的概念,需要在用户进程中直接访问设备,那我们就要支持在用户态直接发起DMA操作了,用户态发起DMA,它自己在分配iova,直接设置下来,要求iommu就用这个iova,那我内核对这个设备做dma_map,也要分配iova。这两者冲突怎么解决呢?
VFIO这样解决:
默认情况下,iommu上会绑定一个default_domain,它具有IOMMU_DOMAIN_DMA属性,原来怎么弄就怎么弄,这时你可以调用dma_map()。
但如果你要用VFIO,你就要先detach原来的驱动,改用VFIO的驱动,VFIO就给你换一个domain,这个domain的属性是IOMMU_DOMAIN_UNMANAGED,之后你爱用哪个iova就用那个iova,你自己保证不会冲突就好,VFIO通过iommu_map(domain, iova, pa)来执行这种映射。
等你从VFIO上detach,把你的domain删除了,这个iommu就会恢复原来的default_domain,这样你就可以继续用你的dma API了。
这种情况下,你必须给你的设备选一种应用模式,非此即彼。
所以这就是直通模式 和 SR-IOV模式时,必须先将PCIE设备unbind掉并添加到vfio-driver中,才可以导入到虚拟机。 其实就是 IOMMU-DOMAIN的更换。
VFIO框架
- VFIO Interface : 讲group,container,device 给用户层提供了字符设备接口,直接ioctl来控制设备。
- iommu driver 是物理硬件提供IOMMU的驱动实现,然后注册到vfio中,这里都是TYPE1
- pci bus : 物理pci设备驱动
- vfio_iommu 对底层iommu driver封装
- vfio-pci 是对设备驱动的封装。
VFIO的Container,group和device
group 是IOMMU进行DMA隔离的最小单元, 一个group可以有1-N个设备。
container 是由多个group组成,为了更好的管理分割粒度,所以将多个group 看作一个container。
一般来说,一个进程/虚拟机可以看作一个container,这样一个container就共享同一组页表。
VFIO的三个子系统
听说过VFIO,VFIO-PCI,VFIO-MDEV(后边看),但这几个有什么区别?都是干什么的?
Linux iommu和vfio概念空间解构 中给了不错的解释:
首先说,vfio就是一个驱动模块,它的作用是通过device的override_driver接口(通过/sys直接强行重新绑定一个设备的驱动),让自己成为那个设备的驱动,在这个驱动中,把这个设备的io空间和iommu_group直接暴露到用户态。
/home/baiy/workspace/linux-git/drivers/vfio
root@inno-MS-7B89:vfio# ls -al
total 180
drwxrwxrwx 5 baiy baiy 4096 12月 11 09:46 .
drwxrwxrwx 131 baiy baiy 4096 12月 11 09:46 ..
-rwxrwxrwx 1 baiy baiy 1462 12月 11 09:46 Kconfig
-rwxrwxrwx 1 baiy baiy 402 12月 5 15:42 Makefile
drwxrwxrwx 2 baiy baiy 4096 12月 11 09:46 mdev // mdev子系统
drwxrwxrwx 2 baiy baiy 4096 12月 11 09:46 pci // pci 子系统
drwxrwxrwx 3 baiy baiy 4096 12月 11 09:46 platform // platform子系统
-rwxrwxrwx 1 baiy baiy 59147 12月 11 09:46 vfio.c
-rwxrwxrwx 1 baiy baiy 33818 12月 11 09:46 vfio_iommu_spapr_tce.c
-rwxrwxrwx 1 baiy baiy 41122 12月 11 09:46 vfio_iommu_type1.c
-rwxrwxrwx 1 baiy baiy 2812 12月 11 09:46 vfio_spapr_eeh.c
-rwxrwxrwx 1 baiy baiy 5597 12月 11 09:46 virqfd.c
VFIO使用
- 首先确保是否开启了VT-d等硬件功能。
```bash
因为IOMMU和EPT共用页表项,所以也需要判断下是否支持内存虚拟化
判断CPU是否支持虚拟化,Intel系列CPU支持虚拟化的标志为“vmx”,AMD系列CPU的标志为“svm”
baiy@baiy-ThinkPad-E470c:~$ grep -E ‘svm|vmx’ /proc/cpuinfo flags : ….. vmx …..
判断是否开启了基础虚拟化功能
baiy@internal:baiy$ kvm-ok INFO: /dev/kvm exists KVM acceleration can be used
判断是否开启VT-D,这个只支持Intel机器
baiy@internal:baiy$ dmesg | grep “DMAR-IR: Enabled IRQ remapping” [ 0.004000] DMAR-IR: Enabled IRQ remapping in x2apic mode
- 其次确保内核有配置相关模块,对于编译成模块的在Ubuntu下建议放到 /etc/modules,参考[Loadable_Modules](https://help.ubuntu.com/community/Loadable_Modules)
```bash
root@inno-MS-7B89:linux-git# vim /boot/config-$(uname -r)
.....
CONFIG_VFIO_IOMMU_TYPE1=y
CONFIG_VFIO_VIRQFD=y
CONFIG_VFIO=y
CONFIG_VFIO_NOIOMMU=y
CONFIG_VFIO_PCI=y
CONFIG_VFIO_PCI_VGA=y
CONFIG_VFIO_PCI_MMAP=y
CONFIG_VFIO_PCI_INTX=y
CONFIG_VFIO_PCI_IGD=y
CONFIG_VFIO_MDEV=m // 使用vfio-mdev需要将这两个模块也添加进去
CONFIG_VFIO_MDEV_DEVICE=m
- 开启虚拟化时加入vfio设备-qemu方式
```bash
假如设备如下
26:00.0 Memory controller: Xilinx Corporation Device 9032 (rev 03)
确定这个设备所属group,因为group 是IOMMU 进行DMA隔离的最小单元
如果设备对应的RC以及switch设备一旦不支持PCIE ACS路由功能,那么凉凉,
因为IOMMU会将整个RC都挂到同一个group中, 后边分离后会将整个RC桥都分离。
root@inno-MS-7B89:pci0000:00# dmesg | grep group [ 1.070368] pci 0000:26:00.0: Adding to iommu group 18 [ 1.070400] pci 0000:26:00.1: Adding to iommu group 18 root@inno-MS-7B89:pci0000:00# ls -al /sys/bus/pci/devices/0000\:26\:00.0/iommu_group lrwxrwxrwx 1 root root 0 12月 19 15:23 /sys/bus/pci/devices/0000:26:00.0/iommu_group -> ../../../../kernel/iommu_groups/18
解除绑定
注:也可通过 https://github.com/andre-richter/vfio-pci-bind 提供的方法进行解除绑定 root@inno-MS-7B89:iommu_groups# lspci -s 26:00.0 -knx …… Kernel driver in use: xxxx
echo 0000:26:00.0 >/sys/bus/pci/devices/0000:26:00.0/driver/unbind
将设备挂到vfio上
echo “${vendor} ${device}” > /sys/bus/pci/drivers/vfio-pci/new_id
直通或者SR-IOV设备
-device vfio-pci,host=26:00.0,addr=xx # 在Guest上,这个addr就是在Guest上BDF的Device号,避免编号冲突而已。随便找个8*[0~31]就好了
- 开启Guest时加入vfio设备-libvirtd方式-TBD,没找到相关资料
<a name="34RWV"></a>
### VFIO代码分析
> 这里不考虑 CONFIG_VFIO_NOIOMMU 的情况,虽然ubuntu默认支持。详情参考: [VFIO No-IOMMU支持](https://cateee.net/lkddb/web-lkddb/VFIO_NOIOMMU.html)
![image.png](https://cdn.nlark.com/yuque/0/2020/png/2819254/1608449457897-69f27e33-ac4a-42c4-b9b1-04a75c2aa65a.png#crop=0&crop=0&crop=1&crop=1&height=255&id=lGwn8&margin=%5Bobject%20Object%5D&name=image.png&originHeight=340&originWidth=397&originalType=binary&ratio=1&rotation=0&showTitle=false&size=251620&status=done&style=none&title=&width=298)<br />代码流程无非就是:** 容器操作,组操作,设备操作 三类,**这里参考《QEMU/KVM源码解析及应用-李强》 7.7章节,不在重复
<a name="nDNLZ"></a>
#### 容器的初始化代码
容器初始化代码基本固定的。
```c
#include <linux/vfio.h>
cfd = open("/dev/vfio/vfio", O_RDWR);
if(ioctl(cfd, VFIO_GET_API_VERSION, buf) == 0){
if(ioctl(cfd, VFIO_CHECK_EXTENSION, VFIO_TYPE1_IOMMU) == 0){
ioctl(cfd, VFIO_SET_IOMMU, VFIO_TYPE1_IOMMU);
}
}
close(cfd)
在内核初始化,以及容器 这部分初始化代码下,代码结构如下
所以用户态调用ioctl接口,在vfio_ioctl_set_iommu之后,大部分都调用到 vfio_iommu_type1_ioctl 中(除获取版本等)。
# linux-git\drivers\vfio\vfio.c
vfio_init
misc_register(&vfio_dev); // /dev/vfio/vfio misc字符设备,操作接口是:vfio_dev的vfio_fops
这里可以看到ioctl在用户空间VFIO_SET_IOMMU ,会调用 vfio_iommu_type1_ioctl
group初始化
我们先不讨论一个pci设备是如何添加到 /dev/vfio/$(group num)的,假设已经添加进来(也就是后边pci设备bind到vfio中),有什么变化
// group操作
gfd = open("/dev/vfio/18", O_RDWR); // file->private == vfio_group
if(gfd < 0){
perror("open gfd failed\n");
goto err1;
}
// 判断是否是个属于vfio的有效group,并绑定到容器中 , 根据container fd,将group绑定到container中
err = ioctl(gfd, VFIO_GROUP_GET_STATUS, &group_status);
if( (err == -1) || ((group_status.flags & VFIO_GROUP_FLAGS_VIABLE) == 0) ){
perror("invaild group for vfio\n");
goto err2;
}
if( group_status.flags & VFIO_GROUP_FLAGS_CONTAINER_SET) {
printf("group has set\n");
} else {
ioctl(gfd, VFIO_GROUP_SET_CONTAINER, cfd);
}
err = ioctl(gfd, VFIO_GROUP_GET_STATUS, &group_status);
pci device
VFIO模拟PCIE 配置空间的方式.docx
先看下在probe之前vfio_pci都做了哪些准备. TBD
struct perm_bits {
u8 *virt; /* read/write virtual data, not hw */
u8 *write; /* writeable bits */
int (*readfn)(struct vfio_pci_device *vdev, int pos, int count,
struct perm_bits *perm, int offset, __le32 *val);
int (*writefn)(struct vfio_pci_device *vdev, int pos, int count,
struct perm_bits *perm, int offset, __le32 val);
};
static struct perm_bits cap_perms[PCI_CAP_ID_MAX + 1] = {
[0 ... PCI_CAP_ID_MAX] = { .readfn = vfio_direct_config_read }
};
vfio_pci_init_perm_bits();
在进行
echo “${vendor} ${device}” > /sys/bus/pci/drivers/vfio-pci/new_id
的时候,肯定会进入到probe,看下probe都做了什么
vfio_add_group_dev(&pdev->dev, &vfio_pci_ops, vdev);
struct iommu_group *iommu_group = iommu_group_get(dev); // 根据实际pcie设备获取所属的iommu_gruop
struct vfio_group *group = vfio_group_get_from_iommu(iommu_group);
// struct vfio_device * = vfio_group_get_device(group, dev); 这里只是检测下是否已经有该设备,不允许多次添加
struct vfio_device *device = vfio_group_create_device(group, dev, ops, device_data);
可见这部分代码主要是: 例化了vfio_group和vfio_device
接下来看用户如何操作设备的
// 设备操作接口
dfd = ioctl(gfd, VFIO_GROUP_GET_DEVICE_FD, "0000:26:00.0");
if( (dfd == -1)){
perror("get device failed\n");
goto err2;
}
printf("get device OK\n");
ioctl(dfd,VFIO_DEVICE_GET_INFO,&device_info);
ioctl(dfd,VFIO_GET_IRQ_INFO,&irq);
ioctl(dfd,VFIO_DEVICE_RESET);
VFIO_GROUP_GET_DEVICE_FD 是如何获取fd的?(重点)
vfio_group_fops_unl_ioctl
vfio_group_get_device_fd(group, buf);
device->ops->open(device->device_data);
vfio_pci_open(vfio_pci_device) // vfio_pci_probe 中的vdev
vfio_pci_enable(vdev); // 第一次会使能PCIE
pci_enable_device(pdev);
pci_try_reset_function(pdev); // 所以之前遇到过 SR-IOV中VF复位异常原因
anon_inode_getfile("[vfio-device]", &vfio_device_fops,); // 重点接口,vfio_device_fops 的接口最终都会调用到 vfio_pci_ops
fd_install(ret, filep); // 返回文件描述符
重要接口快速查询
这里避免混乱,强调几个重要接口
container的操作接口: vfio_fops (在用户空间执行 VFIO_SET_IOMMU 之后,基本都是 vfio_iommu_driver_ops_type1 )
group的操作接口: vfio_group_fops
device的接口 vfio_pci_ops ,在VFIO_GROUP_GET_DEVICE_FD 获取到设备的文件描述符后,给用户层对接的接口是: vfio_device_fops, 然后这部分最终都调用vfio_pci_ops
VFIO-MDEV
相关资料
使用前提:
insmod /lib/modules/5.4.0-58-generic/kernel/drivers/vfio/mdev/mdev.ko
insmod /lib/modules/5.4.0-58-generic/kernel/drivers/vfio/mdev/vfio_mdev.ko
vfio-mdev模型的核心在于mdev会对硬件设备的状态进行抽象,将硬件设备的“状态”保存在mdev device数据结构中, 设备驱动层面要求实现一个调度器,将多个mdev设备在硬件设备上进行调度(分时复用), 从而实现把一个物理硬件设备分享给多个虚拟机实例进行使用
框架看起来很简单,接下来分析下: KERNEL_DOC-VFIO Mediated devices 中提供的 mtty.c 以及 vfio-mdev源码.
初始化过程
这里经过测试,很疑惑为什么会出现 mtty-1和mtty-2
root@inno-MS-7B89:testntty# pwd
/sys/devices/virtual/mtty/testntty
root@inno-MS-7B89:testntty# tree mdev_supported_types/
mdev_supported_types/
├── mtty-1
│ ├── available_instances
│ ├── create
│ ├── device_api
│ ├── devices
│ └── name
└── mtty-2
├── available_instances
├── create
├── device_api
├── devices
│ └── 83b8f4f2-509f-382f-3c1e-e6bfe0fa1001 -> ../../../83b8f4f2-509f-382f-3c1e-e6bfe0fa1001
└── name
分析代码:
mdev_register_device
parent_create_sysfs_files(parent); // struct mdev_parent *parent;
kset_create_and_add("mdev_supported_types",...); // 创建mdev_supported_types目录
add_mdev_supported_type_groups(parent);
parent->ops->supported_type_groups[i] // 所以主要看这部分 创建了 mtty-1和mtty-2,
add_mdev_supported_type(parent, parent->ops->supported_type_groups[i]);
这里ops 来源 调用mdev_register_device
mtty_dev_init
mdev_register_device(&mtty_dev.dev, &mdev_fops); // mdev_fops 中 .supported_type_groups = mdev_type_groups,
static struct attribute_group *mdev_type_groups[] = { // 两边属性一致,随意使用
&mdev_type_group1,
&mdev_type_group2,
NULL,
};
这里有几个部分需要注意下:
- mtty_dev_init->class_compat_register(“mdev_bus”) 所以 必须注册任意一个vfio-mdev驱动后,才会有 /sys/class/mdev_bus 节点
问题:加入有N个虚拟机,需要用channel来区分,怎么办?
考虑用uuid作为区分,然后根据uuid来索引channel index
问题:如何区分channel
mdev_device_create
struct mdev_device *mdev; // 创建了mdev;
list_add(&mdev->next, &mdev_list); // 添加到全局变量
memcpy(&mdev->uuid, &uuid, sizeof(uuid_le)); // uuid, 所以可以遍历mdev来根据uuid查找 mdev_device
在用户空间创建接口的时候:
mtty_create
mdev_set_drvdata(mdev, mdev_state); // 分配私有的mdev_state结构 . 所以要加channel在这里加
// 上层调用了 open/read/write/close等接口,会传递 struct mdev_device,
// 在mtty中根据:
struct mdev_state *mdev_state;
mdev_state = mdev_get_drvdata(mdev); // 获取私有的mdev_state结构
比如:
echo "83b8f4f2-509f-382f-3c1e-e6bfe0fa1000" > /sys/devices/virtual/mtty/mtty/mdev_supported_types/mtty-1/create
内核mtty_create打印uuid:
注意:在create_store 会将断续转换
[51509.484676] [vfio_get_channel:855] mdev id is
[51509.484678] 0xf2
[51509.484679] 0xf4
[51509.484679] 0xb8
[51509.484679] 0x83
[51509.484679] 0x9f
[51509.484680] 0x50
[51509.484680] 0x2f
[51509.484680] 0x38
[51509.484681] 0x3c
[51509.484681] 0x1e
[51509.484681] 0xe6
[51509.484681] 0xbf
[51509.484682] 0xe0
[51509.484682] 0xfa
[51509.484682] 0x10
[51509.484683] 0x00
[51509.484880] vfio_mdev 83b8f4f2-509f-382f-3c1e-e6bfe0fa1000: Adding to iommu group 24
[51509.484882] vfio_mdev 83b8f4f2-509f-382f-3c1e-e6bfe0fa1000: MDEV: group_id = 24
echo 1 > /sys/bus/mdev/devices/83b8f4f2-509f-382f-3c1e-e6bfe0fa1000/remove
这里提供一个channel 控制脚本
if [ "$1" == "create" ]
then
echo "Start create 16 channel"
for ((i=0; i<3; ++i))
do
a=$(printf "%02x" $i)
echo "83b8f4f2-509f-382f-3c1e-e6bfe0fa10$a" > /sys/devices/virtual/mtty/mtty/mdev_supported_types/mtty-1/create
done
else
echo "Start delete 16 channel"
for ((i=0;i<3;++i))
do
a=$(printf "%02x" $i)
echo 1 > "/sys/bus/mdev/devices/83b8f4f2-509f-382f-3c1e-e6bfe0fa10$a/remove"
done
fi
使用
注:环境支持参考 VFIO使用章节。
-device vfio-pci,addr=08,\
sysfsdev=/sys/bus/mdev/devices/83b8f4f2-509f-382f-3c1e-e6bfe0fa1001
注意:这些字段间不能又空格
看见初始化代码,整体比较完整,但好像缺了一部分? mdev_bus_type注册了没使用? 目前由于没看到vfio-pci部分,猜测vfio-pci注册了vfio_mdev_driver 会调用这部分接口
static const struct vfio_device_ops vfio_mdev_dev_ops = {
.name = "vfio-mdev",
.open = vfio_mdev_open, ===> mdev_fops.open ==> mtty_open
.release = vfio_mdev_release, ===> mdev_fops.release ==> mtty_close
.mmap = vfio_mdev_mmap, ===> mdev_fops.mmap ==> NULL
// 重点接口
.ioctl = vfio_mdev_unlocked_ioctl, ===> mdev_fops.ioctl ==> mtty_ioctl
.read = vfio_mdev_read, ===> mdev_fops.read ==> mtty_read
.write = vfio_mdev_write, ===> mdev_fops.write ==> mtty_write
};
所以,在vfio-mdev被Guest识别到后,调用的其实是 vfio_mdev_dev_ops,但这部分最终 只需要关心 mdev_fops 接口就好了。
create
echo "83b8f4f2-509f-382f-3c1e-e6bfe0fa1001" > \
/sys/devices/virtual/mtty/mtty/mdev_supported_types/mtty-2/create
实现如下:
mtty_create
struct mdev_state *mdev_state;
mdev_state->vconfig = kzalloc(MTTY_CONFIG_SPACE_SIZE, GFP_KERNEL); // 模拟PCI设备,创建配置空间
mtty_create_config_space(mdev_state);
list_add(&mdev_state->next, &mdev_devices_list);
qemu初始化操作
还记得前边描述的vfio是如何使用设备的?
- open
- ioctl VFIO_DEVICE_RESET
- ioctl VFIO_DEVICE_GET_INFO
- ioctl VFIO_DEVICE_GET_REGION_INFO
- ioctl VFIO_GET_IRQ_INFO
// 设备操作接口
dfd = ioctl(gfd, VFIO_GROUP_GET_DEVICE_FD, "0000:26:00.0");
vfio_group_get_device_fd(group, buf);
device->ops->open(device->device_data);
mtty_open(vfio_pci_device) //===> 先会调用open接口,获取到文件描述符,提供对用层VFS的接口 vfio_device_fops
// 其实不用关心 vfio_device_fops,只需要关注自己设备注册的接口就好,最后调用的还是 mdev_fops
// 可能会调用reset接口:VFIO_DEVICE_RESET
printf("get device OK\n");
ioctl(dfd,VFIO_DEVICE_GET_INFO,&device_info); // VFIO_DEVICE_GET_INFO
ioctl(dfd,VFIO_DEVICE_GET_REGION_INFO,®); // VFIO_DEVICE_GET_REGION_INFO
ioctl(dfd,VFIO_GET_IRQ_INFO,&irq); // VFIO_DEVICE_GET_IRQ_INFO
ioctl(dfd,VFIO_DEVICE_RESET); // VFIO_DEVICE_RESET
Guest系统初始化
注:调试阶段最好吧mtty的打印信息打开
#define DEBUG_REGS 1
#define DEBUG_INTR 1
#define DEBUG 1
- 读取配置空间
- 读取OPTION ROM
- set IRQ
读写操作
上层接口无论读写BAR空间还是配置空间,接口都会调用到:mtty_write 和 mtty_read
还记得在:ioctl中 VFIO_DEVICE_GET_REGION_INFO 有一个很重要的信息:
mtty_get_region_info
#define MTTY_VFIO_PCI_OFFSET_SHIFT 40
region_info->offset = MTTY_VFIO_PCI_INDEX_TO_OFFSET(bar_index); // region_info->offset[63:40]代表区域
mtty_read / mtty_write 中 ppos有个很大的作用:
然后在 mdev_access (mtty_read / mtty_write 中会调用)
index = MTTY_VFIO_PCI_OFFSET_TO_INDEX(pos); // 所以 ppos[63:40]代表读写区域
offset = pos & MTTY_VFIO_PCI_OFFSET_MASK; // 所以 ppos[40:0]为串口操作属性,比如THR,IER等配置,或RX/TX 数据消息
enum {
VFIO_PCI_BAR0_REGION_INDEX,
VFIO_PCI_BAR1_REGION_INDEX,
VFIO_PCI_BAR2_REGION_INDEX,
VFIO_PCI_BAR3_REGION_INDEX,
VFIO_PCI_BAR4_REGION_INDEX,
VFIO_PCI_BAR5_REGION_INDEX,
VFIO_PCI_ROM_REGION_INDEX,
VFIO_PCI_CONFIG_REGION_INDEX,
VFIO_PCI_VGA_REGION_INDEX,
VFIO_PCI_NUM_REGIONS = 9 /* Fixed user ABI, region indexes >=9 use */
/* device specific cap to define content. */
};
先简单看下串口的一些属性:
在分析打印日志的时候,注意读写操作的关键信息
# 数据读写
mdev_access: BAR0 WR @0x4 MCR val:0x00 dlab:0
BARn: 代表操作第几个port
WR/RD: 读还是写
MCR/TX/RX: 这串口的属性,也就是 ppos[39:0]的内容
中断触发
在测试的时候,会发现:如果没数据,单纯读,可能会无限阻塞,因为没中断。
在 KERNEL_DOC-VFIO Mediated devices 中最后描述了一句: 数据从主机mtty驱动程序环回。
所以我们要看读是怎么触发的,需要分析的是mtty_write->handle_bar_write->mtty_trigger_interrupt(mdev_state)
/*
* Trigger interrupt if receive data interrupt is
* enabled and fifo reached trigger level
*/
if ((mdev_state->s[index].uart_reg[UART_IER] & UART_IER_RDI) && \
(mdev_state->s[index].rxtx.count ==mdev_state->s[index].intr_trigger_level))
{
mtty_trigger_interrupt( mdev_uuid(mdev_state->mdev)); // mdev_uuid 在echo时调用 create_stor会赋值并绑定到mdev上
}
这里判断 中断是否使能, 判断数据个数是否达到fifo阈值,然后进行触发中断
注:这里mttytrigger_interrupt 使用 Linux进程间通信:eventfd 来给qemu发送中断信息_,暂时不是我研究的重点,可以参考 Insight Into VFIO。
总结:如果说,VFIO PCI是 Guest 去访问硬件的中间层的话, 那么VFIO-MDEV就是模拟硬件,接到VFIO PCI上,作为一个伪PCI设备 来 欺骗VFIO_PCI。
VFIO-MTTY
其实看vfio-mtty代码得时候,我们不一定非要把pci设备当作一个uart设备,可以单纯当作一个PCIE设备,自己修改修改配置空间 ,
作为一个PCI设备,然后在Guest层加入自己得驱动。
另外,mtty read/write虽然代码只支持 1,2,4字节读写,看起来很low,但Guest 去读写后,也会转成1,2,4字节读写。
virt-manager如何添加vfio-pci设备
先在/etc/libvirt/qemu/ubuntu18.04.xml 中添加自己的配置
重点参考:formatdomain.html 中mdev描述
<hostdev mode='subsystem' type='mdev' managed='no' model='vfio-pci'>
<source>
<address uuid='83b8f4f2-509f-382f-3c1e-e6bfe0fa1001'/>
</source>
<address type='pci' domain='0x0000' bus='0x00' slot='0x09' function='0x0'/>
</hostdev>
注意:修改完成后一定要:更新配置 virsh define /etc/libvirt/qemu/ubuntu18.04.xml