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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM