4.设备虚拟化-8.IOVA和VFIO-《计算机知识》

admin 2025-11-02 22:28:50 系统网络 来源:ZONE.CI 全球网 0 阅读模式
  • IOVA
    • DMA
    • IOVA
    • IOMMU下的DMA buffer申请
      • dma_alloc_coherent 一致性DMA
      • dma_map_single 流式DMA
    • IOVA是怎么解决物理地址不连续的问题?
      • LLDMA 是如何实现的
      • IOMMU是如何实现的?
  • VFIO
    • VFIO基础知识
      • VFIO的作用
      • vfio的基本思想和原理
      • VFIO工作的机制:
      • VFIO框架
      • VFIO的Container,group和device
      • VFIO的三个子系统
    • 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设备
    • group初始化
    • pci device
    • 重要接口快速查询
  • VFIO-MDEV
    • 相关资料
    • 初始化过程
    • 使用
      • create
      • qemu初始化操作
      • Guest系统初始化
      • 读写操作
      • 中断触发
    • VFIO-MTTY

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

    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时,会分配一个domainhttps://patchwork.kernel.org/project/platform-driver-x86/patch/[email protected]/

    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基础知识

    参考:

    • 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的基本思想:

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

    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.pnggroup 是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_dev的vfio_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_deviceimage.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_fopsdevice的接口 vfio_pci_ops ,在VFIO_GROUP_GET_DEVICE_FD 获取到设备的文件描述符后,给用户层对接的接口是: vfio_device_fops, 然后这部分最终都调用vfio_pci_ops

    VFIO-MDEV

    相关资料

    • VFIO Mediated Devices Introduction
    • Insight Into VFIO
    • KERNEL_DOC-VFIO Mediated devices mtty 使用说明

    使用前提:

    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-1和mtty-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

    01-shell脚本介绍-《shell脚本》 系统网络

    01-shell脚本介绍-《shell脚本》

    一、shell脚本是什么二、为什么要学shell,而不是其他计算机语言三、学习这门课程的优势四、学了能干什么五、学习什么内容六、学习的技巧七、成长路径八、学习环
    评论:0   参与:  26