0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

VFIO设备直通原理

冬至子 来源:openEuler 作者:openEuler 2023-06-07 17:40 次阅读

设备直通给虚拟机能够极大提升虚拟机对物理设备访问的性能,本文通过vfio内核模块和qemu用户态实现介绍vfio设备直通时的关键部分,包括:用户态访问设备IO地址空间,DMA重映射,中断重映射等.

VFIO访问直通设备IO地址空间

1.PIO和MMIO

设备的IO地址空间的访问有PIO和MMIO两种方式,前者通过独立的IO端口访问设备,而MMIO是在物理内存中映射一段区间,直接访问该内存就可以访问设备的配置空间.在虚拟化的场景下,虚拟机通过PIO访问直通设备时,首先会VM-exit到qemu,由qemu通过转换表完成对该PIO操作的转发.对于PCI设备而言,其bar空间地址是通过PIO的方式设置的,如果将设备的PIO访问完全暴露给虚拟机,虚拟机修改了真实的物理设备的PCI Bar空间基地址配置,与host上不一致,可能会出现严重的问题,所以对于设备的PIO访问需要建立转换表,在VM-exit之后由qemu来完成设置的转发.

对于设备的MMIO空间访问,则可以通过建立EPT页表将设备的MMIO物理内存映射到虚拟的MMIO地址空间,让虚拟机能够直接通过MMIO访问PCI设备的bar空间,提高IO性能.

2.获取直通设备信息

通过VFIO提供的接口可以获取到设备的基本信息,包括设备的描述符、region的数量等。

vfio_get_device:
    fd = ioctl(group- >fd, VFIO_GROUP_GET_DEVICE_FD, name);
    ret = ioctl(fd, VFIO_DEVICE_GET_INFO, &dev_info);

    vbasedev- >fd = fd;
    vbasedev- >group = group;
    QLIST_INSERT_HEAD(&group- >device_list, vbasedev, next);

    vbasedev- >num_irqs = dev_info.num_irqs;
    vbasedev- >num_regions = dev_info.num_regions;
    vbasedev- >flags = dev_info.flags;

2.直通设备PCI配置空间模拟

Qemu为每个PCI直通设备都建立一个虚拟数据结构 VFIOPCIDevice,保存物理PCI设备的相关信息,由vfio_get_device来获取,保存到vbasedev中。

typedef struct VFIOPCIDevice {
    PCIDevice pdev;
    VFIODevice vbasedev;

VFIO设备作为qemu的设备模型的一部分,qemu对直通设备的模拟初始化入口在 vfio_realize,通过vfio_get_device获取到直通设备的基本信息之后,会调用pread设备的fd获取到设备的配置空间信息的一份拷贝,qemu会写入一些自定义的config配置。

vfio_realize:
    /* Get a copy of config space */
    ret = pread(vdev- >vbasedev.fd, vdev- >pdev.config,
                MIN(pci_config_size(&vdev- >pdev), vdev- >config_size),
                vdev- >config_offset);
    if (ret < (int)MIN(pci_config_size(&vdev- >pdev), vdev- >config_size)) {
        ret = ret < 0 ? -errno : -EFAULT;
        error_setg_errno(errp, -ret, "failed to read device config space");
        goto error;
    }

    /* vfio emulates a lot for us, but some bits need extra love */
    vdev- >emulated_config_bits = g_malloc0(vdev- >config_size);

    /* QEMU can choose to expose the ROM or not */
    memset(vdev- >emulated_config_bits + PCI_ROM_ADDRESS, 0xff, 4);

    /* QEMU can change multi-function devices to single function, or reverse */
    vdev- >emulated_config_bits[PCI_HEADER_TYPE] =
                                              PCI_HEADER_TYPE_MULTI_FUNCTION;

    /* Restore or clear multifunction, this is always controlled by QEMU */
    if (vdev- >pdev.cap_present & QEMU_PCI_CAP_MULTIFUNCTION) {
        vdev- >pdev.config[PCI_HEADER_TYPE] |= PCI_HEADER_TYPE_MULTI_FUNCTION;
    } else {
        vdev- >pdev.config[PCI_HEADER_TYPE] &= ~PCI_HEADER_TYPE_MULTI_FUNCTION;
    }

    /*
     * Clear host resource mapping info.  If we choose not to register a
     * BAR, such as might be the case with the option ROM, we can get
     * confusing, unwritable, residual addresses from the host here.
     */
    memset(&vdev- >pdev.config[PCI_BASE_ADDRESS_0], 0, 24);
    memset(&vdev- >pdev.config[PCI_ROM_ADDRESS], 0, 4);

3.直通设备MMIO映射

直通PCI设备的MMIO内存主要是指其Bar空间,qemu使用vfio_populate_device函数调用VFIO接口获取到PCI设备的Bar空间信息,然后通过vfio_region_setup获取到对应region的信息,并将qemu内存虚拟化的MemoryRegion设置为IO类型的region。重要的是,qemu会为该IO类型的MemoryRegion设置ops为vfio_region_ops,这样后续对于该块内存的读写会经过qemu VFIO模块注册的接口来进行。

vfio_populate_device:
    for (i = VFIO_PCI_BAR0_REGION_INDEX; i < VFIO_PCI_ROM_REGION_INDEX; i++) {
        char *name = g_strdup_printf("%s BAR %d", vbasedev- >name, i);

        ret = vfio_region_setup(OBJECT(vdev), vbasedev, &vdev- >bars[i].region, i, name);
            - > vfio_get_region_info
            - > memory_region_init_io(region- >mem, obj, &vfio_region_ops,
                              region, name, region- >size);
        QLIST_INIT(&vdev- >bars[i].quirks);
    }
    ret = vfio_get_region_info(vbasedev, VFIO_PCI_CONFIG_REGION_INDEX, ®_info);
            - >    ioctl(vbasedev- >fd, VFIO_DEVICE_GET_REGION_INFO, *info))
    ret = ioctl(vdev- >vbasedev.fd, VFIO_DEVICE_GET_IRQ_INFO, &irq_info);

到这里已经获取到了PCI设备的MMIO内存信息,但是还没有真正的将物理内存中的Bar空间映射到qemu,这一动作在vfio_bars_setup中完成,vfio_region_mmap会对region中每个需要map的内存地址完成映射,然后将映射的物理内存通过qemu注册到虚拟机作为一段虚拟机的物理地址空间。

vfio_bars_setup:
    for (i = 0; i < PCI_ROM_SLOT; i++)
        vfio_bar_setup(vdev, i);
            vfio_region_mmap(&bar- >region)
                    for (i = 0; i < region- >nr_mmaps; i++) {
                    region- >mmaps[i].mmap = mmap(NULL, region- >mmaps[i].size, prot,
                                                MAP_SHARED, region- >vbasedev- >fd,
                                                region- >fd_offset +
                                                region- >mmaps[i].offset);
                    memory_region_init_ram_device_ptr
                    memory_region_add_subregion
            pci_register_bar(&vdev- >pdev, nr, type, bar- >region.mem);

这里的映射mmap接口对应的是VFIO设备在内核中注册的vfio_pci_mmap 函数,在内核中,该函数会为vma注册一个mmap的ops,对应着注册了一个缺页处理函数,当用户态程序访问该段虚拟内存缺页时,调用注册的缺页处理函数,完成虚拟地址到实际物理地址的映射。

vfio_pci_mmap:
    vma- >vm_flags |= VM_IO | VM_PFNMAP | VM_DONTEXPAND | VM_DONTDUMP;
    vma- >vm_ops = &vfio_pci_mmap_ops;
        - > .fault = vfio_pci_mmap_fault,
                - > if (remap_pfn_range(vma, vma- >vm_start, vma- >vm_pgoff,
                    vma- >vm_end - vma- >vm_start, vma- >vm_page_prot))

简单来说,对于MMIO内存的的映射,主要是将物理内存中的MMIO空间映射到了qemu的虚拟地址空间,然后再由qemu将该段内存注册进虚拟机作为虚拟机的一段物理内存,在这个过程中会建立从gpa到hpa的EPT页表映射,提升MMIO的性能。

DMA重映射

首先关于DMA,设备通过DMA可以直接使用iova地址访问物理内存,从iova到实际物理地址的映射是在IOMMU中完成的,一般在dma_allooc分配设备能够访问的内存的时候,会分配iova地址和实际的物理地址空间,并在iommu中建立映射关系。 所以说要让设备进行DMA最关键的几个部分:

  • 设备能够识别的地址:IOVA
  • 一段物理内存
  • IOVA到物理内存在IOMMU中的映射关系

基于这几点来看VFIO的DMA重映射就比较清晰,首先从VFIO设备的初始化开始,在获取设备信息之前会先获取到设备所属的group和Container,并调用VFIO_SET_IOMMU完成container和IOMMU的绑定,并attach由VFIO管理的所有设备。此外注意到这里的 pci_device_iommu_address_space 函数,意思是qemu为设备dma注册了一段专门的地址空间,这段内存作为虚拟机的一段物理内存存在,在VFIO_SET_IOMMU之后,注册该地址空间,其region_add函数为 vfio_listener_region_add,意思是当内存空间布局发生变化这里是增加内存的时候都会调用该接口。

vfio_realize:
    group = vfio_get_group(groupid, pci_device_iommu_address_space(pdev), errp);
        vfio_connect_container(group, as, errp)
            ret = ioctl(fd, VFIO_SET_IOMMU, container- >iommu_type);
            container- >listener = vfio_memory_listener;
            memory_listener_register(&container- >listener, container- >space- >as);
                    - > .region_add = vfio_listener_region_add,

那么跟DMA有什么关系呢,当为设备进行DMA分配一块内存时,实际是以MemoryRegion的形式存在的,也就是说虚拟机进行dma alloc 会调用region_add函数,进而调用注册的memory_listener_region_add函数,MemoryRegion有了意味着分配了一块物理内存,还需要IOVA和映射关系才行。这里,IOVA地址使用的是section->offset_within_address_space,为什么可以这样,因为IOVA地址只是作为设备识别的地址,只要建立了映射关系就有意义。

vfio_listener_region_add:
    iova = TARGET_PAGE_ALIGN(section- >offset_within_address_space);
    /* Here we assume that memory_region_is_ram(section- >mr)==true */
    vaddr = memory_region_get_ram_ptr(section- >mr) +
            section- >offset_within_region +
            (iova - section- >offset_within_address_space);
    ret = vfio_dma_map(container, iova, int128_get64(llsize),
                       vaddr, section- >readonly);

vfio_dma_map:
    struct vfio_iommu_type1_dma_map map = {
        .argsz = sizeof(map),
        .flags = VFIO_DMA_MAP_FLAG_READ,
        .vaddr = (__u64)(uintptr_t)vaddr,
        .iova = iova,
        .size = size,
    };

    ioctl(container- >fd, VFIO_IOMMU_MAP_DMA, &map)

建立映射的关键在于vfio_dma_map,通过ioctl调用container->fd接口VFIO_IOMMU_MAP_DMA完成DMA重映射。为什么是container->fd,因为VFIO Container管理内存资源,与IOMMU直接绑定,而IOMMU是完成IOVA到实际物理内存映射的关键。值得注意的是qemu只知道这一段内存的虚拟地址vaddr,所以将vaddr,iova和size传给内核,由内核获取物理内存信息完成映射。

vfio_dma_do_map:
    vfio_pin_map_dma
        while (size) {
            /* Pin a contiguous chunk of memory */
            npage = vfio_pin_pages_remote(dma, vaddr + dma- >size,
                                size > > PAGE_SHIFT, &pfn, limit);
            /* Map it! */
            vfio_iommu_map(iommu, iova + dma- >size, pfn, npage,
                            dma- >prot);
                list_for_each_entry(d, &iommu- >domain_list, next)
                    iommu_map(d- >domain, iova, (phys_addr_t)pfn < < PAGE_SHIFT,
                            npage < < PAGE_SHIFT, prot | d- >prot);
                        arm_smmu_map
                            __arm_lpae_map
            size -= npage < < PAGE_SHIFT;
            dma- >size += npage < < PAGE_SHIFT;
        }

内核完成建立iova到物理内存的映射之前会将分配的DMA内存给pin住,使用vfio_pin_pages_remote接口可以获取到虚拟地址对应的物理地址和pin住的页数量,然后vfio_iommu_map进而调用iommu以及smmu的map函数,最终用iova,物理地址信息pfn以及要映射的页数量在设备IO页表中建立映射关系。

+--------+  iova  +--------+  gpa  +----+
| device |   - >   | memory |   < -  | vm |
+--------+        +--------+       +----+

最终完成了DMA重映射,设备使用qemu分配的iova地址通过IOMMU映射访问内存,虚拟机使用gpa通过Stage 2页表映射访问内存

中断重映射

对于PCI直通设备中断的虚拟化,主要包括三种类型INTx,Msi和Msi-X。

1.INTx中断初始化及enable

对于INTx类型的中断,在初始化的时候就进行使能了,qemu通过VFIO device的接口将中断irq set设置到内核中,并且会注册一个eventfd,设置了eventfd的handler,当发生intx类型的中断时,内核会通过eventfd通知qemu进行处理,qemu会通知虚拟机进行处理。

vfio_realize:
    if (vfio_pci_read_config(&vdev- >pdev, PCI_INTERRUPT_PIN, 1)) {
        pci_device_set_intx_routing_notifier(&vdev- >pdev, vfio_intx_update);
        ret = vfio_intx_enable(vdev, errp);
            *pfd = event_notifier_get_fd(&vdev- >intx.interrupt);
            qemu_set_fd_handler(*pfd, vfio_intx_interrupt, NULL, vdev);
            ret = ioctl(vdev- >vbasedev.fd, VFIO_DEVICE_SET_IRQS, irq_set);

2.MSI-X初始化

MSIX在vfio_realzie初始化时,首先获取到物理设备的中断相关的配置信息,将其设置到注册给对应的MMIO内存中

vfio_msix_early_setup:
    pos = pci_find_capability(&vdev- >pdev, PCI_CAP_ID_MSIX);

    if (pread(fd, &ctrl, sizeof(ctrl),
            vdev- >config_offset + pos + PCI_MSIX_FLAGS) != sizeof(ctrl)) {
    if (pread(fd, &table, sizeof(table),
            vdev- >config_offset + pos + PCI_MSIX_TABLE) != sizeof(table)) {
    if (pread(fd, &pba, sizeof(pba),
            vdev- >config_offset + pos + PCI_MSIX_PBA) != sizeof(pba)) {

    ctrl = le16_to_cpu(ctrl);
    table = le32_to_cpu(table);
    pba = le32_to_cpu(pba);

    msix = g_malloc0(sizeof(*msix));
    msix- >table_bar = table & PCI_MSIX_FLAGS_BIRMASK;
    msix- >table_offset = table & ~PCI_MSIX_FLAGS_BIRMASK;
    msix- >pba_bar = pba & PCI_MSIX_FLAGS_BIRMASK;
    msix- >pba_offset = pba & ~PCI_MSIX_FLAGS_BIRMASK;
    msix- >entries = (ctrl & PCI_MSIX_FLAGS_QSIZE) + 1;

vfio_msix_setup:
    msix_init(&vdev- >pdev, vdev- >msix- >entries,
                    vdev- >bars[vdev- >msix- >table_bar].region.mem,
                    vdev- >msix- >table_bar, vdev- >msix- >table_offset,
                    vdev- >bars[vdev- >msix- >pba_bar].region.mem,
                    vdev- >msix- >pba_bar, vdev- >msix- >pba_offset, pos);
        
        memory_region_init_io(&dev- >msix_table_mmio, OBJECT(dev), &msix_table_mmio_ops, dev,
                          "msix-table", table_size);
        memory_region_add_subregion(table_bar, table_offset, &dev- >msix_table_mmio);
        memory_region_init_io(&dev- >msix_pba_mmio, OBJECT(dev), &msix_pba_mmio_ops, dev,
                            "msix-pba", pba_size);
        memory_region_add_subregion(pba_bar, pba_offset, &dev- >msix_pba_mmio);

vfio_realize:
    /* QEMU emulates all of MSI & MSIX */
    if (pdev- >cap_present & QEMU_PCI_CAP_MSIX) {
        memset(vdev- >emulated_config_bits + pdev- >msix_cap, 0xff,
               MSIX_CAP_LENGTH);

    if (pdev- >cap_present & QEMU_PCI_CAP_MSI) {
        memset(vdev- >emulated_config_bits + pdev- >msi_cap, 0xff,
               vdev- >msi_cap_size);
  1. MSI/MSI-X enable 与 irqfd的注册

当虚拟机因为写PCI配置空间而发生VM-exit时,最终会完成msi和msix的使能,以MSIX的使能为例,在qemu侧会设置eventfd的处理函数,并通过kvm将irqfd注册到内核中,进而注册虚拟中断给虚拟机。

kvm_cpu_exec:
    vfio_pci_write_config:
        vfio_msi_enable(vdev);
        vfio_msix_enable(vdev);
            for (i = 0; i < vdev- >nr_vectors; i++) {
                if (event_notifier_init(&vector- >interrupt, 0)) {

                qemu_set_fd_handler(event_notifier_get_fd(&vector- >interrupt),
                                    vfio_msi_interrupt, NULL, vector);

                vfio_add_kvm_msi_virq(vdev, vector, i, false);
                    kvm_irqchip_add_msi_route(kvm_state, vector_n, &vdev- >pdev);
                    vfio_add_kvm_msi_virq
                        kvm_irqchip_add_irqfd_notifier_gsi
                            kvm_vm_ioctl(s, KVM_IRQFD, &irqfd);
            /* Set interrupt type prior to possible interrupts */
            vdev- >interrupt = VFIO_INT_MSI;
            ret = vfio_enable_vectors(vdev, false);
                ret = ioctl(vdev- >vbasedev.fd, VFIO_DEVICE_SET_IRQS, irq_set);

小结

要让设备直通给虚拟机,需要将设备的DMA能力、中断响应和IO地址空间访问安全地暴露给用户态,本文主要介绍了VFIO设备直通关键的几个环节,包括如何在用户态访问物理设备的IO地址空间、如何进行DMA重映射和中断重映射。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • dma
    dma
    +关注

    关注

    3

    文章

    559

    浏览量

    100411
  • 虚拟机
    +关注

    关注

    1

    文章

    908

    浏览量

    28064
  • MMU
    MMU
    +关注

    关注

    0

    文章

    91

    浏览量

    18263
  • 串口中断
    +关注

    关注

    0

    文章

    64

    浏览量

    13855
  • qemu
    +关注

    关注

    0

    文章

    57

    浏览量

    5330
收藏 人收藏

    评论

    相关推荐

    详解交换机测试中的直通时延和存储转发时延二者区别

    有朋友问我,用我司BigTao200测试仪测试交换机时,在时延的统计项中有“直通时延”和“存储转发时延”,二者的区别在哪里?我的理解是这样的——存储转发时延是数据最后一个比特到达设备输入端口的时间
    发表于 08-26 16:29

    布线技巧二:UTP直通线

    布线技巧二:UTP直通线 本系列的这一部分将关注CAT5网线安装,因为它是目前最常见的UTP网线类型。它非常灵活,
    发表于 04-23 18:04 2271次阅读

    什么是直通线?直通线的做法?

    什么是直通线 直通线是常用的一种计算机到交换机连线,两端压制的线序都应该按照t-568b方式进行压制。交叉线是常用的计算机到计算机
    发表于 04-23 23:59 8428次阅读

    Linux设备与驱动的手动解绑与手动绑定

    但是有时候,这种自动匹配并不一定是我们想要的。比如我们有时候就是希望XXX设备用YYY驱动,而不是用XXX驱动。工程中有手动匹配的需求,最典型的场景是VFIO的场景,想让设备与内核空间原本绑定的驱动解绑,转而采用内核空间的通用
    的头像 发表于 08-03 16:25 2879次阅读
    Linux<b class='flag-5'>设备</b>与驱动的手动解绑与手动绑定

    关于VFIO的详细研究解析

    由于VFIO是将设备直接透传给虚拟机,所以Guest中与该设备相关的IO性能会大幅提高,接近native性能。
    的头像 发表于 05-02 11:20 3479次阅读
    关于<b class='flag-5'>VFIO</b>的详细研究解析

    一个简单的直通式监听板

    电子发烧友网站提供《一个简单的直通式监听板.zip》资料免费下载
    发表于 07-29 14:39 2次下载
    一个简单的<b class='flag-5'>直通</b>式监听板

    IGBT直通短路过程问题分析

    目录 1、IGBT的工作原理和退饱和 1.1 IGBT 和 MOSFET结构比较 1.2 IGBT 和 MOSFET 在对饱和区的定义差别 1.3 IGBT 退饱和过程和保护 2、电感短路和直通短路
    发表于 02-22 15:14 9次下载
    IGBT<b class='flag-5'>直通</b>短路过程问题分析

    Arm对虚拟化下设备直通的支持

    虚拟机里使用直通设备,的确可以带来最大的性能提升。但是却暴漏出一系列的系统安全问题。比如提供DMA的设备通常可以写内存的任意页,因此虚拟机里的Guest OS拥有创建DMA的能力就等同于用户空间拥有了root权限
    的头像 发表于 04-28 15:30 1203次阅读
    Arm对虚拟化下<b class='flag-5'>设备</b><b class='flag-5'>直通</b>的支持

    为什么使用直通技术可以帮助延长储能系统的使用寿命

    直通™ mode 是一种控制器操作,使电源能够直接连接到负载。直通模式用于降压-升压或升压转换器,以提高效率和电磁兼容性。1,2本文介绍了配备直通技术的控制器相对于其他控制器的优势,以及直通
    的头像 发表于 06-15 09:55 981次阅读
    为什么使用<b class='flag-5'>直通</b>技术可以帮助延长储能系统的使用寿命

    SMT贴片加工生产怎么提高直通

    一站式PCBA智造厂家今天为大家讲讲如何提高SMT贴片加工的直通率?提高smt贴片加工的直通率方法。SMT贴片加工对加工质量评价有一个重要指标,那就是直通率。在贴片加工中。SMT加工直通
    的头像 发表于 09-13 09:38 580次阅读

    直通串口和交叉串口的区别 如何辨别交叉串口线与直连串口线?

    直通串口和交叉串口的区别 如何辨别交叉串口线与直连串口线?什么时候用交叉,什么时候用直通直通串口和交叉串口是在计算机设备或系统中用于连接串行设备
    的头像 发表于 11-28 15:45 2439次阅读

    smt贴片加工直通率为什么如此重要

    smt贴片加工,smt 直通率堪称是贴片加工厂的生命线,有些公司的直通率必须达到95%才是达标线,因此直通率的高低,反应了贴片加工厂的技术实力、工艺品质,直通率高能够提升公司的产能效率
    的头像 发表于 12-05 10:23 1881次阅读

    直通网线和交叉网线的区别有哪些呢?

    T-568B线序标准。 在实际运用中,直通网线常用来连接不同的设备,例如电脑和路由器、路由器和交换机等,而交叉网线用来连接相同的设备,例如电脑和电脑、路由器和路由器。 此外,直通网线和
    的头像 发表于 03-07 10:34 1347次阅读

    直通网线和交叉网线区别

    白、棕。而交叉网线则在一端采用568A线序,另一端采用568B线序,其线缆两端的线序是交叉的。568A的线序是:绿白、绿、橙白、蓝、蓝白、橙、棕白、棕。 使用场景:直通网线主要用于连接不同类型的设备,例如路由器与交换机、个人电脑
    的头像 发表于 05-10 10:06 2899次阅读

    直通网线的作用及制作步骤

    直通网线的主要作用是连接不同的设备,如电脑和路由器、路由器和交换机等,以建立网络连接。以下是直通网线的制作步骤: 一、直通网线的作用 直通
    的头像 发表于 07-04 09:51 595次阅读