注:以下备注 个人理解的不保证绝对正确,也在持续学习中。

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这个总线地址

  1. CPU CPU Bus
  2. Virtual Physical Address
  3. Address Address Space
  4. Space Space
  5. +-------+ +------+ +------+
  6. | | |MMIO | Offset | |
  7. | | Virtual |Space | applied | |
  8. C +-------+ --------> B +------+ ----------> +------+ A
  9. | | mapping | | by host | |
  10. +-----+ | | | | bridge | | +--------+
  11. | | | | +------+ | | | |
  12. | CPU | | | | RAM | | | | Device |
  13. | | | | | | | | | |
  14. +-----+ +-------+ +------+ +------+ +--------+
  15. | | Virtual |Buffer| Mapping | |
  16. X +-------+ --------> Y +------+ <---------- +------+ Z
  17. | | mapping | RAM | by IOMMU
  18. | | | |
  19. | | | |
  20. +-------+ +------+

IOMMU这个部分是怎么转换的? 什么是IOVA?
这里要理解两个关键点:

  • 外设访问主存,只能通过DMA方式
  • DMA设备使用的是总线地址,在有IOMMU的情况,总线地址和物理地址不一定相等

IOVA

IOVA- io virtual address 文章中描述了几个部分:

  • 总线地址的由来?因为给设备上带了个页表,也就是 IOMMU的诞生,使得 从DMA 去看内存 用的是虚拟地址。
  • 有了IOMMU,总线地址不一定等于物理地址咯,所以 dma_alloc_coherent / dma_map_single 等 对于buffer的管理都需要先转到总线地址上。
  • IOMMU也解决了 设备端 和 主机端 总线位数不一致情况。
  • IOMMU可以使DMA支持虚拟地址, 因此可以将DMA导入到用户空间可用。

image.pngimage.png

IOMMU下的DMA buffer申请

dma_alloc_coherent 一致性DMA


先简单看下DMA的代码流程

  1. // 一致性dma buffer的申请流程
  2. dmam_alloc_coherent // drivers/base/dma-mapping.c
  3. dma_alloc_coherent // include/linux/dma-mapping.h
  4. dma_alloc_attrs // 重点接口
  5. const struct dma_map_ops *ops = get_dma_ops(dev);
  6. // dma_alloc_from_dev_coherent(.....) // 不用管,没实现
  7. // arch_dma_alloc_attrs(&dev, &flag) // 也不用管,不支持CONFIG_HAVE_GENERIC_DMA_COHERENT
  8. cpu_addr = ops->alloc(dev, size, dma_handle, flag, attrs);
  9. // debug_dma_alloc_coherent(dev, size, *dma_handle, cpu_addr); // 调试的接口
  10. // 先看下intel的 get_dma_ops(dev),也就是ops接口
  11. // 没开IOMMU
  12. const struct dma_map_ops nommu_dma_ops = {
  13. .alloc = dma_generic_alloc_coherent,
  14. .free = dma_generic_free_coherent,
  15. .map_sg = nommu_map_sg,
  16. .map_page = nommu_map_page,
  17. .sync_single_for_device = nommu_sync_single_for_device,
  18. .sync_sg_for_device = nommu_sync_sg_for_device,
  19. .is_phys = 1,
  20. .mapping_error = nommu_mapping_error,
  21. .dma_supported = x86_dma_supported,
  22. };
  23. // 开了IOMMU 在intel_iommu_init中有: dma_ops = &intel_dma_ops;
  24. dma_ops = &intel_dma_ops;
  25. const struct dma_map_ops intel_dma_ops = {
  26. .alloc = intel_alloc_coherent,
  27. .free = intel_free_coherent,
  28. .map_sg = intel_map_sg,
  29. .unmap_sg = intel_unmap_sg,
  30. .map_page = intel_map_page,
  31. .unmap_page = intel_unmap_page,
  32. .mapping_error = intel_mapping_error,
  33. #ifdef CONFIG_X86
  34. .dma_supported = x86_dma_supported,
  35. #endif
  36. };

所以,在开启IOMMU之后,调用的dma接口是

  1. intel_alloc_coherent(dev,size,dma_addr_t, gfp_t, 0)
  2. iommu_no_mapping // ???
  3. if (gfpflags_allow_blocking(flags)) { // 申请内存允许阻塞的话(GFP带阻塞标记)
  4. // 尝试从CMA去分配内存
  5. page = dma_alloc_from_contiguous(dev, count, order, flags);
  6. }
  7. // 如果是非阻塞方式,或者CMA分配内存失败,通过alloc_pages
  8. page = alloc_pages(flags, order);
  9. // 管理虚拟地址到总线地址的映射 (重点)
  10. __intel_map_single(dev, page_to_phys(page), size,
  11. DMA_BIDIRECTIONAL,
  12. dev->coherent_dma_mask);

注释: 非PCIE 设备 使用 dma buffer时,会分配一个domain
https://patchwork.kernel.org/project/platform-driver-x86/patch/20190123230131.42094-1-sfruhwald@google.com/

  1. > + #if IS_ENABLED(CONFIG_IOMMU_API) &&
  2. > defined(CONFIG_INTEL_IOMMU)
  3. > + dev->dev.archdata.iommu = INTEL_IOMMU_DUMMY_DOMAIN;
  4. > + #endif

dma_map_single 流式DMA

  1. dma_map_single_attrs
  2. addr = ops->map_page(dev, virt_to_page(ptr),...)
  3. // 在开启IOMMU情况 ops->map_page == intel_map_page
  4. intel_map_page
  5. __intel_map_single(dev, page_to_phys(page) + offset, size,
  6. 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驱动框架分析 的图片画的不错:
image.png

IOMMU是如何实现的?

image.png
可以看到,DMA加入了IOMMU后,只要给总线域的地址(IOVA-在开启stage2 MMU情况下,IOVA就是GPA,在开启stage1+2 MMU情况下,IOVA是GVA)保持连续就好,也就是虚拟地址连续就行。DMA发出地址信号后,由IOMMU去关联到对应的存储域地址。

VFIO

VFIO基础知识

参考:

VFIO的作用

VFIO的目的是把设备的DMA能力直接暴露到用户态,也就是说:基于IOMMU功能,用户层可以直接用设备的DMA功能

vfio的基本思想和原理

vfio是一个用户态驱动框架,利用硬件I/O虚拟化技术,将设备直通给虚拟机。

vfio的基本思想:

  • 分解:将设备资源分解,并将资源接口导出到用户空间。
  • 聚合:将硬件设备资源聚合,对虚拟化展示一个完整的设备接口。

image.pngimage.png

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框架

image.png

  • VFIO Interface : 讲group,container,device 给用户层提供了字符设备接口,直接ioctl来控制设备。
  • iommu driver 是物理硬件提供IOMMU的驱动实现,然后注册到vfio中,这里都是TYPE1
  • pci bus : 物理pci设备驱动
  • vfio_iommu 对底层iommu driver封装
  • vfio-pci 是对设备驱动的封装。

VFIO的Container,group和device

image.png
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直接暴露到用户态

  1. /home/baiy/workspace/linux-git/drivers/vfio
  2. root@inno-MS-7B89:vfio# ls -al
  3. total 180
  4. drwxrwxrwx 5 baiy baiy 4096 12 11 09:46 .
  5. drwxrwxrwx 131 baiy baiy 4096 12 11 09:46 ..
  6. -rwxrwxrwx 1 baiy baiy 1462 12 11 09:46 Kconfig
  7. -rwxrwxrwx 1 baiy baiy 402 12 5 15:42 Makefile
  8. drwxrwxrwx 2 baiy baiy 4096 12 11 09:46 mdev // mdev子系统
  9. drwxrwxrwx 2 baiy baiy 4096 12 11 09:46 pci // pci 子系统
  10. drwxrwxrwx 3 baiy baiy 4096 12 11 09:46 platform // platform子系统
  11. -rwxrwxrwx 1 baiy baiy 59147 12 11 09:46 vfio.c
  12. -rwxrwxrwx 1 baiy baiy 33818 12 11 09:46 vfio_iommu_spapr_tce.c
  13. -rwxrwxrwx 1 baiy baiy 41122 12 11 09:46 vfio_iommu_type1.c
  14. -rwxrwxrwx 1 baiy baiy 2812 12 11 09:46 vfio_spapr_eeh.c
  15. -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

  1. - 其次确保内核有配置相关模块,对于编译成模块的在Ubuntu下建议放到 /etc/modules,参考[Loadable_Modules](https://help.ubuntu.com/community/Loadable_Modules)
  2. ```bash
  3. root@inno-MS-7B89:linux-git# vim /boot/config-$(uname -r)
  4. .....
  5. CONFIG_VFIO_IOMMU_TYPE1=y
  6. CONFIG_VFIO_VIRQFD=y
  7. CONFIG_VFIO=y
  8. CONFIG_VFIO_NOIOMMU=y
  9. CONFIG_VFIO_PCI=y
  10. CONFIG_VFIO_PCI_VGA=y
  11. CONFIG_VFIO_PCI_MMAP=y
  12. CONFIG_VFIO_PCI_INTX=y
  13. CONFIG_VFIO_PCI_IGD=y
  14. CONFIG_VFIO_MDEV=m // 使用vfio-mdev需要将这两个模块也添加进去
  15. 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]就好了

  1. - 开启Guest时加入vfio设备-libvirtd方式-TBD,没找到相关资料
  2. <a name="34RWV"></a>
  3. ### VFIO代码分析
  4. > 这里不考虑 CONFIG_VFIO_NOIOMMU 的情况,虽然ubuntu默认支持。详情参考: [VFIO No-IOMMU支持](https://cateee.net/lkddb/web-lkddb/VFIO_NOIOMMU.html)
  5. ![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章节,不在重复
  6. <a name="nDNLZ"></a>
  7. #### 容器的初始化代码
  8. 容器初始化代码基本固定的。
  9. ```c
  10. #include <linux/vfio.h>
  11. cfd = open("/dev/vfio/vfio", O_RDWR);
  12. if(ioctl(cfd, VFIO_GET_API_VERSION, buf) == 0){
  13. if(ioctl(cfd, VFIO_CHECK_EXTENSION, VFIO_TYPE1_IOMMU) == 0){
  14. ioctl(cfd, VFIO_SET_IOMMU, VFIO_TYPE1_IOMMU);
  15. }
  16. }
  17. close(cfd)

在内核初始化,以及容器 这部分初始化代码下,代码结构如下
image.png
所以用户态调用ioctl接口,在vfio_ioctl_set_iommu之后,大部分都调用到 vfio_iommu_type1_ioctl 中(除获取版本等)。

  1. # linux-git\drivers\vfio\vfio.c
  2. vfio_init
  3. misc_register(&vfio_dev); // /dev/vfio/vfio misc字符设备,操作接口是:vfio_devvfio_fops
  4. 这里可以看到ioctl在用户空间VFIO_SET_IOMMU ,会调用 vfio_iommu_type1_ioctl

group初始化

我们先不讨论一个pci设备是如何添加到 /dev/vfio/$(group num)的,假设已经添加进来(也就是后边pci设备bind到vfio中),有什么变化

  1. // group操作
  2. gfd = open("/dev/vfio/18", O_RDWR); // file->private == vfio_group
  3. if(gfd < 0){
  4. perror("open gfd failed\n");
  5. goto err1;
  6. }
  7. // 判断是否是个属于vfio的有效group,并绑定到容器中 , 根据container fd,将group绑定到container中
  8. err = ioctl(gfd, VFIO_GROUP_GET_STATUS, &group_status);
  9. if( (err == -1) || ((group_status.flags & VFIO_GROUP_FLAGS_VIABLE) == 0) ){
  10. perror("invaild group for vfio\n");
  11. goto err2;
  12. }
  13. if( group_status.flags & VFIO_GROUP_FLAGS_CONTAINER_SET) {
  14. printf("group has set\n");
  15. } else {
  16. ioctl(gfd, VFIO_GROUP_SET_CONTAINER, cfd);
  17. }
  18. err = ioctl(gfd, VFIO_GROUP_GET_STATUS, &group_status);

pci device

VFIO模拟PCIE 配置空间的方式.docx
先看下在probe之前vfio_pci都做了哪些准备. TBD

  1. struct perm_bits {
  2. u8 *virt; /* read/write virtual data, not hw */
  3. u8 *write; /* writeable bits */
  4. int (*readfn)(struct vfio_pci_device *vdev, int pos, int count,
  5. struct perm_bits *perm, int offset, __le32 *val);
  6. int (*writefn)(struct vfio_pci_device *vdev, int pos, int count,
  7. struct perm_bits *perm, int offset, __le32 val);
  8. };
  9. static struct perm_bits cap_perms[PCI_CAP_ID_MAX + 1] = {
  10. [0 ... PCI_CAP_ID_MAX] = { .readfn = vfio_direct_config_read }
  11. };
  12. vfio_pci_init_perm_bits();

image.png

在进行
echo “${vendor} ${device}” > /sys/bus/pci/drivers/vfio-pci/new_id
的时候,肯定会进入到probe,看下probe都做了什么

  1. vfio_add_group_dev(&pdev->dev, &vfio_pci_ops, vdev);
  2. struct iommu_group *iommu_group = iommu_group_get(dev); // 根据实际pcie设备获取所属的iommu_gruop
  3. struct vfio_group *group = vfio_group_get_from_iommu(iommu_group);
  4. // struct vfio_device * = vfio_group_get_device(group, dev); 这里只是检测下是否已经有该设备,不允许多次添加
  5. struct vfio_device *device = vfio_group_create_device(group, dev, ops, device_data);

可见这部分代码主要是: 例化了vfio_group和vfio_device
image.png

接下来看用户如何操作设备的

  1. // 设备操作接口
  2. dfd = ioctl(gfd, VFIO_GROUP_GET_DEVICE_FD, "0000:26:00.0");
  3. if( (dfd == -1)){
  4. perror("get device failed\n");
  5. goto err2;
  6. }
  7. printf("get device OK\n");
  8. ioctl(dfd,VFIO_DEVICE_GET_INFO,&device_info);
  9. ioctl(dfd,VFIO_GET_IRQ_INFO,&irq);
  10. ioctl(dfd,VFIO_DEVICE_RESET);

image.png

VFIO_GROUP_GET_DEVICE_FD 是如何获取fd的?(重点)

  1. vfio_group_fops_unl_ioctl
  2. vfio_group_get_device_fd(group, buf);
  3. device->ops->open(device->device_data);
  4. vfio_pci_open(vfio_pci_device) // vfio_pci_probe 中的vdev
  5. vfio_pci_enable(vdev); // 第一次会使能PCIE
  6. pci_enable_device(pdev);
  7. pci_try_reset_function(pdev); // 所以之前遇到过 SR-IOV中VF复位异常原因
  8. anon_inode_getfile("[vfio-device]", &vfio_device_fops,); // 重点接口,vfio_device_fops 的接口最终都会调用到 vfio_pci_ops
  9. 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

相关资料

使用前提:

  1. insmod /lib/modules/5.4.0-58-generic/kernel/drivers/vfio/mdev/mdev.ko
  2. insmod /lib/modules/5.4.0-58-generic/kernel/drivers/vfio/mdev/vfio_mdev.ko

vfio-mdev模型的核心在于mdev会对硬件设备的状态进行抽象,将硬件设备的“状态”保存在mdev device数据结构中, 设备驱动层面要求实现一个调度器,将多个mdev设备在硬件设备上进行调度(分时复用), 从而实现把一个物理硬件设备分享给多个虚拟机实例进行使用
image.png

框架看起来很简单,接下来分析下: KERNEL_DOC-VFIO Mediated devices 中提供的 mtty.c 以及 vfio-mdev源码.

初始化过程

image.png
这里经过测试,很疑惑为什么会出现 mtty-1和mtty-2

  1. root@inno-MS-7B89:testntty# pwd
  2. /sys/devices/virtual/mtty/testntty
  3. root@inno-MS-7B89:testntty# tree mdev_supported_types/
  4. mdev_supported_types/
  5. ├── mtty-1
  6. ├── available_instances
  7. ├── create
  8. ├── device_api
  9. ├── devices
  10. └── name
  11. └── mtty-2
  12. ├── available_instances
  13. ├── create
  14. ├── device_api
  15. ├── devices
  16. └── 83b8f4f2-509f-382f-3c1e-e6bfe0fa1001 -> ../../../83b8f4f2-509f-382f-3c1e-e6bfe0fa1001
  17. └── name
  18. 分析代码:
  19. mdev_register_device
  20. parent_create_sysfs_files(parent); // struct mdev_parent *parent;
  21. kset_create_and_add("mdev_supported_types",...); // 创建mdev_supported_types目录
  22. add_mdev_supported_type_groups(parent);
  23. parent->ops->supported_type_groups[i] // 所以主要看这部分 创建了 mtty-1mtty-2,
  24. add_mdev_supported_type(parent, parent->ops->supported_type_groups[i]);
  25. 这里ops 来源 调用mdev_register_device
  26. mtty_dev_init
  27. mdev_register_device(&mtty_dev.dev, &mdev_fops); // mdev_fops .supported_type_groups = mdev_type_groups,
  28. static struct attribute_group *mdev_type_groups[] = { // 两边属性一致,随意使用
  29. &mdev_type_group1,
  30. &mdev_type_group2,
  31. NULL,
  32. };

image.png

这里有几个部分需要注意下:

  • mtty_dev_init->class_compat_register(“mdev_bus”) 所以 必须注册任意一个vfio-mdev驱动后,才会有 /sys/class/mdev_bus 节点

问题:加入有N个虚拟机,需要用channel来区分,怎么办?
考虑用uuid作为区分,然后根据uuid来索引channel index

  1. 问题:如何区分channel
  2. mdev_device_create
  3. struct mdev_device *mdev; // 创建了mdev;
  4. list_add(&mdev->next, &mdev_list); // 添加到全局变量
  5. memcpy(&mdev->uuid, &uuid, sizeof(uuid_le)); // uuid 所以可以遍历mdev来根据uuid查找 mdev_device
  6. 在用户空间创建接口的时候:
  7. mtty_create
  8. mdev_set_drvdata(mdev, mdev_state); // 分配私有的mdev_state结构 . 所以要加channel在这里加
  9. // 上层调用了 open/read/write/close等接口,会传递 struct mdev_device,
  10. // mtty中根据:
  11. struct mdev_state *mdev_state;
  12. mdev_state = mdev_get_drvdata(mdev); // 获取私有的mdev_state结构
  13. 比如:
  14. echo "83b8f4f2-509f-382f-3c1e-e6bfe0fa1000" > /sys/devices/virtual/mtty/mtty/mdev_supported_types/mtty-1/create
  15. 内核mtty_create打印uuid
  16. 注意:在create_store 会将断续转换
  17. [51509.484676] [vfio_get_channel:855] mdev id is
  18. [51509.484678] 0xf2
  19. [51509.484679] 0xf4
  20. [51509.484679] 0xb8
  21. [51509.484679] 0x83
  22. [51509.484679] 0x9f
  23. [51509.484680] 0x50
  24. [51509.484680] 0x2f
  25. [51509.484680] 0x38
  26. [51509.484681] 0x3c
  27. [51509.484681] 0x1e
  28. [51509.484681] 0xe6
  29. [51509.484681] 0xbf
  30. [51509.484682] 0xe0
  31. [51509.484682] 0xfa
  32. [51509.484682] 0x10
  33. [51509.484683] 0x00
  34. [51509.484880] vfio_mdev 83b8f4f2-509f-382f-3c1e-e6bfe0fa1000: Adding to iommu group 24
  35. [51509.484882] vfio_mdev 83b8f4f2-509f-382f-3c1e-e6bfe0fa1000: MDEV: group_id = 24
  36. echo 1 > /sys/bus/mdev/devices/83b8f4f2-509f-382f-3c1e-e6bfe0fa1000/remove

这里提供一个channel 控制脚本

  1. if [ "$1" == "create" ]
  2. then
  3. echo "Start create 16 channel"
  4. for ((i=0; i<3; ++i))
  5. do
  6. a=$(printf "%02x" $i)
  7. echo "83b8f4f2-509f-382f-3c1e-e6bfe0fa10$a" > /sys/devices/virtual/mtty/mtty/mdev_supported_types/mtty-1/create
  8. done
  9. else
  10. echo "Start delete 16 channel"
  11. for ((i=0;i<3;++i))
  12. do
  13. a=$(printf "%02x" $i)
  14. echo 1 > "/sys/bus/mdev/devices/83b8f4f2-509f-382f-3c1e-e6bfe0fa10$a/remove"
  15. done
  16. fi

使用

注:环境支持参考 VFIO使用章节。

  1. -device vfio-pci,addr=08,\
  2. sysfsdev=/sys/bus/mdev/devices/83b8f4f2-509f-382f-3c1e-e6bfe0fa1001

注意:这些字段间不能又空格

看见初始化代码,整体比较完整,但好像缺了一部分? mdev_bus_type注册了没使用? 目前由于没看到vfio-pci部分,猜测vfio-pci注册了vfio_mdev_driver 会调用这部分接口

  1. static const struct vfio_device_ops vfio_mdev_dev_ops = {
  2. .name = "vfio-mdev",
  3. .open = vfio_mdev_open, ===> mdev_fops.open ==> mtty_open
  4. .release = vfio_mdev_release, ===> mdev_fops.release ==> mtty_close
  5. .mmap = vfio_mdev_mmap, ===> mdev_fops.mmap ==> NULL
  6. // 重点接口
  7. .ioctl = vfio_mdev_unlocked_ioctl, ===> mdev_fops.ioctl ==> mtty_ioctl
  8. .read = vfio_mdev_read, ===> mdev_fops.read ==> mtty_read
  9. .write = vfio_mdev_write, ===> mdev_fops.write ==> mtty_write
  10. };

所以,在vfio-mdev被Guest识别到后,调用的其实是 vfio_mdev_dev_ops,但这部分最终 只需要关心 mdev_fops 接口就好了

接下来,我们根据设备使用来研究

create
  1. echo "83b8f4f2-509f-382f-3c1e-e6bfe0fa1001" > \
  2. /sys/devices/virtual/mtty/mtty/mdev_supported_types/mtty-2/create

实现如下:

  1. mtty_create
  2. struct mdev_state *mdev_state;
  3. mdev_state->vconfig = kzalloc(MTTY_CONFIG_SPACE_SIZE, GFP_KERNEL); // 模拟PCI设备,创建配置空间
  4. mtty_create_config_space(mdev_state);
  5. 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
    1. // 设备操作接口
    2. dfd = ioctl(gfd, VFIO_GROUP_GET_DEVICE_FD, "0000:26:00.0");
    3. vfio_group_get_device_fd(group, buf);
    4. device->ops->open(device->device_data);
    5. mtty_open(vfio_pci_device) //===> 先会调用open接口,获取到文件描述符,提供对用层VFS的接口 vfio_device_fops
    6. // 其实不用关心 vfio_device_fops,只需要关注自己设备注册的接口就好,最后调用的还是 mdev_fops
    7. // 可能会调用reset接口:VFIO_DEVICE_RESET
    8. printf("get device OK\n");
    9. ioctl(dfd,VFIO_DEVICE_GET_INFO,&device_info); // VFIO_DEVICE_GET_INFO
    10. ioctl(dfd,VFIO_DEVICE_GET_REGION_INFO,&reg); // VFIO_DEVICE_GET_REGION_INFO
    11. ioctl(dfd,VFIO_GET_IRQ_INFO,&irq); // VFIO_DEVICE_GET_IRQ_INFO
    12. ioctl(dfd,VFIO_DEVICE_RESET); // VFIO_DEVICE_RESET

Guest系统初始化

注:调试阶段最好吧mtty的打印信息打开

  1. #define DEBUG_REGS 1
  2. #define DEBUG_INTR 1
  3. #define DEBUG 1
  • 读取配置空间
  • 读取OPTION ROM
  • set IRQ

读写操作

上层接口无论读写BAR空间还是配置空间,接口都会调用到:mtty_write 和 mtty_read

还记得在:ioctl中 VFIO_DEVICE_GET_REGION_INFO 有一个很重要的信息:

  1. mtty_get_region_info
  2. #define MTTY_VFIO_PCI_OFFSET_SHIFT 40
  3. region_info->offset = MTTY_VFIO_PCI_INDEX_TO_OFFSET(bar_index); // region_info->offset[63:40]代表区域
  4. mtty_read / mtty_write ppos有个很大的作用:
  5. 然后在 mdev_access mtty_read / mtty_write 中会调用)
  6. index = MTTY_VFIO_PCI_OFFSET_TO_INDEX(pos); // 所以 ppos[63:40]代表读写区域
  7. offset = pos & MTTY_VFIO_PCI_OFFSET_MASK; // 所以 ppos[40:0]为串口操作属性,比如THR,IER等配置,或RX/TX 数据消息
  8. enum {
  9. VFIO_PCI_BAR0_REGION_INDEX,
  10. VFIO_PCI_BAR1_REGION_INDEX,
  11. VFIO_PCI_BAR2_REGION_INDEX,
  12. VFIO_PCI_BAR3_REGION_INDEX,
  13. VFIO_PCI_BAR4_REGION_INDEX,
  14. VFIO_PCI_BAR5_REGION_INDEX,
  15. VFIO_PCI_ROM_REGION_INDEX,
  16. VFIO_PCI_CONFIG_REGION_INDEX,
  17. VFIO_PCI_VGA_REGION_INDEX,
  18. VFIO_PCI_NUM_REGIONS = 9 /* Fixed user ABI, region indexes >=9 use */
  19. /* device specific cap to define content. */
  20. };

先简单看下串口的一些属性:
image.png
在分析打印日志的时候,注意读写操作的关键信息

  1. # 数据读写
  2. mdev_access: BAR0 WR @0x4 MCR val:0x00 dlab:0
  3. BARn 代表操作第几个port
  4. WR/RD: 读还是写
  5. MCR/TX/RX 这串口的属性,也就是 ppos[39:0]的内容

中断触发

在测试的时候,会发现:如果没数据,单纯读,可能会无限阻塞,因为没中断。
KERNEL_DOC-VFIO Mediated devices 中最后描述了一句: 数据从主机mtty驱动程序环回。
所以我们要看读是怎么触发的,需要分析的是mtty_write->handle_bar_write->mtty_trigger_interrupt(mdev_state)

  1. /*
  2. * Trigger interrupt if receive data interrupt is
  3. * enabled and fifo reached trigger level
  4. */
  5. if ((mdev_state->s[index].uart_reg[UART_IER] & UART_IER_RDI) && \
  6. (mdev_state->s[index].rxtx.count ==mdev_state->s[index].intr_trigger_level))
  7. {
  8. mtty_trigger_interrupt( mdev_uuid(mdev_state->mdev)); // mdev_uuid 在echo时调用 create_stor会赋值并绑定到mdev上
  9. }

这里判断 中断是否使能, 判断数据个数是否达到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描述

  1. <hostdev mode='subsystem' type='mdev' managed='no' model='vfio-pci'>
  2. <source>
  3. <address uuid='83b8f4f2-509f-382f-3c1e-e6bfe0fa1001'/>
  4. </source>
  5. <address type='pci' domain='0x0000' bus='0x00' slot='0x09' function='0x0'/>
  6. </hostdev>

注意:修改完成后一定要:更新配置 virsh define /etc/libvirt/qemu/ubuntu18.04.xml