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编译选项:

./configure --enable-debug --target-list=x86_64-softmmu

使用以下方式启动gdb debug qemu:

sudo gdb --args x86_64-softmmu/qemu-system-x86_64 -m 4096 -smp 4 -hda ~/ewan/Workspace/img/Ubuntu18.04_loop.img -enable-kvm -device vfio-pci,host=06:00.0

在vfio_realize中设置断点并启动qemu运行:

(gdb) b vfio_realize
Breakpoint 1 at 0x3d3125: file /home/ewan/ewan/Workspace/qemu-5.0.0-rc4/hw/vfio/pci.c, line 2716.
(gdb) r
Starting program: /home/ewan/ewan/Workspace/qemu-5.0.0-rc4/x86_64-softmmu/qemu-system-x86_64 -m 4096 -smp 4 -hda /home/ewan/ewan/Workspace/img/Ubuntu18.04_loop.img -enable-kvm -cpu host -device vfio-pci,host=06:00.0
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7fffde5a6700 (LWP 12964)]
[New Thread 0x7fffddbb3700 (LWP 12965)]
WARNING: Image format was not specified for '/home/ewan/ewan/Workspace/img/Ubuntu18.04_loop.img' and probing guessed raw.
Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted.
Specify the 'raw' format explicitly to remove the restrictions.
[New Thread 0x7fffdd1d0700 (LWP 12968)]
[New Thread 0x7fffdc9cf700 (LWP 12969)]
[New Thread 0x7ffecfdff700 (LWP 12970)]
[New Thread 0x7ffecf5fe700 (LWP 12971)]

Thread 1 "qemu-system-x86" hit Breakpoint 1, vfio_realize (pdev=0x555557a94360, errp=0x7fffffffdcf8) at /home/ewan/ewan/Workspace/qemu-5.0.0-rc4/hw/vfio/pci.c:2716
2716	{
(gdb) bt
#0  0x0000555555927125 in vfio_realize (pdev=0x555557a94360, errp=0x7fffffffdcf8) at /home/ewan/ewan/Workspace/qemu-5.0.0-rc4/hw/vfio/pci.c:2716
#1  0x0000555555bb45e2 in pci_qdev_realize (qdev=0x555557a94360, errp=0x7fffffffdd68) at hw/pci/pci.c:2098
#2  0x0000555555acde6e in device_set_realized (obj=0x555557a94360, value=true, errp=0x7fffffffdf40) at hw/core/qdev.c:891
#3  0x0000555555d23da6 in property_set_bool (obj=0x555557a94360, v=0x555557a8d6f0, name=0x555555fdef5a "realized", opaque=0x55555693d4e0, errp=0x7fffffffdf40) at qom/object.c:2238
#4  0x0000555555d21bdf in object_property_set (obj=0x555557a94360, v=0x555557a8d6f0, name=0x555555fdef5a "realized", errp=0x7fffffffdf40) at qom/object.c:1324
#5  0x0000555555d2557d in object_property_set_qobject (obj=0x555557a94360, value=0x555557a8c340, name=0x555555fdef5a "realized", errp=0x7fffffffdf40) at qom/qom-qobject.c:26
#6  0x0000555555d21ec4 in object_property_set_bool (obj=0x555557a94360, value=true, name=0x555555fdef5a "realized", errp=0x7fffffffdf40) at qom/object.c:1390
#7  0x0000555555a3a5e5 in qdev_device_add (opts=0x55555693a2b0, errp=0x5555568680a0 <error_fatal>) at qdev-monitor.c:680
#8  0x00005555559a998c in device_init_func (opaque=0x0, opts=0x55555693a2b0, errp=0x5555568680a0 <error_fatal>) at /home/ewan/ewan/Workspace/qemu-5.0.0-rc4/softmmu/vl.c:2079
#9  0x0000555555e97d47 in qemu_opts_foreach (list=0x5555567c4f00 <qemu_device_opts>, func=0x5555559a9965 <device_init_func>, opaque=0x0, errp=0x5555568680a0 <error_fatal>) at util/qemu-option.c:1170
#10 0x00005555559aee2a in qemu_init (argc=12, argv=0x7fffffffe378, envp=0x7fffffffe3e0) at /home/ewan/ewan/Workspace/qemu-5.0.0-rc4/softmmu/vl.c:4367
#11 0x0000555555e18928 in main (argc=12, argv=0x7fffffffe378, envp=0x7fffffffe3e0) at /home/ewan/ewan/Workspace/qemu-5.0.0-rc4/softmmu/main.c:48

程序停到了vfio_realize.通过bt命令可以看到vfio_realize调用栈的情况,可以看到,vfio_realize最终操作的是pdev=0x555557a94360这个物理设备。下面通过单步调试查看vfio_realize中的各关键步骤。

检查传入的设备是否符合要求

if (!vdev->vbasedev.sysfsdev) {
    if (!(~vdev->host.domain || ~vdev->host.bus ||
          ~vdev->host.slot || ~vdev->host.function)) {
        error_setg(errp, "No provided host device");
        error_append_hint(errp, "Use -device vfio-pci,host=DDDD:BB:DD.F "
                          "or -device vfio-pci,sysfsdev=PATH_TO_DEVICE\n");
        return;
    }
    vdev->vbasedev.sysfsdev =
        g_strdup_printf("/sys/bus/pci/devices/%04x:%02x:%02x.%01x",
                        vdev->host.domain, vdev->host.bus,
                        vdev->host.slot, vdev->host.function);
}

由于是第一次使用,这里的vdev->vbasedev.sysfsdev一定为0,会直接进入第一个if,检查启动qemu时传入的“host=”的参数是否为空,如果不为空,将传入的这些参数赋值给vdev->vbasedev.sysfsdev.因此上面这段代码执行完成之后,vdev->vbasedev.sysfsdev变为了"/sys/bus/pci/devices/0000:00:1f.6"。

(gdb) p vdev->vbasedev.sysfsdev 
$2 = 0x0
(gdb) p vdev->host 
$3 = {domain = 0, bus = 6, slot = 0, function = 0}
(gdb) p vdev->vbasedev.sysfsdev 
$4 = 0x555557a8c380 "/sys/bus/pci/devices/0000:00:1f.6"

将设备信息放入buff(一个stat结构体)

if (stat(vdev->vbasedev.sysfsdev, &st) < 0) {
    error_setg_errno(errp, errno, "no such host device");
    error_prepend(errp, VFIO_MSG_PREFIX, vdev->vbasedev.sysfsdev);
    return;
}

stat()函数的作用是获得文件(参数1)的属性,存储到buff(参数2)中,如果该文件不存在,返回负值。下面gdb打出的buff中的值有变化,且if语句直接跳过,说明传入的host device真实存在。

(gdb) p /x st
$2 = {st_dev = 0x7fff00000000, st_ino = 0x0, st_nlink = 0x7fff00000001, st_mode = 0x0, st_uid = 0x0, st_gid = 0xffffcc00, __pad0 = 0x7fff, st_rdev = 0x3000000030, st_size = 0x7fffffffcc50,
p /x st
$4 = {st_dev = 0x7fff00000000, st_ino = 0x0, st_nlink = 0x7fff00000001, st_mode = 0x0, st_uid = 0x0, st_gid = 0xffffcc00, __pad0 = 0x7fff, st_rdev = 0x3000000030, st_size = 0x7fffffffcc50,
  st_blksize = 0x5555560c60b8, st_blocks = 0x7fffffffd200, st_atim = {tv_sec = 0x7fffffffd380, tv_nsec = 0xa}, st_mtim = {tv_sec = 0x7fffffffd210, tv_nsec = 0x1}, st_ctim = {
    tv_sec = 0x7ffff24c7a3a, tv_nsec = 0x7fffffffcca0}, __glibc_reserved = {0x0, 0x5555560c60bc, 0x73657400000000}}
(gdb) n
(gdb) p /x st
$6 = {st_dev = 0x16, st_ino = 0x1bb5, st_nlink = 0x4, st_mode = 0x41ed, st_uid = 0x0, st_gid = 0x0, __pad0 = 0x0, st_rdev = 0x0, st_size = 0x0, st_blksize = 0x1000, st_blocks = 0x0,
  st_atim = {tv_sec = 0x5f4c8c32, tv_nsec = 0x71bb269}, st_mtim = {tv_sec = 0x5f4c8c2f, tv_nsec = 0x24f472ff}, st_ctim = {tv_sec = 0x5f4c8c2f, tv_nsec = 0x24f472ff}, __glibc_reserved = {
    0x0, 0x0, 0x0}}

检查设备是否支持迁移

if (!pdev->failover_pair_id) {
    error_setg(&vdev->migration_blocker,
               "VFIO device doesn't support migration");
    ret = migrate_add_blocker(vdev->migration_blocker, &err);
    if (ret) {
        error_propagate(errp, err);
        error_free(vdev->migration_blocker);
        vdev->migration_blocker = NULL;
        return;
    }
}

这里的pdev->failover_pair_id,是物理设备的一个属性,与是否可迁移有关,debug得出该设备无法迁移,因此进入migrate_add_blocker函数,生成一个migration_blockers链表,存储vdev->migration_blocker这个blocker,返回值ret为0,即记录blocker成功。

(gdb) p pdev->failover_pair_id 
$10 = 0x0
(gdb) p ret
$11 = 0

vfio_get_group

  • 赋值
vdev->vbasedev.name = g_path_get_basename(vdev->vbasedev.sysfsdev);
vdev->vbasedev.ops = &vfio_pci_ops;
vdev->vbasedev.type = VFIO_DEVICE_TYPE_PCI;
vdev->vbasedev.dev = DEVICE(vdev);

开头3行为vdev->vbaseddev的名字,操作,类型赋值,第4行将VFIOPCIDevice类转化为device_state类,存储在vdev->vbasedev.dev中。

(gdb) p vdev->vbasedev.name
$17 = 0x555557a8db10 "0000:00:1f.6"
(gdb) p vdev->vbasedev.ops
$18 = (VFIODeviceOps *) 0x5555567575b0 <vfio_pci_ops>
(gdb) p vdev->vbasedev.type
$19 = 0
(gdb) p vdev->vbasedev.dev
$20 = (DeviceState *) 0x555557a94360
  • 检查device对应的iommu_group路径是否有效
tmp = g_strdup_printf("%s/iommu_group", vdev->vbasedev.sysfsdev);
len = readlink(tmp, group_path, sizeof(group_path));
g_free(tmp);

if (len <= 0 || len >= sizeof(group_path)) {
    error_setg_errno(errp, len < 0 ? errno : ENAMETOOLONG,
                     "no iommu_group found");
    goto error;
}
group_path[len] = 0;

group_name = basename(group_path);
if (sscanf(group_name, "%d", &groupid) != 1) {
    error_setg_errno(errp, errno, "failed to read %s", group_path);
    goto error;
}

在经历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)是否有效。

(gdb) p tmp
$2 = 0x555557961f50 "/sys/bus/pci/devices/0000:00:1f.6/iommu_group"
(gdb) p len
$4 = 34
group = vfio_get_group(groupid, pci_device_iommu_address_space(pdev), errp);
if (!group) {
    goto error;
}

QLIST_FOREACH(vbasedev_iter, &group->device_list, next) {
    if (strcmp(vbasedev_iter->name, vdev->vbasedev.name) == 0) {
        error_setg(errp, "device is already attached");
        vfio_put_group(group);
        goto error;
    }
}

AddressSpace *pci_device_iommu_address_space(PCIDevice *dev)
{
    PCIBus *bus = pci_get_bus(dev);
    PCIBus *iommu_bus = bus;
    uint8_t devfn = dev->devfn;

    while (iommu_bus && !iommu_bus->iommu_fn && iommu_bus->parent_dev) {
        PCIBus *parent_bus = pci_get_bus(iommu_bus->parent_dev);

        if (!pci_bus_is_express(iommu_bus)) {
            PCIDevice *parent = iommu_bus->parent_dev;

            if (pci_is_express(parent) &&
                pcie_cap_get_type(parent) == PCI_EXP_TYPE_PCI_BRIDGE) {
                devfn = PCI_DEVFN(0, 0);
                bus = iommu_bus;
            } else {
                devfn = parent->devfn;
                bus = parent_bus;
            }
        }

        iommu_bus = parent_bus;
    }
    if (iommu_bus && iommu_bus->iommu_fn) {
        return iommu_bus->iommu_fn(bus, iommu_bus->iommu_opaque, devfn);
    }
    return &address_space_memory;
}

这里的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。

vfio_connect_container
=> vfio_get_address_space // 没有合适的VFIOAddressSpace,就分配一个space,并使space->as=as. 初始化一个space->containers的链表,向vfio_address_spaces中插入一个子链表,子链表头为space的地址.返回值为该space,类型为VFIOAddressSpace。
=> // 分配一个container,将其space设置为刚刚分配的space,其fd为"/dev/vfio/vfio"。初始化2个链表,记录该container使用的giommu和hostwin,即GuestIOMMU和hostDMAWindow.
=> vfio_init_container
   => vfio_get_iommu_type // 获取vfio的类型,trace时的vfio类型为VFIO_TYPE1v2_IOMMU(3)
   => // 利用ioctl(VFIO_GROUP_SET_CONTAINER)将group attach到container中
   => // 利用ioctl(VFIO_SET_IOMMU)为container设置IOMMU (这一步将container与IOMMU连接,是最重要的步骤)
   => // 为container的iommu_type赋值(trace过程中为3)
=> // 根据container的iommu_type,做出不同的行为,对于VFIO_TYPE1v2_IOMMU和VFIO_TYPE1_IOMMU,利用ioctl(VFIO_IOMMU_GET_INFO)获取IOMMU信息中的iova_pgsize,用于向container的hostwin_list中添加HostDmaWindow.
=> vfio_kvm_device_add_group
   => // 检验全局变量vfio_kvm_device_fd,这个fd是“vfio”这个设备的fd,每个VM有一个。
   => kvm_vm_ioctl(kvm_state, KVM_CREATE_DEVICE, &cd) // 利用kvm提供的ioctl在vm内创建一个vfio设备,该设备的fd会由cd.fd提供
   => ioctl(vfio_kvm_device_fd, KVM_SET_DEVICE_ATTR, &attr) // 利用kvm提供的ioctl将group加入到kvm中的"VFIO"设备上
=> QLIST_INIT(&container->group_list) // 初始化一个链表,用于记录container中的group
=> QLIST_INSERT_HEAD(&space->containers, container, next) // 向space->containers链表中插入container这个元素
=> group->container = container
   QLIST_INSERT_HEAD(&container->group_list, group, container_next) // 向container->group_list链表中加入group这个元素
=> container->listener = vfio_memory_listener // 每当向该container中增删Memory的时候,都会触发这个listener
=> memory_listener_register(&container->listener, container->space->as)
    => listener->address_space=as // listener监控的地址空间
    => // 因为memory_listeners这个全局memory listenner链表不为空,且当前listener的优先级比memory_listeners链表中最后一个listener的优先级低,所以在memory_listeners链表中找到恰好比当前listener优先级高的listener,然后将当前listener插入到该listener的前面。
    => // 如果该as(addressSpace)没有listener,或者待注册的listener比该as中已注册的listener的优先级高,那么就将该listener也加入到该as的listeners链表中。如果以上两种情况都不符合,那么就对比待注册listener与as的所有已注册listener,如果待注册listener的优先级比已注册的listener的任一优先级低,就将待注册listener插入到as->listeners中那个恰好比待注册listener优先级高的listener的前面。
    => listener_add_address_space(listener,as)
       => // 如果listener存在begin函数,则调用listener->begin()函数
       => // 如果global_dirty_log这个全局变量为true,且listener->log_global_start存在,则调用listener->log_global_start函数。
       => // 很显然上面两种情况都不存在,因为vfio_memory_listener只有region_add和region_del函数。
       => view = address_space_get_flatview(as) // 将addressSpace转化为FlatView形式
       => // 遍历该flatview中的每一个MemoryRegionSection,调用vfio_listener_region_add
    		=> vfio_listener_region_add
    		   => vfio_listener_skipped_section(section) // 如果该section所属的mr既不是ram_memory_region,也不是iommu_memory_region,则返回值为true。如果该section既是iommu,又是ram,那么结果取决于该section在address_space中的offset是否超出了64bit地址的极限,如果超过则返回True。这里在本次检测不能跳过该section的add,因为该section所属的mr是ram且section在as中的offset为0。
          => // 检查section是否在其region中对齐,如果不对齐则报错,观测对齐
          => // 根据section在address_space中的offset,获得该offset的在对齐页中的上边界地址,称为iova。
          => // 计算出该iova空间的末端在哪里。这个计算过程是:首先获取该section在所属as中的offset,通过宏TARGET_PAGE_ALIGN获取该offset的页对齐地址,该页对齐地址为页顶端地址,记为iova。 然后通过宏int128_make64产生一个128bit的offset的128bit数,并加上该section的大小,0xa0000,即640k。之后利用int128_and抹去llend最低12bit的值,目的是为了页对齐。最后检查llend是否大于iova,因为iova为起始地址,llend为末端地址,因此iova不能大于等于llend,而应该小于。最后用llend-1可以获得最终的64bit地址end。整个过程获得了以iova为起始地址,以end为终止地址的一段memory。
    		   => // 在container->hostwin_list中寻找可以将该iova空间放进去的HostDMAWindow(但并不执行将该iova空间放入window操作).
          => memory_region_ref(section->mr) // 增加该section所属memroy_region的ref(+1),该ref影响对memory的访问
          => // 检查该section所属的mr是否为iommu,如果是则需要分配对应的iommu_region.这里的section所属mr不是iommu。
          => // 假设该section所属的mr是ram_region,然后通过memory_region_get_ram_ptr获得mr的HVA,再加上section在mr中的offset,再加上iova在section中的偏移量,即iova减去section在address_space中的offset,最终获得iova的HVA。如此便获得了一个IOVA space。起始地址为iova,终止地址为end。虚拟地址为vaddr。这里是iova=0,end=0x9ffff,vaddr=0x7ffecfe00000,size为0xa0000.
          => // 检验该section所属的mr是否为ram_device,如果是,由于ram_device没有dma_map的说法,只有更小的概念ram,才有dma_map的说法,所以终止配置dma_map,返回。如果不是ram_device,则继续region_add配置。
          => vfio_dma_map // 将进程的虚拟地址映射为IO虚拟地址
             => // 首先定义一个vfio_iommu_type1_dma_map的结构体,存储vaddr,iova,size和是否可由设备读写。
             => // 重复调用ioctl(VFIO_IOMMU_MAP_DMA)建立从vaddr到iova的映射,即从用户空间到设备空间的映射。

建立qemu地址空间和设备IOVA之间的映射

上面的程序中,container拥有一个address_space,该address_space被转化为了FlatView形式,而FlatView中有很多MemoryRegionSection,qemu将这每一个section都包装成一个iova space,然后利用vfio_dma_map完成从虚拟地址(HVA)到iova space(关联到特定vfio设备)的映射。

vfio_dma_map
=> ioctl(container->fd, VFIO_IOMMU_MAP_DMA, &map) // map包含了section包装成的iova space的起始地址,结束地址,长度,是否可由device 读写等信息

VFIO_IOMMU_MAP_DMA关联的ioctl信息会与内核进行通信。

qemu: ioctl(VFIO_IOMMU_MAP_DMA)
---
kernel:vfio_iommu_type1_ioctl(VFIO_IOMMU_MAP_DMA)
       => // 首先从用户空间获得关于iova空间的详细信息(&map),以及iommu的fd(container->fd,也就是/dev/vfio/vfio的fd)
       => vfio_dma_do_map(container->fd,&map)

内核调用vfio_dmap_do_map实现最终的iova+vaddr映射。

vfio_dma_do_map
=> vfio_find_dma(iommu,iova,size) // 查找该iova space是否已经被映射过
=> vfio_iommu_iova_dma_valid // 确认该映射提供的iova space的有效性
=> 	iommu->dma_avail--; // /dev/vfio/vfio的可用dma数量减少1
	  dma->iova = iova; // iova是device使用的dma地址
	  dma->vaddr = vaddr; // vaddr是CPU使用的地址
	  dma->prot = prot; // 读写flags
=> vfio_link_dma(iommu, dma); // 将dma链接到红黑树中,以确保下次查找该dma(iova space mapping)是否被映射过时能快速找到
=> vfio_pin_map_dma(iommu, dma, size); // 
   => vfio_pin_pages_remote // 将从vaddr开始,长度为size内存区域的内存空间中的连续页锁定,避免其他应用使用这些页.同时获得这些页的物理地址HPA。
   => vfio_iommu_map // 利用iommu driver提供的iommu_map函数,将qemu地址空间的一部分(HVA)映射到iova空间,等到将该段IOVA空间和device绑定后,以后device的DMA主动发起的动作最终会落到qemu地址空间的一段内存上,也就是落在GPA空间上。

至此,给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中:

if (QLIST_EMPTY(&vfio_group_list)) { // 即在第一次获取group的时候,注册一个vfio复位处理函数,该处理函数存储在全局链表reset_handlers中
    qemu_register_reset(vfio_reset_handler, NULL);
}
// 将处理完成的group存入全局链表vfio_group_list中
QLIST_INSERT_HEAD(&vfio_group_list, group, next);

return 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 能力检查

QLIST_FOREACH(vbasedev_iter, &group->device_list, next) {
    if (strcmp(vbasedev_iter->name, vdev->vbasedev.name) == 0) {
        error_setg(errp, "device is already attached");
        vfio_put_group(group);
        goto error;
    }
}

返回到vfio_realize,获得了device所属group之后,需要检查group中的device_list,如果device_list中存在与当前设备名相同的设备,说明设备早已经attach到了group,应该报错,因为在这之前,device不应该被attach到group.

/*
 * Mediated devices *might* operate compatibly with memory ballooning, but
 * we cannot know for certain, it depends on whether the mdev vendor driver
 * stays in sync with the active working set of the guest driver.  Prevent
 * the x-balloon-allowed option unless this is minimally an mdev device.
 */
tmp = g_strdup_printf("%s/subsystem", vdev->vbasedev.sysfsdev);
subsys = realpath(tmp, NULL);
g_free(tmp);
is_mdev = subsys && (strcmp(subsys, "/sys/bus/mdev") == 0);
free(subsys);

trace_vfio_mdev(vdev->vbasedev.name, is_mdev);

if (vdev->vbasedev.balloon_allowed && !is_mdev) {
    error_setg(errp, "x-balloon-allowed only potentially compatible "
               "with mdev devices");
    vfio_put_group(group);
    goto error;
}

这是关于混杂设备的一段检验代码,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的参数如下:

vfio_get_device (group=0x555557a93820, name=0x555557a8e6b0 "0000:00:1f.6", vbasedev=0x555557a8dc50,errp=0x7fffffffdcf8)

group是通过vfio_get_group得到的,name是根据读取传入qemu的参数得到的,vbasedev是qemu自己构造的一个虚拟设备描述符。

vfio_get_device
=> fd = ioctl(group->fd, VFIO_GROUP_GET_DEVICE_FD, name) // 根据group->fd和device name获取device的fd,即设备文件描述符
=> ret = ioctl(fd, VFIO_DEVICE_GET_INFO, &dev_info) // 根据device的fd获取设备信息,用于填充到vbasedev的相应field中
=> vbasedev->fd = fd;
   vbasedev->group = group;
   vbasedev->num_irqs = dev_info.num_irqs;
   vbasedev->num_regions = dev_info.num_regions;
   vbasedev->flags = dev_info.flags;
=> QLIST_INSERT_HEAD(&group->device_list, vbasedev, next) // 将vbasedev加入到group->device_list链表中

最终加入到group->device_list中的虚拟设备vbasedev详细参数如下:

(gdb) p *vbasedev
$19 = {next = {le_next = 0x0, le_prev = 0x555557a93830}, group = 0x555557a93820,sysfsdev = 0x555557a8cf20 "/sys/bus/pci/devices/0000:00:1f.6", name = 0x555557a8e6b0 "0000:00:1f.6",dev = 0x555557a8d360, fd = 19, type = 0, reset_works = false, needs_reset = false, no_mmap = false,
  balloon_allowed = false, ops = 0x5555567575b0 <vfio_pci_ops>, num_irqs = 5, num_regions = 9,flags = 3}

说明该设备(网卡)有5种中断,9个region,flags(可读可写标志)为0x11,可读可写,不支持reset。

至此,vfio_get_device结束,vbasedev中具有该device的详细信息,group->device_list中的第一个元素(也是唯一一个)也具有该device的详细信息。

vfio_populate_device

传入vfio_populate_device的参数如下:

vfio_populate_device (vdev=0x555557a8d360,errp=0x7fffffffcbd8)

vdev即在前面构造完成的VFIOPCIDevice类型的结构,包含了emulated_config_bits(qemu模拟的配置),pdev(物理设备),BAR空间等子域,总之vdev是一个能提供完整PCI设备功能的结构。

vfio_populate_device // populate,就是指将设备中的资源抽象出来
=> // 检查是否为PCI设备
=> // 检查设备的配置区域数量、irq数量是否正常
=> vfio_region_setup
   => vfio_get_region_info
      => ioctl(vbasedev->fd, VFIO_DEVICE_GET_REGION_INFO, *info) // 根据设备的fd和info->index,获取设备的region信息

建立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_pci_ioctl
=> // 根据传入的cmd(这里是VFIO_DEVICE_GET_REGION_INFO),做出不同的操作,如果是VFIO_DEVICE_GET_REGION_INFO,则根据info->index,分辨此次获取信息请求的目标是config_region,BAR_region,ROM_regioin,还是VGA_Region并根据请求的region类型填充info的size,offset,flags.

vfio_get_region_info执行完成之后,即已经获取了region信息,存储在info变量中,本次执行的region信息为:

(gdb) p *info
$30 = {argsz = 32, flags = 7, index = 0, cap_offset = 0, size = 131072, offset = 0}

即BAR0的flags为7(0x111),即BAR0支持读、写、mmap。BAR0的index为0,第一个cap在info结构中的offset为0,BAR0的大小为131072个字节,BAR0在设备fd起始的区域中的offset为0.

接下来继续将region信息补充完整。

region->vbasedev = vbasedev;
region->flags = info->flags;
region->size = info->size;
region->fd_offset = info->offset;
region->nr = index;

接着为region申请存储空间:

if (region->size) { // 如果region->size不为0,说明需要申请空间
    // 申请一个MemoryRegioin类型的空间,大小为1个字节
    region->mem = g_new0(MemoryRegion, 1);
    // memory_region_init_io => memory_region_init => memory_region_do_init. 在最后一个函数中,对mr(memoryRegion)的name("0000:00:1f.6 BAR 0"),owner,ram_block进行设置,并将region->mem设置为vdev的孩子属性,该孩子属性的名字为:"0000:00:1f.6 BAR 0[0]".最后还注册了对该region->mem的操作,即读写操作。
    memory_region_init_io(region->mem, obj, &vfio_region_ops,
                          region, name, region->size);
		 // vbasedev未被mmap,且该region支持mmap,所以对该region进行mmap
    if (!vbasedev->no_mmap &&
        region->flags & VFIO_REGION_INFO_FLAG_MMAP) {
				  // vfio_setup_region_sparse_mmaps => vfio_get_region_info_cap 后者检测到透传的网卡不支持sparse mmap.所以直接返回。 sparse mmap capabiliy能为mmap一个region中的area时提供更好的粒度。
        ret = vfio_setup_region_sparse_mmaps(region, info);
        
        // 如果建立sparse mmap失败,就要建立普通mmap.
        if (ret) {
            region->nr_mmaps = 1;
            region->mmaps = g_new0(VFIOMmap, region->nr_mmaps); // 分配一个全为0的类型为VFIOMmap的区域,大小为1个字节
            // 给region的mmap结构赋值
            region->mmaps[0].offset = 0;
            region->mmaps[0].size = region->size;
        }
    }
}

最后并未给BAR region分配对应的存储空间,只有变量存储空间,总之,在建立BAR region的过程中,对vdev->bars[0-5].region进行了设置,除了对应的存储空间,其它的所有信息,和物理设备pdev的BAR Region没有区别。

配置vdev的config设置

与建立BAR Region时一样,首先利用vfio_get_region_info从内核中获取设备信息,获取到的信息为reg_info.

ret = vfio_get_region_info(vbasedev,
                               VFIO_PCI_CONFIG_REGION_INDEX, &reg_info)
    
(gdb) p *reg_info 
$70 = {argsz = 32, flags = 3, index = 7, cap_offset = 0, size = 256, offset = 7696581394432}

即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).

vdev->config_size = reg_info->size;
if (vdev->config_size == PCI_CONFIG_SPACE_SIZE) {
    vdev->pdev.cap_present &= ~QEMU_PCI_CAP_EXPRESS;
}
vdev->config_offset = reg_info->offset;

然后将获得的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总线上。

if (vdev->features & VFIO_FEATURE_ENABLE_VGA) {
    ret = vfio_populate_vga(vdev, errp);
    if (ret) {
        error_append_hint(errp, "device does not support "
                          "requested feature x-vga\n");
        return;
    }
}

本次透传的网卡没有VGA资源,所以程序没有进行这部分的处理。

中断信息获取

irq_info.index = VFIO_PCI_ERR_IRQ_INDEX; // 不知道为什么,要将irq_info.index设置为3
ret = ioctl(vdev->vbasedev.fd, VFIO_DEVICE_GET_IRQ_INFO, &irq_info)

同样,利用vfio-driver提供的ioctl获取中断信息,而在内核中,对于VFIO_DEVICE_GET_IRQ_INFO,处理如下:

...
else if (cmd == VFIO_DEVICE_GET_IRQ_INFO) {
    struct vfio_irq_info info;
    ...
    switch (info.index) { // 看来将index设置为3是为了分辨pci和pcie.
            case VFIO_PCI_ERR_IRQ_INDEX:
            	if (pci_is_pcie(vdev->pdev))
				break;
    }
    info.flags = VFIO_IRQ_INFO_EVENTFD;// 该设备支持基于信号的eventfd.
		 info.count = vfio_pci_get_irq_count(vdev, info.index); // 获取irq数量

		 if (info.index == VFIO_PCI_INTX_IRQ_INDEX)
			 info.flags |= (VFIO_IRQ_INFO_MASKABLE |
				       VFIO_IRQ_INFO_AUTOMASKED);
		 else
			 info.flags |= VFIO_IRQ_INFO_NORESIZE; // NORESIZE的意思是interrupt lines是一个set,如果想要新使能subindex,只能先disable所有Interrupt lines才可以。这用于MSI和MSI-X。

		 return copy_to_user((void __user *)arg, &info, minsz) ?
			 -EFAULT : 0; // 将中断信息拷贝到qemu
	}           

最终获得的irq_info如下:

(gdb) p irq_info 
$78 = {argsz = 16, flags = 0, index = 3, count = 0}

本次获取irq_info失败,因为网卡设备较老。但即使获取irq_info成功,也不会有进一步的赋值给vdev的操作,也就是说,整个中断信息获取过程,只是将irq_info->index设置为了VFIO_PCI_ERR_IRQ_INDEX(3).

复制物理设备的配置空间并进行相关修改

复制物理设备的配置空间

/* Get a copy of config space */
/* 
读取vdev->vbasedev.fd,即物理设备的fd中的数据,读取到vdev->pdev.config buffer中,读取大小为pdev和vdev二者配置空间的较小值,读取的起始地址为fd+vdev->config_offset,即物理设备的配置空间相较于fd的偏移地址 
*/
ret = pread(vdev->vbasedev.fd, vdev->pdev.config,
            MIN(pci_config_size(&vdev->pdev), vdev->config_size),
            vdev->config_offset);

这里有一个问题,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自定义设备功能的要求。

/* 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 also add or extend BARs */
memset(vdev->emulated_config_bits + PCI_BASE_ADDRESS_0, 0xff, 6 * 4);

分配了一块配置空间大小的区域,称为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就行)。

/*
     * The PCI spec reserves vendor ID 0xffff as an invalid value.  The
     * device ID is managed by the vendor and need only be a 16-bit value.
     * Allow any 16-bit value for subsystem so they can be hidden or changed.
     */
if (vdev->vendor_id != PCI_ANY_ID) {
    if (vdev->vendor_id >= 0xffff) {
        error_setg(errp, "invalid PCI vendor ID provided");
        goto error;
    }
    vfio_add_emulated_word(vdev, PCI_VENDOR_ID, vdev->vendor_id, ~0);
    trace_vfio_pci_emulated_vendor_id(vdev->vbasedev.name, vdev->vendor_id);
} else {
    vdev->vendor_id = pci_get_word(pdev->config + PCI_VENDOR_ID);
}

if (vdev->device_id != PCI_ANY_ID) {
    if (vdev->device_id > 0xffff) {
        error_setg(errp, "invalid PCI device ID provided");
        goto error;
    }
    vfio_add_emulated_word(vdev, PCI_DEVICE_ID, vdev->device_id, ~0);
    trace_vfio_pci_emulated_device_id(vdev->vbasedev.name, vdev->device_id);
} else {
    vdev->device_id = pci_get_word(pdev->config + PCI_DEVICE_ID);
}

if (vdev->sub_vendor_id != PCI_ANY_ID) {
    if (vdev->sub_vendor_id > 0xffff) {
        error_setg(errp, "invalid PCI subsystem vendor ID provided");
        goto error;
    }
    vfio_add_emulated_word(vdev, PCI_SUBSYSTEM_VENDOR_ID,
                           vdev->sub_vendor_id, ~0);
    trace_vfio_pci_emulated_sub_vendor_id(vdev->vbasedev.name,
                                          vdev->sub_vendor_id);
}

if (vdev->sub_device_id != PCI_ANY_ID) {
    if (vdev->sub_device_id > 0xffff) {
        error_setg(errp, "invalid PCI subsystem device ID provided");
        goto error;
    }
    vfio_add_emulated_word(vdev, PCI_SUBSYSTEM_ID, vdev->sub_device_id, ~0);
    trace_vfio_pci_emulated_sub_device_id(vdev->vbasedev.name,
                                          vdev->sub_device_id);
}

自定义SingleFunction/MultipleFunction

qemu提供了将device的SingleFunction和MultipleFunction之间的互相转换,根据PCI spec,配置空间的0xE地址的bit7为0时,设备为单功能设备,为1时,为多功能设备。

/* 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) {// 如果pdev.cap_present的bit3为1,说明qemu模拟的PCI设备具有多功能,那么就将配置空间的0xE位置的bit7设置为1,否则就使bit7为0
    vdev->pdev.config[PCI_HEADER_TYPE] |= PCI_HEADER_TYPE_MULTI_FUNCTION;
} else {
    vdev->pdev.config[PCI_HEADER_TYPE] &= ~PCI_HEADER_TYPE_MULTI_FUNCTION;
}

清除Host的硬件映射信息

这里清除的Host硬件映射信息,只是在Qemu内读取的硬件映射信息,表现形式为vdev->pdev.config,只是一个数据结构,但最终作用于Guest。修改vdev->pdev.config并不会真正修改硬件设备中的内容。

/*
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);

PCI设备的ROM的相关处理

ROM是PCI协议中的read-only memory,可以预先存储一些需要执行的代码在其中。如果透传的设备具有ROM,那么就会在vfio_realize的vfio_pci_size_rom中进行初始化和注册ROM空间(只有变量空间没有存储空间),并注册了该ROM BAR。

vfio_pci_size_rom
=> // 检查设备是否具有ROM功能
=> // 获取ROM空间的大小
=> // 检查qemu是否屏蔽了ROM功能
=> // 将名为“vfio[设备名].rom”作为name调用memory_region_init_io
=> memory_region_init_io => memory_region_init => memory_region_do_init. //在最后一个函数中,对mr(memoryRegion)的name,owner,ram_block进行赋值,并将region->mem设置为vdev的孩子属性,到这里,region->mem的大小只有1个字节。最后还注册了对该region->mem的操作,即读写操作。
=> pci_register_bar // 注册ROM BAR Region

最后的pci_register_bar是比较重要的一个函数,其函数原型为:

void pci_register_bar(PCIDevice *pci_dev, int region_num,
                      uint8_t type, MemoryRegion *memory)

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位空间,的信息。

for (i = 0; i < PCI_ROM_SLOT; i++) {
    vfio_bar_prepare(vdev, i);
}

vfio_bar_prepare
=> // 读取物理设备的BAR[i]中的内容,记作pci_bar
=> bar->ioport = (pci_bar & PCI_BASE_ADDRESS_SPACE_IO);
		bar->mem64 = bar->ioport ? 0 : (pci_bar & PCI_BASE_ADDRESS_MEM_TYPE_64);
		bar->type = pci_bar & (bar->ioport ? ~PCI_BASE_ADDRESS_IO_MASK :                               ~PCI_BASE_ADDRESS_MEM_MASK);
		bar->size = bar->region.size;

在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.

// BAR0中的数据
(gdb) p /x pci_bar
$48 = 0xdf200000
// qemu中BAR0结构体中的相关信息
(gdb) p /x *bar
$49 = {region = {vbasedev = 0x555557a8dc50, fd_offset = 0x0, mem = 0x555557a93af0, size = 0x20000,
    flags = 0x7, nr_mmaps = 0x1, mmaps = 0x555557a94010, nr = 0x0}, mr = 0x0, size = 0x20000, type = 0x0,
  ioport = 0x0, mem64 = 0x0, quirks = {lh_first = 0x0}}

vfio_msix_early_setup msi-x中断的早期准备

vfio_msix_early_setup函数的前面有一大段注释:

/*
 We don't have any control over how pci_add_capability() inserts capabilities into the chain.  In order to setup MSI-X we need a MemoryRegion for the BAR.  In order to setup the BAR and not attempt to mmap the MSI-X table area, which VFIO won't allow, we need to first look for where the MSI-X table lives.  So we unfortunately split MSI-X setup across two functions.
 */

大概意思是,由于不确定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_msix_early_setup
=> pos = pci_find_capability(&vdev->pdev, PCI_CAP_ID_MSIX) // 寻找在设备Config space中,指向MSIX功能的capability pointer在配置空间中的offset.
=> pread(fd, &ctrl, sizeof(ctrl),
              vdev->config_offset + pos + PCI_MSIX_FLAGS) // 在设备配置空间的MSIX capability 域中,偏移量为2的位置展示MSIX_FLAGS信息,长度为2个字节。
=> (pread(fd, &table, sizeof(table),
              vdev->config_offset + pos + PCI_MSIX_TABLE) // 在设备配置空间的MSIX capability 域中,偏移量为4的位置展示MSIX table地址,长度为4个字节。
=> pread(fd, &pba, sizeof(pba),
              vdev->config_offset + pos + PCI_MSIX_PBA) // 在设备配置空间的MSIX capability 域中,偏移量为8的位置展示正在等待处理的中断Pending Bits,长度为4个字节。

=> /* 将上面的ctrl,table,pba从PCI地址形式转换为CPU可识别的地址形式  */
    ctrl = le16_to_cpu(ctrl);
    table = le32_to_cpu(table);
    pba = le32_to_cpu(pba);

=> /* 然后就是通过对msix结构体及其子域赋值,包括为msix table分配一个BAR空间,为msix的pba分配一个BAR空间。*/
    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_pci_fixup_msix_region // 修复msix region
=> vfio_pci_relocate_msix // 重新分配msix

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[*]”,最后的方括号+星号会在后面的处理中作为待插入属性的标志被检测到。

vfio_bar_register
=> bar->mr = g_new0(MemoryRegion, 1);
   name = g_strdup_printf("%s base BAR %d", vdev->vbasedev.name, nr);
=> (bar->mr, OBJECT(vdev), NULL, NULL, name, bar->size);
   => memory_region_init(mr, owner, name, size)
      |=> object_initialize(mr, sizeof(*mr), TYPE_MEMORY_REGION) // 利用QOM的机制完成对类型为TYPE_MEMORY_REGION的对象结构的初始化
         => type_get_by_name // 在type哈希表中寻找键值为TYPE_MEMORY_REGION的TypeImpl(这里指MemoryRegion的typeImpl)
         => object_initialize_with_type // 利用上面得到的typeImpl,初始化一个object(这里指bar->mr,也就是一个MemoryRegion Object),建立该object的属性哈希表,
            => type_initialize // 由于MemoryRegion本身已经有了Obejct(因为MemoryRegion TypeImpl肯定在qemu的memory初始化阶段就已经初始化了Object),所以该函数不执行,直接返回。
            => object_class_property_init_all // 初始化object类中的所有属性,即调用属性的对应.init函数完成属性初始化,但是bar->mr中的prop均没有init方法,所以该函数没有作用,只是走个流程。
            => object_init_with_type // 调用object(bar->mr)的instance_init(MemoroyRegion对应的instance_init函数为memory_region_initfn)函数进行实例初始化操作。
       				   => memory_region_initfn // MemoryRegion对象的实例化函数
            => object_post_init_with_type // 因为MemoryRegion没有.post_init函数,因此不执行该函数,该函数本意是做一些初始化object之后的收尾操作。
      |=> memory_region_do_init // 将初始化完成的MemoryRegion添加到vdev的属性哈希表中,并使该MemoryRegion的父类指向vdev.
         => object_property_add_child
            => object_property_add // 向vdev添加属性,名为"0000:00:1f.6 base BAR 0[0]",类型为"child<qemu:memory-region>",get方法为object_get_child_property函数,release方法为object_finalize_child_property,obejct就是这里的vdev,该属性的resove方法为object_resolve_child_property,即返回该MemoryRegioin.
            => child->parent = obj // 该child属性(MemoryRegion)的父类为vdev。

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 => memory_region_init_io => memory_region_init => object_initialize=> object_initialize_with_type => object_init_with_type => memory_region_initfn

在vfio_bar_register中的第一步就是构造一个memoryRegion,该MemoryRegion的实例化函数为memory_region_initfn。总体来说就是初始化该MemoryRegion的读写方法,给该MemoryRegion添加一些属性,分别为: container,addr,priority,size. 下面详细来看。

memory_region_initfn
=> /* 为该memory region的一些field赋值 */
   mr->ops = &unassigned_mem_ops; // 对该region的操作(读写)
		mr->enabled = true; // 启用标志
		mr->romd_mode = true; // 当memory region是一个rom device时,romd模式下,guest可以直接读取该memory region中的内容而无需调用该region注册的.read函数,但guest写该memory region的时候需要调用.write函数。
		mr->global_locking = true; // qemu全局锁,持有该锁时访问该memory region,那么读取到的内容和cache中的内容的一致性由qemu维护。
		mr->destructor = memory_region_destructor_none; // 该region的析构函数是一个空函数。
=> /* 维护2个该memory region的链表 */
   QTAILQ_INIT(&mr->subregions); // 子region链表
		QTAILQ_INIT(&mr->coalesced);  // 组成该region的memory range的链表
=> object_property_add // 向memory obejct添加一个名为container的属性,类型为TYPE_MEMORY_REGION,get方法为memory_region_get_container.其余如set.release.opaqua都为空。
=> op->resolve = memory_region_resolve_container // 当调用memory obejct的container属性的resolve方法时,应该返回OBEJCT(mr->container)的值,也就是container抽象对象的值。
=> /* 继续向memory object添加属性 */
   // 1. 属性名addr,类型为mr->addr(也就是64bit地址类型),get方法为OBJ_PROP_FLAG_READ
   // 2. 属性名priority,类型为32bit无符号整数类型,get方法为memory_region_get_priority函数
   // 3. 属性名为size, 类型为64bit无符号整数类型,get方法为memory_region_get_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->mr, 0, bar->region.mem)
=> memory_region_add_subregion_common
   => subregion->container = mr;
      subregion->addr = offset;
      memory_region_update_container_subregions(subregion)
      => memory_region_transaction_begin
         // 向mr->subregions链表中添加该subregion(即向bar->mr.subregions链表中添加bar->region.mem)
         memory_region_transaction_commit
  

传入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
memory_region_transaction_begin
=> qemu_flush_coalesced_mmio_buffer
   => // 如果使用kvm,调用kvm_flush_coalesced_mmio_buffer
      => // KVM 中对某些 MMIO 做了 batch 优化:KVM 遇到 MMIO ⽽ VMEXIT 时,将MMIO 操作记录到 kvm_coalesced_mmio 结构中,然后塞到kvm_coalesced_mmio_ring 中,不退出到 QEMU 。直到某⼀次退回到 QEMU ,要更新内存空间之前的那⼀刻,把 kvm_coalesced_mmio_ring 中的 kvm_coalesced_mmio取出来做⼀遍,保证内存的⼀致性。这事就是 kvm_flush_coalesced_mmio_buffer ⼲的
=> ++memory_region_transaction_depth

也就是说,在qemu使用kvm时,memory_region_transaction_begin会将coalesced_mmio_ring缓冲区中的mmio操作写到实际物理地址上(gva),而且不论是否使用kvm,都会增加内存操作计数器memory_region_transaction_depth的值。

  • memory_region_transaction_commit
memory_region_transaction_commit
=> --memory_region_transaction_depth
=> // 如果memory_region_transaction_depth为0且memory_region_update_pending大于0
   => MEMORY_LISTENER_CALL_GLOBAL // 从前向后调用全局列表memory_listeners中所有的listener的begin方法
   => // 对qemu全局地址变量address_spaces中的每一个地址空间,调用address_space_set_flatview和address_space_update_ioeventfds更新地址空间结构和ioeventfds的内容
   => MEMORY_LISTENER_CALL_GLOBAL // 从后向前调用全局列表memory_listeners中所有listener的commit方法

memory_region_transaction_commit使用所有listener更新地址空间的结构,以确保对地址空间的修改能够立即生效。

接下来看memory_region_update_container_subregions的主体内容。

memory_region_ref(subregion);
QTAILQ_FOREACH(other, &mr->subregions, subregions_link) {
    if (subregion->priority >= other->priority) {
        QTAILQ_INSERT_BEFORE(other, subregion, subregions_link);
        goto done;
    }
}
QTAILQ_INSERT_TAIL(&mr->subregions, subregion, subregions_link);
done:
memory_region_update_pending |= mr->enabled && subregion->enabled;

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是可读可写的,因此映射到内存中也应该是可读可写的)。

// 确认应该映射到什么权限的页上
prot |= region->flags & VFIO_REGION_INFO_FLAG_READ ? PROT_READ : 0;
prot |= region->flags & VFIO_REGION_INFO_FLAG_WRITE ? PROT_WRITE : 0;

然后对刚刚添加到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本身。

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将刚才mmap得到的QEMU的内存空间(后端为一个MemoryRegion)加入到QEMU为Guest提供的ram空间链表中。详细情况如下:(下面用region代替之前添加到bar->mr中的subregion)

memory_region_init_ram_device_ptr
=> memory_region_init // 实例化region->mmaps.mem(也是MemoryRegion类型),将该MemoryRegion也设置为vdev的child属性,属性名为:"0000:00:1f.6 BAR 0 mmaps[0]",大小与BAR0的大小相同
=> mr->ram = true; // 表示该MemoryRegion是一个RAM区域
   mr->terminates = true; // 标志MemoryRegion是否是叶子节点(这个虚拟机的内存是一个MemoryRegion的图状结构,由MemoryRegion层层填充)
		mr->ram_device = true; // 该MemoryRegion是否是一个ram device,所谓ram device,即代表对物理设备的一个映射,如对PCI BAR的映射就是一个PCI BAR ram device。
   mr->destructor = memory_region_destructor_ram; // 该MemoryRegion的析构函数
   mr->dirty_log_mask = tcg_enabled() ? (1 << DIRTY_MEMORY_CODE) : 0; // 与tcg相关,使用kvm就不会使用tcg。
=>qemu_ram_alloc_from_ptr // 将region->mmaps.mem添加到为Guest提供的ram_list中

这样全局RAM空间链表中就多了一块与vdev->bar相关的ram空间,之后继续调用

memory_region_add_subregion(region->mem, region->mmaps[i].offset,
                            &region->mmaps[i].mem);

将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。

void pci_register_bar(PCIDevice *pci_dev, int region_num,
                      uint8_t type, MemoryRegion *memory)

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()的参数。

pci_dev    = (PCIDevice *) 0x555557a8d360
nr         = 0
type       = 0
memory     = (MemoryRegion *) 0x555557a943a0

然后看看在vfio_bar_register中,具体传入pci_register_bar()的内容:

pci_register_bar(&vdev->pdev, nr, bar->type, bar->mr);
=> pcibus_t size = memory_region_size(memory);
=> r = &pci_dev->io_regions[region_num]

在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]的地址。

r->addr = PCI_BAR_UNMAPPED

将该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).

r->addr = PCI_BAR_UNMAPPED;
r->size = size;
r->type = type;
r->memory = memory;
r->address_space = type & PCI_BASE_ADDRESS_SPACE_IO
    ? pci_get_bus(pci_dev)->address_space_io
    : pci_get_bus(pci_dev)->address_space_mem;

如果本次需要注册的IO_Region是一个ROM,那么表明该PCI设备允许自己的expansion ROM被访问,所以将wmask的bit0置1,wmask是什么东西?wmask,用于实现PCI设备的R/W标志。其取值为size-1取反的结果。

wmask = ~(size - 1);
if (region_num == PCI_ROM_SLOT) {
    /* ROM enable bit is writable */
    wmask |= PCI_ROM_ADDRESS_ENABLE;
}

获取pci设备中,传入的region在PCI config space中的相对地址,在获取region的相对地址时对ROM region区别对待,因为ROM region的位置和普通的BAR region的位置不一样,而且根据该PCI设备是否是一个pci桥,ROM region的位置也不一样。

addr = pci_bar(pci_dev, region_num); 

将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,即:

pdev->config[PCI_STATUS] & PCI_STATUS_CAP_LIST != 0

而要添加new capability,位于配置空间0x34处的功能链表头页不能为空,即:

pdev->config[PCI_CAPABILITY_LIST] != 0

vfio_add_capabilities函数首先进行了以上描述的条件检测,如果通过,才会进行下一步。

vfio_add_std_cap

该函数利用了递归方法,对new capability链表中的所有功能进行设置。

vfio_add_std_cap
=> // 获取new capability链表中的下一个功能,并对下一个功能调用vfio_add_std_cap。
=> // 如果下一个功能为空,则将物理设备的配置空间0x34位置置0,将vdev的模拟配置空间的0x34位置置为0xFF,将vdev的模拟配置空间的状态寄存器的bit4置为1.
=> // 调用vfio_add_virt_caps为透传的设备提供peer2peer特性,该函数是一个quirks函数,只对 NVIDIA GPUDirect P2P Vendor 有效。
=> // 根据从物理设备的new capability链表中读取到的当前功能id,即cap_id,进行对应的处理。qemu将这些id分为几类,分别为MSI功能,PCIE功能,MSIX功能,PCI电源管理接口功能,PCI高级features功能。

对于不同的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。)

case PCI_CAP_ID_AF:
=> vfio_check_af_flr // 通过检查配置空间中偏移量为0xE0+3的位置的bit0,bit1是否为1,如果为1,则证明该物理设备具有flr。
=> pci_add_capability

由于在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(pdev,cap_id,offset,size,errp)
=> // 首先检查offset是否为0,如果为0需要在PCI配置空间中找到size大小的capability结构,获得该结构的offset。这一步的目的是检查offset是否有效。
=> // 然后检查即将添加的capability结构是否在PCI配置空间中与现有的capability结构重叠,如果重叠则报错。
=> // 使pdev配置空间0x34处,也就是capability list的链表头指向新capability结构,并使新capability结构指向之前capability list的链表头指向的capability 结构。整个过程的结果是将新capability结构插入到了capability链表中的第一个位置上。
=> // 设置pdev->used+offset位置为全1,设置大小为size,表明配置空间中以offset为起始,大小size的范围,已经被使用。
=> // 设置pdev->wmask+offset的位置为全0,设置大小为size,表明配置空间中以offset为起始,大小为size的范围的内容为只读的。
=> // 设置pdev->cmask+offset的位置为全1,设置大小为size,表明配置空间中以offset为起始,大小为size的范围是一个capability结构,需要在载入时检查。
=> // 返回新添加的capability结构在PCI配置空间中的偏移量

直到了pci_add_capability的功能,关于PCI_CAP_ID_AF的具体处理也就清晰了,即首先检查pdev是否具有FLR功能,如果有,则向pdev的配置空间中插入AF capability单元。

MSI功能

如果设备支持MSI功能,那么设备就可以向处理器传送中断,传送方式是将一个预定义好的数据结构(message)写到预定义好的地址上去。

case PCI_CAP_ID_MSI:
=> vfio_msi_setup

即如果capability id为PCI_CAP_ID_MSI(0x5), 就直接调用vfio_msi_setup而不是pci_add_capability。

vfio_msi_setup
=> // 首先从MSI capability结构中的offset 2 位置读取2个字节,这里存储着该capability结构的Flags(也就是PCI spec中所说的Message Control for MSI),将Flags转换为cpu可以识别的形式,记为ctrl.
=> // 从Message Control中获取该capability结构提供的能力,查询bit7获得设备是否支持发送64bit message address;查询bit8获得设备是否支持MSI per-vector 掩码;查询bit3:1获得所请求的vector数量(bit3:1存放vector数量以2为底的幂次,即如果存0,那么请求的vector数量为1,如果存2,请求vector数量为4.)
=> msi_init

vfio_msi_setup在获得了MSI capability结构中关于:

  1. 是否具有发送64bit message address的能力
  2. 是否具有mask掉任一vector的能力
  3. 设备所需中断vector数量

之后,调用msi_init对该设备的MSI进行初始化。首先看看msi_init的函数原型和本次trace时,msi_init被传入的参数。

int msi_init(struct PCIDevice *dev, uint8_t offset,
             unsigned int nr_vectors, bool msi64bit,
             bool msi_per_vector_mask, Error **errp)

dev表示被配置MSI的PCI设备,offset是MSI capability结构在PCI配置空间中的偏移地址,nr_vectors是设备所需的中断vector数量,msi64bit是设备是否具有发送64bit message address的能力,msi_per_vector_mask是设备是否具有mask掉任一vector的能力,errp存储错误信息。

msi_init (dev=0x555557a8d360, offset=208(0xd0) '\320', nr_vectors=1, msi64bit=true,msi_per_vector_mask=false, errp=0x7fffffffcae0)

知道了msi_init的参数意义之后,开始详细查看具体代码:

msi_init
=> vectors_order = ctz32(nr_vectors); // 查看传入的nr_vectors后面有几个0,由于nr_vectors肯定是2的幂次方,假如nr_vector为16,那么其二进制数后面就有4个0,所以vector_order获得的是nr_vectors写为2^x时的x的值。
=> flags = vectors_order << ctz32(PCI_MSI_FLAGS_QMASK); // 将vector_order左移1个bit,感觉这一步是废操作,因为vector的可能取值为0,1,2,3,4,5,那么0-5左移1个bit为0,2,4,6,8,10.这会导致flags的bit3:1改变,如果nr_vectors为8,还会导致bit3:1为110而使flags变为reserved状态。
=>  if (msi64bit) { // 将是否支持64bit 消息地址发送信息放入flags中
        flags |= PCI_MSI_FLAGS_64BIT;
    }
    if (msi_per_vector_mask) { // 将是否支持mask掉任一vector放入flags中
        flags |= PCI_MSI_FLAGS_MASKBIT;
    }    
			cap_size = msi_cap_sizeof(flags); // 根据flags获取cap_size,如果支持64bit消息地址发送,那么cap_size取PCI_MSI_64M_SIZEOF,否则cap_size取PCI_MSI_32M_SIZEOF.
=> config_offset = pci_add_capability(dev, PCI_CAP_ID_MSI, offset,
                                        cap_size, errp);// 向capability链表中添加MSI capability structure。
=> dev->msi_cap = config_offset; // 将MSI capability structure在配置空间中的位置记录到dev->msi_cap中
=>  dev->cap_present |= QEMU_PCI_CAP_MSI; // dev->cap_present记录的是该设备的capabilities mask,将该设备支持MSI的标志添加到cap_present中去。
=> pci_set_word(dev->config + msi_flags_off(dev), flags); // 将MSI Flags(message control)记录到地址dev->config+dev->msi_cap+2的位置上
=> pci_set_word(dev->wmask + msi_flags_off(dev),
                 PCI_MSI_FLAGS_QSIZE | PCI_MSI_FLAGS_ENABLE); // 将dev->wmask + dev->msi_cap + 2的地址中的内容写为0x71,表示dev的配置空间中的MSI capability structures中message control结构中的bit6:4,bit0是可写的。
=> pci_set_long(dev->wmask + msi_address_lo_off(dev),
                 PCI_MSI_ADDRESS_LO_MASK); // 将dev->wmask + dev->msi_cap + 4的地址中的内容(4个字节)的最后2bit置为0,其余bit置为1,表示dev的配置空间中的MSI capability structures中Message Address(32bit)的最后2bit为只读的,其余bit为可写可读的。
=> if (msi64bit)
        pci_set_long(dev->wmask + msi_address_hi_off(dev), 0xffffffff); // f(dev),
                 PCI_MSI_ADDRESS_LO_MASK); // 将dev->wmask + dev->msi_cap + 8的地址中的内容(4个字节)全置为1,表示dev配置空间中的MSI capability structures中Message Upper Address的全部32bit都是可读可写的。
=> pci_set_word(dev->wmask + msi_data_off(dev, msi64bit), 0xffff); // 将dev->wmask + dev->msi_cap + 8(32bit地址空间)/12(64bit地址空间)的位置写全1,表示dev的配置空间中的MSI capability structures中Message Data的全部16bit都是可读可写的。
=>  if (msi_per_vector_mask) {
        /* Make mask bits 0 to nr_vectors - 1 writable. */
        pci_set_long(dev->wmask + msi_mask_off(dev, msi64bit),
                     0xffffffff >> (PCI_MSI_VECTORS_MAX - nr_vectors));
    } // 如果设备支持独立mask掉任意中断vector,那么就将dev->wmask + dev->msi_cap + 12(32bit地址空间)/16(64bit地址空间)的内容置为0xFFFF_FFFF - 32-nr_vectors,表示dev的配置空间中的MSI capability structures中Mask bits的bit0到bit(nr_vectors - 1)是可读可写的。
=> return 0;

所以来看,msi_init做了什么事情呢?

  1. 建立了一个由传入的nr_vectors,msi_per_vector_mask,msi64bit,offset信息拼凑成的MSI capability structure并插入到了dev的capability list中。
  2. 编辑了dev MSI capability structure中的相关bit的可读可写性,导致提供了以下能力:
    1. 软件可以编辑Message Control的bit6:4,确认需要分配的中断vector数量
    2. 软件可以编辑Message Control的bit0,即自由使能/禁止MSI
    3. 软件可以编辑Message Address的bit31:2(32bit)/bit63:2(64bit),即自由配置MSI 目标地址
    4. 软件可以编辑Message Data的全部16bit,即自由配置MSI数据
    5. 软件可以编辑Mask bits的bit0-bit(设备所需vector数量-1),即自由Mask掉vector.

回到vfio_msi_setup,函数的最后根据该MSI capability structure是否具有mask vector的能力以及MSI地址是32bit还是64bit,将vdev->msi_cap_size的内容填充正确的值。最后返回0.

vdev->msi_cap_size = 0xa + (msi_maskbit ? 0xa : 0) + (msi_64bit ? 0x4 : 0);
return 0;

最后总结一下添加PCI_CAP_ID_MSIX时所做的工作,即建立了一个MSI capability structure并插入到了设备的capability链表中。

PCI电源管理接口功能

这个功能单元提供了对PCI电源管理进行控制的标准接口。具体的操作为:

case PCI_CAP_ID_PM:
=>	vfio_check_pm_reset // 核心函数
=> vdev->pm_cap = pos; // 将电源管理capability structure在配置空间中的offset记录到vdev->pm_cap中
=> pci_add_capability // 将电源管理capability structure插入到capability链表的第一个

可见与电源管理最相关的处理就是vfio_check_pm_reset,详细看看。

先看函数原型,再看传入参数。

原型:

static void vfio_check_pm_reset(VFIOPCIDevice *vdev, uint8_t pos)

其中vdev就是传入该函数的需要被操作的设备,pos是电源管理capability structure在dev的配置空间中的位置。

传入参数:

vfio_check_pm_reset (vdev=0x555557a8d360, pos=200(0xc8) '\310')

接下来看看具体操作。

vfio_check_pm_reset
=> uint16_t csr = pci_get_word(vdev->pdev.config + pos + PCI_PM_CTRL); // 获取设备配置空间中电源管理capability structure + 4的地址中,2个字节的数据,记入csr,这个csr其实是PCI电源管理spec中所说的PMCSR(power management control/status register)
=>    if (!(csr & PCI_PM_CTRL_NO_SOFT_RESET)) {
        trace_vfio_check_pm_reset(vdev->vbasedev.name);
        vdev->has_pm_reset = true;} // 检查PMCSR的bit3是否为1,PMCSR的bit3被设置为1时,表明设备不会因为powerstate命令从D3转换到D1,而进行内部reset。从D3转换到D1状态之后,无需os的任何干预即可保留配置上下文(不然还要通过写入PowerState位来保留配置上下文)。如果PMCSR的bit3为0,则将vdev的has_pm_reset设置为true。

所以qemu对CAP_ID == 0x1的处理为:

  1. 根据设备PMCSR的bit3确定设备是否会在powerstate的命令下进行内部reset,并置vdev的相应filed(has_pm_reset)。
  2. 将电源管理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模拟的配置空间。

/* 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); // 如果物理设备支持MSIX, 就将emulated_config_bits空间内的MSI-X capability structure中的内容全部置1,之后该capability structure就会无效,并等待后续操作。
}

if (pdev->cap_present & QEMU_PCI_CAP_MSI) {
    memset(vdev->emulated_config_bits + pdev->msi_cap, 0xff,
           vdev->msi_cap_size);// MSI配置,类似于MSI-X
}

本次透传的网卡只支持MSI,在经历上面的代码之后,vdev的模拟配置空间中MSI capability structure中的内容为全1,此时模拟配置空间中的MSI capability为无效状态。

if (vfio_pci_read_config(&vdev->pdev, PCI_INTERRUPT_PIN, 1)) { 
    vdev->intx.mmap_timer = timer_new_ms(QEMU_CLOCK_VIRTUAL,
                                         vfio_intx_mmap_enable, vdev);
    pci_device_set_intx_routing_notifier(&vdev->pdev,
                                         vfio_intx_routing_notifier); 
    vdev->irqchip_change_notifier.notify = vfio_irqchip_change; 
    kvm_irqchip_add_change_notifier(&vdev->irqchip_change_notifier); 
    ret = vfio_intx_enable(vdev, errp);
    if (ret) {
        goto out_deregister;
    }
}

这个代码段含有特别多函数,逐一分析才能明白这段代码做了什么。

基础函数分析

vfio_pci_read_config

如果传入的读取的配置空间中的内容由qemu模拟,则读取保存在qemu中vdev->pdev的内容,如果不由qemu模拟,则调用pread读取物理设备配置空间中对应的内容。

函数原型:

uint32_t vfio_pci_read_config(PCIDevice *pdev, uint32_t addr, int len);

传入参数:

vfio_pci_read_config (pdev=0x555557a8d360, addr=61(0x3d), len=1)

具体分析:

uint32_t vfio_pci_read_config(PCIDevice *pdev, uint32_t addr, int len)
{
    VFIOPCIDevice *vdev = PCI_VFIO(pdev);
    uint32_t emu_bits = 0, emu_val = 0, phys_val = 0, val;

    memcpy(&emu_bits, vdev->emulated_config_bits + addr, len);
    emu_bits = le32_to_cpu(emu_bits);

    if (emu_bits) {
        emu_val = pci_default_read_config(pdev, addr, len);
    }

    if (~emu_bits & (0xffffffffU >> (32 - len * 8))) {
        ssize_t ret;

        ret = pread(vdev->vbasedev.fd, &phys_val, len,
                    vdev->config_offset + addr);
        if (ret != len) {
            error_report("%s(%s, 0x%x, 0x%x) failed: %m",
                         __func__, vdev->vbasedev.name, addr, len);
            return -errno;
        }
        phys_val = le32_to_cpu(phys_val);
    }

    val = (emu_val & emu_bits) | (phys_val & ~emu_bits);

    trace_vfio_pci_read_config(vdev->vbasedev.name, addr, len, val);

    return val;
}

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级别的时钟。

/**
 * timer_new_ms:
 * @type: 计时器关联的时钟类型
 * @cb: 当该计时器计时到0时,需要调用的回调函数
 * @opaque: 需要传递给回调函数的opaque指针
 *
 * 创建一个新的ms级别的计时器,该计时器基于type提供的时钟运行。
 *
 * Returns: 新创建的计时器的指针
 */
static inline QEMUTimer *timer_new_ms(QEMUClockType type, QEMUTimerCB *cb,
                                      void *opaque)
{
    return timer_new(type, SCALE_MS, cb, opaque);
}

vfio_intx_mmap_enable

在源文件中,vfio_intx_mmap_enable有一段英文注释,大意为,不用BAR mmap会导致虚拟机性能降低,但是在INTx中断附近关闭BAR mmap也会导致巨大的负载,因此设计了一种方式,在INTx中断被服务之后的某个时间点,使能BAR mmap。这样既能利用BAR mmap的高性能,又能在INTx中断期间关闭BAR mmap。

static void vfio_intx_mmap_enable(void *opaque)
{
    VFIOPCIDevice *vdev = opaque;

    if (vdev->intx.pending) {
        timer_mod(vdev->intx.mmap_timer,
                  qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) + vdev->intx.mmap_timeout);
        return;
    }

    vfio_mmap_set_enabled(vdev, true);
}

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

void pci_device_set_intx_routing_notifier(PCIDevice *dev,
                                          PCIINTxRoutingNotifier notifier)
{
    dev->intx_routing_notifier = notifier;
}

该函数只是简单的将传入设备dev的intx_routing_notifier(路由通知函数)设置为传入的notifier。

vfio_intx_routing_notifier

将INTx的具体管脚映射到IRQ,并进行qemu-kvm全局范围内的INTx中断映射的更新。

static void vfio_intx_routing_notifier(PCIDevice *pdev)
{
    VFIOPCIDevice *vdev = PCI_VFIO(pdev);
    PCIINTxRoute route;

    if (vdev->interrupt != VFIO_INT_INTx) {
        return;
    }

    route = pci_device_route_intx_to_irq(&vdev->pdev, vdev->intx.pin);

    if (pci_intx_route_changed(&vdev->intx.route, &route)) {
        vfio_intx_update(vdev, &route);
    }
}

在vfio_intx_routing_notifier中,首先检查vdev的当前中断类型是否为INTx,如果不是,那么该函数就没有继续执行的意义了。

然后调用pci_device_route_intx_to_irq将vdev->intx.pin,即INTERRUPT_PIN上面产生的中断路由到irq上。

PCIINTxRoute pci_device_route_intx_to_irq(PCIDevice *dev, int pin)
{
    PCIBus *bus;

    do {
        bus = pci_get_bus(dev); // 获取dev所属bus
        pin = bus->map_irq(dev, pin); // 将pin分配给dev
        dev = bus->parent_dev;
    } while (dev);

    if (!bus->route_intx_to_irq) {
        error_report("PCI: Bug - unimplemented PCI INTx routing (%s)",
                     object_get_typename(OBJECT(bus->qbus.parent)));
        return (PCIINTxRoute) { PCI_INTX_DISABLED, -1 };
    }

    return bus->route_intx_to_irq(bus->irq_opaque, pin);
}

在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的中断机制。

  1. 利用qemu启动虚拟机时,在不传入kernel-irqchip参数的情况下,该参数默认=on,也就是kvm负责全部的IOAPIC、PIC、LAPIC的模拟。

  2. 在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创建模拟中断芯片。

  3. 在kvm中,收到创建模拟芯片的请求:

    case KVM_CREATE_IRQCHIP: {
        if (irqchip_in_kernel(kvm)) // 检查kvm中是否已经存在模拟中断芯片
        if (kvm->created_vcpus) // 检查kvm中是否已经创建了vcpu
        // 如果上面两项检查有一项结果为是,则返回-17,意为IRQCHIP已经存在
        kvm_pic_init // 创建PIC芯片
        kvm_ioapic_init // 创建IOAPIC芯片
        kvm_setup_default_irq_routing // 设置中断路由
        
        vm->arch.irqchip_mode = KVM_IRQCHIP_KERNEL // 最后设置特定flag,避免下次收到KVM_CREATE_IRQCHIP的ioctl又重新创建模拟中断芯片
    }
    
    • 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_routing_entry(irq)
      {
          .gsi = irq, // 中断
          .type = KVM_IRQ_ROUTING_IRQCHIP, // routing类型
          .u.irqchip = { // 中断芯片信息
              .irqchip = KVM_IRQCHIP_IOAPIC, // 中断芯片类型
              .pin = (irq) // 中断芯片管脚
          }
      }
      

      可以看到,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
=> vfio_intx_disable_kvm // 利用irqfd的ioctl通知kvm停止监听INTx的irqfd
=> vdev->intx.route = *route; // 修改vdev的irq映射结构
=> vfio_intx_enable_kvm // 利用irqfd的ioctl重采样INTx中断

即vfio_intx_update在qemu和kvm的整个范围内更新了INTx中断映射。

中断相关处理

中断处理涉及ioeventfd和irqfd,整个机制比较复杂,等到弄清设备内存相关知识之后再系统来看。

在了解了一些基础处理函数之后,回头再看这部分代码。

if (vfio_pci_read_config(&vdev->pdev, PCI_INTERRUPT_PIN, 1)) { 
    vdev->intx.mmap_timer = timer_new_ms(QEMU_CLOCK_VIRTUAL,
                                         vfio_intx_mmap_enable, vdev);
    pci_device_set_intx_routing_notifier(&vdev->pdev,
                                         vfio_intx_routing_notifier);
    vdev->irqchip_change_notifier.notify = vfio_irqchip_change; 
    kvm_irqchip_add_change_notifier(&vdev->irqchip_change_notifier);
    ret = vfio_intx_enable(vdev, errp);
    if (ret) {
        goto out_deregister;
    }
}

vfio_pci_read_config读取了配置空间中0x3d也就是PCI Spec中所说的Interrupt Pin的内容,只有在当前设备支持INTx中断时,该field才为正数。

  1. 基于QEMU_CLOCK_VIRTUAL设置了一个计时器,用于在该计时器计时到0,并且没有中断正在等待的情况下,才使能BAR mmap。
  2. 设置了vdev->dev.intx_routing_notifier,该notifier能将INTx的具体管脚映射到IRQ,并进行qemu-kvm全局范围内的INTx中断映射的更新。
  3. 设置了vdev->irqchip_change_notifier.notify,该notifier能进行qemu-kvm全局范围内的INTx中断映射的更新。
  4. 将3中设置的notifier注册到kvm中
  5. 最后使能vfio中的INTx

结尾

最后就是一些基于特定设备的收尾处理,以及错误、请求notifer的注册。

这部分不关键,暂时不trace了。

posted @ 2021-02-24 12:53  EwanHai  阅读(1600)  评论(3编辑  收藏  举报