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_enable
和vfio_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种情况:
- 如果只是qemu单独运行guest,没有kvm,那么qemu会用自身的eventfd机制,每当需要发送中断时,就读eventfd,导致调用
vfio_msi_interrupt
,进而发送中断。 - 如果qemu+kvm运行guest,那么qemu会注册eventfd+irqfd,使vfio设备的msi中断在触发之后,改变eventfd的状态,进而触发kvm中的irq_routing中的某entry,最终通过Local APIC注入到Guest中。
- 在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大同小异。