中斷虛擬化-內核端
由於歷史原因,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種比較特殊的情況,會為中斷注入帶來延時。
- 中斷產生時,vCPU處於休眠狀態,中斷無法被Guest及時處理。
- 中斷產生時,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部分信息形成:
- 即將作為IO設備注冊到Guest的設備相關信息
- 一個定時器
- 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部分信息:
- 即將作為IO設備注冊到Guest的設備信息
- IOAPIC的基礎,如用於標記IOAPIC身份的APIC ID,用於選擇APIC-page中寄存器的寄存器IOREGSEL。
- 重定向表
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_read
和ioapic_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
ioapicsThis 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類信息:
-
中斷號及其對應的中斷生成函數set
-
該中斷號對應的中斷芯片類型及對應管腳
- 用戶中斷路由結構
還引入了一個用於表示用戶空間傳入的中斷路由信息的結構,即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()
:
- 將用戶傳入的gsi直接賦值給內核中斷路由entry的gsi
- 根據用戶傳入的中斷路由entry的中斷芯片類型為entry設備不同的set函數
- 根據中斷芯片類型為內核中斷路由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 Pointer
、Capability ID
這兩個field是PCI的任何Capability都具有的field,分別表示下一個Capability在配置空間的位置、以及當前Capability的ID。
Message Address
和Message 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 Data
和Message 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 BIR
和PBA 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注入中斷。