QEMU-KVM中的VFIO MSI機制


QEMU-KVM中的VFIO-MSI機制

當Guest的bios-kernel通過寫入vfio-device的配置空間,來配置msi、msi-x時(也就是向msi/msi-x的capability structure中寫入msi/msi-x enable bit),就會調用提前注冊好的處理函數,即vfio_pci_write_config

void vfio_pci_write_config(PCIDevice *pdev,
                           uint32_t addr, uint32_t val, int len)

其中pdev是vfio-device設備,addr是待寫入的設備地址,val是待寫入的數據,len是數據長度(bytes).

如果填入該函數中的數據為能夠正確配置MSI、MSI-X功能的數據,那么就會調用vfio_msi_enablevfio_msix_enable.

MSI初始化

vfio_msi_enable的主要代碼路徑如下:

staic void vfio_msi_enable(vdev)
{
    // 獲取PCIDevice的msi-capability中設置的"可分配的vector數量"
    vdev->nr_vectors = msi_nr_vectors_allocated(&vdev->pdev);
    
    // 分配vectors所需占用的空間
    vdev->msi_vectors = g_new0(VFIOMSIVector, vdev->nr_vectors);
    
    for (i = 0; i < vdev->nr_vectors; i++) { // 遍歷該設備上所有的msi vector
        vector = &vdev->msi_vectors[i];
        vector->virq = -1;
        vector->use = true;
        
        // 初始化vector->interrupt對應的eventfd,但不激活
        event_notifier_init(&vector->interrupt, 0)
        
        /*注冊該eventfd對應的讀callback,當該eventfd可讀時,觸發vfio_msi_interrupt.
         * vfio_msi_interrupt中,首先獲取msi消息內容,然后通過寫地址的方法發送msi中斷
         */
        qemu_set_fd_handler(event_notifier_get_fd(&vector->interrupt),
                            vfio_msi_interrupt, NULL, vector);
        
------------------------以上為qemu中的eventfd機制,即eventfd toggle觸發vfio_msi_interrupt-------------        
    /* 1. 向kvm添加vector對應的msi的irq_routing_table的entry。
     * 2. 將eventfd與irq關聯,並在kvm中注冊eventfd的poll,invoke函數。
     * 3. 如果支持PI,還會注冊一個consumer,consumer.add_producer函數會
     *    更新producer->host_irq對應的IOMMU->IRTE
 			*/
    vfio_add_kvm_msi_virq(vdev, vector, i, false); 
--------------------------以上為向kvm中關聯qemu的eventfd,並與對應的處理函數綁定過程-----------------------
    }
    /* 1. 構造irq_set,包含了設備的irq數量,irq類型,irq對應的eventfd的fd集合等。
     *    然后ioctl(VFIO_DEVICE_SET_IRQS)
     * 2. 為vdev分配irq數量的中斷,並將這些中斷類型確定為msi或msix.
     * 3. 為傳入的所有fds(eventfd)注冊trigger函數(vfio_msihandler),以及注冊producer.
     *    producer的注冊會導致與前面注冊的consumer連接。
     *    vfio_msihandler的調用會toggle eventfd,進而引起producer狀態變化,最終導致consumer狀態變化。
     */
    vfio_enable_vectors(vdev, false); 
    
}

以下是vfio_msi_enable函數中相關函數的框架分析。

vfio_add_kvm_msi_virq(VFIOPCIDevice *vdev, VFIOMSIVector *vector,
                                  int vector_n, bool msix)
{
    // 初始化vector->kvm_interrupt對應eventfd,但不激活
    event_notifier_init(&vector->kvm_interrupt, 0)
    
    // 向kvm的irq_routing_table中為該vector添加一個entry
    virq = kvm_irqchip_add_msi_route(kvm_state, vector_n, &vdev->pdev);
    
    // 
    kvm_irqchip_add_irqfd_notifier_gsi(kvm_state, &vector->kvm_interrupt,
                                       NULL, virq)
    
}

 kvm_irqchip_add_msi_route(KVMState *s, int vector, PCIDevice *dev)
 {
     // 獲取msi消息內容
     msg = pci_get_msi_message(dev, vector);
     // 找到kvm的gsi_bitmap中最低的尚未使用的bit 即找到一個尚未使用的最低irq index 
     virq = kvm_irqchip_get_virq(s);
     
     {
         // 一段構造一個routing_entry(kroute)的代碼
     }
     kvm_add_routing_entry(s, &kroute);
     kvm_arch_add_msi_route_post(&kroute, vector, dev); // 通知更新了msi entry
     kvm_irqchip_commit_routes(s); // 調用ioctl(KVM_SET_GSI_ROUTING)
     
     return virq;
 }

int kvm_irqchip_add_irqfd_notifier_gsi(KVMState *s, EventNotifier *n,
                                       EventNotifier *rn, int virq)
{
    return kvm_irqchip_assign_irqfd(kvm_state, &vector->kvm_interrupt, NULL, virq, true);
}

kvm_irqchip_assign_irqfd(KVMState *s, EventNotifier *event,
                                    EventNotifier *resample, int virq,
                                    bool assign)
{
    fd = fd_of(event);
    rfd = -1;
    
    struct kvm_irqfd irqfd = {
        .fd = fd,
        .gsi = virq,
        .flags = 0,
    };
    
    kvm_vm_ioctl(s, KVM_IRQFD, &irqfd);
}

// kvm中
kvm_irqfd(kvm_irqfd args)
{
    return kvm_irqfd_assign(kvm, 0);
}
kvm_irqfd_assign(struct kvm *kvm, struct kvm_irqfd *args)
{
    irqfd->gsi = args->gsi;
    INIT_WORK(&irqfd->inject, irqfd_inject); // 初始化irq對應的callabck-irqfd_inject
    f = fdget(args->fd);
    eventfd = eventfd_ctx_fileget(f.file);
    
    irqfd->eventfd = eventfd;
    // 注冊監視eventfd變動的被動通知函數
    init_waitqueue_func_entry(&irqfd->wait, irqfd_wakeup);
    // 注冊主動poll該eventfd的查詢函數
    init_poll_funcptr(&irqfd->pt, irqfd_ptable_queue_proc);
    // 更新系統中的irqfd
    irqfd_update(kvm, irqfd);

    /* 注冊一個irq的consumer. 調用該consumer的.add_producer成員會導致更新irq對應的host_irq的IOMMU-IRTE */
    #ifdef CONFIG_HAVE_KVM_IRQ_BYPASS
    if (kvm_arch_has_irq_bypass()) {
        irqfd->consumer.token = (void *)irqfd->eventfd;
        irqfd->consumer.add_producer = kvm_arch_irq_bypass_add_producer;
        irqfd->consumer.del_producer = kvm_arch_irq_bypass_del_producer;
        irqfd->consumer.stop = kvm_arch_irq_bypass_stop;
        irqfd->consumer.start = kvm_arch_irq_bypass_start;
        ret = irq_bypass_register_consumer(&irqfd->consumer);
        if (ret)
            pr_info("irq bypass consumer (token %p) registration fails: %d\n",
                    irqfd->consumer.token, ret);
    }
    #endif
}

vfio_enable_vectors
=> // 構造irq_set,包含了設備的irq數量,irq類型,irq對應的eventfd的fd結合等
=> ret = ioctl(vdev->vbasedev.fd, VFIO_DEVICE_SET_IRQS, irq_set);
		=> vfio_pci_set_irqs_ioctl
      => vfio_pci_set_msi_trigger 
         => vfio_msi_enable // 為vdev分配irq數量的中斷,並將這些中斷類型確定為msi或msix
         => vfio_msi_set_block // 為傳入的所有fds(eventfd)注冊trigger函數(vfio_msihandler),以及producer
            												 // vfio_msihandler的調用會toggle eventfd,進而引起producer狀態變化

MSI-X初始化

vfio_msi_enable在Guest寫MSI-X Capability Structure的Enable Bit時觸發。

vfio_msix_enbale函數的主要代碼如下:

/* 對0號vector進行enable並迅速釋放vector0,這樣
 * 就會讓vfio設備處於沒有vector enabled的狀態,但
 * MSI-X capability 被使能,這種狀態是Guest驅動需
 * 要的狀態.
 */
static void vfio_msix_enable(VFIOPCIDevice *vdev)
{
    /* 分配msix的vectors所需的空間 */
    vdev->msi_vectors = g_new0(VFIOMSIVector, vdev->msix->entries);

    vfio_msix_vector_do_use(&vdev->pdev, 0, NULL, NULL);
    
    vfio_msix_vector_release(&vdev->pdev, 0);
    
    msix_set_vector_notifiers(&vdev->pdev, vfio_msix_vector_use,
                                  vfio_msix_vector_release, NULL);
}
/* 由於傳入的vector號碼為0,msg和handler均為NULL,所以該函數的功能實際變為:
 * 1. 為vector->interrupt注冊eventfd
 * 2. 為vector0對應的eventfd注冊trigger函數(vfio_msihandler)以及producer
 * 目的就是使VFIO設備的MSIX的Enable bit為1.
 */
static int vfio_msix_vector_do_use(PCIDevice *pdev, unsigned int nr=0,
                                   MSIMessage *msg=NULL, IOHandler *handler=NULL)
{
    vector = &vdev->msi_vectors[nr];// 獲取msi_vector的地址
    
    if (!vector->use) { // 第一次進入該函數時,該vector->use肯定為False
        vector->vdev = vdev;
        vector->virq = -1;
        if (event_notifier_init(&vector->interrupt, 0)) { // 初始化vector對應的notifier(fd)(qemu)
            error_report("vfio: Error: vfio_enable_vectorsevent_notifier_init failed");
        }
        vector->use = true;
        msix_vector_use(pdev, nr); // vdev->msix_entry_used[nr]++
    }
    /* 將vector->interrupt對應的eventfd的讀觸發handler設備handler,寫觸發handler設置為0
     * 事實上傳入的handler為0,所以該eventfd沒有讀/寫觸發handler。
     */
    qemu_set_fd_handler(event_notifier_get_fd(&vector->interrupt),
                        handler, NULL, vector);
    ----------------------------------vfio_enable_vectors()的內容----------------------------
    
    /* 1. 構造irq_set,包含了設備的irq數量=1,irq類型(msix),irq的flags等。
     *    然后ioctl(VFIO_DEVICE_SET_IRQS),在ioctl中:
     * 2. 為vdev分配irq數量的中斷,並將這些中斷類型確定為msix.
     * 3. 為傳入的所有fd(eventfd)注冊trigger函數(vfio_msihandler),以及注冊producer.
     *    vfio_msihandler的調用會toggle eventfd.
     */
    ret = vfio_enable_vectors(vdev, true);
        
    /* 禁用PBA模擬 */
    clear_bit(nr, vdev->msix->penmsix_msgding);
    if (find_first_bit(vdev->msix->pending,
                       vdev->nr_vectors) == vdev->nr_vectors) {
        memory_region_set_enabled(&vdev->pdev.msix_pba_mmio, false);
        trace_vfio_msix_pba_disable(vdev->vbasedev.name);
    }
}

/* 由於0號vector在前面只初始化了qemu的eventfd而不是內核的irqfd,所以在這個函數里什么也不用做其實 */
static void vfio_msix_vector_release(PCIDevice *pdev, unsigned int nr)
{
    vector = &vdev->msi_vectors[nr];
}

/* 將設備的所有msix_vector對應的use_noifier,release_notifier,poll_notifier都進行記錄 ,方便之后調用 */
int msix_set_vector_notifiers(PCIDevice *dev= vdev->pdev,
                              MSIVectorUseNotifier use_notifier=vfio_msix_vector_use,
                              MSIVectorReleaseNotifier release_notifier=vfio_msix_vector_release,
                              MSIVectorPollNotifier poll_notifier=NULL)
{
    dev->msix_vector_use_notifier = use_notifier;
    dev->msix_vector_release_notifier = release_notifier;
    dev->msix_vector_poll_notifier = poll_notifier;
    
    return 0;
}

以上,MSI-X中斷的功能被Enable,Message Control的最高bit置了1.

接下來是當Guest對MSI-X Table中的Vector-msg地址-msg數據進行寫入時,具體的處理情況。

由於MSI-X Table存儲在某個BAR中,而BAR又分為可mmap的BAR和不可mmap的BAR,現在一般都是用可mmap的BAR作為MSI-X Table的存儲BAR。

Guest配置MSIX Table:
=> 由於MMIO Exit而退到QEMU
=> address_space_rw
   => address_space_write
      => flatview_write
         ...(一系列的調用)

之后調用到:

// 通過下面這些調用,會對某vector對應的notifier進行調用,即vfio_msix_vector_do_use
// 目的是對某vector對應的中斷鏈路進行一次初始化
=> msix_table_mmio_write
   => msix_handle_mask_update
      => msix_fire_vector_notifier
         => vfio_msix_vector_use
            => vfio_msix_vector_do_use
/* POST STATE: msix-msg已經從MSIX-Table中提取了出來,nr對應vector號碼,handler為vfio_msi_interrupt
 * 								 vfio_msi_interrupt會被注冊為該vector對應的處理函數,當該vector對應的eventfd toggle時,
 *             就會發送一個MSI message
 */
static int vfio_msix_vector_do_use(PCIDevice *pdev, unsigned int nr,
                                   MSIMessage *msg, IOHandler *handler=vfio_msi_interrupt)
{
    if (!vector->use) { // 如果是vector0,那么在之前的第一次調用該函數過程中已經初始化了對應eventfd,use為1
        										 // 其它vector需要初始化一個eventfd
        vector->vdev = vdev;
        vector->virq = -1;
        if (event_notifier_init(&vector->interrupt, 0)) { // 初始化vector對應的notifier(fd)
            error_report("vfio: Error: event_notifier_init failed");
        }
        vector->use = true;
        msix_vector_use(pdev, nr);
    }
    // 設置用戶空間(qemu)的eventfd對應的handler
    qemu_set_fd_handler(event_notifier_get_fd(&vector->interrupt),
                        handler, NULL, vector);
    
    /*
     * Attempt to enable route through KVM irqchip,
     * default to userspace handling if unavailable.
     */
    if (vector->virq >= 0) { // 因為是初始化階段,所有vector都沒有申請kernel的eventfd,因此virq肯定為-1
        if (!msg) {
            vfio_remove_kvm_msi_virq(vector);
        } else {
            vfio_update_kvm_msi_virq(vector, *msg, pdev);
        }
    } else {
        if (msg) { // 這次msg不為NULL,因此調用vfio_add_kvm_msi_virq
            /* 
 							  * 1. 向kvm添加vector對應的msi的irq_routing_table的entry。
             * 2. 將eventfd與irq關聯,並在kvm中注冊eventfd的poll,invoke函數。
             * 3. 如果支持PI,還會注冊一個consumer,consumer.add_producer函數會
             *    更新producer->host_irq對應的IOMMU->IRTE
             */
            vfio_add_kvm_msi_virq(vdev, vector, nr, true);
        }
    }
    
    /* 由於在之前的Enable-MSIX時,已經將vdev->nr_vectors+1了,因此這里if條件不滿足*/
    if (vdev->nr_vectors < nr + 1) { // *  Enable-MSIX時,這里的nr=0,vdev->nr_vectors=0
        vfio_disable_irqindex(&vdev->vbasedev, VFIO_PCI_MSIX_IRQ_INDEX); // * 禁用INTx
        vdev->nr_vectors = nr + 1;
        ret = vfio_enable_vectors(vdev, true);
        if (ret) {
            error_report("vfio: failed to enable vectors, %d", ret);
        }
    }else {
        Error *err = NULL;
        int32_t fd;

        if (vector->virq >= 0) { // vfio_add_kvm_msi_virq中將vector->virq置為了正值
            fd = event_notifier_get_fd(&vector->kvm_interrupt); // 獲取vector對應的kernel的eventfd
        } else {
            fd = event_notifier_get_fd(&vector->interrupt);
        }
					
        /* 1. 構造irq_set,包含了設備的irq數量=1,irq類型(msix),irq的flags等。
         *    然后ioctl(VFIO_DEVICE_SET_IRQS),在ioctl中:
         * 2. 為vdev分配irq數量的中斷,並將這些中斷類型確定為msix.
         * 3. 為傳入的所有fd(eventfd)注冊對應的Host IRQ,trigger函數(vfio_msihandler),以及producer.
         *    vfio_msihandler的調用會toggle eventfd.
         */
        vfio_set_irq_signaling(&vdev->vbasedev,
                               VFIO_PCI_MSIX_IRQ_INDEX, nr,
                               VFIO_IRQ_SET_ACTION_TRIGGER, fd, &err)
         
}

總結

在Guest配置MSI/MSI-X功能時,需要對設備的配置空間進行寫操作,而VFIO設備在初始化時,就注冊了Guest寫操作對應的callback,即vfio_pci_write_config

該函數中,對於MSI/MSI-X的大致配置路徑如下圖所示。經過一系列的條件檢查,最終對VFIO設備的MSI、MSI-X配置會落實到vfio_msi_enable()vfio_msix_enable()上。

MSI配置

在qemu配置msi中斷時,有3種情況:

  1. 如果只是qemu單獨運行guest,沒有kvm,那么qemu會用自身的eventfd機制,每當需要發送中斷時,就讀eventfd,導致調用vfio_msi_interrupt,進而發送中斷。
  2. 如果qemu+kvm運行guest,那么qemu會注冊eventfd+irqfd,使vfio設備的msi中斷在觸發之后,改變eventfd的狀態,進而觸發kvm中的irq_routing中的某entry,最終通過Local APIC注入到Guest中。
  3. 在2的情況下,如果kvm支持irq_bypass feature,且CPU支持posted Interrupt,那么會將設備的在vfio設備的msi中斷觸發后,會改變eventfd的狀態,然后導致host kernel中與msi中斷綁定的host_irq觸發,進而觸發IOMMU的中斷重定向機制(發送一個Posted Interrupt),最終將中斷發送到Guest內部。

MSI-X配置

相比於msi,msi-x多了一步提前使能硬件設備的msi-x功能的工作,其它步驟與msi大同小異。


免責聲明!

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



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