vfio_realize实际运行过程观测
vfio_realize实际运行过程观测
使用的工具为gdb,将测试网卡通过vfio的形式透传到虚拟机中,查看vfio_realize中对于memory,中断的分配是怎样的。
用gdb启动qemu
在启动qemu之前,已经完成了以下工作:
- 启动host时添加了intel_iommu=on
- vfio-pci module的加载
- 待透传设备与原驱动解绑并绑定到vfio-pci驱动上
另外,使用gdb debug qemu时,需要提前使用qemu源码重新编译qemu,添加debug编译选项:
使用以下方式启动gdb debug qemu:
在vfio_realize中设置断点并启动qemu运行:
程序停到了vfio_realize.通过bt命令可以看到vfio_realize调用栈的情况,可以看到,vfio_realize最终操作的是pdev=0x555557a94360这个物理设备。下面通过单步调试查看vfio_realize中的各关键步骤。
检查传入的设备是否符合要求
由于是第一次使用,这里的vdev->vbasedev.sysfsdev一定为0,会直接进入第一个if,检查启动qemu时传入的“host=”的参数是否为空,如果不为空,将传入的这些参数赋值给vdev->vbasedev.sysfsdev
.因此上面这段代码执行完成之后,vdev->vbasedev.sysfsdev
变为了"/sys/bus/pci/devices/0000:00:1f.6"。
将设备信息放入buff(一个stat结构体)
stat()函数的作用是获得文件(参数1)的属性,存储到buff(参数2)中,如果该文件不存在,返回负值。下面gdb打出的buff中的值有变化,且if语句直接跳过,说明传入的host device真实存在。
检查设备是否支持迁移
这里的pdev->failover_pair_id
,是物理设备的一个属性,与是否可迁移有关,debug得出该设备无法迁移,因此进入migrate_add_blocker
函数,生成一个migration_blockers链表,存储vdev->migration_blocker这个blocker,返回值ret为0,即记录blocker成功。
vfio_get_group
- 赋值
开头3行为vdev->vbaseddev
的名字,操作,类型赋值,第4行将VFIOPCIDevice类转化为device_state类,存储在vdev->vbasedev.dev
中。
- 检查device对应的iommu_group路径是否有效
在经历tmp = g_strdup_printf("%s/iommu_group", vdev->vbasedev.sysfsdev)
之后,tmp变为了“/sys/bus/pci/devices/0000:00:1f.6/iommu_group”,通过readlink读取到的tmp的字符数为34,这两步的目的是检查该设备对应的iommu_group的路径(/sys/kernel/iommu_groups/$groupid)是否有效。
这里的group_name指的是iommu_group下的数字ID,即GroupID,将group_name赋值给group_id. 这部分最重要的语句为:group = vfio_get_group(groupid, pci_device_iommu_address_space(pdev), errp)
,其中首先调用pci_device_iommu_address_space(pdev)
,由于传入的pdev是系统总线上的设备,系统总线没有父设备,所以会直接返回一个空的地址空间结构,即return address_space_memory
.也就是说,vfio_get_group的第二个参数是一个地址空间,类型为memory,而非IO。
返回到vfio_get_group, 由于是第一次建立group,所以group中的device_list内没有内容,vfio_get_group中的第一个循环不会执行,之后为group分配空间,通过文件操作打开group获得group的fd,将fd赋值给group->fd,将groupid赋值给group->groupid, 先用这个fd查看该group的状态,即是否存在,是否可见。初始化一个链表,用于记录该group中的device。然后调用vfio_connnect_container。
建立qemu地址空间和设备IOVA之间的映射
上面的程序中,container拥有一个address_space,该address_space被转化为了FlatView形式,而FlatView中有很多MemoryRegionSection,qemu将这每一个section都包装成一个iova space,然后利用vfio_dma_map完成从虚拟地址(HVA)到iova space(关联到特定vfio设备)的映射。
VFIO_IOMMU_MAP_DMA关联的ioctl信息会与内核进行通信。
内核调用vfio_dmap_do_map实现最终的iova+vaddr映射。
至此,给container->as注册listener结束(memory_listener_register),最终建立了qemu虚拟空间到iova(用于设备DMA)的映射。通过iommu提供的domain->map方法,将qemu虚拟地址空间与设备访问的iova之间的映射建立起来,qemu虚拟地址空间背后是实际物理空间,之后当dma访问该iova空间时,会因为该映射的关系,首先访问qemu虚拟空间地址,进而访问真正的iommu提供的物理地址。
这里有2个问题,一是containner拥有的IOVA空间是怎么给到device的(整个vfio_dma_do_map过程中没有提到该IOVA空间是给container中的哪个group的哪个device用的,难道是container对应iommu_domain,所以container中的所有group中的所有device都使用这块儿IOVA空间?好像有点道理),二是qemu如何让Guest知道落在这段GPA空间上的操作就是设备的DMA操作呢?
vfio_connect_container()执行完毕。
返回到vfio_get_group中:
最后vfio_get_group返回了group,该group就是我们透传进qemu的那个设备所属的group,在最前面我们看到这个group中只有一个device,该group的:
fd=9, group_id=9,container已经申请并处理完毕,device_list中没有数据。也就是说,该vfio_get_group获得的group中没有设备信息,但有设备所属的group以及container信息,并且最重要的是,其中已经实现了qemu虚拟地址到iova空间的映射。
memory ballooning 能力检查
返回到vfio_realize,获得了device所属group之后,需要检查group中的device_list,如果device_list中存在与当前设备名相同的设备,说明设备早已经attach到了group,应该报错,因为在这之前,device不应该被attach到group.
这是关于混杂设备的一段检验代码,qemu支持的一项内存特性为memory ballooning,即允许在允许时膨胀和缩小VM的内存,但是我们透传进虚拟机的设备不一定支持memory ballooning,该设备如果支持,那么就可以在系统中找到/sys/bus/mdev路径,而不存在/sys/bus/mdev的系统,不一定支持memory ballooning,所以为了确保正确,对比设备所在的路径/sys/bus/“subsystem”和/sys/bus/mdev,如果两者相同说明是mdev。如果不是mdev但支持memory ballooning,就需要报错。一般我们透传的设备均在/sys/bus/pci下面,所以不会经过该代码路径。
经过trace,我们透传进guest的网卡不是mdev,也不支持memory ballooning。
vfio_get_device
传入vfio_get_device的参数如下:
group是通过vfio_get_group得到的,name是根据读取传入qemu的参数得到的,vbasedev是qemu自己构造的一个虚拟设备描述符。
最终加入到group->device_list中的虚拟设备vbasedev详细参数如下:
说明该设备(网卡)有5种中断,9个region,flags(可读可写标志)为0x11,可读可写,不支持reset。
至此,vfio_get_device结束,vbasedev中具有该device的详细信息,group->device_list中的第一个元素(也是唯一一个)也具有该device的详细信息。
vfio_populate_device
传入vfio_populate_device的参数如下:
vdev即在前面构造完成的VFIOPCIDevice类型的结构,包含了emulated_config_bits(qemu模拟的配置),pdev(物理设备),BAR空间等子域,总之vdev是一个能提供完整PCI设备功能的结构。
建立BAR region
info(region_info结构体)包含的内容主要有,该Region是否支持读、写、mmap和caps,以及region_index,region_size,regioin_offset(from device fd).
上面的ioctl(vbasedev->fd, VFIO_DEVICE_GET_REGION_INFO, *info)
调用内核中vfio-driver提供的ioctl函数进行信息获取操作,具体操作为:
vfio_get_region_info
执行完成之后,即已经获取了region信息,存储在info变量中,本次执行的region信息为:
即BAR0的flags为7(0x111),即BAR0支持读、写、mmap。BAR0的index为0,第一个cap在info结构中的offset为0,BAR0的大小为131072个字节,BAR0在设备fd起始的区域中的offset为0.
接下来继续将region信息补充完整。
接着为region申请存储空间:
最后并未给BAR region分配对应的存储空间,只有变量存储空间,总之,在建立BAR region的过程中,对vdev->bars[0-5].region进行了设置,除了对应的存储空间,其它的所有信息,和物理设备pdev的BAR Region没有区别。
配置vdev的config设置
与建立BAR Region时一样,首先利用vfio_get_region_info从内核中获取设备信息,获取到的信息为reg_info.
即ConfigRegion的flags为3(0x11支持读、写),ConfigRegion的index为7(0-5是BAR Region,6是ROM Region),第一个cap在info结构中的offset为0,ConfigRegion的大小为256(0x100)个字节,ConfigRegion在设备fd起始的区域中的offset为7696581394432(0x70000000000).
然后将获得的ConfigRegion信息复制到vdev的配置信息相关field中,如果ConfigRegion的大小为0x100,就将vdev->pdev.cap_present中的bit2置为0.cap_present代表该设备的capability功能mask。
注意这里也没有为ConfigRegion配置实际空间,只是将Conifg信息写入了vdev的config_offset和config_size filed.
populate VGA设置
如果vdev->features中的bit0置1,说明透传的设备拥有VGA资源,需要将VGA资源populate出来到vdev中。套路也是一样的,首先用vfio_get_region_info从内核中请求到VGARegion的相关信息,然后将信息赋值到vdev的相关field中,并为VGA设置memory(非实际memory,只有一个memory结构被分配出来)。与前面的BAR和Config Region不同的是,populate vga的最后,会将该VGA资源注册到qemu中的PCI总线上。
本次透传的网卡没有VGA资源,所以程序没有进行这部分的处理。
中断信息获取
同样,利用vfio-driver提供的ioctl获取中断信息,而在内核中,对于VFIO_DEVICE_GET_IRQ_INFO,处理如下:
最终获得的irq_info如下:
本次获取irq_info失败,因为网卡设备较老。但即使获取irq_info成功,也不会有进一步的赋值给vdev的操作,也就是说,整个中断信息获取过程,只是将irq_info->index设置为了VFIO_PCI_ERR_IRQ_INDEX(3).
复制物理设备的配置空间并进行相关修改
复制物理设备的配置空间
这里有一个问题,pread(实际调用函数为vfio-pci驱动提供的vfio_pci_read)读取的数据要存储到一个buffer中,这里的buffer为vdev->pdev.config,可是代码中也没有看到给该buffer分配空间的内容啊。
问题先留下,按代码的逻辑,vdev->pdev.config一定是有对应memory的,而且经过测试pci_config_size(&vdev->pdev)为256.
决定是否暴露device ROM,是否扩展(添加)BAR
在获得物理设备的配置空间的复制后,需要对配置空间中的值进行适当修改,达到qemu自定义设备功能的要求。
分配了一块配置空间大小的区域,称为emulated_config_bits(qemu模拟的配置空间),然后可以在该模拟配置空间中进行一些qemu的自定义修改。
自定义vendorID、deviceID、subvendorID、subdeviceID
qemu还提供了自定义VendorID,deviceID,sub_vendor_id,sub_device_id的功能,需要通过修改代码实现,一般情况下默认使用物理设备的VendorID,deviceID,sub_vendor_id,sub_device_id。修改代码时只需要在以下代码之前将vdev->vendor_id,vdev->device_id,vdev->sub_vendor_id,vdev->sub_device_id改为想要修改的值(只要不是0xfff就行)。
自定义SingleFunction/MultipleFunction
qemu提供了将device的SingleFunction和MultipleFunction之间的互相转换,根据PCI spec,配置空间的0xE地址的bit7为0时,设备为单功能设备,为1时,为多功能设备。
清除Host的硬件映射信息
这里清除的Host硬件映射信息,只是在Qemu内读取的硬件映射信息,表现形式为vdev->pdev.config,只是一个数据结构,但最终作用于Guest。修改vdev->pdev.config并不会真正修改硬件设备中的内容。
PCI设备的ROM的相关处理
ROM是PCI协议中的read-only memory,可以预先存储一些需要执行的代码在其中。如果透传的设备具有ROM,那么就会在vfio_realize的vfio_pci_size_rom中进行初始化和注册ROM空间(只有变量空间没有存储空间),并注册了该ROM BAR。
最后的pci_register_bar是比较重要的一个函数,其函数原型为:
pci_dev指的是Region即将要注册到的pci设备,region_num指的是region的index(0-5是BAR Region,6是ROM Region,7是Config Region).type指的而是注册类型,分为2种,即PCI_BASE_ADDRESS_SPACE_MEMORY(0x0)和PCI_BASE_ADDRESS_SPACE_IO(0x1)。memory指的是待注册Region对应的MemoryRegion。
由于本次透传的网卡不具有ROM Region,vfio_pci_size_rom根本不会执行完毕,由于获得的物理设备的ROM size为0,因此会立即返回到vfio_realize中去。
而对于pci_register_bar的具体执行代码分析,放到后面注册BAR region时进行tarce,网卡没ROM,还没BAR吗? 哈哈哈。
vfio_bars_prepare
对index为0-5的BAR region在注册之前进行最后的准备。针对每一个BAR region,调用vfio_bar_prepare(vdev,i),获取该设备的BAR提供的内存映射空间的类型(io/memory),大小,是否是64位空间,的信息。
在vfio_bar_prepare中,首先读取物理设备中BAR[0-5]中的内容,记作pci_bar,然后将该BAR映射的地址空间的类型(io/memory --- bit0)、是32bit地址空间还是64bit地址空间(mem_type_64/mem_type_32 --- bit2)、以及BAR映射的地址空间的大小,通过pci_bar的不同bit获得。(BAR映射的地址空间大小实际中可以通过先向BAR中写全1,然后读该BAR得到,但是我们这里的bar->size,已经在之前的vfio_populate_device时通过ioctl(VFIO_DEVICE_GET_REGION_INFO)从内核读取到,这里无需再进行读操作,直接赋值即可)
这里读取到的pci_bar,是已经经过系统分配的属于该PCI设备的存储空间的地址。本次透传的网卡只使用了BAR0.
vfio_msix_early_setup msi-x中断的早期准备
vfio_msix_early_setup函数的前面有一大段注释:
大概意思是,由于不确定pci_add_capability会不会向vfio-pci设备添加msi-x功能,所以qemu需要自己做msi-x的配置事宜。为了建立msi-x机制,需要在BAR中添加一个MemoryRegion,即通过一个BAR映射一个MemoryRegion。但是由于VFIO协议不允许直接将msi-x table通过mmap映射到内存中,所以需要首先找到msi-x table的位置。这也是为什么函数名中有“early”的原因。
本次透传的网卡的capability_list中不包含MSIX功能,因此该函数会直接返回,这里只能对vfio_msix_early_setup进行纯理论分析。
vfio_bars_register
针对每一个vdev->bars[0-5],调用vfio_bar_register.(由于本次透传的网卡只使用了BAR0,所以只会为vdev->bar[0]调用vfio_bar_register.)
在vfio_bar_register中,与populate设备资源时类似,首先为bar->mr分配一个MemoryRegion变量占用的空间大小,将BAR region名设置为“0000:00:1f.6 base BAR 0”(在前面populate中建立该BAR region时,传入的名字为”0000:00:1f.6 BAR 0“),将该BAR region名处理为“0000:00:1f.6 BAR 0[*]”,最后的方括号+星号会在后面的处理中作为待插入属性的标志被检测到。
memory_region_init_io
为vdev->bars[i].mr实例化一个MemoryRegion.
memory_region_init_io调用memory_region_init,后者中主要有2个函数,一个是object_initialize,用于实例化一个MemoryRegion;一个是memory_region_do_init,用于将实例化完成的MemoryRegion作为child属性注册到vdev中去。
关于这2个函数,在上面的代码框中都做了大概解释,这里提出来整个过程中的单个函数,memory_region_initfn,详细说明在实例化一个MemoryRegion时,做了哪些工作。
- memory_region_initfn
在vfio_bar_register中的第一步就是构造一个memoryRegion,该MemoryRegion的实例化函数为memory_region_initfn。总体来说就是初始化该MemoryRegion的读写方法,给该MemoryRegion添加一些属性,分别为: container,addr,priority,size. 下面详细来看。
- object_property_add
在memory_region_do_init时,核心函数就是object_property_add,用该函数来向vdev添加child属性。
object_property_add: 首先确认传入的property的name的结尾是否为[*],如果是,将name的结尾[*]置为‘\0’, 设置一个新的full_name,full_name是结尾被置为‘\0’后再在结尾加上[i](i是变量),然后重新进入object_property_add(此次进入时传入的name为fullname),其余参数未变。新的一次进入object_property_add时,name的结尾不是[*],因此跳过第一个if进入第二个if,查找是否在obj中已经存在名为name的属性,如果存在,说明软件逻辑错误,试图重复创建property。如果不存在,就创建一个property,以传入参数初始化其name,type,get,set,release,opaque参数,最后将该property插入到obj的属性哈希表中去,并返回property的地址,表示创建成功。一旦property创建成功,obj的属性哈希表中就会存在名为类似于“0000:00:1f.6 BAR 0[0]”的属性,如果创建不成功,object_property_add就会一直循环,不断增加“0000:00:1f.6 BAR 0[i]”中i的值,直到创建属性成功为止。
memory_region_add_subregion
向vdev->bars[i].mr添加subregion
在利用memory_region_init_io为bar->mr初始化一个MemoryRegion实例并添加到vdev的属性哈希表之后,vfio_bar_register会利用memory_region_add_subregion将在vfio_populate_device中建立的region->mem作为子region添加到bar->mr中。
传入memory_region_add_subregion的参数中,第一个参数为刚刚申请好并实例化的,概念上属于该BAR的MemoryRegion(bar->mr);第二个参数是需要添加到该BAR的MemoryRegion的子region在MemoryRegion中的offset(0);第三个参数是需要添加到该BAR的MemoryRegion的子region(bar->region.mem,在vfio_populate_device中就已经准备好了),类型也是MemoryRegion。
在memory_region_add_subregion中,首先将子region的优先级设置为0,子region的container设置为bar->mr(这里的container跟VFIO的container不是一个概念,这里的container在所有的MemoryRegion中都有,是一个MemoryRegion概念,而不是VFIO概念。),子regioin的地址设置为offset,然后调用memory_region_update_container_subregions。
接下来就进入了函数memory_region_update_container_subregions。
该函数以memory_region_transaction_begin开始,以memory_region_transaction_commit结束。qemu中,任何对AddressSpace和MemoryRegion的操作,都会以memory_region_transaction_begin开头,以memory_region_transaction_commit结束,因为我们要将一个MemoryRegion作为子region插入到另一个MemoryRegion中,属于对MemoryRegion的操作,因此需要调用这两个函数。下面先介绍以下这两个函数做了什么。
- memory_region_transaction_begin
也就是说,在qemu使用kvm时,memory_region_transaction_begin会将coalesced_mmio_ring缓冲区中的mmio操作写到实际物理地址上(gva),而且不论是否使用kvm,都会增加内存操作计数器memory_region_transaction_depth的值。
- memory_region_transaction_commit
memory_region_transaction_commit使用所有listener更新地址空间的结构,以确保对地址空间的修改能够立即生效。
接下来看memory_region_update_container_subregions的主体内容。
memory_region_ref会对subregion的owner,也就是vdev的ref,+1. 这样可以确保在操作过程中,系统不会丢失对某region的引用。
如果bar[i]->mr的subregion链表不为空,就针对bar[i]->mr中的每一个subregion执行:如果待插入的subregion的优先级高于某subregion的优先级,那么就将待插入subregion插入到bar[i]->mr的subregion链表中该subregion的前面。
这样的操作可以使bar[i]->mr的subregion链表中的subregion按优先级降序排列。之后更新标志memory_region_update_pending,以通知memory_region_transaction_commit需要更新全局memory_region的分布情况。
如果bar[i]->mr的subregion链表为空,则直接将待插入的subregion插入到bar[i]->mr的subregion链表中去。之后更新标志memory_region_update_pending,以通知memory_region_transaction_commit需要更新全局memory_region的分布情况。
可以看到,向MemoryRegion添加subregion是通过向MemoryRegion的subregion链表中添加元素并更新全局MemoryRegion分布实现的。
vfio_region_mmap
为vdev->bars[i].mr的subregion的mmaps.mem实例化MemoryRegion,将该subregion mmap(本质上调用vfio_pci_map)到QEMU内存空间中,将映射完成的内存空间添加到全局RAM空间链表ram_list.blocks中去。
接下来需要将刚才添加到vdev->bars[i].mr的subregion mmap到Host系统内存中,以提高对该MemoryRegion的访问速度。
利用mmap将MemoryRegion映射到Host系统内存时,需要获取MemoryRegion的权限标志,即希望映射到系统中的页之后,该页的访问权限应该被设置为什么。(本次透传的网卡的BAR0是可读可写的,因此映射到内存中也应该是可读可写的)。
然后对刚刚添加到vdev->bars[i].mr的subregion进行mmap。mmap的fd是vbasedev的fd,也就是物理device的fd;被映射区域的偏移量为device的fd + region(BAR0)相对device fd的偏移量 + region的mmaps子成员在region中的偏移量;mmap的大小为该subregion的大小(因为BAR0的mem中只有一个subregion,所以也就是BAR0的大小),映射形式为MAP_SHARED,即与其它所有映射该subregion的进程共享该内存区域,对该内存区域的写入不会影响到原文件(这里指物理设备),但会影响其它使用该内存区域的进程对该内存区域的读取内容。mmap的作用是将物理设备(BAR0)对应的物理地址空间映射到QEMU进程地址空间中,mmap返回物理设备映射成功的QEMU进程地址空间的地址,对该地址空间中的修改会直接影响物理设备地址空间中的内容。
注意这里mmap的是该subregion的mmaps子成员,而不是subregion本身。
然后调用memory_region_init_ram_device_ptr将刚才mmap得到的QEMU的内存空间(后端为一个MemoryRegion)加入到QEMU为Guest提供的ram空间链表中。详细情况如下:(下面用region代替之前添加到bar->mr中的subregion)
这样全局RAM空间链表中就多了一块与vdev->bar相关的ram空间,之后继续调用
将region也就是bar->region的mmaps[i].mem作为子region添加到region->mem中,也就是将region->mmaps[i].mem的地址作为subregion的地址添加到bar->region.mem的subregions链表中去。
至此完成了一个BAR从HOST物理地址到HOST虚拟地址再到Guest物理地址的映射建立,以后Guest访问该Guest物理地址就会直接访问HOST上该BAR空间中的内容。
pci_register_bar
将memory以type类型注册到pci_dev上,其实主要是设置该memory对应的pci设备的pci_bar_region中的首字节用于指明该Region的类型(io/mem),并设置该pci设备的wmask和cmask。
pci_dev指的是Region即将要注册到的pci设备,region_num指的是region的index(0-5是BAR Region,6是ROM Region,7是Config Region).type指的而是注册类型,分为2种,即PCI_BASE_ADDRESS_SPACE_MEMORY(0x0)和PCI_BASE_ADDRESS_SPACE_IO(0x1)。memory指的是待注册Region对应的MemoryRegion。
先看看传递给pci_register_bar()的参数。
然后看看在vfio_bar_register中,具体传入pci_register_bar()的内容:
在pci_register_bar中,首先获取传入的MemoryRegion(bar->mr)的大小(131072),然后获取pci_dev->io_regions[region_num]的地址,也就是pci设备的区域,pci_dev->io_regions为一个PCIIORegion类型的具有7个元素的数组,pci_dev->io_regions[region_num]为其中一个PCIIORegion,在当前情况下,获取的是vdev的IO_Region[0]的地址。
将该IO_Region的地址设置为-1,表示尚未映射。
将IO_Region的大小设置为传入的MemoryRegion的大小(131072).
将IO_Region的类型设置为传入的注册类型,由于传入的type=0,因此将IO_Region类型设置为Memory类型而不是IO类型。
将IO_Region对应的memory设置为传入的memory,这里指bar-<mr。
将IO_Regioin的地址空间设置为传入的PCI设备所属bus的地址空间(地址空间分io_space和mem_space,本次trace输入mem_space).
如果本次需要注册的IO_Region是一个ROM,那么表明该PCI设备允许自己的expansion ROM被访问,所以将wmask的bit0置1,wmask是什么东西?wmask,用于实现PCI设备的R/W标志。其取值为size-1取反的结果。
获取pci设备中,传入的region在PCI config space中的相对地址,在获取region的相对地址时对ROM region区别对待,因为ROM region的位置和普通的BAR region的位置不一样,而且根据该PCI设备是否是一个pci桥,ROM region的位置也不一样。
将type写入pci设备配置空间中该region对应的地址。然后根据该region的类型是io还是mem,是64bit地址类型还是32bit地址类型,将wmask和cmask写入pci设备数据结构的对应wmask和cmask中。cmask用来使能在Region载入时的配置检查。
vfio_add_capabilities
通过trace vfio_add_capabilities就会发现,其实pdev的capability list中早就已经含有了各个支持的capability结构和信息,为什么还要add呢?
答案是,pdev具有的capability结构不能全部展示给Guest,因此qemu需要对这些capability进行一些预处理,然后将这些capability 结构维护到一个软件dev结构(vdev)中。
该函数的作用是向vdev添加新的capability,vdev是qemu根据物理设备模拟出来的虚拟设备,可以根据需要虚拟出一些新的capability,途径为修改vdev的配置空间中的相关bit。
PCI协议中,pci配置空间偏移量0x06处是Device Status寄存器,反应设备的各种状态。Device Status寄存器的bit4标志着当前设备是否在配置空间偏移量0x34的位置实现了Capability Linked list,即new capability链表,bit4为1代表实现了,为0代表没实现。
本函数要添加capability,一定要确定该设备存在new capability链表,所以Device Status的bit4需要为1,即:
而要添加new capability,位于配置空间0x34处的功能链表头页不能为空,即:
vfio_add_capabilities函数首先进行了以上描述的条件检测,如果通过,才会进行下一步。
vfio_add_std_cap
该函数利用了递归方法,对new capability链表中的所有功能进行设置。
对于不同的CAP_ID的具体处理形式暂不详细阅读,只对本次透传的网卡具有的CAP_ID进行trace。
本次透传的网卡具有的CAP_ID有:
- 0x01, PCI电源管理接口功能
这个功能单元提供了对PCI电源管理进行控制的标准接口
- 0x05, MSI功能
这个功能单元提供了一个PCI Function,该Function能够进行MSI(message-signaled-interrupts)的传送。
- 0x13,PCI高级features功能
设备如果支持该功能单元,那么内部显卡的第二个function能独立于第一个function 被reset。
下面一一看qemu对这些CAP_ID的不同处理。
在处理具体CAP_ID之前,总是会首先将物理设备的配置空间0x34位置置0,将vdev的模拟配置空间的0x34位置置为0xFF,将vdev的模拟配置空间的状态寄存器的bit4置为1。然后找到当前功能单元和最邻近的功能单元在配置空间中的距离,以算出当前功能单元在配置空间中所占大小,记为size。最后将vdev的模拟配置空间中,当前单元的下一个单元写为0xFF,这样如果想要禁止下一个CAP的功能,只需要将模拟配置空间中的下一个单元的值赋值给物理设备配置空间的对应位置即可。
PCI高级features功能
如果该功能单元的offset 3的位置的8个bit中,bit0为1,表示支持TP(trasanction pending),bit1为1,表示支持FLR(function level Reset. 就是上面描述的function2能独立于function1被reset。)
由于在vfio_add_capabilities中,pci_add_capability被频繁调用,这里详细看看它做了什么。
- pci_add_capability
将capability ID为cap_id的capability结构插入到pdev的capability list链表中的第一个位置,并设置pdev的cmask,wmask,use用于标志该capability结构是否需要在载入时检查、是否可写、以及pdev中已经使用了的空间。
传入pci_add_capability的参数中,pdev指的是物理设备,即vdev->pdev;cap_id指要添加的capability的编号,每个capability有且只有一个编号;offset是指该capability结构在PCI配置空间中的偏移量,size是指该capability结构的大小,errp用于存储错误信息。
直到了pci_add_capability的功能,关于PCI_CAP_ID_AF的具体处理也就清晰了,即首先检查pdev是否具有FLR功能,如果有,则向pdev的配置空间中插入AF capability单元。
MSI功能
如果设备支持MSI功能,那么设备就可以向处理器传送中断,传送方式是将一个预定义好的数据结构(message)写到预定义好的地址上去。
即如果capability id为PCI_CAP_ID_MSI(0x5), 就直接调用vfio_msi_setup而不是pci_add_capability。
vfio_msi_setup在获得了MSI capability结构中关于:
- 是否具有发送64bit message address的能力
- 是否具有mask掉任一vector的能力
- 设备所需中断vector数量
之后,调用msi_init对该设备的MSI进行初始化。首先看看msi_init的函数原型和本次trace时,msi_init被传入的参数。
dev表示被配置MSI的PCI设备,offset是MSI capability结构在PCI配置空间中的偏移地址,nr_vectors是设备所需的中断vector数量,msi64bit是设备是否具有发送64bit message address的能力,msi_per_vector_mask是设备是否具有mask掉任一vector的能力,errp存储错误信息。
知道了msi_init的参数意义之后,开始详细查看具体代码:
所以来看,msi_init做了什么事情呢?
- 建立了一个由传入的nr_vectors,msi_per_vector_mask,msi64bit,offset信息拼凑成的MSI capability structure并插入到了dev的capability list中。
- 编辑了dev MSI capability structure中的相关bit的可读可写性,导致提供了以下能力:
- 软件可以编辑Message Control的bit6:4,确认需要分配的中断vector数量
- 软件可以编辑Message Control的bit0,即自由使能/禁止MSI
- 软件可以编辑Message Address的bit31:2(32bit)/bit63:2(64bit),即自由配置MSI 目标地址
- 软件可以编辑Message Data的全部16bit,即自由配置MSI数据
- 软件可以编辑Mask bits的bit0-bit(设备所需vector数量-1),即自由Mask掉vector.
回到vfio_msi_setup,函数的最后根据该MSI capability structure是否具有mask vector的能力以及MSI地址是32bit还是64bit,将vdev->msi_cap_size的内容填充正确的值。最后返回0.
最后总结一下添加PCI_CAP_ID_MSIX时所做的工作,即建立了一个MSI capability structure并插入到了设备的capability链表中。
PCI电源管理接口功能
这个功能单元提供了对PCI电源管理进行控制的标准接口。具体的操作为:
可见与电源管理最相关的处理就是vfio_check_pm_reset,详细看看。
先看函数原型,再看传入参数。
原型:
其中vdev就是传入该函数的需要被操作的设备,pos是电源管理capability structure在dev的配置空间中的位置。
传入参数:
接下来看看具体操作。
所以qemu对CAP_ID == 0x1的处理为:
- 根据设备PMCSR的bit3确定设备是否会在powerstate的命令下进行内部reset,并置vdev的相应filed(has_pm_reset)。
- 将电源管理capability structure插入到设备的capability 链表中。
本次trace的网卡不会在powerstate的命令下进行内部reset。
返回到vfio_add_capabilities中,该函数在最后执行vfio_add_ext_cap,以向设备添加扩展capability,由于本次透传的网卡不是pcie设备,所以无法通过该函数开头的检查,会跳过vfio_add_ext_cap的执行。
扩展capability只会在PCIE设备中提供,vfio_add_ext_cap的执行也是类似于vfio_add_std_cap,在物理设备中找到扩展capability的详细信息,然后通过pci_add_capability将capability添加到vdev中。
这里不再详细trace该函数,等到以后遇到了再细看。
至此,vfio_add_capabilities就结束了,该函数主要就是通过读取物理设备的capability list,获取了物理设备的各项capability,然后将这些capability添加到vdev维护的capability list中。
vfio_vga_quirk_setup
该函数针对Nvidia和ATi的两个厂家的显卡做了specific设置,本次透传的是网卡,不会进入该函数,因此不再详细trace。
vfio_bar_quirk_setup
由于部分PCI设备的部分BAR需要特殊设置,所以才有了该函数,需要特殊处理的有:
- ATi显卡的BAR4
- ATI显卡的BAR2
- NVIDIA显卡的BAR5
- NVIDIA显卡的BAR0
- RTL8168显卡的BAR2
- 集成显卡的BAR4
本次透传的是Intel网卡,因此不会进入该函数。
vfio_pci_igd_opregion_init
VFIO提供的集成显卡操作,本次透传不涉及这部分内容。
修改Qemu模拟配置空间的MSI/MSI-X的capability structure
在之前的vfio_add_capabilities中,向pdev的cap_present添加了物理设备具有的capability,因为cap_present的一个bit对应一项capability,bit0代表MSI capability,bit1代表MSI-X capability,本次trace中,经过vfio_add_capabilities,pdev->cap_present的值为0x301,即具有MSI但不具有MSI-X capability。
qemu会对MSI/MSI-X进行全模拟,即Guest在使用MSI/MSI-X过程中实际获得的MSI/MSI-X配置全部来自于qemu模拟的配置空间。
本次透传的网卡只支持MSI,在经历上面的代码之后,vdev的模拟配置空间中MSI capability structure中的内容为全1,此时模拟配置空间中的MSI capability为无效状态。
这个代码段含有特别多函数,逐一分析才能明白这段代码做了什么。
基础函数分析
vfio_pci_read_config
如果传入的读取的配置空间中的内容由qemu模拟,则读取保存在qemu中vdev->pdev的内容,如果不由qemu模拟,则调用pread读取物理设备配置空间中对应的内容。
函数原型:
传入参数:
具体分析:
vfio_pci_read_config中,首先检查传入的地址中的值是否由qemu模拟,检查方式为将模拟配置空间中地址偏移为addr位置的内容拷贝len个字节到emu_bits中,如果emu_bits中不为0,说明传入的地址中的值由qemu模拟。
其实在本节开头可以看到,如果qemu对某个capability structure进行模拟,那么其对应的vdev->emulated_config_bits + cap_addr中的置会被全置1,因此,如果qemu对某个capability structure进行模拟,那么上面用memcpy拷贝出来的emu_bits为全1.
如果传入的地址中的值由qemu模拟,则直接读取维护在vdev->pdev的配置空间中对应地址的值,而无需再读取物理设备的对应值。
如果传入的地址中的值不由qemu模拟,那么emu_bits就会为0,vfio_pci_read_config会利用pread(实际调用vfio_pci_read)读取物理设备中的capability structure中的内容,返回值的val也是phys_val的内容。
timer_new_ms
该函数是qemu提供的一个时钟函数,用于新建一个ms级别的时钟。
vfio_intx_mmap_enable
在源文件中,vfio_intx_mmap_enable有一段英文注释,大意为,不用BAR mmap会导致虚拟机性能降低,但是在INTx中断附近关闭BAR mmap也会导致巨大的负载,因此设计了一种方式,在INTx中断被服务之后的某个时间点,使能BAR mmap。这样既能利用BAR mmap的高性能,又能在INTx中断期间关闭BAR mmap。
vfio_intx_mmap_enable的实现中,如果有INTx中断正在等待服务,就修改vdev->intx.mmap_timer使其继续运转,并返回。如果没有INTx中断正在等待服务,就使能BAR mmap。
这样的设计能够使BAR mmap只有在vdev->intx.mmap_timer时钟递减到0,并且没有INTx中断正在等待,这2个条件同时满足的情况下使能。
pci_device_set_intx_routing_notifier
该函数只是简单的将传入设备dev的intx_routing_notifier(路由通知函数)设置为传入的notifier。
vfio_intx_routing_notifier
将INTx的具体管脚映射到IRQ,并进行qemu-kvm全局范围内的INTx中断映射的更新。
在vfio_intx_routing_notifier中,首先检查vdev的当前中断类型是否为INTx,如果不是,那么该函数就没有继续执行的意义了。
然后调用pci_device_route_intx_to_irq将vdev->intx.pin,即INTERRUPT_PIN上面产生的中断路由到irq上。
在pci_device_route_intx_to_irq中,首先不断迭代,调用dev所属bus的map_irq方法,建立Pin和Irq的逐级映射,最终调用bus的route_intx_to_irq方法,建立pin(INTx)到irq的映射,并返回一个PCIINTxRoute类型的路由结构,该路由中只有2个元素,irq和INTx模式,irq与INTx一对一对应,INTx模式可选的有使能、禁止、翻转INTx信号。
返回到vfio_intx_routing_notifier中,调用pci_intx_route_changed函数对比新旧route,如果通过对比vdev->intx.route和新建立的route,发现route的irq或route的INTx模式改变了,pci_intx_route_changed就返回true,否则pci_intx_route_changed返回false。
vfio_intx_routing_notifier的最后,如果发现route改变了,就调用vfio_intx_update更新vdev的intx映射。
vfio_intx_update
更新整个qemu、kvm系统中的INTx映射情况。
首先回顾一下qemu-kvm的中断机制。
-
利用qemu启动虚拟机时,在不传入kernel-irqchip参数的情况下,该参数默认=on,也就是kvm负责全部的IOAPIC、PIC、LAPIC的模拟。
-
在qemu中,main => qemu_init => configure_accelerators => do_configure_accelerator => accel_init_machine => kvm_init => kvm_irqchip_create. 也就是在初始化时会调用kvm_irqchip_create来创建模拟中断芯片。kvm_irqchip_create会直接调用
kvm_vm_ioctl(s, KVM_CREATE_IRQCHIP)
请求kvm创建模拟中断芯片。 -
在kvm中,收到创建模拟芯片的请求:
-
kvm_pic_init
向KVM_PIO_BUS上注册了3个设备,即master、slave、eclr(控制中断触发模式的),并为对这3个设备的读写操作提供了操作函数。
-
kvm_ioapic_init
向KVM_MMIO_BUS上注册了设备ioapic,并为对该设备的读写操作提供了操作函数。
-
kvm_setup_default_irq_routing
irq_routing是指中断路由表,表中有entry,entry的结构如下:
可以看到,irq和gsi对应,PIC最多有16个irq,所以kvm默认的irq_routing中,PIC和IOAPIC均具有0-15号irq,但是16-24号irq只属于IOAPIC。
具体的中断芯⽚(如PIC、IOAPIC)通过实现 kvm_irq_routing_entry 的 set 函数,实现生成中断功能。之后就可以通过entry.set方法控制中断管脚。
-
回到vfio_intx_update。
即vfio_intx_update在qemu和kvm的整个范围内更新了INTx中断映射。
中断相关处理
中断处理涉及ioeventfd和irqfd,整个机制比较复杂,等到弄清设备内存相关知识之后再系统来看。
在了解了一些基础处理函数之后,回头再看这部分代码。
vfio_pci_read_config读取了配置空间中0x3d也就是PCI Spec中所说的Interrupt Pin的内容,只有在当前设备支持INTx中断时,该field才为正数。
- 基于QEMU_CLOCK_VIRTUAL设置了一个计时器,用于在该计时器计时到0,并且没有中断正在等待的情况下,才使能BAR mmap。
- 设置了vdev->dev.intx_routing_notifier,该notifier能将INTx的具体管脚映射到IRQ,并进行qemu-kvm全局范围内的INTx中断映射的更新。
- 设置了vdev->irqchip_change_notifier.notify,该notifier能进行qemu-kvm全局范围内的INTx中断映射的更新。
- 将3中设置的notifier注册到kvm中
- 最后使能vfio中的INTx
结尾
最后就是一些基于特定设备的收尾处理,以及错误、请求notifer的注册。
这部分不关键,暂时不trace了。
__EOF__

本文链接:https://www.cnblogs.com/haiyonghao/p/14440747.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了