中断是外部设备向操作系统发起请求,打断CPU正在执行的任务,转而处理特殊事件的操作。设备并不能直接连接到CPU,而是统一连接到中断控制器上,由中断控制器管理和分发设备中断。为了模拟一个完整的操作系统,虚拟化层也必须完成设备中断的模拟。虚拟机的中断控制器通过VMM创建,VMM可以利用虚拟机的中断控制器向其注入中断。
  
  在x86_64架构下,中断控制器包括PIC和APIC两种类型。PIC控制器通过两块Intel8259芯片级联,支持15个中断。受到PIC中断引脚数量和不支持多CPU限制,Intel随后引入了APIC中断控制器。APIC中断控制器由I/OAPIC和LAPIC两部分组成,外部设备连接在I/OAPIC上,每个CPU内部都有LAPIC,I/OAPIC与LAPIC通过系统总线相连。当产生中断时,I/OAPIC可以将中断分发给对应的LAPIC,然后与LAPIC相关联的CPU开始执行中断处理例程。除了上述两种中断控制器,还有MSI/MSI-x的中断方式。它绕过了I/OAPIC,直接通过系统总线,将中断向量号写入对应CPU的LAPIC。使用MSI/MSI-x中断技术,将不再受管脚数量的约束,支持更多中断,减少中断延迟。
  
  在aarch64架构下,中断控制器被称为GIC(GenericInterruptController),目前有v1~v4这四个版本。当前StratoVirt只支持GICv3版。同样的,aarch64也支持MSI/MSI-x中断方式。
  
  INTx中断机制会在一些传统的老旧设备上使用。但实际上,在PCIe总线中,很多设备已经很少使用,甚至直接将该功能禁止了。所以,StratoVirt当前也不支持INTx中断机制。
  
  创建中断芯片
  
  由于中断控制器在KVM中模拟的性能更高,因此StratoVirt将中断芯片的具体创建过程和中断投递过程交给了KVM。在StratoVirt启动虚拟机之前,会具现化x86_64或aarch64的虚拟主板,即调用realize()函数,完成初始化。在这个阶段,就创建了中断控制器。其初始化代码如下:

fn realize(
        vm: &Arc<Mutex<Self>>,
        vm_config: &mut VmConfig,
        is_migrate: bool,
    ) -> MachineResult<()> {
      ...
      locked_vm.init_interrupt_controller(u64::from(vm_config.machine_config.nr_cpus))?;
      ...
    }

  StratoVirt提供了MachineOpstrait。无论是轻量化主板或者标准化主板,在x86_64和aarch64架构下都分别实现了init_interrupt_controller(),初始化中断控制器函数。
  
  x86_64架构
  
  上述调用了初始化中断控制器函数,在其内部的执行过程中,主要作用是调用create_irq_chip()函数,后者在vm_fd上调用ioctl(self,KVM_CREATE_IRQCHIP())系统调用,告诉内核需要在KVM模拟中断控制器。后续该系统调用进入了KVM模块,会同时创建PIC和APIC中断芯片,并生成默认的中断路由表。

fn init_interrupt_controller(&mut self, _vcpu_count: u64) -> MachineResult<()> {
  ...
     KVM_FDS
            .load()
            .vm_fd
            .as_ref()
            .unwrap()
            .create_irq_chip()
            .chain_err(|| MachineErrorKind::CrtIrqchipErr)?;
     ...
}

  aarch64架构
  
  GIC中断控制器由四个组件组成:Distributor,CPUInterface,Redistributor,ITS。与x86_64类似,也需要在KVM创建中断控制器。但是不同的是,在创建过程中,需要提前告诉KVM模块,GIC组件在虚拟机内存布局的地址范围。通过dist_range,redist_region_ranges,its_range三个变量,向KVM传递了组件的内存地址。除此之外,内部仍然使用vm_fd,通过系统调用创建了vGICv3和vGICITS中断设备。

fn init_interrupt_controller(&mut self, vcpu_count: u64) -> Result<()> {
    ...
    let intc_conf = InterruptControllerConfig {
            version: kvm_bindings::kvm_device_type_KVM_DEV_TYPE_ARM_VGIC_V3,
            vcpu_count,
            max_irq: 192,
            msi: true,
            dist_range: MEM_LAYOUT[LayoutEntryType::GicDist as usize],
            redist_region_ranges: vec![
                MEM_LAYOUT[LayoutEntryType::GicRedist as usize],
                MEM_LAYOUT[LayoutEntryType::HighGicRedist as usize],
            ],
            its_range: Some(MEM_LAYOUT[LayoutEntryType::GicIts as usize]),
        };
        let irq_chip = InterruptController::new(&intc_conf)?;
        self.irq_chip = Some(Arc::new(irq_chip));
        self.irq_chip.as_ref().unwrap().realize()?;
        ...
}

  创建MSI-x
  
  在设计StratoVirt的VirtioPCI设备,使用MSI-x中断方式通知虚拟机。因此,使用MSI-x设备前,需要在VitioPCI设备具现化过程中调用init_msix(),进行相关的初始化。该函数的主要功能是在PCI设备的配置空间协商MSI相关信息。另外,具现化阶段提供了assign_interrupt_cb()函数,用来封装设备的中断回调函数。在VirtioPCI设备处理完I/O请求后,会调用中断回调,向KVM发送中断通知。

fn realize(mut self) -> PciResult<()> {
  ...
    init_msix(
            VIRTIO_PCI_MSIX_BAR_IDX as usize,
            nvectors as u32,
            &mut self.config,
            self.dev_id.clone(),
        )?;
        self.assign_interrupt_cb();
        ...
}

  管理中断路由表
  
  上文提到,在KVM创建中断芯片时,会生成默认的中断路由表。但是某些设备(例如直通设备),需要向KVM添加额外的全局中断号,这时需要StratoVirt额外维护一份中断路由表,并向KVM同步。
  
  在StratoVirt初始化中断控制器时,会创建中断路由表。内部统一调用init_irq_route_table()函数,但是架构不同,默认的中断路由表信息也不同。
  
  除了可以生成默认的中断路由表,还需要向KVM同步。commit_irq_routing()函数提供了该功能,内部使用vm_fd的系统调用ioctl_with_ref(self,KVM_SET_GSI_ROUTING(),irq_routing),该系统调用将覆盖KVM模块内的中断路由表信息。
  

fn init_interrupt_controller(&mut self, vcpu_count: u64) -> Result<()> {
     ...
     KVM_FDS
            .load()
            .irq_route_table
            .lock()
            .unwrap()
            .init_irq_route_table();
        KVM_FDS
            .load()
            .commit_irq_routing()
            .chain_err(|| "Failed to commit irq routing for arm gic")?;
     ...
}

  当设备需要动态申请或释放全局中断号时,StratoVirt提供了两个函数add_msi_route(),update_msi_route(),用于增加或修改中断路由表信息。
  
  中断流程
  
  对于模拟virtio设备,虚拟机通过触发VMExit退出到KVM。因为StratoVirt在起始阶段绑定了I/O地址空间与ioeventfd,并向KVM注册了这些信息。所以guestOS通知设备处理I/O的流程会从KVM直接返回到StratoVirt循环。接着由StratoVirt分发和处理I/O操作。当完成I/O请求或其他事件后,需要再次通知虚拟机继续往下执行,就通过注入中断的方式让虚拟机得到事件通知。
  
  StratoVirt同时支持两种架构:microVM和standardVM,两种架构下使用的中断方式稍有不同。在microVM架构下,将一个evenetfd与一个全局中断号关联,并向KVM注册对应关系。当需要发送中断时,StratoVirt只需要向设备对应的eventfd发送信号,就会导致对应的中断被KVM模块注入到虚拟机。在standardVM架构,使用msixnotify()发起中断。经过一系列的函数调用,最后在vm_fd上调用ioctl_with_ref(self,KVM_SIGNAL_MSI(),&msi),向KVM发起中断通知,最终由KVM模块完成虚拟机的中断注入。
  

  轻量化机型
  
  在virtio设备激活阶段,将中断回调函数interrupt_cb,作为activate()函数的入参传入,保存在设备对应的IOhandler中。当需要发送中断时,会调用该中断回调函数。activate()函数声明如下:

fn activate(
        &mut self,
        mem_space: Arc<AddressSpace>,
        interrupt_cb: Arc<VirtioInterrupt>,
        queues: &[Arc<Mutex<Queue>>],
        queue_evts: Vec<EventFd>,
    ) -> Result<()>;

  轻量机型架构下的设备使用VirtioMMIO协议,处理完I/O请求后,会调用中断回调函数,发送中断。中断回调函数具体内容如下:

let cb = Arc::new(Box::new(
            move |int_type: &VirtioInterruptType, _queue: Option<&Queue>| {
                let status = match int_type {
                    VirtioInterruptType::Config => VIRTIO_MMIO_INT_CONFIG,
                    VirtioInterruptType::Vring => VIRTIO_MMIO_INT_VRING,
                };
                interrupt_status.fetch_or(status as u32, Ordering::SeqCst);
                interrupt_evt
                    .write(1)
                    .chain_err(|| ErrorKind::EventFdWrite)?;

                Ok(())
            },
        ) as VirtioInterrupt);

  在上面我们提到该eventfd和中断号信息已经告诉了KVM。中断回调通过向interrupt_evt写1,KVM就可以poll到相应事件,接着找到eventfd对应的全局中断号,注入到虚拟机中。
  
  标准机型
  
  与轻量机型不同,标准机型架构下实现的设备使用VirtioPCI协议。因此,中断方式也改为了MSI-x。与上面相同是,设备在激活阶段,都会保存中断回调函数。标准机型对应的中断回调函数如下:
  

let cb = Arc::new(Box::new(
            move |int_type: &VirtioInterruptType, queue: Option<&Queue>| {
                let vector = match int_type {
                    VirtioInterruptType::Config => cloned_common_cfg
                        .lock()
                        .unwrap()
                        .msix_config
                        .load(Ordering::SeqCst),
                    VirtioInterruptType::Vring => {
                        queue.map_or(0, |q| q.vring.get_queue_config().vector)
                    }
                };

                if let Some(msix) = &cloned_msix {
                    msix.lock().unwrap().notify(vector, dev_id);
                } else {
                    bail!("Failed to send interrupt, msix does not exist");
                }
                Ok(())
            },
        ) as VirtioInterrupt);

  在中断回调函数中,获取中断向量号vector,然后使用notify()函数把中断信息发送给KVM。内部首先使用get_message()填充MSImessage结构的address和data成员。接着向KVM发送封装好的message。最后在内核KVM模块,根据中断路由表项,向虚拟机注入对应的中断。


欢快的板栗
1 声望0 粉丝

引用和评论

0 条评论