前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux中断虚拟化(二)

Linux中断虚拟化(二)

作者头像
Linux阅码场
发布2021-12-02 10:06:06
5.8K0
发布2021-12-02 10:06:06
举报
文章被收录于专栏:LINUX阅码场LINUX阅码场

作者简介

王柏生 资深技术专家,先后就职于中科院软件所、红旗Linux和百度,现任百度主任架构师。在操作系统、虚拟化技术、分布式系统、云计算、自动驾驶等相关领域耕耘多年,有着丰富的实践经验。著有畅销书《深度探索Linux操作系统》(2013年出版)。

谢广军 计算机专业博士,毕业于南开大学计算机系。资深技术专家,多年的IT行业工作经验。现担任百度智能云副总经理,负责云计算相关产品的研发。多年来一直从事操作系统、虚拟化技术、分布式系统、大数据、云计算等相关领域的研发工作,实践经验丰富。

本文内容节选自《深度探索Linux虚拟化技术》,已获得机械工业出版社华章公司授权。

PIC虚拟化

计算机系统有很多的外设需要服务,显然,CPU采用轮询的方式逐个询问外设是否需要服务,是非常浪费CPU的计算的,尤其是对那些并不是频繁需要服务的设备。因此,计算机科学家们设计了外设主动向CPU发起服务请求的方式,这种方式就是中断。采用中断方式后,在没有外设请求时,CPU就可以继续其他计算任务,而不是进行很多不必要的轮询,极大地提高了系统的吞吐[1] 在每个指令周期结束后,如果CPU关中断标识(IF)没有被设置,那么其会去检查是否有中断请求,如果有中断请求,则运行对应的中断服务程序,然后返回被中断的计算任务继续执行。

CPU不可能为每个硬件都设计专门的管脚接收中断,管脚数量的限制、电路的复杂度、灵活度等方方面面都不现实,因此,需要设计一个专门管理中断的单元。由中断管理单元接受来自外围设备的请求,确定请求的优先级,并向CPU发出中断。1981年IBM推出的第1代个人电脑PC/XT使用了一个独立的8259A作为中断控制器,自此,8259A就成为了单核时代中断芯片事实上的标准。因为可以通过软件编程对其进行控制,比如当管脚收到设备信号时,可以编程控制其发出的中断向量号,因此,中断控制器又称为可编程中断控制器(programmable interrupt controller),简称PIC。单片8259A可以连接8个外设的中断信号线,可以多片级联支持更多外设。

8259A和CPU的连接如图5所示。

图5 8259A和CPU连接

片选和地址译码器相连,当CPU准备访问8259A前,需要向地址总线发送8259A对应的地址,经过译码器后,译码器发现是8259A对应的地址,因此会拉低与8259A的CS连接的管脚的电平,从而选中8259A,通知8259A,CPU准备与其交换数据了。

8259A的D0~7管脚与CPU的数据总线相连。从CPU向8259A发送ICW和OCW,从8259A向CPU传送8259A的状态以及中断向量号,都是通过数据总线传递的。

当CPU向8259A发送ICW、OCW时,当把数据送上数据总线后,需要通知8259A读数据,CPU通过拉低WR管脚的电平的方式通知8259A,当8259A的WR管脚收到低电平后,读取数据总线的数据。类似的,CPU准备好读取8259A的状态时,拉低RD管脚通知8259A。

8259A和CPU之间的中断信号的通知使用专用的连线,8259A的管脚INTR(interrupt request)和INTA(interrupt acknowledge)分别与处理器的INTR和INTA管脚相连。8259A通过管脚INTR向CPU发送中断请求,CPU通过管脚INTA向PIC发送中断确认,告诉PIC其收到中断并且开始处理了。8259A与CPU之间的具体中断过程如下:

1)8259A的IR0~7管脚高电平有效,所以当中断源请求服务时,拉高连接IR0~7的管脚,产生中断请求。

2)8259A需要将这些信号记录下来,因此其内部有个寄存器IRR(Interrupt Request Register),负责记录这个中断请求,针对这个例子,IRR的bit 0将被设置为1。

3)有的时候,我们会屏蔽掉某个设备的中断。换句话说,就是的当这个中断源向8259A发送信号后,8259A并不将这个中断信号发送给CPU。读者不要将屏蔽和CPU通过cli命令关中断混淆,CPU关中断时,中断还会发送给CPU,只是在关中断期间CPU不处理中断。8259A中的寄存器IMR(Interrupt Mask Register)负责记录某个中断源是否被屏蔽,比如0号中断源被设备了屏蔽,那么IMR的bit 0将被设置。那么这个IMR是谁设置的呢?当然是CPU中的操作系统。因此这一步,8259A将会检查收到的中断请求是否被屏蔽。

4)在某一个时刻,可能有多个中断请求,或者是之前存在IRR中的中断并没有被处理,8259A中积累了一些中断。某一个时刻,8259A只能向CPU发送一个中断请求,因此,当存在多个中断请求时,8259A需要判断一下中断优先级,这个单元叫做priority resolver,priority resolver将在IRR中选出优先级最高的中断。

5)选出最高优先级的中断后,8259A拉高管脚INTR的电平,向CPU发出信号。

6)当CPU执行完当前指令周期后,其将检查寄存器FLAGS的中断使能位IF(Interrupt enable flag),如果允许中断,那么将检查INTR是否有中断,如果有中断,那么将通过管脚INTR通知8259A处理器将开始处理中断。

7)8259A收到CPU发来的INTA信号后,将置位最高优先级的中断在ISR(In-Service Register)中对应的位,并清空IRR中对应的位。

8)通常,x86 CPU会发送第2次INTA,在收到第2次INTA后,8259A会将中断向量号(vector)送上数据总线D0~D7。

9)如果8259A设置为AEOI(Automatic End Of Interrupt)模式,那么8259A复位ISR中对应的bit,否则ISR中对应的bit一直保持到收到系统的中断服务程序发来的EOI命令。

我们知道,中断服务程序保存在一个数组中,数组中的每一项对应一个中断服务程序。在实模式下,这个数组称为IVT(interrupt vector table);在保护模式下,这个数组称为IDT(Interrupt descriptor table)。

这个数组中保存的服务程序,并不是全部都是外部中断,还有处理CPU内部异常的以及软中断服务程序。x86CPU前32个中断号(0-31)留给处理器异常的,比如第0个中断号,是处理器出现除0(Divide by Zero)异常的,不能被占用。因此,假设我们计划IVT数组中第32个元素存放管脚IR0对应的ISR,那么我们初始化8259A时,通过ICW,设置起始的irq base为32,那么当8259A发出管脚IR0的中断请求时,则发出的值是32,管脚IR1对应的是33,依此类推。这个32、33就是所谓的中断向量(vector)。换句话说,中断向量就是中断服务程序在IVT/IDT中的索引。下面就是设置irq_base的代码,在初始化时,通过第2个初始化命令字(ICW2)设置:

commit 85f455f7ddbed403b34b4d54b1eaf0e14126a126

KVM: Add support for in-kernel PIC emulation

linux.git/drivers/kvm/i8259.c

static void pic_ioport_write(void *opaque, u32addr, u32 val)

{

 switch(s->init_state) {

case 1:

s->irq_base = val & 0xf8;

}

}

后来,随着APIC和MSI的出现,中断向量设置的更为灵活,可以为每个PCI设置其中断向量,这个中断向量存储在PCI设备的配置空间中。

内核中抽象了一个结构体kvm_kpic_state来记录每个8259A的状态:

commit 85f455f7ddbed403b34b4d54b1eaf0e14126a126

KVM: Add support for in-kernel PIC emulation

struct kvm_kpic_state {

u8last_irr; /* edge detection */

u8 irr; /* interrupt request register */

u8imr; /* interrupt mask register */

u8isr; /* interrupt service register */

};

struct kvm_pic {

structkvm_kpic_state pics[2]; /* 0 is master pic, 1 is slave pic*/

irq_request_func *irq_request;

void*irq_request_opaque;

intoutput; /* intr from master PIC */

structkvm_io_device dev;

};

1片8259A只能连接最多8个外设,如果需要支持更多外设,需要多片8259A级联。在结构体kvm_pic中,我们看到有2片8259A:pic[0]和pic[1]。KVM定义了结构体kvm_kpic_state记录8259A的状态,其中包括我们之前提到的IRR、IMR、ISR等等。

1 虚拟设备向PIC发送中断请求

如同物理外设请求中断时拉高与8259A连接的管脚的电压,虚拟设备请求中断的方式是通过一个API告诉虚拟的8259A芯片中断请求,以kvmtool中的virtio blk虚拟设备为例:

commit 4155ba8cda055b7831489e4c4a412b073493115b

kvm: Fix virtio block device support some more

kvmtool.git/blk-virtio.c

static bool blk_virtio_out(…)

{

 caseVIRTIO_PCI_QUEUE_NOTIFY: {

while(queue->vring.avail->idx != queue->last_avail_idx) {

if(!blk_virtio_read(self, queue))

return false;

}

kvm__irq_line(self, VIRTIO_BLK_IRQ, 1);

break;

}

}

当Guest内核的块设备驱动发出I/O通知VIRTIO_PCI_QUEUE_NOTIFY时,将触发CPU从Guest模式切换到Host模式,KVM中的块模拟设备开始I/O操作,比如访问保存Guest文件系统的镜像文件。virtio blk这个提交,块设备的I/O处理是同步的,也就是说,一直要等到文件操作完成,才会向Guest发送中断,返回Guest。当然同步阻塞在这里是不合理的,而是应该马上返回Guest,这样Guest可以执行其他的任务,虚拟设备完成I/O操作后,再通知Guest,这是kvmtool初期的实现,后来已经改进为异步的方式。代码中在一个while循环处理完设备驱动的I/O请求后,调用了函数kvm__irq_line,irq_line对应8259A的管脚IR0~7,其代码如下:

commit 4155ba8cda055b7831489e4c4a412b073493115b

kvm: Fix virtio block device support some more

kvmtool.git/kvm.c

void kvm__irq_line(struct kvm *self, int irq, intlevel)

{

structkvm_irq_level irq_level;

irq_level= (struct kvm_irq_level) {

{

.irq = irq,

},

.level = level,

};

if(ioctl(self->vm_fd, KVM_IRQ_LINE, &irq_level) < 0)

die_perror("KVM_IRQ_LINE failed");

}

函数kvm__irq_line将irq number和管脚电平信息,这里是1,表示拉高电平了,封装到结构体kvm_irq_level中,传递给内核中的KVM模块:

commit 85f455f7ddbed403b34b4d54b1eaf0e14126a126

KVM: Add support for in-kernel PIC emulation

linux.git/drivers/kvm/kvm_main.c

static long kvm_vm_ioctl(…)

{

 caseKVM_IRQ_LINE: {

kvm_pic_set_irq(pic_irqchip(kvm),

irq_event.irq,

irq_event.level);

break;

}

}

KVM模块将kvmtool中组织的中断信息从用户空间复制到内核空间中,然后调用虚拟8259A的模块中提供的API kvm_pic_set_irq,向8259A发出中断请求。

2 记录中断到IRR

中断处理需要一个过程,从外设发出请求,一直到ISR处理完成发出EOI。而且可能中断来了并不能马上处理,或者之前已经累加了一些中断,大家需要排队依次请求CPU处理,等等,因此,需要一些寄存器来记录这些状态。当外设中断请求到来时,8259A首先需要将他们记录下来,这个寄存器就是IRR(Interrupt Request Register),8259A用他来记录有哪些pending的中断需要处理。

当KVM模块收到外设的请求,调用虚拟8259A的API kvm_pic_set_irq是,其第1件事就是将中断记录到IRR寄存器中:

commit 85f455f7ddbed403b34b4d54b1eaf0e14126a126

KVM: Add support for in-kernel PIC emulation

linux.git/drivers/kvm/i8259.c

void kvm_pic_set_irq(void *opaque, int irq, intlevel)

{

structkvm_pic *s = opaque;

pic_set_irq1(&s->pics[irq >> 3], irq & 7, level);

……

}

static inline void pic_set_irq1(structkvm_kpic_state *s,

int irq, int level)

{

int mask;

mask = 1<< irq;

if(s->elcr & mask) /* level triggered */

else /* edge triggered */

if(level) {

if((s->last_irr & mask) == 0)

s->irr |= mask;

s->last_irr |= mask;

} else

s->last_irr &= ~mask;

}

信号有边缘触发和水平触发,在物理上可以理解为,8329A在前一个周期检测到管脚信号是0,当前周前检测到管脚信号是1,如果是上升沿触发模式,那么8259A就认为外设有请求了,这种触发模式就是边缘触发。对于水平触发,以高电平触发为例,当8259A检测到管脚处于高电平,则认为外设来请求了。

在虚拟8259A的结构体kvm_kpic_state中,寄存器elcr就是用来记录8259A被设置的触发模式的。参数level即相当于硬件层面的电信号,0表示低电平,1表示高电平。以边缘触发为例,当管脚收到一个低电平时,即level的值为0,代码进入else分支,结构体kvm_kpic_state中的字段last_irr中会清除该IRQ对应IRR的位,即相当于设置该中断管脚为低电平状态。当管脚收到高电平时,即level的值为1,代码进入if分支,此时8259A将判断之前该管脚的状态,也就是判断结构体kvm_kpic_state中的字段last_irr中该IRQ对应IRR的位,如果为低电平,那么则认为中断源有中断请求,将其记录到IRR中。当然,同时需要在字段last_irr记录下当前该管脚的状态。

3 设置中断标识

当8259A将中断请求记录到IRR中后,下一步就是开启一个中断评估(evaluate)过程了,包括中断是否被屏蔽,多个中断请求的优先级等等,最后将通过管脚INTA通知CPU处理外部中断。我们看到这里是8259A主动发起中断过程,但是虚拟中断有些不同,中断的发起的时机不再是虚拟中断芯片主动发起,而是在每次准备切入Guest时,KVM查询中断芯片,如果有pending的中断,则执行中断注入。模拟8259A在收到中断请求后,在记录到IRR后,设置一个变量,后面在切入Guest前KVM会查询这个变量:

commit 85f455f7ddbed403b34b4d54b1eaf0e14126a126

KVM: Add support for in-kernel PIC emulation

linux.git/drivers/kvm/i8259.c

void kvm_pic_set_irq(void *opaque, int irq, intlevel)

{

structkvm_pic *s = opaque;

pic_set_irq1(&s->pics[irq >> 3], irq & 7, level);

 pic_update_irq(s);

}

static void pic_update_irq(struct kvm_pic *s)

{

irq =pic_get_irq(&s->pics[0]);

if (irq>= 0)

s->irq_request(s->irq_request_opaque, 1);

else

s->irq_request(s->irq_request_opaque, 0);

}

static void pic_irq_request(void *opaque, intlevel)

{

struct kvm*kvm = opaque;

pic_irqchip(kvm)->output = level;

}

在函数vmx_vcpu_run中,在准备切入Guest之前将调用函数vmx_intr_assist去检查虚拟中断芯片是否有等待处理的中断,相关代码如下:

commit 85f455f7ddbed403b34b4d54b1eaf0e14126a126

KVM: Add support for in-kernel PIC emulation

linux.git/drivers/kvm/vmx.c

static int vmx_vcpu_run(…)

{

vmx_intr_assist(vcpu);

}

static void vmx_intr_assist(struct kvm_vcpu*vcpu)

{

has_ext_irq= kvm_cpu_has_interrupt(vcpu);

if(!has_ext_irq)

return;

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));

}

其中函数kvm_cpu_has_interrupt查询8259A设置的变量output:

commit 85f455f7ddbed403b34b4d54b1eaf0e14126a126

KVM: Add support for in-kernel PIC emulation

linux.git/drivers/kvm/irq.c

int kvm_cpu_has_interrupt(struct kvm_vcpu *v)

{

structkvm_pic *s = pic_irqchip(v->kvm);

if(s->output) /* PIC */

return1;

return 0;

}

如果有output设置了,就说明有外部中断等待处理,然后接着判断Guest是否可以被中断,包括Guest是否中断了,Guest是否正在执行一些不能被中断的指令,如果可以注入,则调用vmx_inject_irq完成中断的注入。其中,传递给函数vmx_inject_irq的第2个参数是函数kvm_cpu_get_interrupt返回的结果,该函数获取需要注入的中断,这个过程就是中断评估过程,我们下一节讨论。

4 中断评估

在上一节我们看到在执行注入前,vmx_inject_irq调用函数kvm_cpu_get_interrupt获取需要注入的中断。函数kvm_cpu_get_interrupt的核心逻辑就是中断评估(evaluate),包括:这个pending的中断有没有被屏蔽?pending的中断的优先级是否比CPU正在处理的中断优先级高?代码如下:

commit 85f455f7ddbed403b34b4d54b1eaf0e14126a126

KVM: Add support for in-kernel PIC emulation

linux.git/drivers/kvm/irq.c

int kvm_cpu_get_interrupt(struct kvm_vcpu *v)

{

……

vector =kvm_pic_read_irq(s);

if (vector!= -1)

returnvector;

}

linux.git/drivers/kvm/i8259.c

int kvm_pic_read_irq(struct kvm_pic *s)

{

int irq,irq2, intno;

irq =pic_get_irq(&s->pics[0]);

if (irq>= 0) {

intno = s->pics[0].irq_base + irq;

} else {

returnintno;

}

kvm_pic_read_irq调用函数pic_get_irq获取评估后的中断,如上面黑体标识的,我们可以清楚的看到中断向量和中断管脚的关系,叠加了一个irq_base,这个irq_base就是通过初始化8259A时通过ICW设置的,完成从IRn到中断向量的转换。

一个中断芯片通常连接有多个外设,所以在某一个时刻,可能会有多个设备请求到来,这时就有一个优先处理哪个请求的问题,因此,中断就有了优先级的概念。以8259A为例,典型的有2种中断优先级模式:

1)固定优先级(Fixedpriority),即优先级是固定的,从IR0到IR7依次降低,IR0的优先级永远最高,IR7的优先级永远最低。

2)循环优先级(rotatingpriority),即当前处理完的IRn其优先级调整为最低,当前处理的优先级下个,即IRn+1,调整为优先级最高。比如,当前处理的中断是irq 2,那么紧接着irq3的优先级设置为是最高,然后依次是irq4、irq5、irq6、irq7、irq1、irq2、irq3。假设此时irq5和irq2同时来了中断,那么irq5显然会被优先处理。然后irq6被设置为优先级最高,接下来依次是irq7、irq1、irq2、irq3、irq4、irq5。

理解了循环优先级算法后,从8259A中获取最高优先级请求的代码就很容易理解了:

commit85f455f7ddbed403b34b4d54b1eaf0e14126a126

KVM: Add support for in-kernel PIC emulation

linux.git/drivers/kvm/i8259.c

static int pic_get_irq(struct kvm_kpic_state *s)

{

int mask,cur_priority, priority;

mask =s->irr & ~s->imr;

priority =get_priority(s, mask);

if(priority == 8)

return-1;

mask =s->isr;

cur_priority = get_priority(s, mask);

if(priority < cur_priority)

/*

*higher priority found: an irq should be generated

*/

return(priority + s->priority_add) & 7;

else

return-1;

}

static inline int get_priority(structkvm_kpic_state *s, int mask)

{

intpriority;

if (mask== 0)

return8;

priority =0;

while((mask & (1 << ((priority + s->priority_add) & 7))) == 0)

priority++;

returnpriority;

}

函数pic_get_irq分成2部分,第1部分是从当前pending的中断中取得最高优先级的中断,当前之前需要滤掉被被屏蔽的中断。第2部分是获取正在被CPU处理的中断的优先级的中断的优先级,通过这里,读者更能具体的理解了8259A为什么需要这些寄存器记录中断的状态。然后比较2个中断的优先级,如果pending的优先级高,那么就通过拉低INTR管脚电压,向CPU发出中断请求。

再来看一下计算优先级的函数get_priority。其中变量priority_add记录的是当前最高优先级的管脚,所以逻辑上就是从当前最高的优先级管脚开始,从高向低依次检查是否有pending的中断。比如当前IR4最高,那么priority_add的值就是4。while循环,从管脚IR(4+0)开始,依次检查管脚IR(4+1)、IR(4+2),依此类推。

5 中断ACK

在物理上,CPU在准备处理一个中断请求后,将通过管脚INTA向8259A发出确认脉冲。同样,软件模拟上,也需要类似处理。在完成中断评估后,准备注入Guest前,需要向虚拟8259A执行确认状态的操作,代码如下:

commit 85f455f7ddbed403b34b4d54b1eaf0e14126a126

KVM: Add support for in-kernel PIC emulation

linux.git/drivers/kvm/i8259.c

int kvm_pic_read_irq(struct kvm_pic *s)

{

int irq,irq2, intno;

irq =pic_get_irq(&s->pics[0]);

if (irq>= 0) {

pic_intack(&s->pics[0], irq);

}

static inline void pic_intack(structkvm_kpic_state *s, int irq)

{

 if(s->auto_eoi) {

} else

s->isr |= (1 << irq);

/*

* Wedon't clear a level sensitive interrupt here

*/

if(!(s->elcr & (1 << irq)))

s->irr &= ~(1 << irq);

}

在中断评估中,在调用函数kvm_pic_read_irq获取评估的中断结果后,马上就调用函数pic_intack完成了中断确认的动作。在收到CPU发来的中断确认后,8259A需要更新自己的状态,包括,因为中断已经开始得到服务了,所以从IRR中清除等待服务请求。另外,需要设置ISR位,记录正在被服务的中断,但是这里稍微有一点点复杂。

设置ISR表示正在服务的位,表示处理器正在处理中断。设置ISR的一个典型作用是中断的作用是当ISR处理完中断,向8259A发送EOI时,8259A知道正在处理的IRn,比如说如果8259A使用的是循环优先级,那么需要最高优先级为当前处理的IRn的下一个。

如果8259A是AEOI模式,那么就无须设置ISR了,因为中断服务程序执行完毕后不会发送EOI命令。所以在AEOI模式下(上面代码的if分支),需要将收到EOI时8259A需要处理的逻辑完成,这部分内容我们在下一节会讨论。

对于边缘触发的,进入到ISR阶段后,需要复位对于IRR。对于level trigger的,在收到中断请求后8259A处理,不过多讨论了,如果读者有兴趣,可以阅读函数pic_set_irq1中关于水平触发部分的逻辑。

6 关于EOI的处理

中断服务程序执行完成后,会向8259A发送EOI,告知8259A中断处理完成。8259A在收到这个EOI时,复位ISR,如果是采用的循环优先级,还需要设置变量priority_add,使其指向当前处理IRn的下一个:

commit85f455f7ddbed403b34b4d54b1eaf0e14126a126

KVM: Add support for in-kernel PIC emulation

linux.git/drivers/kvm/i8259.c

static void pic_ioport_write(void *opaque, u32addr, u32 val)

{

 case1: /* end of interrupt */

case5:

priority = get_priority(s, s->isr);

if(priority != 8) {

irq = (priority + s->priority_add) & 7;

s->isr &= ~(1 << irq);

if(cmd == 5)

s->priority_add = (irq + 1) & 7;

pic_update_irq(s->pics_state);

}

break;

}

如果8259A被设置为AEOI模式,不会再收到后续中断服务程序的EOI命令,那么8259A在收到CPU的ACK后,就必须把收到EOI命令时执行的逻辑现在处理,前面看到AEOI模式不必设置ISR,所以这里也无需复位ISR,只需要调整变量priority_add,记录最高优先级位置即可:

commit 85f455f7ddbed403b34b4d54b1eaf0e14126a126

KVM: Add support for in-kernel PIC emulation

static inline void pic_intack(structkvm_kpic_state *s, int irq)

{

if(s->auto_eoi) {

if(s->rotate_on_auto_eoi)

s->priority_add = (irq + 1) & 7;

} else

}

7 中断注入

对于外部中断,每个CPU在指令周期结束后,将会去检查INTR是否有中断请求。那么对于处于Guest模式的CPU,其如何知道有中断请求呢?Intel在VMCS中设置了一个字段:VM-entry interruption-information,在VM entry时CPU将会检查这个字段,这个字段格式表3-1所示。

表3-1 VM-entry interruption-information格式(部分)

内容

7:0

中断或异常向量

10:8

中断类型:0: External interrupt1: Reserved2: Non-maskable interrupt (NMI)3: Hardware exception4: Software interrupt5: Privileged software exception6: Software exception7: Other event

31

是否有效[2]

在VM entry前,KVM模块检查虚拟8259A中如果有pending中断需要处理,则将需要处理的中断信息写入到VMCS中的这个字段VM-entry interruption-information:

commit 85f455f7ddbed403b34b4d54b1eaf0e14126a126

KVM: Add support for in-kernel PIC emulation

linux.git/drivers/kvm/vmx.c

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);

}

前面我们看到,中断注入是在每次VM entry时,KVM模块检查8259A是否有pending的中断等待处理。这样就有可能给中断带来一定的延迟,典型如下面2类情况:

(1)CPU可能正处在Guest模式,那么就需要等待下一次VM exit 和VM entry。

(2)VCPU这个线程也许正在睡眠,比如Guest VCPU运行hlt指令时,就会切换回Host模式,线程挂起。

对于第1种情况,是多处理器系统下的一个典型情况,目标CPU的正在运行Guest。KVM需要想办法触发Guest发生一次VM exit,切换到Host。我们知道,当处于Guest模式的CPU收到外部中断时,会触发VM exit,由Host来处理这次中断。所以,KVM可以向目标CPU发送一个IPI中断,触发目标CPU发生一次VM exit。

对于第2种情况,首先需要唤醒睡眠的VCPU线程,使其进入CPU就绪队列,准备接受调度。对于多处理器系统,然后再向目标CPU发送一个“重新调度”的IPI中断,那么被唤醒的VCPU线程很快就会被调度,执行切入Guest的过程,从而完成中断注入。

所以当有中断请求时,虚拟中断芯片将主动“kick”一下目标CPU,这个“踢”的函数就是kvm_vcpu_kick:

commit b6958ce44a11a9e9425d2b67a653b1ca2a27796f

KVM: Emulate hlt in the kernel

linux.git/drivers/kvm/i8259.c

static void pic_irq_request(void *opaque, intlevel)

{

pic_irqchip(kvm)->output = level;

if (vcpu)

kvm_vcpu_kick(vcpu);

}

如果虚拟CPU线程在睡眠,则“踢醒”他。如果目标CPU运行在Guest模式,则将其从Guest模式“踢”到Host模式,在VM entry时完成中断注入,kick的手段就是我们刚刚提到的IPI,代码如下:

commit b6958ce44a11a9e9425d2b67a653b1ca2a27796f

KVM: Emulate hlt in the kernel

linux.git/drivers/kvm/irq.c

void kvm_vcpu_kick(struct kvm_vcpu *vcpu)

{

intipi_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);

}

如果VCPU线程睡眠在等待队列上,则唤醒使其进入CPU的就绪任务队列。如果是多CPU的情况且目标CPU处于Guest模式,则需要发送核间中断。如果目标CPU正在执行Guest,那么这个IPI中断将导致VM exit,从而在下一次进入Guest时,可以注入中断。

事实上,目标CPU无须执行任何callback,也无须等待IPI返回,所以也无须使用smp_call_function_single,而是直接发送一个请求目标CPU重新调度的IPI即可,因此后来直接调用了函数smp_send_reschedule。函数smp_send_reschedule简单直接,直接发送了一个RESCHEDULE的IPI:

commit 32f8840064d88cc3f6e85203aec7b6b57bebcb97

KVM: use smp_send_reschedule in kvm_vcpu_kick

linux.git/arch/x86/kvm/x86.c

void kvm_vcpu_kick(struct kvm_vcpu *vcpu)

{

smp_send_reschedule(cpu);

}

linux.git/arch/x86/kernel/smp.c

static void native_smp_send_reschedule(int cpu)

{

apic->send_IPI_mask(cpumask_of(cpu), RESCHEDULE_VECTOR);

}

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-12-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Linux阅码场 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 虚拟设备向PIC发送中断请求
  • 2 记录中断到IRR
  • 3 设置中断标识
  • 4 中断评估
  • 5 中断ACK
  • 6 关于EOI的处理
  • 7 中断注入
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档