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


__EOF__

本文作者EwanHai
本文链接https://www.cnblogs.com/haiyonghao/p/14709880.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   EwanHai  阅读(3960)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示