1.中断概述
软件部分
硬件部分
中断硬件部分就是产生中断脉冲,传给中断控制器,而后通知CPU,CPU在执行下调指令前会去查询中断状况,若是有中断信号,就执行中断。
所以中断的模拟分为两个主要部分:一个是中断源的模拟,一个是给虚拟机的VCPU响应中断。
中断源模拟
虚拟机响应虚拟中断
KVM中断虚拟化主要依赖于VT-x技术,VT-x主要提供了两种中断事件机制,分别是中断退出和中断注入。
2.x86 中断机制
中断从设备发送到CPU需要经过中断控制器,现代x86架构采用的中断控制器被称为APIC(Advanced Programmable Interrupt Controller)。APIC是伴随多核处理器产生的,所有的核共用一个I/O APIC,用于统一接收来自外部I/O设备的中断,而后根据软件的设定,格式化出一条包含该中断所有信息的Interrupt Message,发送给对应的CPU。
每个核有一个Local APIC,用于接收来自I/O APIC的Interrupt Message,内部的时钟和温控中断,以及来自其他核的中断,也就是IPI(Inter-Processor Interrupt)。
物理CPU中断:
在虚拟化环境中,VMM需要为guest VM展现一个与物理中断架构类似的虚拟中断架构。每个虚拟CPU都对应一个虚拟的Local APIC,多个虚拟CPU共享一个虚拟的I/O APIC。
和虚拟CPU一样,虚拟的Local APIC和虚拟的I/O APIC都是VMM维护的软件实体(以下统称虚拟中断控制器)。中断虚拟化的主要任务就是实现下图描述的虚拟中断架构,包括虚拟中断控制器的创建,中断采集和中断注入。
中断采集
将guest VM的设备中断请求送入对应的虚拟中断控制器中。Guest VM的中断有两种可能的来源:
来自于软件模拟的虚拟设备,比如一个模拟出来的串口,可以产生一个虚拟中断。从VMM的角度来看,虚拟设备只是一个软件模块,可以通过调用虚拟中断控制器提供的接口函数,实现虚拟设备的中断发送。
来自于直接分配给guest VM的物理设备的中断,比如一个物理网卡,可以产生一个真正的物理中断。一个物理设备被直接分配给一个guest VM,意味着当该设备发生中断时,中断的处理函数(ISR)应该位于guest OS中。
可是在虚拟化环境中,物理中断控制器是由VMM控制的,因而VMM在收到中断后,会首先判断该中断是不是由分配给guest VM的设备产生的,如果是的话,就将中断发送给对应的虚拟中断控制器。而后,虚拟中断控制器会在适当的时机将该中断注入guest VM,由guest OS中的ISR进行处理。那中断注入的过程是怎样的,这个“适当的时机”又该如何选择呢?
中断注入
同样地,只有在VM entry,也就是某个虚拟CPU被调度到重新获得物理CPU的使用权时,VMM才可以将中断注入到该虚拟CPU中。
为了保证中断的及时注入,就需要通过一定的手段,强制虚拟CPU发生VM exit,然后在VM entry返回guest VM的时候注入中断。强制产生VM exit最常用的办法就是往虚拟CPU对应的物理CPU发送一个IPI核间中断。这个IPI就像一把手枪,把guest VM打下来,落回到VMM中。
是不是VMM注入的中断,虚拟CPU就必须照单全收呢?Linux中的进程可以通过设置SIG_IGN来忽略某个信号(SIGKILL和SIGSTOP除外),同样地,虚拟CPU也可以通过配置虚拟IMR(Interrupt Mask Register)来选择是否屏蔽某个中断。
3.中断源模拟
一个操作系统要跑起来,必须有Time Tick,它就像是身体的脉搏。普通情况下,OS Time Tick由PIT(i8254)或APIC Timer设备提供—PIT定期(1ms in Linux)产生一个timer interrupt,作为global tick, APIC Timer产生一个local tick。在虚拟化情况下,必须为guest OS模拟一个PIT和APIC Timer。
模拟的PIT和APIC Timer不能像真正硬件那样物理计时,所以一般用HOST的某种系统服务或软件计时器来为这个模拟PIT提供模拟”时钟源”。
目前两种方案:
在QEMU中,用SIGALARM信号来实现:QEMU利用某种机制,使timer interrupt handler会向QEMU process发送一个SIGALARM信号,处理该信号过程中再模拟PIT中产生一次时钟。QEMU再通过某种机制,将此模拟PIT发出的模拟中断交付给kvm,再由kvm注入到虚拟机中去。
目前的kvm版本支持内核PIT、APIC和内核PIC,因为这两个设备是频繁使用的,在内核模式中模拟比在用户模式模拟性能更高。这里重点是讲内核PIT的模拟实现,弄清楚它是如何为guest OS提供时钟的。
3.1.物理芯片介绍
PIC主要为8259A PIC芯片;
PIC(Programmable Interrupt Controller) 可编程中断控制器,它具有IR0~IR7共8个中断管脚连接外部设备。中断管脚具有优先级,其中IR0优先级最高,IR7最低。PIC有三个重要的寄存器:
IRR Interrupt Request Register, 中断请求寄存器 共8位,对应IR0~IR7这8个中断管脚。某位置1代表收到对应管脚的中断但还未提交给CPU。
ISR In Service Register 服务中寄存器:共8位,某位置1代表对应管脚的中断已经提交给CPU处理,但CPU还未处理完。
IMR Interrupt Mask Register 中断屏蔽寄存器:共8位,某位置1对应的中断管脚被屏蔽。
3.2.代码分析
用户空间qemu通过KVM_CREATE_DEVICE API接口进入KVM的kvm_vm_ioctl处理函数,继而进入kvm_arch_vm_ioctl,根据参数中的KVM_CREATE_IRQCHIP标志进入初始化中断控制器的流程,首先肯定是注册pic和io APIC,中断路由表的初始化通过kvm_setup_default_irq_routing函数实现。
arch/x86/kvm/x86.c:
kvm_vm_ioctl->kvm_arch_vm_ioctl:
4674 case KVM_CREATE_IRQCHIP: {
4685 r = kvm_pic_init(kvm);
4689 r = kvm_ioapic_init(kvm);
4695 r = kvm_setup_default_irq_routing(kvm);
第一步:虚拟PIC的创建
r = kvm_pic_init(kvm);
第二步:
r = kvm_ioapic_init(kvm);
第三步:中断路由表的初始化
r = kvm_setup_default_irq_routing(kvm);
377 int kvm_setup_default_irq_routing(struct kvm *kvm)
378 {
379 return kvm_set_irq_routing(kvm, default_routing,
380 ARRAY_SIZE(default_routing), 0);
381 }
KVM将所有类型的IRQ CHIP抽象出一个接口,类似于C++的interface抽象基类,定义了中断的触发方法set()以及每个引脚和GSI的映射关系,这些都是和芯片类型无关的,具体的芯片都是继承和实现接口,虚拟设备的中断发送给IRQ CHIP接口开始中断的模拟;分别实现了PIC、IOAPIC以及MSI的set方法进行,通过set方法完成对于VCPU的中断注入。其中IOAPIC中对于每个引脚,又定义了PRT,IOAPIC收到中断后根据RTE格式化出中断消息并发送给目标LAPIC,LAPIC完成中断的选举和注入实现。
中断注入函数流程:
50 struct kvm_pic {
51 spinlock_t lock;
52 bool wakeup_needed;
53 unsigned pending_acks;
54 struct kvm *kvm;
55 struct kvm_kpic_state pics[2]; /* 0 is master pic, 1 is slave pic */
56 int output; /* intr from master PIC */
57 struct kvm_io_device dev_master;
58 struct kvm_io_device dev_slave;
59 struct kvm_io_device dev_eclr;
60 void (*ack_notifier)(void *opaque, int irq);
61 unsigned long irq_states[PIC_NUM_PINS];
62 };
3.3.具体注入过程:
中断注入实际是向客户机CPU注入一个事件,这个事件包括异常和外部中断和NMI。异常我们一般看作为同步,中断被认为异步。硬件具体实现就中断注入实际就是设置VMCS中字段VM-Entry interruption-infomation字段。中断注入实际在VM运行前完成的,具体代码如下:
static int vcpu_enter_guest(struct kvm_vcpu *vcpu) {
/*注入中断在vcpu加载到真实cpu上后,相当于某些位已经被设置*/
inject_pending_event(vcpu); //中断注入
}
vcpu_enter_guest->inject_pending_event->中检查是否有中断到来(其检测的为vcpu->arch.interrupt.pending)
virt/kvm/kvm_main.c:
2573 static struct file_operations kvm_vcpu_fops = {
2574 .release = kvm_vcpu_release,
2575 .unlocked_ioctl = kvm_vcpu_ioctl,
2576 .mmap = kvm_vcpu_mmap,
2577 .llseek = noop_llseek,
2578 KVM_COMPAT(kvm_vcpu_compat_ioctl),
2579 };
2706 static long kvm_vcpu_ioctl(struct file *filp,
2707 unsigned int ioctl, unsigned long arg)
2731 switch (ioctl) {
2732 case KVM_RUN:
r = kvm_arch_vcpu_ioctl_run(vcpu, vcpu->run);
refer to