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大同小异。

posted @ 2021-04-27 17:08  EwanHai  阅读(3762)  评论(0编辑  收藏  举报