中斷虛擬化-內核端(一)


中斷虛擬化-內核端

由於歷史原因,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注入中斷。



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM