虚拟化技术之 kvm (二)- 中断虚拟化

  • 中断虚拟化

1.中断概述

  • 软件部分

    软件部分在操做系统中实现,如Linux中断的x86,每个中断对应一个中断门,中断门中包含中断处理函数(ISR或者别的)地址,优先级等等。CPU能够经过LIDT加载这个描述符表,跳转到指定的中断门。

  • 硬件部分

    中断硬件部分就是产生中断脉冲,传给中断控制器,而后通知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中断:
在这里插入图片描述
  虚拟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中。

1.创建客户机的时候,给客户机分配中断号,然后通知VMM相关绑定信息。
2.当CPU收到中断,执行标准的中断处理,最后跳转到IDT表对应的处理函数,该处理函数是VMM提供的。
3.VMM的中断处理函数对中断进行检查,发现该中断是分配给客户机的设备产生的,因此就调用虚拟中断控制器的接口,将中断发送到虚拟Local APIC,之后虚拟Local APIC在适当时机将该中断注入客户机,由客户机os的处理函数处理。
4.将中断注入后,VMM进行后续处理,比如开中断等。

在这里插入图片描述

  可是在虚拟化环境中,物理中断控制器是由VMM控制的,因而VMM在收到中断后,会首先判断该中断是不是由分配给guest VM的设备产生的,如果是的话,就将中断发送给对应的虚拟中断控制器。而后,虚拟中断控制器会在适当的时机将该中断注入guest VM,由guest OS中的ISR进行处理。那中断注入的过程是怎样的,这个“适当的时机”又该如何选择呢?

  • 中断注入

    • 如果VCPU在运行,需要发IPI使之VM-Exit, 然后再VM-Entry注入。
    • 如果VCPU当前无法中断,通过中断窗口(VMCS的一个特定字段告诉CPU当前VCPU有一个中断需要注入),一旦VCPU可以接收中断,物理CPU会主动触发VM-Exit,然后就可以注入等待的中断了。
      在这里插入图片描述
      虚拟中断控制器采集到的中断请求,将按照VMM排定的优先级,被逐一注入到对应的虚拟CPU中。在Linux的信号发送与接收机制中,只有从内核空间返回到用户空间,也就是某个进程被调度到重新获得CPU的使用权时,该进程才可以处理之前发送给它的那些信号,或者说此时内核才可以将这些未处理的信号注入到进程中。

    同样地,只有在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);
  • 内核态模拟方案(KVM);

  在QEMU中,用SIGALARM信号来实现:QEMU利用某种机制,使timer interrupt handler会向QEMU process发送一个SIGALARM信号,处理该信号过程中再模拟PIT中产生一次时钟。QEMU再通过某种机制,将此模拟PIT发出的模拟中断交付给kvm,再由kvm注入到虚拟机中去。

  目前的kvm版本支持内核PIT、APIC和内核PIC,因为这两个设备是频繁使用的,在内核模式中模拟比在用户模式模拟性能更高。这里重点是讲内核PIT的模拟实现,弄清楚它是如何为guest OS提供时钟的。

3.1.物理芯片介绍

  • PIT 主要为Intel 8254 PIT芯片;
    PIT (Programmable Interval Timer) 可编程间隔定时器,每个PC机中都有一个PIT,通过IRQ产生周期性的时钟中断信号来充当系统定时器。i386中使用的通常是Intel 8254 PIT芯片,它的I/O端口地址范围是40h~43h。

  • 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完成中断的选举和注入实现。

  中断注入函数流程:
在这里插入图片描述

  • kvm_set_pic_irq
    它主要是设置kvm里面的虚拟中断控制器结构体struct kvm_pic完成虚拟终端控制器的设置。若是是边缘触发,须要触发电平先0再1或者先1再0,完成一个正常的中断模拟。
 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 };   
  • kvm_set_ioapic_irq
    kvm_set_ioapic_irq -> kvm_ioapic_set_irq -> ioapic_service -> ioapic_deliver -> kvm_irq_delivery_to_apic -> __apic_accept_irq -> apic_set_vector

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

  • https://www.izhangchao.com/internet/internet_234676.html
  • https://blog.csdn.net/leoufung/article/details/53081207
  • https://zhuanlan.zhihu.com/p/75649437
  • https://www.shangmayuan.com/a/463a899b37e843f19643e94e.html
  • http://www.cnhalo.net/2016/06/15/cpu-virtualization/

猜你喜欢

转载自blog.csdn.net/weixin_41028621/article/details/113481550