中断虚拟化-内核端(一)
中断虚拟化-内核端
由于历史原因,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注入中断。