博客 / 詳情

返回

一種StratoVirt中斷的處理方法

  中斷是外部設備向操作系統發起請求,打斷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模塊,根據中斷路由表項,向虛擬機注入對應的中斷。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.