KVM 中断系统结构关系
现在,KVM虚拟机的中断是在KVM内核模块中实现的(一般我们都是这么使用的),理解数据结构就可以了解一个软件是如何实现的,所以本博文就来对虚拟中断子系统所涉及的数据结构进行梳理,给大家一个中断系统直观简单的感受。这里不多介绍apicv PI以及vt-d PI,以后博文再介绍,也可以直接请教Intel OTC的中断大牛WuFeng同学(邮件地址去邮件列表里面去搜索,姓氏在名字之后,别说我告诉你,哈哈哈)
1. 硬件基本知识
简单介绍点硬件知识,方便后面的讲解
最老的中断控制芯片就是8259A了,也就是PIC芯片,该种芯片8个引脚,为了扩展引脚数目,引入了主从两块芯片,利用一个引脚连接主从两个芯片,这样就一共有15个引脚了。IRQ = 其实base号 + pin。接收到设备中断后,通过拉高引脚通知CPU,CPU收到中断发送两个INT ACK到INTA,第二个INTA期间,PIC向CPU提交中断vector。
后来出现了APIC,APIC分为IOAPIC和LAPIC。IOAPIC位于南桥,有24个引脚针,LAPIC位与CPU内部。LAPIC每CPU一个;IO APIC可以一个系统里面有多个,但是KVM中只实现了一个,可以根据自己的需要进行添加。设备发送中断,发送到IO APIC;IO APIC中存在着一个PRT表,每个PRT的表项成为RTE,RTE是每个引脚一个,IOAPIC引脚收到中断消息后,根据RTE得到目标LAPIC,并格式化出一个中断消息发送给LAPIC,同时置位remote irr(level)。LAPIC接受到中断消息后,提取其中的VECTOR并设置IRR后,进行中断的选取,取得优先级最高的中断后,清除IRR,设置ISR,提交CPU进行中断处理,CPU处理完中断后,写LAPIC的EOI,通知IOAPIC清除remote irr(level且deassert)。
后来又出来的MSI中断,设备直接如果支持MSI的话,直接构造出MSI消息,MSI有个字段Address标明了中断目标地址,然后设备直接发送中断给LAPIC提交CPU,这种方式下,直接绕过了IOAPIC,效率更高。
这里需要额外说明一下,中断的触发方式分为level触发和edge触发;level触发就是一直将引脚保持在高电平(电平为1)直到中断完成,现在PCI/PCIE设备都是level触发;edge就是通过一个电平边缘跳变来触发的中断,电平从0到1,或者从1到0,原来的ISA设备都是edge触发,kvm中通过发送level = 0和level = 1的两个中断来模拟一个edge中断。
GSI 是ACPI 引入的概念,全称是Global System Interrupt。它为系统中每个中断源指定一个唯一的中断号。如果GSI base 为0,每个管脚的GSI=GSI base + pin,15以上的GSI号和IRQ值相等,但是[0~15]是按照规范映射的,其实试试GSI 2映射到了IRQ 0上了,在KVM中引脚号就是IRQ号。
2. 虚拟中断子系统逻辑图
KVM中断系统逻辑图如下
KVM将所有类型的IRQ CHIP抽象出一个接口,类似于C++的interface抽象基类,定义了中断的触发方法set()以及每个引脚和GSI的映射关系,这些都是和芯片类型无关的,具体的芯片都是继承和实现接口,虚拟设备的中断发送给IRQ CHIP接口开始中断的模拟;分别实现了PIC、IOAPIC以及MSI的set方法进行,通过set方法完成对于VCPU的中断注入。其中IOAPIC中对于每个引脚,又定义了PRT,IOAPIC收到中断后根据RTE格式化出中断消息并发送给目标LAPIC,LAPIC完成中断的选举和注入实现。
3 虚拟中断子系统的关系
这个小节我们来看看KVM的具体实现。
3.1 IRQ抽象接口层
KVM中irq_routing指向了IRQ CHIP抽象接口的表kvm_irq_routing_table结构体。
kvm_irq_routing_table .chip每个成员对应一个引脚,内容记录了该引脚的GSI号,其实该成员已经不用了。kvm_irq_routing_table.map成员指向一个hash桶,桶上的每个元素对应一个抽象引脚,即每个链对一个IRQ号,链上的每个元素指向kvm_kernel_irq_routing_entry结构体,每个该结构体代表了一个具体芯片的set()方法的实现,在进行中断注入的时候,每种芯片的set方法都会被调用一下,在guest中会忽略没有实现的芯片类型发送的中断消息(其实这里是可以优化的,必经不需要多次的中断注入,在QEMU注册中断设备的时候就不要注册不需要的中断芯片类型)
kvm_kernel_irq_routing_entry结构体对应具体芯片的一个引脚,按照芯片类型实现kvm_kernel_irq_routing_entry.set接口,kvm_kernel_irq_routing_entry.chip成员记录了该结构体对应的具体是哪种芯片的那个引脚,kvm_kernel_irq_routing_entry.gsi记录了该引脚的gsi号。
可以参考kvm_set_routing_entry()、kvm_set_irq()函数
3.2 PIC 芯片结构
PIC是系统一个,由kvm.arch.vpic成员指向
Kvm_pic结构体就是虚拟PIC的实现dev_master、dev_slave、dev_eclr实现了虚拟PIC设备的PIO地址空间,并注册在KVM内核的PIO总线上,以便完成设备的IO操作,这个我会在后面的博文中对大家介绍,ECLR就是控制中断触发方式的寄存器; pics执行一个数组,每个元素指向kvm_kpic_state结构体,kvm_kpic_state结构体芯片的所有寄存器和状态,包括IRR,ISR,IMR等
3.3 IOAPIC芯片结构
IOAPIC在KVM实现中只有一个,由kvm.arch.vioapic指向kvm_ioapic结构体。
kvm_ioapic结构体就是虚拟IOAPIC。irr成员就是IRR寄存器。redirtbl[IOAPIC_NUM_PINS]是一个数组,构成PRT表,每个成员对一个IOAPIC的引脚,执行kvm_ioapic_redirect_entry结构体。
kvm_ioapic_redirect_entry结构体就是一个RTE,fields成员执行了RTE中的各个域,如dest_id和dest_mode用来确定目标CPU,vector用来确定中断向量号,remote_irr在level触发时候保证中断共享情况下可以处理到所有设备的中断请求。Fields和kvm_kernel_irq_routing_entry的gsi配合起来确定了一个完整的中断信息内容
3.4 XXOO
3.5 LAPIC芯片结构体
LAPIC是每个VCPU一个,由kvm.vcpus[i].arch.apic指向kvm_lapic结构体。
kvm_lapic结构体代表一个虚拟LAPIC。base_address为LAPIC的基地址,这是一个GPA,是GUEST设置的LAPIC的基地址,在GUEST写某个LAPIC寄存器的时候,会带入该寄存器的GPA',然后GPA'减去base_address可以算出寄存器对于基地址的偏移量,然后到regs成员中真正的找到虚拟寄存器加以修改,请参见apic_mmio_read,apic_mmio_write;dev成员将lapic在KVM内核空间的KVM MMIO BUS总线上注册了IO地址空间,通过对IO地址空间的操作完成对LAPIC的操作。regs成员指向一个页面,这是一个HVA,指向一个HOST的页面,这个页面总记录了LAPIC是所有虚拟寄存,IRR,ISR,LVT等等,通过偏移量就可以得到某个虚拟寄存器的HVA,完成对该虚拟寄存器的读写操作。
kvm_lapic_irq结构体是中断经过IOAPIC的PRT格式化出来的中断消息,从IOAPIC发送给LAPIC。
LAPIC在GUEST运行之前会对IRR中的进行选举,找到最高优先级的中断,该中断会被记录在kvm_queued_interrupt结构体中并且pending成员设置为true;nr成员记录了该中断的中断向量号,soft说明是否是硬中断。如果该中断大于PPR,就进行注入。如果注入成功,在虚拟机退出的时候,将pending设置为false,这个结构体保存着kvm.vcpus[i].arch.interrupt中。如果注入不成功,会在再次进入虚拟机的时候再次进行注入。
为了加速对于目标VCPU的查找,在kvm.arch.apic_map中保存了kvm_apic_map结构体。phys_map成员和logical_map成员记录了RTE的destination filed同VLAPIC结构体的对应的关系,分别对应physical mode和logic mode。在发送中断的时候,如果有该map表,且中断不是lowest priority和广播,则通过RTE的destination filed就可以直接找到目标VCPU,进行快速的分发。否则需要遍历所有的VCPU,逐一的和RTE的destination filed进行匹配。