中断虚拟化-内核端(一)
中断虚拟化-内核端
由于历史原因,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
结构描述。
可以看到kvm_kpic_state
结构完整描述了一个8259A中断控制器应该具有的所有基本特征,包括寄存器IRR、IMR、ISR等。
在该结构的最后定义了指向kvm_pic
的指针,kvm_pic
是一个更加高级的结构:
kvm_pic
中包含了2片8259A,一片为master,一片为slave,具体来说,pic[0]
为master,pic[1]
为slave。
通过这两个数据结构,kvm对PIC进行了模拟。
PIC的创建过程
KVM为PIC的创建提供了ioctl接口,QEMU只需调用相关接口就可以在KVM中创建一个PIC。
当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通过ioctl(KVM_IRQ_LINE)向kvm发送中断注入请求。
总结一下,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
函数。
所以大体上的流程就是如果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));
.
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
在调用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
函数来定义。
发送EOI
在pci_ioport_wirte
中还有一个比较重要的工作,就是发送EOI. 在PIC工作在非AEOI模式时,CPU处理中断完成之后,需要向PIC发送一个EOI,通知PIC清掉ISR中的相应bit,发送EOI的动作由Guest向PIC写入特定数据完成。
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().
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
:
可以看到该结构主要由3部分信息形成:
- 即将作为IO设备注册到Guest的设备相关信息
- 一个定时器
- Lapic的apic-page信息
IOAPIC
kvm为IOAPIC定义了结构体kvm_ioapic
:
该结构也可以看做3部分信息:
- 即将作为IO设备注册到Guest的设备信息
- IOAPIC的基础,如用于标记IOAPIC身份的APIC ID,用于选择APIC-page中寄存器的寄存器IOREGSEL。
- 重定向表
APIC的创建过程
虚拟IOAPIC的创建
之前在看PIC的创建过程时,提到QEMU可以通过IOCTL(KVM_CREATE_IRQCHIP)
在内核中申请创建一个虚拟PIC。那么IOAPIC也一样,在不知道Guest支持什么样的中断芯片时,会对PIC和IOAPIC都进行创建,Guest会自动选择合适自己的中断芯片。
注意,上面的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。
使用APIC时的中断流程
与PIC稍有不同的是,APIC既可以接收外部中断,也可以处理核间中断,而外部中断和核间中断的虚拟化流程是不同的,因此这里分为两个部分。
外部中断
假设Guest需要从一个外设读取数据,一般流程为vmexit到kvm/qemu,传递相关信息后直接返回Guest,当数据获取完成之后,qemu/kvm向Guest发送外部中断,通知Guest数据准备就绪。
那么qemu/kvm如何通过APIC向Guest发送中断呢?
QEMU的动作与向PIC发送中断请求时一样,都是调用kvm_set_irq()
向KVM发送中断请求。
可以看到,一旦QEMU发送了IOCTL(KVM_IRQ_LINE)
,KVM就会通过以下一系列的调用:
最终通过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()
.
可以看到,核间中断由Guest通过写apic-page中的ICR(中断控制寄存器)发起,vmexit到KVM,最终调用apic_mmio_write().
中断路由
KVM中有了APIC和PIC的实现之后,出现一个问题,一个来自外部的中断到底要走PIC还是APIC呢?
早期KVM的策略是,如果irq小于16,则APIC和PIC都走,即这两者的中断设置函数都调用。如果大于等于16,则只走APIC. Guest支持哪个中断芯片,就和哪个中断芯片进行交互。代码如下:
出于代码风格和接口一致原则,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
.
一个kvm_kernel_irq_routing_entry
提供了2类信息:
-
中断号及其对应的中断生成函数set
-
该中断号对应的中断芯片类型及对应管脚
- 用户中断路由结构
还引入了一个用于表示用户空间传入的中断路由信息的结构,即kvm_irq_routing_entry
.
用户空间的中断路由信息看起来更加简单,包含了一些中断类型、标志。最终中断怎样路由,由内核端决定。
默认中断路由
该patch中定义了一个默认中断路由,在QEMU创建中断芯片(IOCTL(KVM_CREATE_IRQCHIP))时创建。
也就是说,在用户空间向KVM申请创建内核端的中断芯片时,就会建立一个默认的中断路由表。代码路径为:
路由表由路由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。更加具体的默认中断路由代码实现如下。
接下来看看,在拥有了一个确认的中断路由时,setup_routing_entry()
具体如何操作。
setup_routing_entry()
:
- 将用户传入的gsi直接赋值给内核中断路由entry的gsi
- 根据用户传入的中断路由entry的中断芯片类型为entry设备不同的set函数
- 根据中断芯片类型为内核中断路由entry的pin设置正确的值
用户自定义中断路由
当然,该patch也为用户空间提供了传入其自定义中断路由表的接口,即IOCTL(KVM_SET_GSI_ROUTING)
.
可以看到这里同样调用了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的映射。
提前说明,具体的MSI/MSIX初始化是在vfio_add_capabilities
中完成的,那为什么要进行vfio_msix_early_setup
呢?
注释中写的很清楚,因为之后的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做了预处理。
到了这里分了两个部分,一个是MSI的初始化,一个是MSIX的初始化。分别来看。
MSI初始化
经过msi_init()
之后,一个完整的MSI Capability Structure就已经呈现在了vdev->pdev.config + capability_list + msi_offset
的位置。当然这个结构是QEMU根据实际物理设备而模拟出来的,实际物理设备中的Capability Structure没有任何变化。
MSIX初始化
在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的建立。
vfio_pci_dev_class_init()
将vfio_pci_write_config
注册为vfio设备的写配置空间的callback,当Guest对vfio设备的配置空间进行写入时,就会触发:
在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注入中断。
__EOF__

本文链接:https://www.cnblogs.com/haiyonghao/p/14440424.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了