中断虚拟化-内核端(一)

中断虚拟化-内核端

由于历史原因,QEMU和KVM均独立实现了PIC、APIC(IOAPIC+LAPIC).本文档试图说明清楚KVM中实现的PIC和APIC的逻辑。

本文档首先针对PIC、APIC、“Interrupt-Window Exiting”、“Virtual Interrupt Delivery”、“Posted Interrupt Process”多个中断相关功能第一次引入内核时的patch进行分析,最终利用较新的Linux-5.9串联这些功能。

PIC虚拟化

KVM中第一次加入PIC的模拟的Patch

commit id : 85f455f7ddbed403b34b4d54b1eaf0e14126a126

KVM: Add support for in-kernel PIC emulation

Signed-off-by: Yaozu (Eddie) Dong eddie.dong@intel.com
Signed-off-by: Avi Kivity avi@qumranet.com

PIC的硬件逻辑

要弄清楚软件模拟PIC的逻辑,必须清除硬件逻辑。PIC的硬件逻辑结构如下:

中断由IR0-IR7进入,进入之后PIC会自动设置IRR的对应bit,经过IMR和优先级处理之后,选择优先级最高的中断,通过INT管脚发送中断给CPU,CPU收到中断后,如果决定处理该中断,就会通过INTA管脚向PIC发送中断已接收通知,PIC接到该通知之后,设置ISR,表明某中断正在由CPU处理。

当CPU处理完该中断之后,会向PIC发送EOI(end of interrupt)信号,PIC收到EOI后,会将ISR中对应的bit清除掉。PIC记录ISR的作用之一是当后续收到新的中断时,会将新的中断和正在处理的中断的优先级进行比较,进而决定是否打断CPU正在处理的中断。如果PIC处于AEOI(auto EOI)模式,CPU无需向PIC发送EOI信号。

在x86中,CPU使用的中断vector号码和PIC使用的中断IR号码有所区别,x86规定,系统中使用的前32号vector用于CPU自身,不对外开放,因此需要对PIC的IRn加上一个base(32),才能转化为真正的CPU使用的vector号码。

数据结构

第一次引入内核的PIC设计中,PIC的状态由kvm_kpic_state结构描述。

commit ID: 85f455f7ddbed403b34b4d54b1eaf0e14126a126 KVM: Add support for in-kernel PIC emulation linux.git/drivers/kvm/irq.h struct kvm_kpic_state { u8 last_irr; /* edge detection */ u8 irr; /* interrupt request register IRR */ u8 imr; /* interrupt mask register IMR */ u8 isr; /* interrupt service register ISR */ u8 priority_add; /* highest irq priority 中断最高优先级 */ u8 irq_base; // 用于将IRQn转化为VECTORx u8 read_reg_select; // 选择要读取的寄存器 u8 poll; u8 special_mask; u8 init_state; u8 auto_eoi; // 标志auto_eoi模式 u8 rotate_on_auto_eoi; u8 special_fully_nested_mode; u8 init4; /* true if 4 byte init */ u8 elcr; /* PIIX edge/trigger selection */ u8 elcr_mask; struct kvm_pic *pics_state; // 指向高级PIC抽象结构 };

可以看到kvm_kpic_state结构完整描述了一个8259A中断控制器应该具有的所有基本特征,包括寄存器IRR、IMR、ISR等。

在该结构的最后定义了指向kvm_pic的指针,kvm_pic是一个更加高级的结构:

commit ID: 85f455f7ddbed403b34b4d54b1eaf0e14126a126 KVM: Add support for in-kernel PIC emulation linux.git/drivers/kvm/irq.h struct kvm_pic { struct kvm_kpic_state pics[2]; /* 0 is master pic, 1 is slave pic */ irq_request_func *irq_request; // 模拟PIC向外输出中断的callback void *irq_request_opaque; int output; /* intr from master PIC */ struct kvm_io_device dev; // 这里说明PIC是一个KVM内的IO设备 };

kvm_pic中包含了2片8259A,一片为master,一片为slave,具体来说,pic[0]为master,pic[1]为slave。

通过这两个数据结构,kvm对PIC进行了模拟。

PIC的创建过程

KVM为PIC的创建提供了ioctl接口,QEMU只需调用相关接口就可以在KVM中创建一个PIC。

QEMU: static int kvm_irqchip_create(KVMState *s) { ... ret = kvm_vm_ioctl(s, KVM_CREATE_IRQCHIP); ... }
KVM: static long kvm_vm_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg) { ... case KVM_CREATE_IRQCHIP: ... kvm->vpic = kvm_create_pic(kvm); ... ... } struct kvm_pic *kvm_create_pic(struct kvm *kvm) { struct kvm_pic *s; s = kzalloc(sizeof(struct kvm_pic), GFP_KERNEL); if (!s) return NULL; s->pics[0].elcr_mask = 0xf8; s->pics[1].elcr_mask = 0xde; s->irq_request = pic_irq_request; s->irq_request_opaque = kvm; s->pics[0].pics_state = s; s->pics[1].pics_state = s; /* * Initialize PIO device */ s->dev.read = picdev_read; s->dev.write = picdev_write; s->dev.in_range = picdev_in_range; s->dev.private = s; kvm_io_bus_register_dev(&kvm->pio_bus, &s->dev); return s; } /* * callback when PIC0 irq status changed */ static void pic_irq_request(void *opaque, int level) { struct kvm *kvm = opaque; pic_irqchip(kvm)->output = level; }

当QEMU调用ioctl(KVM_CREATE_IRQCHIP)时,KVM调用kvm_create_pic为其服务,后者创建了由2个 8259A 级联的PIC,并将该PIC注册为KVM的IO设备,以及注册了该IO设备的read、write、in_range方法,用于之后Guest对该设备进行配置(通过vmexit)。

使用PIC时的中断流程

假设Guest需要从一个外设读取数据,一般流程为vmexit到kvm/qemu,传递相关信息后直接返回Guest,当数据获取完成之后,qemu/kvm向Guest发送中断,通知Guest数据准备就绪。

那么qemu/kvm如何通过PIC向Guest发送中断呢?

PIC产生输出

这里分两种情况,设备在qemu中模拟或设备在kvm中模拟,事实上,设备在kvm中模拟的中断流程只是设备在qemu中模拟的中断流程的一个子集,因此我们只说明设备在qemu中模拟的中断流程。

当设备在qemu中模拟时:

QEMU: int kvm_init(void) { ... s = g_malloc0(sizeof(KVMState)); s->irq_set_ioctl = KVM_IRQ_LINE; ... } int kvm_set_irq(KVMState *s, int irq, int level) { ... ret = kvm_vm_ioctl(s, s->irq_set_ioctl, &event); ... }

即qemu通过ioctl(KVM_IRQ_LINE)向kvm发送中断注入请求。

commit ID: 85f455f7ddbed403b34b4d54b1eaf0e14126a126 KVM: Add support for in-kernel PIC emulation linux.git/drivers/kvm/irq.h KVM: static long kvm_vm_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg) { ... case KVM_IRQ_LINE: { if (irqchip_in_kernel(kvm)) { // 如果PIC在内核(kvm)中实现 if (irq_event.irq < 16) kvm_pic_set_irq(pic_irqchip(kvm), irq_event.irq, irq_event.level); } ... } void kvm_pic_set_irq(void *opaque, int irq, int level) { ... pic_set_irq1(&s->pics[irq >> 3], irq & 7, level); // 设置PIC的IRR寄存器 pic_update_irq(s); // 更新pic->output ... } /* * set irq level. If an edge is detected, then the IRR is set to 1 */ static inline void pic_set_irq1(struct kvm_kpic_state *s, int irq, int level) { int mask; mask = 1 << irq; if (s->elcr & mask) /* level triggered */ if (level) { // 将IRR对应bit置1 s->irr |= mask; s->last_irr |= mask; } else { // 将IRR对应bit置0 s->irr &= ~mask; s->last_irr &= ~mask; } else /* edge triggered */ if (level) { if ((s->last_irr & mask) == 0) // 如果出现了一个上升沿 s->irr |= mask; // 将IRR对应bit置1 s->last_irr |= mask; } else s->last_irr &= ~mask; } /* * raise irq to CPU if necessary. must be called every time the active * irq may change */ static void pic_update_irq(struct kvm_pic *s) { int irq2, irq; irq2 = pic_get_irq(&s->pics[1]); if (irq2 >= 0) { // 先检查slave PIC的IRQ情况 /* * if irq request by slave pic, signal master PIC */ pic_set_irq1(&s->pics[0], 2, 1); pic_set_irq1(&s->pics[0], 2, 0); } irq = pic_get_irq(&s->pics[0]); if (irq >= 0) s->irq_request(s->irq_request_opaque, 1); // 调用pic_irq_request使PIC的output为1 else s->irq_request(s->irq_request_opaque, 0); } /* * return the pic wanted interrupt. return -1 if none */ static int pic_get_irq(struct kvm_kpic_state *s) { int mask, cur_priority, priority; mask = s->irr & ~s->imr; // 过滤掉由IMR屏蔽的中断 priority = get_priority(s, mask); // 获取过滤后的中断的优先级 if (priority == 8) return -1; /* * compute current priority. If special fully nested mode on the * master, the IRQ coming from the slave is not taken into account * for the priority computation. */ mask = s->isr; if (s->special_fully_nested_mode && s == &s->pics_state->pics[0]) mask &= ~(1 << 2); cur_priority = get_priority(s, mask); // 获取当前CPU正在服务的中断的优先级 if (priority < cur_priority) // 如果新中断的优先级大于正在服务的中断的优先级(数值越小,优先级越高) /* * higher priority found: an irq should be generated */ return (priority + s->priority_add) & 7; // 循环优先级的算法: 某IRQ的优先级 + 最高优先级(动态) = IRQ号码 else return -1; }

总结一下,QEMU通过ioctl(KVM_IRQ_LINE)向KVM中的PIC申请一次中断,KVM收到该请求后,调用kvm_pic_set_irq进行PIC设置,具体流程为,首先通过pic_set_irq1设置PIC的IRR寄存器的对应bit,然后通过pic_get_irq获取PIC的IRR状态,并通过IRR状态设置模拟PIC的output.

通过以上流程,成功将PIC的output设置为了1。但pic->output是如何和Guest取得联系的呢?

将中断注入Guest

与物理PIC向CPU主动发起中断不同,而是在每次准备进入Guest时,KVM查询中断芯片,如果有待处理的中断,则执行中断注入。之前的流程最终设置的pic->output,会在每次切入Guest之前被检查:

早期内核中还没有用于切入Guest的vcpu_enter_guest函数,而是有一个与该函数功能类似的vmx_vcpu_run函数。

commit ID: 85f455f7ddbed403b34b4d54b1eaf0e14126a126 KVM: Add support for in-kernel PIC emulation linux.git/drivers/kvm/irq.h static int vmx_vcpu_run(struct kvm_vcpu *vcpu, struct kvm_run *kvm_run) { ... if (irqchip_in_kernel(vcpu->kvm)) // 如果PIC在内核(KVM)中模拟 vmx_intr_assist(vcpu); ... } static void vmx_intr_assist(struct kvm_vcpu *vcpu) { ... has_ext_irq = kvm_cpu_has_interrupt(vcpu); // 检测pic->output是否为1 ... if (has_ext_irq) enable_irq_window(vcpu); ... /* interrupt window 涉及VMCS的“Interrupt-window exiting” control,我们随后再探索, * 这里只需要知道如果Interrupt Window处于open状态,就可以利用vmx_inject_irq()向vcpu * 注入中断了 */ interrupt_window_open = ((vmcs_readl(GUEST_RFLAGS) & X86_EFLAGS_IF) && (vmcs_read32(GUEST_INTERRUPTIBILITY_INFO) & 3) == 0); if (interrupt_window_open) vmx_inject_irq(vcpu, kvm_cpu_get_interrupt(vcpu)); else enable_irq_window(vcpu); }

所以大体上的流程就是如果pic->output为1,就使"Interrupt-Window"处于open状态,然后利用vmx_inject_irq()向vcpu注入中断。关于vmx_intr_assist的其它逻辑,会在之后的"Interrupt-Window exiting" control相关的分析中深入了解。

接下来详细看vmx_inject_irq(vcpu, kvm_cpu_get_interrupt(vcpu));.

commit ID: 85f455f7ddbed403b34b4d54b1eaf0e14126a126 KVM: Add support for in-kernel PIC emulation linux.git/drivers/kvm/irq.h static void vmx_inject_irq(struct kvm_vcpu *vcpu, int irq) { ... vmcs_write32(VM_ENTRY_INTR_INFO_FIELD, irq | INTR_TYPE_EXT_INTR | INTR_INFO_VALID_MASK); ... }

vmx_inject_irq中涉及了一个VMCS field,即VM-entry interruption-information field ,该field的格式如下:

联系vmx_inject_irq()的代码可以看到,vmentry时,kvm将irq(vector_number)写入了Format of the VM-Entry Interruption-Information Field的bit7:0,并标记了该中断/异常类型为External Interrupt,并将bit31置为1,表明本次vmentry应该注入该中断。

所以在vmentry之后,vcpu就会获得这个外部中断,并利用自己的IDT去处理该中断。


IRQn转换为VECTORx

commit ID: 85f455f7ddbed403b34b4d54b1eaf0e14126a126 KVM: Add support for in-kernel PIC emulation linux.git/drivers/kvm/irq.h /* * Read pending interrupt vector and intack(interrupt acknowledge). */ int kvm_cpu_get_interrupt(struct kvm_vcpu *v) { struct kvm_pic *s = pic_irqchip(v->kvm); int vector; s->output = 0; vector = kvm_pic_read_irq(s); // 将irq转化为Vector if (vector != -1) return vector; /* * TODO: APIC */ return -1; } int kvm_pic_read_irq(struct kvm_pic *s) { int irq, irq2, intno; irq = pic_get_irq(&s->pics[0]); // 读取master pic 的irq if (irq >= 0) { // 如果master pic上产生了中断,需要模拟CPU向PIC发送ACK信号,并设置ISR pic_intack(&s->pics[0], irq); if (irq == 2) { // master pic的IRQ2连接的是slave pic的输出 irq2 = pic_get_irq(&s->pics[1]); if (irq2 >= 0) pic_intack(&s->pics[1], irq2); else /* * spurious IRQ on slave controller */ irq2 = 7; intno = s->pics[1].irq_base + irq2; irq = irq2 + 8; } else intno = s->pics[0].irq_base + irq; } else { /* * spurious IRQ on host controller */ irq = 7; intno = s->pics[0].irq_base + irq; } pic_update_irq(s); // 更新pic->output return intno; } /* * acknowledge interrupt 'irq' */ static inline void pic_intack(struct kvm_kpic_state *s, int irq) { if (s->auto_eoi) { // PIC在auto_eoi模式下时,无需设置ISR if (s->rotate_on_auto_eoi) s->priority_add = (irq + 1) & 7; } else // 非auto_eoi模式时,设置ISR s->isr |= (1 << irq); /* * We don't clear a level sensitive interrupt here */ if (!(s->elcr & (1 << irq))) // elcr的bit1为1表示IRQ1电平触发,所以这里表示在沿触发时,需要手动clear掉IRR s->irr &= ~(1 << irq); }

在调用vmx_inject_irq()之前,需要获取正确的vector号码,由kvm_cpu_get_interrupt => kvm_pic_read_irq 完成,在kvm_pic_read_irq中,根据IRQn在master还是slave pic上,计算出正确的vector号码,如果在master上,正确的vector号码 = master_pic.irq_base + irq_number,如果在slave上,正确的vector号码 = slave_pic.irq_base + irq_number. 而master和slave pic的irq_base初始化时为0,之后由Guest配置PIC时,通过创建PIC时注册的picdev_write函数来定义。

static void pic_ioport_write(void *opaque, u32 addr, u32 val) { ... switch (s->init_state) { case 1: s->irq_base = val & 0xf8; s->init_state = 2; break; } }

发送EOI

pci_ioport_wirte中还有一个比较重要的工作,就是发送EOI. 在PIC工作在非AEOI模式时,CPU处理中断完成之后,需要向PIC发送一个EOI,通知PIC清掉ISR中的相应bit,发送EOI的动作由Guest向PIC写入特定数据完成。

static void pic_ioport_write(void *opaque, u32 addr, u32 val) { ... cmd = val >> 5; switch (cmd) { ... case 1: /* end of interrupt */ case 5: priority = get_priority(s, s->isr); // 刚刚处理完成的IRQn的优先级 if (priority != 8) { irq = (priority + s->priority_add) & 7; // 刚刚处理完成的IRQn s->isr &= ~(1 << irq); // 清掉ISR对应bit if (cmd == 5) s->priority_add = (irq + 1) & 7; // 最高优先级轮转到IRQ(n+1) pic_update_irq(s->pics_state); // 看看是否有新的中断来临 } break; } }

vCPU处于Guest中或vCPU处于睡眠状态时的中断注入

在将中断注入Guest时,我们看到,在每次vmentry时,kvm才会检测是否有中断,并将中断信息写入VMCS,但是还有2种比较特殊的情况,会为中断注入带来延时。

  1. 中断产生时,vCPU处于休眠状态,中断无法被Guest及时处理。
  2. 中断产生时,vCPU正运行在Guest中,要处理本次中断只能等到下一次vmexit并vmentry时。

针对这两种情况,kvm在早期的代码中设计了kvm_vcpu_kick().

针对情况1,即vCPU处于休眠状态,也就是代表该vCPU的线程正在睡眠在等待队列(waitqueue)中,等待系统调度运行,此时,kvm_vcpu_kick()会将该vCPU踢醒,即对该等待队列调用wake_up_interruptible()

针对情况2,即vCPU运行在Guest中,此时,kvm_vcpu_kcik()会向运行该vCPU的物理CPU发送一个IPI(核间中断),该物理CPU就会vmexit,以尽快在下次vmentry时收取新的中断信息。

在PIC emulation刚刚引入kvm中时,对kvm_vcpu_kick()的定义只考虑到了情况2,所以这里给出第一次考虑完整的kvm_vcpu_kick().

commit b6958ce44a11a9e9425d2b67a653b1ca2a27796f KVM: Emulate hlt in the kernel void kvm_vcpu_kick(struct kvm_vcpu *vcpu) { int ipi_pcpu = vcpu->cpu; if (waitqueue_active(&vcpu->wq)) { wake_up_interruptible(&vcpu->wq); ++vcpu->stat.halt_wakeup; } if (vcpu->guest_mode) smp_call_function_single(ipi_pcpu, vcpu_kick_intr, vcpu, 0, 0); }

APIC虚拟化

为什么要将PIC换为APIC,主要原因是PIC无法发挥SMP系统的并发优势,PIC也无法发送IPI(核间中断)。

KVM中第一次加入IOAPIC的模拟的Patch

1fd4f2a5ed8f80cf6e23d2bdf78554f6a1ac7997

KVM: In-kernel I/O APIC model

This allows in-kernel host-side device drivers to raise guest interrupts
without going to userspace.

[avi: fix level-triggered interrupt redelivery on eoi]
[avi: add missing #include]
[avi: avoid redelivery of edge-triggered interrupt]
[avi: implement polarity]
[avi: don't deliver edge-triggered interrupts when unmasking]
[avi: fix host oops on invalid guest access]

Signed-off-by: Yaozu (Eddie) Dong eddie.dong@intel.com
Signed-off-by: Avi Kivity avi@qumranet.com

APIC 硬件逻辑

APIC包含2个部分,LAPIC和IOAPIC.

LAPIC即local APIC,位于处理器中,除接收来自IOAPIC的中断外,还可以发送和接收来自其它核的IPI。

IOAPIC一般位于南桥上,相应来自外部设备的中断,并将中断发送给LAPIC,然后由LAPIC发送给对应的CPU,起初LAPIC和IOAPIC通过专用总线即Interrupt Controller Communication BUS通信,后来直接使用了系统总线。

当IO APIC收到设备的中断请求时,通过寄存器决定将中断发送给哪个LAPIC(CPU)。 IO APIC的寄存器如Table 2. 所示。

位于0x10-0x3F 地址偏移的地方,存放着24个64bit的寄存器,每个对应IOAPIC的24个管脚之一,这24个寄存器统称为Redirection Table,每个寄存器都是一个entry。该重定向表会在系统初始化时由内核设置,在系统启动后亦可动态修改该表。

如下是每个entry的格式:

其中Destination Field负责确认中断的目标CPU(s),Interrupt Vector负责表示中断号码(注意这里就可以实现从irq到vector的转化),其余Field在需要时查询IOAPIC datasheet即可。

数据结构

LAPIC

kvm为LAPIC定义了结构体kvm_lapic:

struct kvm_lapic { unsigned long base_address; // LAPIC的基地址 struct kvm_io_device dev; // 准备将LAPIC注册为IO设备 struct { // 定义一个基于HRTIMER的定时器 atomic_t pending; s64 period; /* unit: ns */ u32 divide_count; ktime_t last_update; struct hrtimer dev; } timer; struct kvm_vcpu *vcpu; // 所属vcpu struct page *regs_page; // lapic所有的寄存器都在apic-page上存放。 void *regs; };

可以看到该结构主要由3部分信息形成:

  1. 即将作为IO设备注册到Guest的设备相关信息
  2. 一个定时器
  3. Lapic的apic-page信息

IOAPIC

kvm为IOAPIC定义了结构体kvm_ioapic:

struct kvm_ioapic { u64 base_address; // IOAPIC的基地址 u32 ioregsel; // 用于选择APIC-page中的寄存器 u32 id; // IOAPIC的ID u32 irr; // IRR u32 pad; // 是什么? union ioapic_redir_entry { // 重定向表 u64 bits; struct { u8 vector; u8 delivery_mode:3; u8 dest_mode:1; u8 delivery_status:1; u8 polarity:1; u8 remote_irr:1; u8 trig_mode:1; u8 mask:1; u8 reserve:7; u8 reserved[4]; u8 dest_id; } fields; } redirtbl[IOAPIC_NUM_PINS]; struct kvm_io_device dev; // 准备注册为Guest的IO设备 struct kvm *kvm; };

该结构也可以看做3部分信息:

  1. 即将作为IO设备注册到Guest的设备信息
  2. IOAPIC的基础,如用于标记IOAPIC身份的APIC ID,用于选择APIC-page中寄存器的寄存器IOREGSEL。
  3. 重定向表

APIC的创建过程

虚拟IOAPIC的创建

之前在看PIC的创建过程时,提到QEMU可以通过IOCTL(KVM_CREATE_IRQCHIP)在内核中申请创建一个虚拟PIC。那么IOAPIC也一样,在不知道Guest支持什么样的中断芯片时,会对PIC和IOAPIC都进行创建,Guest会自动选择合适自己的中断芯片。

QEMU: static int kvm_irqchip_create(KVMState *s) { ... ret = kvm_vm_ioctl(s, KVM_CREATE_IRQCHIP); ... }
KVM: static long kvm_vm_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg) { ... case KVM_CREATE_IRQCHIP: ... kvm->vpic = kvm_create_pic(kvm); if (kvm->vpic) { r = kvm_ioapic_init(kvm); ... } ... ... } int kvm_ioapic_init(struct kvm *kvm) { struct kvm_ioapic *ioapic; int i; ioapic = kzalloc(sizeof(struct kvm_ioapic), GFP_KERNEL); if (!ioapic) return -ENOMEM; kvm->vioapic = ioapic; // for (i = 0; i < IOAPIC_NUM_PINS; i++) // 24次循环 ioapic->redirtbl[i].fields.mask = 1; // 暂时屏蔽所有外部中断 ioapic->base_address = IOAPIC_DEFAULT_BASE_ADDRESS; // 0xFEC0_0000 ioapic->dev.read = ioapic_mmio_read; // vmexit后的mmio读 ioapic->dev.write = ioapic_mmio_write; // vmexit后的mmio写 ioapic->dev.in_range = ioapic_in_range; // vmexit后确定是否在该设备范围内 ioapic->dev.private = ioapic; ioapic->kvm = kvm; kvm_io_bus_register_dev(&kvm->mmio_bus, &ioapic->dev); // 注册为MMIO设备,注册到KVM的MMIO总线上 return 0; }

注意,上面的ioapic_mmio_readioapic_mmio_write用于Guest对IOAPIC进行配置,具体到达方式为MMIO Exit.

虚拟LAPIC的创建

因为LAPIC每个vcpu都应该有一个,所以KVM将创建虚拟LAPIC的工作放在了创建vcpu时,即当QEMU发出IOCTL(KVM_CREATE_VCPU),KVM在创建vcpu时,检测中断芯片(PIC/IOAPIC)是否在内核中,如果在内核中,就调用kvm_create_lapic()创建虚拟LAPIC。

int kvm_create_lapic(struct kvm_vcpu *vcpu) { struct kvm_lapic *apic; ... apic = kzalloc(sizeof(*apic), GFP_KERNEL); // 分配kvm_lapic结构 ... vcpu->apic = apic; apic->regs_page = alloc_page(GFP_KERNEL); // 为apic-page分配空间 ... apic->regs = page_address(apic->regs_page); memset(apic->regs, 0, PAGE_SIZE); apic->vcpu = vcpu;// 确定属于哪个vcpu // 初始化一个计时器 hrtimer_init(&apic->timer.dev, CLOCK_MONOTONIC, HRTIMER_MODE_ABS); apic->timer.dev.function = apic_timer_fn; apic->base_address = APIC_DEFAULT_PHYS_BASE; // 0xFEE0_0000 vcpu->apic_base = APIC_DEFAULT_PHYS_BASE; // 0xFEE0_0000 lapic_reset(vcpu); // 对apic-page中的寄存器,计时器的分频等数据进行初始化 apic->dev.read = apic_mmio_read; // MMIO设备,但未注册到kvm->mmio_bus上 apic->dev.write = apic_mmio_write; apic->dev.in_range = apic_mmio_range; apic->dev.private = apic; return 0; nomem: kvm_free_apic(apic); return -ENOMEM; }

使用APIC时的中断流程

与PIC稍有不同的是,APIC既可以接收外部中断,也可以处理核间中断,而外部中断和核间中断的虚拟化流程是不同的,因此这里分为两个部分。

外部中断

假设Guest需要从一个外设读取数据,一般流程为vmexit到kvm/qemu,传递相关信息后直接返回Guest,当数据获取完成之后,qemu/kvm向Guest发送外部中断,通知Guest数据准备就绪。

那么qemu/kvm如何通过APIC向Guest发送中断呢?

QEMU: int kvm_init(void) { ... s = g_malloc0(sizeof(KVMState)); s->irq_set_ioctl = KVM_IRQ_LINE; ... } int kvm_set_irq(KVMState *s, int irq, int level) { ... ret = kvm_vm_ioctl(s, s->irq_set_ioctl, &event); ... }

QEMU的动作与向PIC发送中断请求时一样,都是调用kvm_set_irq()向KVM发送中断请求。

KVM: static long kvm_vm_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg) { ... case KVM_IRQ_LINE: { if (irqchip_in_kernel(kvm)) { // 如果PIC在内核(kvm)中实现 if (irq_event.irq < 16) kvm_pic_set_irq(pic_irqchip(kvm), // 如果IRQn的n小于16,则调用PIC和IOAPIC产生中断 irq_event.irq, irq_event.level); kvm_ioapic_set_irq(kvm->vioapic, // 如果IRQn的n不小于16,则调用IOAPIC产生中断 irq_event.irq, irq_event.level); } ... } /* 根据输入level设置ioapic->irr, 并根据重定向表的对应entry决定是否为irq生成中断(ioapic_service) */ void kvm_ioapic_set_irq(struct kvm_ioapic *ioapic, int irq, int level) { u32 old_irr = ioapic->irr; u32 mask = 1 << irq; union ioapic_redir_entry entry; if (irq >= 0 && irq < IOAPIC_NUM_PINS) { // irq在0-23之间 entry = ioapic->redirtbl[irq]; // irq对应的entry level ^= entry.fields.polarity; // polarity: 0-高电平有效 1-低电平有效 if (!level) // 这里的level已经表示输入有效性而非电平高低 1-有效,0-无效 ioapic->irr &= ~mask; // 如果输入无效,则clear掉irq对应的IRR else { // 如果输入有效 ioapic->irr |= mask; // set irq对应的IRR if ((!entry.fields.trig_mode && old_irr != ioapic->irr) // trigger mode为边沿触发 且 IRR出现了沿 || !entry.fields.remote_irr) // 或 远方的LAPIC没有正在处理中断 ioapic_service(ioapic, irq); // 就对irq进行service } } } static void ioapic_service(struct kvm_ioapic *ioapic, unsigned int idx) { union ioapic_redir_entry *pent; pent = &ioapic->redirtbl[idx]; if (!pent->fields.mask) { // 如果irq没有被ioapic屏蔽 ioapic_deliver(ioapic, idx); // 决定是否向特定LAPIC发送中断 if (pent->fields.trig_mode == IOAPIC_LEVEL_TRIG) // 电平触发时,还需要将remote_irr置1,在收到LAPIC的EOI信息后将remote_irr置0 pent->fields.remote_irr = 1; } if (!pent->fields.trig_mode) // 沿触发时,ioapic_service为IRR产生一个下降沿 ioapic->irr &= ~(1 << idx); } static void ioapic_deliver(struct kvm_ioapic *ioapic, int irq) { ... /* 获取irq对应的目标vcpu mask */ deliver_bitmask = ioapic_get_delivery_bitmask(ioapic, dest, dest_mode); ... switch (delivery_mode) { // 根据目标vcpu mask和delivery mode确定是否要向Guest注入中断 ... ioapic_inj_irq(ioapic, target, vector, trig_mode, delivery_mode); } } static void ioapic_inj_irq(struct kvm_ioapic *ioapic, struct kvm_lapic *target, u8 vector, u8 trig_mode, u8 delivery_mode) { ... kvm_apic_set_irq(target, vector, trig_mode); } int kvm_apic_set_irq(struct kvm_lapic *apic, u8 vec, u8 trig) { if (!apic_test_and_set_irr(vec, apic)) { // 只有irq对应的irr某bit本来为0时,才可以产生新的中断 /* a new pending irq is set in IRR */ if (trig) // 电平触发模式 apic_set_vector(vec, apic->regs + APIC_TMR); // 设置apic-page中的Trigger Mode Register为1 else // 沿触发模式 apic_clear_vector(vec, apic->regs + APIC_TMR); // 设置apic-page中的Trigger Mode Register为0 kvm_vcpu_kick(apic->vcpu); // 踢醒vcpu ×××这里就在vmentry时查中断即可××× return 1; } return 0; }

可以看到,一旦QEMU发送了IOCTL(KVM_IRQ_LINE),KVM就会通过以下一系列的调用:

kvm_ioapic_set_irq() => ioapic_service() => ioapic_deliver() => ioapic_inj_irq() => kvm_apic_set_irq() |- 设置apic-page的IRR |- 设置apic-page的TMR |- 踢醒(出)vcpu vmentry时触发中断检测

最终通过kvm_apic_set_irq()向Guest发起中断请求.

核间中断

物理机上,CPU-0的LAPIC-x向CPU-1的LAPIC-y发送核间中断时,会将中断向量和目标的LAPIC标识符存储在自己的LAPIC-x的ICR(中断命令寄存器)中,然后该中断会顺着总线到达目标CPU。

虚拟机上,当vcpu-0的lapic-x向vcpu-1的lapic-y发送核间中断时,会首先访问apic-page的ICR寄存器(因为要将中断向量信息和目标lapic信息都放在ICR中),在没有硬件支持的中断虚拟化时,访问(write)apic-page会导致mmio vmexit,在KVM中将所有相关信息放在ICR中,在之后的vcpu-1的vmentry时会检查中断,进而注入IPI中断。

这里涉及到IO(MMIO) vmexit的流程,关于该流程暂时不做trace,只需要知道,在没有硬件辅助中断虚拟化的情况下,对apic-page的读写会vmexit最终调用apic_mmio_write()/apic_mmio_read().

static void apic_mmio_write(struct kvm_io_device *this, gpa_t address, int len, const void *data) { ... switch (offset) { ... case APIC_ICR: /* No delay here, so we always clear the pending bit , 因为ICR的bit12是Delivery Status,0-该LAPIC已经完成了之前的任何IPI的发送,1-有之前的IPI正在pending. 由于我们的LAPIC模拟发送IPI中,没有任何延迟,因此直接clear掉ICR的bit12. */ apic_set_reg(apic, APIC_ICR, val & ~(1 << 12)); apic_send_ipi(apic); break; } } static void apic_send_ipi(struct kvm_lapic *apic) { u32 icr_low = apic_get_reg(apic, APIC_ICR); // ICR低32bit u32 icr_high = apic_get_reg(apic, APIC_ICR2); // ICR高32bit unsigned int dest = GET_APIC_DEST_FIELD(icr_high); // Destination Field unsigned int short_hand = icr_low & APIC_SHORT_MASK; // Destination Shorthand unsigned int trig_mode = icr_low & APIC_INT_LEVELTRIG; // Trigger Mode unsigned int level = icr_low & APIC_INT_ASSERT; // Level unsigned int dest_mode = icr_low & APIC_DEST_MASK; // Destination Mode unsigned int delivery_mode = icr_low & APIC_MODE_MASK; // Delivery Mode unsigned int vector = icr_low & APIC_VECTOR_MASK; // Vector for (i = 0; i < KVM_MAX_VCPUS; i++) { // 针对所有vcpu vcpu = apic->vcpu->kvm->vcpus[i]; if (!vcpu) continue; if (vcpu->apic && apic_match_dest(vcpu, apic, short_hand, dest, dest_mode)) { // 如果vcpu有LAPIC且vcpu是该中断的目标 if (delivery_mode == APIC_DM_LOWEST) // 如果Delivery Mode为Lowest Priority,则将vcpu对应的Lowest Priority Register map中的对应bit设置为1.(因为这种模式下,最终只有一个vcpu会收到本次中断,所以需要最终查看lpr_map的内容决定. ) set_bit(vcpu->vcpu_id, &lpr_map); else // 如果Delivery Mode不为Lowest Priority __apic_accept_irq(vcpu->apic, delivery_mode, vector, level, trig_mode); // 使用LAPIC发送中断 } } } /* 确认传入参数vcpu是否是LAPIC发送中断的目标 返回1表示是,返回0表示不是 */ static int apic_match_dest(struct kvm_vcpu *vcpu, struct kvm_lapic *source, int short_hand, int dest, int dest_mode) { int result = 0; struct kvm_lapic *target = vcpu->apic; ... ASSERT(!target); switch (short_hand) { case APIC_DEST_NOSHORT: // short_hand为00 目标CPU通过Destination指定 if (dest_mode == 0) { /* Physical mode. 如果为全局广播或目标即自身 表示当前vcpu是目标vcpu */ if ((dest == 0xFF) || (dest == kvm_apic_id(target))) result = 1; } else /* Logical mode. Destination不再是物理的APIC ID而是逻辑上代表一组CPU,SDM将此时 * 的Destination称为Message Destination Address (MDA)。 这里为了确认当前vcpu是否 * 在MDA中.如果在则返回1. */ result = kvm_apic_match_logical_addr(target, dest); break; case APIC_DEST_SELF: if (target == source) result = 1; break; case APIC_DEST_ALLINC: result = 1; break; case APIC_DEST_ALLBUT: if (target != source) result = 1; break; default: printk(KERN_WARNING "Bad dest shorthand value %x\n", short_hand); break; } return result; } /* * Add a pending IRQ into lapic. * Return 1 if successfully added and 0 if discarded. */ static int __apic_accept_irq(struct kvm_lapic *apic, int delivery_mode, int vector, int level, int trig_mode) { int result = 0; switch (delivery_mode) { case APIC_DM_FIXED: case APIC_DM_LOWEST: /* 重复设置IRR检测 */ if (apic_test_and_set_irr(vector, apic) && trig_mode) { // 如果IRR为0,则设置为1 apic_debug("level trig mode repeatedly for vector %d", vector); break; } if (trig_mode) { // 根据传入的trig_mode设置apic-page中的TMR. apic_debug("level trig mode for vector %d", vector); apic_set_vector(vector, apic->regs + APIC_TMR); } else apic_clear_vector(vector, apic->regs + APIC_TMR); kvm_vcpu_kick(apic->vcpu); // 踢醒vcpu ×××这里就在vmentry时查中断即可××× result = 1; break; case APIC_DM_REMRD: printk(KERN_DEBUG "Ignoring delivery mode 3\n"); break; case APIC_DM_SMI: printk(KERN_DEBUG "Ignoring guest SMI\n"); break; case APIC_DM_NMI: printk(KERN_DEBUG "Ignoring guest NMI\n"); break; case APIC_DM_INIT: printk(KERN_DEBUG "Ignoring guest INIT\n"); break; case APIC_DM_STARTUP: printk(KERN_DEBUG "Ignoring guest STARTUP\n"); break; default: printk(KERN_ERR "TODO: unsupported delivery mode %x\n", delivery_mode); break; } return result; }

可以看到,核间中断由Guest通过写apic-page中的ICR(中断控制寄存器)发起,vmexit到KVM,最终调用apic_mmio_write().

apic_mmio_write() => apic_send_ipi() => __apic_accept_irq() |- 设置apic-page的IRR |- 设置apic-page的TMR |- 踢醒(出)vCPU vmentry时触发中断检测

中断路由

KVM中有了APIC和PIC的实现之后,出现一个问题,一个来自外部的中断到底要走PIC还是APIC呢?

早期KVM的策略是,如果irq小于16,则APIC和PIC都走,即这两者的中断设置函数都调用。如果大于等于16,则只走APIC. Guest支持哪个中断芯片,就和哪个中断芯片进行交互。代码如下:

KVM: static long kvm_vm_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg) { ... case KVM_IRQ_LINE: { if (irqchip_in_kernel(kvm)) { // 如果PIC在内核(kvm)中实现 if (irq_event.irq < 16) kvm_pic_set_irq(pic_irqchip(kvm), // 如果IRQn的n小于16,则调用PIC和IOAPIC产生中断 irq_event.irq, irq_event.level); kvm_ioapic_set_irq(kvm->vioapic, // 如果IRQn的n不小于16,则调用IOAPIC产生中断 irq_event.irq, irq_event.level); } ... }

出于代码风格和接口一致原则,KVM在之后的更新中设计了IRQ routing方案。

KVM第一次加入用户定义的中断映射,包括irq,对应的中断芯片、中断函数

399ec807ddc38ecccf8c06dbde04531cbdc63e11

KVM: Userspace controlled irq routing

Currently KVM has a static routing from GSI numbers to interrupts (namely,
0-15 are mapped 1:1 to both PIC and IOAPIC, and 16:23 are mapped 1:1 to
the IOAPIC). This is insufficient for several reasons:

  • HPET requires non 1:1 mapping for the timer interrupt
  • MSIs need a new method to assign interrupt numbers and dispatch them
  • ACPI APIC mode needs to be able to reassign the PCI LINK interrupts to the
    ioapics

This patch implements an interrupt routing table (as a linked list, but this
can be easily changed) and a userspace interface to replace the table. The
routing table is initialized according to the current hardwired mapping.

Signed-off-by: Avi Kivity avi@redhat.com

数据结构

  • 内核中断路由结构

该patch引入了一个结构用于表示KVM中的中断路由,即kvm_kernel_irq_routing_entry.

struct kvm_kernel_irq_routing_entry { u32 gsi; void (*set)(struct kvm_kernel_irq_routing_entry *e, struct kvm *kvm, int level); // 生成中断的函数指针 union { struct { unsigned irqchip; // 中断芯片的预编号 unsigned pin; // 类似IRQn } irqchip; }; struct list_head link; // 链接下一个kvm_kernel_irq_routing_entry结构 };

一个kvm_kernel_irq_routing_entry提供了2类信息:

  1. 中断号及其对应的中断生成函数set

  2. 该中断号对应的中断芯片类型及对应管脚

  • 用户中断路由结构

还引入了一个用于表示用户空间传入的中断路由信息的结构,即kvm_irq_routing_entry.

struct kvm_irq_routing_entry { __u32 gsi; __u32 type; __u32 flags; __u32 pad; union { struct kvm_irq_routing_irqchip irqchip; // 包含irqchip和pin两个field __u32 pad[8]; } u; };

用户空间的中断路由信息看起来更加简单,包含了一些中断类型、标志。最终中断怎样路由,由内核端决定。

默认中断路由

该patch中定义了一个默认中断路由,在QEMU创建中断芯片(IOCTL(KVM_CREATE_IRQCHIP))时创建。

KVM: static long kvm_vm_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg) { ... case KVM_CREATE_IRQCHIP: { ... r = kvm_setup_default_irq_routing(kvm); ... } ... } int kvm_setup_default_irq_routing(struct kvm *kvm) { return kvm_set_irq_routing(kvm, default_routing, ARRAY_SIZE(default_routing), 0); } int kvm_set_irq_routing(struct kvm *kvm, const struct kvm_irq_routing_entry *ue, // default_routing unsigned nr, // routing中entry的数量 unsigned flags) { ... for (i = 0; i < nr; ++i) { e = kzalloc(sizeof(*e), GFP_KERNEL); // 为内核中断路由结构 ... r = setup_routing_entry(e, ue); ... list_add(&e->link, &irq_list); } ... list_splice(&irq_list, &kvm->irq_routing); // 将所有kvm_irq_routing_entry链成链表,维护在kvm->irq_routing中 }

也就是说,在用户空间向KVM申请创建内核端的中断芯片时,就会建立一个默认的中断路由表。代码路径为:

kvm_setup_default_irq_routing() => kvm_set_irq_routing(default_routing) => setup_routing_entry(e,default_routing)

路由表由路由Entry构成,内核将irq0-irq15既给了PIC,也给了IOAPIC,如果是32bit架构,那么irq16-irq24专属于IOAPIC,如果是64bit架构,那么irq16-irq47专属于IOAPIC。

中断路由Entry的细节方面,对于IOAPIC entry,gsi == irq == pin,类型为KVM_IRQ_ROUTING_IRQCHIP,中断芯片为KVM_IRQCHIP_IOAPIC。对于PIC entry,gsi == irq,可以通过irq选择PIC的master或slave,pin == irq % 8。更加具体的默认中断路由代码实现如下。

/* KVM中的默认中断路由 */ static const struct kvm_irq_routing_entry default_routing[] = { ROUTING_ENTRY2(0), ROUTING_ENTRY2(1), ROUTING_ENTRY2(2), ROUTING_ENTRY2(3), ROUTING_ENTRY2(4), ROUTING_ENTRY2(5), ROUTING_ENTRY2(6), ROUTING_ENTRY2(7), ROUTING_ENTRY2(8), ROUTING_ENTRY2(9), ROUTING_ENTRY2(10), ROUTING_ENTRY2(11), ROUTING_ENTRY2(12), ROUTING_ENTRY2(13), ROUTING_ENTRY2(14), ROUTING_ENTRY2(15), ROUTING_ENTRY1(16), ROUTING_ENTRY1(17), ROUTING_ENTRY1(18), ROUTING_ENTRY1(19), ROUTING_ENTRY1(20), ROUTING_ENTRY1(21), ROUTING_ENTRY1(22), ROUTING_ENTRY1(23), #ifdef CONFIG_IA64 ROUTING_ENTRY1(24), ROUTING_ENTRY1(25), ROUTING_ENTRY1(26), ROUTING_ENTRY1(27), ROUTING_ENTRY1(28), ROUTING_ENTRY1(29), ROUTING_ENTRY1(30), ROUTING_ENTRY1(31), ROUTING_ENTRY1(32), ROUTING_ENTRY1(33), ROUTING_ENTRY1(34), ROUTING_ENTRY1(35), ROUTING_ENTRY1(36), ROUTING_ENTRY1(37), ROUTING_ENTRY1(38), ROUTING_ENTRY1(39), ROUTING_ENTRY1(40), ROUTING_ENTRY1(41), ROUTING_ENTRY1(42), ROUTING_ENTRY1(43), ROUTING_ENTRY1(44), ROUTING_ENTRY1(45), ROUTING_ENTRY1(46), ROUTING_ENTRY1(47), #endif }; // IOAPIC entry定义 gsi == irq == pin #define IOAPIC_ROUTING_ENTRY(irq) \ { .gsi = irq, .type = KVM_IRQ_ROUTING_IRQCHIP, \ .u.irqchip.irqchip = KVM_IRQCHIP_IOAPIC, .u.irqchip.pin = (irq) } #define ROUTING_ENTRY1(irq) IOAPIC_ROUTING_ENTRY(irq) // PIC entry定义 gsi == irq irq % 8 == pin #define SELECT_PIC(irq) \ ((irq) < 8 ? KVM_IRQCHIP_PIC_MASTER : KVM_IRQCHIP_PIC_SLAVE) # define PIC_ROUTING_ENTRY(irq) \ { .gsi = irq, .type = KVM_IRQ_ROUTING_IRQCHIP, \ .u.irqchip.irqchip = SELECT_PIC(irq), .u.irqchip.pin = (irq) % 8 } # define ROUTING_ENTRY2(irq) \ IOAPIC_ROUTING_ENTRY(irq), PIC_ROUTING_ENTRY(irq)

接下来看看,在拥有了一个确认的中断路由时,setup_routing_entry()具体如何操作。

int setup_routing_entry(struct kvm_kernel_irq_routing_entry *e, const struct kvm_irq_routing_entry *ue) { int r = -EINVAL; int delta; e->gsi = ue->gsi; // global system interrupt 号码直接交换 switch (ue->type) { case KVM_IRQ_ROUTING_IRQCHIP: // 用户传入的entry类型是中断芯片 delta = 0; switch (ue->u.irqchip.irqchip) { case KVM_IRQCHIP_PIC_MASTER: // PIC MASTER e->set = kvm_set_pic_irq;--------------------| break; | case KVM_IRQCHIP_PIC_SLAVE: // PIC SLAVE | e->set = kvm_set_pic_irq;--------------------|--具体中断芯片上的中断生成函数 delta = 8; | break; | case KVM_IRQCHIP_IOAPIC: // IOAPIC | e->set = kvm_set_ioapic_irq;-----------------| break; default: goto out; } e->irqchip.irqchip = ue->u.irqchip.irqchip; // 直接赋值 e->irqchip.pin = ue->u.irqchip.pin + delta; // 在用户传入的基础上加上一个delta,实现从Master/Slave PIC的选择 break; default: goto out; } r = 0; out: return r; }

setup_routing_entry()

  1. 将用户传入的gsi直接赋值给内核中断路由entry的gsi
  2. 根据用户传入的中断路由entry的中断芯片类型为entry设备不同的set函数
  3. 根据中断芯片类型为内核中断路由entry的pin设置正确的值

用户自定义中断路由

当然,该patch也为用户空间提供了传入其自定义中断路由表的接口,即IOCTL(KVM_SET_GSI_ROUTING).

KVM: static long kvm_vm_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg) { ... case KVM_SET_GSI_ROUTING: { ... r = kvm_set_irq_routing(kvm, entries, routing.nr, routing.flags); ... } ... }

可以看到这里同样调用了kvm_set_irq_routing(),不过与建立默认中断路由表(default_routing)不同,这一次传入的中断路由表是由用户空间定义的,而其它流程,则与建立默认中断路由一模一样。

MSI/MSI-X中断

MSI(Message Signaled Interrupts)中断的目的是绕过IOAPIC,使中断能够直接从设备到达LAPIC,达到降低延时的目的。从MSI的名字可以看出,MSI不基于管脚而是基于消息。MSI由PCI2.2引入,当时是PCI设备的一个可选特性,到了2004年,PCIE SPEC发布,MSI成了PCIE设备强制要求的特性,在PCI3.3时,又对MSI进行了一定的增强,称为MSI-X,相比于MSI,MSI-X的每个设备可以支持更多的中断,且每个中断可以独立配置。

除了减少中断延迟外,因为不存在管脚的概念了,所以之前因为管脚有限而共享管脚的问题自然就消失了,之前当某个管脚有信号时,操作系统需要逐个调用共享这个管脚的中断服务程序去试探,是否可以处理这个中断,直到某个中断服务程序可以正确处理,同样,不再受管脚数量的约束,MSI能够支持的中断数也显著变多了。

支持MSI的设备绕过IOAPIC,直接通过系统总线与LAPIC相连。

MSI/MSI-X的硬件逻辑

MSI

从PCI2.1开始,如果设备需要扩展某种特性,可以向配置空间中的Capabilities List中增加一个Capability,MSI利用该特性,将IOAPIC中的功能扩展到设备本身了。

下图为4种 MSI Capability Structure.

Next PointerCapability ID这两个field是PCI的任何Capability都具有的field,分别表示下一个Capability在配置空间的位置、以及当前Capability的ID。

Message AddressMessage Data是MSI的关键,只要将Message Data中的内容写入到Message Address中,就会产生一个MSI中断。

Message Control用于系统软件对MSI的控制,如enable MSI、使能64bit地址等。

Mask Bits用于在CPU处理某中断时可以屏蔽其它同样的中断。类似PIC中的IMR。

Pending Bits用于指示当前正在等待的MSI中断,类似于PIC中的IRR.

MSI-X

为了支持多个中断,MSI-X的Capability Structure做出了变化,如Figure6-12所示:

Capability ID, Next Pointer, Message Control这3个field依然具有原来的功能。

MSI-X将MSI的Message DataMessage Address放到了一个表(MSIX-Table)中,Pending Bits也被放到了一个表(MSIX-Pending Bit Array)中。

MSI-X的Capability Structure中的Table BIR说明MSIX-Table在哪个BAR中,Table Offset说明MSIX-Table在该BAR中的offset。类似的,PBA BIRPBA offset分别说明MSIX-PBA在哪个BAR中,在BAR中的什么位置。

BIR : BAR Indicator Register

Figure 6-13和Figure 6-14分别展示了MSIX Table和MSIX PBA的结构,以此构成多中断的基础。

MSI/MSIX中断流程(以VFIO设备为例)

在QEMU中初始化虚拟设备(VFIO)时,会对MSI/MSIX做最初的初始化。其根据都是对MSI/MSIX Capability Structure的创建,以及对对应memory的映射。

static void vfio_realize(PCIDevice *pdev, Error **errp) { ... vfio_msix_early_setup(vdev, &err); ... ret = vfio_add_capabilities(vdev, errp); ... }

提前说明,具体的MSI/MSIX初始化是在vfio_add_capabilities中完成的,那为什么要进行vfio_msix_early_setup呢?

/* * We don't have any control over how pci_add_capability() inserts * capabilities into the chain. In order to setup MSI-X we need a * MemoryRegion for the BAR. In order to setup the BAR and not * attempt to mmap the MSI-X table area, which VFIO won't allow, we * need to first look for where the MSI-X table lives. So we * unfortunately split MSI-X setup across two functions. */ static void vfio_msix_early_setup(VFIOPCIDevice *vdev, Error **errp) { if (pread(fd, &ctrl, sizeof(ctrl), vdev->config_offset + pos + PCI_MSIX_FLAGS) != sizeof(ctrl)) { error_setg_errno(errp, errno, "failed to read PCI MSIX FLAGS"); return; } // 读取MSIX Capability Structure中的Message Control到ctrl if (pread(fd, &table, sizeof(table), vdev->config_offset + pos + PCI_MSIX_TABLE) != sizeof(table)) { error_setg_errno(errp, errno, "failed to read PCI MSIX TABLE"); return; } // 读取MSIX Capability Structure中的Table Offset + Table BIR到table if (pread(fd, &pba, sizeof(pba), vdev->config_offset + pos + PCI_MSIX_PBA) != sizeof(pba)) { error_setg_errno(errp, errno, "failed to read PCI MSIX PBA"); return; } // 读取MSIX Capability Structure中的PBA Offset + PBA BIR到pba ctrl = le16_to_cpu(ctrl); table = le32_to_cpu(table); pba = le32_to_cpu(pba); msix = g_malloc0(sizeof(*msix)); msix->table_bar = table & PCI_MSIX_FLAGS_BIRMASK; msix->table_offset = table & ~PCI_MSIX_FLAGS_BIRMASK; msix->pba_bar = pba & PCI_MSIX_FLAGS_BIRMASK; msix->pba_offset = pba & ~PCI_MSIX_FLAGS_BIRMASK; msix->entries = (ctrl & PCI_MSIX_FLAGS_QSIZE) + 1; // table中的entry数量 /* 如果VFIO同意映射Table所在的整个BAR,就可以在之后直接mmap. * 如果VFIO不同意映射Table所在的整个BAR,就只对Table进行mmap. */ vfio_pci_fixup_msix_region(vdev); ... }

注释中写的很清楚,因为之后的pci_add_capability()中对MSIX进行如何配置是未知的,也许会配置MSIX Capability,也许不会,但是如果想要配置MSIX,就要给MSIX对应的BAR一个MemoryRegion,VFIO不同意将MSIX Table所在的BAR映射到Guest,因为这将会导致安全问题,所以我们得提前找到MSIX Table所在的BAR,由于该原因,MSIX的建立分成了2部分。

vfio_msix_early_setup()找到了MSIX-Table所在的BAR、MSIX的相关信息,然后记录到了vdev->msix中。并对MSIX Table的mmap做了预处理。

static int vfio_add_capabilities(VFIOPCIDevice *vdev, Error **errp) { ... ret = vfio_add_std_cap(vdev, pdev->config[PCI_CAPABILITY_LIST], errp); ... } // vfio_add_std_cap本身是一个递归算法,通过`next`不断追溯下一个capability,从最后一个capability开始add并回溯。 // 这里只给出vfio_add_std_cap()的核心添加capability的机制。 static int vfio_add_std_cap(VFIOPCIDevice *vdev, uint8_t pos, Error **errp) { ... switch (cap_id) { ... case PCI_CAP_ID_MSI: ret = vfio_msi_setup(vdev, pos, errp); break; ... case PCI_CAP_ID_MSIX: ret = vfio_msix_setup(vdev, pos, errp); break; } ... }

到了这里分了两个部分,一个是MSI的初始化,一个是MSIX的初始化。分别来看。

MSI初始化

static int vfio_msi_setup(VFIOPCIDevice *vdev, int pos, Error **errp) { // 读取MSIX Capability Structure中的Message Control Field到ctrl if (pread(vdev->vbasedev.fd, &ctrl, sizeof(ctrl), vdev->config_offset + pos + PCI_CAP_FLAGS) != sizeof(ctrl)) { error_setg_errno(errp, errno, "failed reading MSI PCI_CAP_FLAGS"); return -errno; } ctrl = le16_to_cpu(ctrl); msi_64bit = !!(ctrl & PCI_MSI_FLAGS_64BIT); // 确定是否支持64bit消息地址 msi_maskbit = !!(ctrl & PCI_MSI_FLAGS_MASKBIT); // 确定是否支持单Vector屏蔽 entries = 1 << ((ctrl & PCI_MSI_FLAGS_QMASK) >> 1); // 确定中断vector的数量 ... // 向vdev->pdev添加的MSI Capability Structure,设置软件维护的配置空间信息 ret = msi_init(&vdev->pdev, pos, entries, msi_64bit, msi_maskbit, &err); ... }

经过msi_init()之后,一个完整的MSI Capability Structure就已经呈现在了vdev->pdev.config + capability_list + msi_offset的位置。当然这个结构是QEMU根据实际物理设备而模拟出来的,实际物理设备中的Capability Structure没有任何变化。

MSIX初始化

static int vfio_msix_setup(VFIOPCIDevice *vdev, int pos, Error **errp) { ... // 因为MSIX的相关信息都已经在vfio_msix_early_setup中获得了,这里只需要用这些信息直接 // 对MSIX进行初始化 ret = msix_init(&vdev->pdev, vdev->msix->entries, vdev->bars[vdev->msix->table_bar].mr, vdev->msix->table_bar, vdev->msix->table_offset, vdev->bars[vdev->msix->pba_bar].mr, vdev->msix->pba_bar, vdev->msix->pba_offset, pos, &err); ... }

msix_init()中,除了向vdev->pdev添加MSIX Capability Structure及MSIX Table和MSIX PBA,还将msix table和PBA table都作为mmio注册到pdev上。mmio操作的回调函数分别为:msix_table_mmio_ops,msix_pba_mmio_ops.

建立IRQ Routing entry

之前我们看到在KVM中建立了一个统一的数据结构,即IRQ Routing Entry,能够针对来自IOAPIC,PIC或类型为MSI的中断。对于每个中断,在Routing Table中都应该有1个entry,中断发生时,KVM会根据entry中的信息,调用具体的中断函数。

那么这个entry是如何传递到KVM中去的呢?在APIC虚拟化一节我们知道可以使用IOCTL(KVM_SET_GSI_ROUTING)传入中断路由表。

一般情况下,对于IRQ Routing Entry的建立和传入KVM应该是在设备(中断模块)初始化时就完成的,但是QEMU为了效率采用的Lazy Mode,即只有在真正使用MSI/MSIX中断时,才进行IRQ Routing Entry的建立。

static void vfio_pci_dev_class_init(ObjectClass *klass, void *data) { DeviceClass *dc = DEVICE_CLASS(klass); PCIDeviceClass *pdc = PCI_DEVICE_CLASS(klass); dc->reset = vfio_pci_reset; device_class_set_props(dc, vfio_pci_dev_properties); dc->desc = "VFIO-based PCI device assignment"; set_bit(DEVICE_CATEGORY_MISC, dc->categories); pdc->realize = vfio_realize; pdc->exit = vfio_exitfn; pdc->config_read = vfio_pci_read_config; pdc->config_write = vfio_pci_write_config; } static void vfio_pci_write_config(PCIDevice *pdev, uint32_t addr, uint32_t val, int len) { /* MSI/MSI-X Enabling/Disabling */ if (pdev->cap_present & QEMU_PCI_CAP_MSI && ranges_overlap(addr, len, pdev->msi_cap, vdev->msi_cap_size)) { int is_enabled, was_enabled = msi_enabled(pdev); ... pci_default_write_config(pdev, addr, val, len); // MSI is_enabled = msi_enabled(pdev); if (!was_enabled && is_enabled) { vfio_enable_msi(vdev); } ... // MSIX is_enabled = msix_enabled(pdev); if (!was_enabled && is_enabled) { vfio_msix_enable(vdev); } ... } static void vfio_enable_msix(VFIODevice *vdev) { ... if (msix_set_vector_notifiers(&vdev->pdev, vfio_msix_vector_use, vfio_msix_vector_release)) { error_report("vfio: msix_set_vector_notifiers failed\n"); } ... }

vfio_pci_dev_class_init()vfio_pci_write_config注册为vfio设备的写配置空间的callback,当Guest对vfio设备的配置空间进行写入时,就会触发:

vfio_pci_write_config() => vfio_enable_msi(vdev)-------|// 如果配置空间中的MSI/MSIX Capability Structure有效 => vfio_msix_enable(vdev)------| static void vfio_msi_enable(VFIOPCIDevice *vdev) { ... vfio_add_kvm_msi_virq(vdev, vector, i, false); ... } static void vfio_add_kvm_msi_virq(VFIOPCIDevice *vdev, VFIOMSIVector *vector, int vector_n, bool msix) { virq = kvm_irqchip_add_msi_route(kvm_state, vector_n, &vdev->pdev); } int kvm_irqchip_add_msi_route(KVMState *s, int vector, PCIDevice *dev) { kroute.gsi = virq; kroute.type = KVM_IRQ_ROUTING_MSI; kroute.flags = 0; kroute.u.msi.address_lo = (uint32_t)msg.address; kroute.u.msi.address_hi = msg.address >> 32; kroute.u.msi.data = le32_to_cpu(msg.data); ... kvm_add_routing_entry(s, &kroute); kvm_arch_add_msi_route_post(&kroute, vector, dev); kvm_irqchip_commit_routes(s); } void kvm_irqchip_commit_routes(KVMState *s) { ret = kvm_vm_ioctl(s, KVM_SET_GSI_ROUTING, s->irq_routes); }

vfio_enable_msi()中,通过层层调用最终调用了IOCTL(KVM_SET_GSI_ROUTING)将Routing Entry传入了KVM,vfio_msix_enable()的调用过程稍微复杂以下,这里没有贴出代码,但最终也是调用了IOCTL(KVM_SET_GSI_ROUTING)将Routing Entry传入了KVM。

中断流程

当KVM收到外设发送的中断时,就会调用该中断GSI对应的Routing Entry对应的.set方法,对于MSI/MSIX,其方法为kvm_set_msi,该方法的大致流程是,首先从MSIX Capability提取信息,找到目标CPU,然后针对每个目标调用kvm_apic_set_irq()向Guest注入中断。



__EOF__

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