中断处理过程

我们在袋鼠中实现自己独特的virtio mmio msi中断处理过程,在virtio transport我们之所以选择mmio而不是pci,主要是mmio不管是在vmm还是内核整个处理相对简单高效,但是在中断处理上mmio还不够成熟,我们增强了它的中断处理机制,可以做到与pci相同性能, 基本原理如下图所示: 1.png

下面针对每一个子系统简述它处理的原理:

kvm irqfd

kvm的中断处理是有vmm调用iotcl KVM_IRQFD来完成,先看下kvm_irqfd;

1050 struct kvm_irqfd {
1051     __u32 fd;
1052     __u32 gsi;
1053     __u32 flags;
1054     __u32 resamplefd;
1055     __u8  pad[16];
1056 };

其中fd是在用户态分配的eventfd,并用于在用户态程序进行中断注入的时候使用的;gsi是在用户态分配的全局中断号,需是唯一;

300     irqfd = kzalloc(sizeof(*irqfd), GFP_KERNEL);
301     if (!irqfd)
302         return -ENOMEM;
303
304     irqfd->kvm = kvm;
305     irqfd->gsi = args->gsi;
306     INIT_LIST_HEAD(&irqfd->list);
307     INIT_WORK(&irqfd->inject, irqfd_inject);
308     INIT_WORK(&irqfd->shutdown, irqfd_shutdown);
309     seqcount_init(&irqfd->irq_entry_sc);
310
311     f = fdget(args->fd);
312     if (!f.file) {
313         ret = -EBADF;
314         goto out;
315     }
316
317     eventfd = eventfd_ctx_fileget(f.file);

在kvm_irqfd_assign处理函数中,会分配irqfd的数据结构,后面kvm主要做的事情是创建针对irqfd的wait队列,并拿到对应fd的eventfd,并调用fd对应的poll函数,将绑定了irqfd_wakeup函数的wait queue加入到poll队列中,当有用户态对eventfd进行写入的时候,会触发回调irqfd_wakeup函数,其中会调用唤醒irqfd->inject的工作队列,最后调用到irqfd_inject中kvm_set_irq对vm进行中断注入

 47 static void
 48 irqfd_inject(struct work_struct *work)
 49 {
 50     struct kvm_kernel_irqfd *irqfd =
 51         container_of(work, struct kvm_kernel_irqfd, inject);
 52     struct kvm *kvm = irqfd->kvm;
 53
 54     if (!irqfd->resampler) {
 55         kvm_set_irq(kvm, KVM_USERSPACE_IRQ_SOURCE_ID, irqfd->gsi, 1,
 56                 false);
 57         kvm_set_irq(kvm, KVM_USERSPACE_IRQ_SOURCE_ID, irqfd->gsi, 0,
 58                 false);
 59     }

中断的注入需要依赖KVM_SET_GSI_ROUTING的ioctl,它是vmm对全局的中断号进行注入的配置,调用ioctl接口把要注入的中断号都下发给kvm;这时kvm会根据下发的中断类型去注册不同的注入函数,包括KVM_IRQ_ROUTING_IRQCHIP、KVM_IRQ_ROUTING_MSI等,其中KVM_IRQ_ROUTING_IRQCHIP还包括KVM_IRQCHIP_PIC_SLAVE,KVM_IRQCHIP_PIC_MASTER、KVM_IRQCHIP_IOAPIC。对于msi中断注入函数是kvm_set_msi,所以上面irqfd_inject在msi中断的时候真正调用的是kvm_set_msi来实现的对vm的注入。 在kvm_set_msi函数中,会得到前面guest配置的dest cpu id, vector, trigger mode等

kvm_make_request(KVM_REQ_EVENT, vcpu);
kvm_vcpu_kick(vcpu);

设置标志,并唤醒vcpu来处理中断。 实际并没有真正处理中断的地方,而是在vcpu进入到guest时检查KVM_REQ_EVENT事件

vcpu_enter_guest->kvm_apic_accept_events->inject_pending_event->kvm_x86_ops->set_irq
->vmx_inject_irq->vmcs_write32(VM_ENTRY_INTR_INFO_FIELD, intr);

IRQ Remapping

在直通设备情况下,外设发出的MSI中断需要在iommu做重映射,先映射到host上某一个中断源上以后,由这个中断处理函数负责将投递到guest,如果在没有中断重映射的时候,请求的格式如下所示: 2.png 其中Address中除了FEE,主要包括投递的APIC ID与投递的CPU,Data中主要包括投递方式与guest分配的vector号。 在开启irq remapping以后,格式就与之前不同,如下: 3.png 其中Address中没有APIC ID与CPU,而是包含了15位的HANDLE索引,与bit2一起组成16位的完整的HANDLE,主要用于在中断重映射表中去索引中断请求,其中16位可以索引64K个中断请求,DATA字段中也没有vector号。 中断重映射表是软件构建,通过上面HANDLE进行索引,其中每一项是IRTE表项(Interrrupt Remapping Tabel Entry),硬件使用中断重映射有下面这些步骤:

  • 硬件识别出中断地址是以0xFEEx_xxxx开始的,就认为是中断请求
  • 当Interrupt Remapping没有使能的时候,所有的中断都按照compitibility format来处理;
  • 当使能的时候:
    • 如果中断请求是Remappable format时,检查请求格式中reserved域是否为0,如果检查失败,这个中断请求会被blocked,否则,会通过HANDLE来计算索引值
      if (address.SHV == 0) {
      interrupt_index = address.handle;
      } else {
      interrupt_index = (address.handle + data.subhandle);
      }
      
    • 如果检查的index是有效的,则会取出对应的IRTE表项,如果IRTE中的Present位被置,则会按照IRTE的格式产生一个中断请求。

IRTE表格式: 4.png 在interrupt remapping 关闭与打开的情况进行对比,中断的流程如下面图所示, 5.png

6.png 从以上可以看到在使用没有interrupt remapping情况下,中断是直接透传给guest vm,并有hypervisor完成中断注入,在remapping时先要通过iommu去做IRTE表的查询,然后由hypervisor介入去注入主动,从这个流程上并不会带来性能的提升,那使用interrupt remapping的原因是什么?我下面找了一些解释: remapping的作用是为了安全与兼容性,并不是性能。 “Interrupt remapping provides isolation and compatibility, not performance.  The hypervisor being able to direct interrupts to a target CPU also allows it the ability to filter interrupts and prevent the device from signaling spurious or malicious interrupts.  This is particularly important with message signaled interrupts since any device capable of DMA is able to inject random MSIs into the host.  The compatibility side is a feature of Intel platforms supporting x2apic. The interrupt remapper provides a translation layer to allow xapic aware hardware, such as ioapics, to function when the processors are switched to x2apic mode”

vfio在使用interrupt remapping主要使能函数是vfio_msi_enable与vfio_msi_set_block,前者主要在host分配pci相关的中断vector,以及调用intel_irq_remapping_alloc准备irte表项,而后者针对每一个vector中断在host上申请中断处理函数vfio_msihandler,主要作用就是换下irqfd,通过kvm_set_irq进行中断注入,上面已经介绍。

336     ret = request_irq(irq, vfio_msihandler, 0,
337               vdev->ctx[vector].name, trigger);
338     if (ret) {
339         kfree(vdev->ctx[vector].name);
340         eventfd_ctx_put(trigger);
341         return ret;
342     }
343
344     vdev->ctx[vector].producer.token = trigger;
345     vdev->ctx[vector].producer.irq = irq;
346     ret = irq_bypass_register_producer(&vdev->ctx[vector].producer);
242 static irqreturn_t vfio_msihandler(int irq, void *arg)
243 {
244     struct eventfd_ctx *trigger = arg;
245
246     eventfd_signal(trigger, 1);
247     return IRQ_HANDLED;
248 }

Post Interrupt

Interrupt-posting是对interrupt ramapping的处理过程扩展,在Intel 64处理器上支持,可以使vmm高效的处理中断,在上面IRTEentry中第15位是用来使能是否使用post interrupt: 7.png 8.png post interrupt中IRTE的格式与remapping不同,其中有一个Posted Descriptor Address,”Posted Interrupt Descriptor”,是一个64字节的数据结构,用来记录post的中断请求,其目的是等待vCPU过来处理,而不是主动去打断vCPU去vmexit,PD包含以下结构:

  • Posted Interrupt Request (PIR)用来存储需要post的中断向量,每一个bit代表vector,一共是256 vectors;
  • Outstanding Notification (ON)用来指使是否有一个notification event产生,主要是硬件来更新;
  • Suppress Notification (SN)对于non-urgent中断请求,此位用来标识是否要对请求进行suppressed
  • Notification Vector (NV)指定notification event的中断vector
  • Notification Destination (NDST)用来标识目标vCPU对应的物理APIC-ID

Post Interrupt的处理流程:

  • 硬件产生中断中,查询中断重定向表,并根据其中IRTE表项判断是否支持post interrupt,并根据里面Posted Interrupt Descriptor的地址找到内存中位置,并根据ON、URG和SN判断是否产生notification event。

参考

https://compas.cs.stonybrook.edu/~nhonarmand/courses/sp17/cse506/slides/hw_io_virtualization.pdf https://software.intel.com/sites/default/files/managed/c5/15/vt-directed-io-spec.pdf https://events.static.linuxfound.org/sites/events/files/slides/VT-d%20Posted%20Interrupts-final%20.pdf