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

中断虚拟化-内核端

由于历史原因,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注入中断。


posted @ 2021-02-24 11:36  EwanHai  阅读(2038)  评论(3编辑  收藏  举报