StratoVirt 的中斷處理是如何實現的?

語言: CN / TW / HK

中斷是外部設備向操作系統發起請求,打斷 CPU 正在執行的任務,轉而處理特殊事件的操作。設備並不能直接連接到 CPU,而是統一連接到中斷控制器上,由中斷控制器管理和分發設備中斷。為了模擬一個完整的操作系統,虛擬化層也必須完成設備中斷的模擬。虛擬機的中斷控制器通過 VMM 創建,VMM 可以利用虛擬機的中斷控制器向其注入中斷。

在 x86_64 架構下,中斷控制器包括 PIC 和 APIC 兩種類型。PIC 控制器通過兩塊 Intel 8259 芯片級聯,支持 15 箇中斷。受到 PIC 中斷引腳數量和不支持多 CPU 限制,Intel 隨後引入了 APIC 中斷控制器。APIC 中斷控制器由 I/O APIC 和 LAPIC 兩部分組成,外部設備連接在 I/O APIC 上,每個 CPU 內部都有 LAPIC,I/O APIC 與 LAPIC 通過系統總線相連。當產生中斷時,I/O APIC 可以將中斷分發給對應的 LAPIC,然後與 LAPIC 相關聯的 CPU 開始執行中斷處理例程。除了上述兩種中斷控制器,還有 MSI/MSI-x 的中斷方式。它繞過了 I/O APIC,直接通過系統總線,將中斷向量號寫入對應 CPU 的 LAPIC。使用 MSI/MSI-x 中斷技術,將不再受管腳數量的約束,支持更多中斷,減少中斷延遲。

在 aarch64 架構下,中斷控制器被稱為 GIC (Generic Interrupt Controller),目前有 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 提供了 MachineOps trait。無論是輕量化主板或者標準化主板,在 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,CPU Interface,Redistributor,ITS。與 x86_64 類似,也需要在 KVM 創建中斷控制器。但是不同的是,在創建過程中,需要提前告訴 KVM 模塊,GIC 組件在虛擬機內存佈局的地址範圍。通過 dist_range,redist_region_ranges,its_range 三個變量,向 KVM 傳遞了組件的內存地址。除此之外,內部仍然使用 vm_fd,通過系統調用創建了 vGIC v3 和 vGIC ITS 中斷設備。

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 的 Virtio PCI 設備,使用 MSI-x 中斷方式通知虛擬機。因此,使用 MSI-x 設備前,需要在 Vitio PCI 設備具現化過程中調用 init_msix(),進行相關的初始化。該函數的主要功能是在 PCI 設備的配置空間協商 MSI 相關信息。另外,具現化階段提供了 assign_interrupt_cb() 函數,用來封裝設備的中斷回調函數。在 Virtio PCI 設備處理完 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 設備,虛擬機通過觸發 VM Exit 退出到 KVM。因為 StratoVirt 在起始階段綁定了 I/O 地址空間與 ioeventfd,並向 KVM 註冊了這些信息。所以 guest OS 通知設備處理 I/O 的流程會從 KVM 直接返回到 StratoVirt 循環。接着由 StratoVirt 分發和處理 I/O 操作。當完成 I/O 請求或其他事件後,需要再次通知虛擬機繼續往下執行,就通過注入中斷的方式讓虛擬機得到事件通知。

StratoVirt 同時支持兩種架構:microVM 和 standardVM,兩種架構下使用的中斷方式稍有不同。在 microVM 架構下,將一個 evenetfd 與一個全局中斷號關聯,並向 KVM 註冊對應關係。當需要發送中斷時,StratoVirt 只需要向設備對應的 eventfd 發送信號,就會導致對應的中斷被 KVM 模塊注入到虛擬機。在 standardVM 架構,使用 msix notify() 發起中斷。經過一系列的函數調用,最後在 vm_fd 上調用 ioctl_with_ref(self, KVM_SIGNAL_MSI(), &msi),向 KVM 發起中斷通知,最終由 KVM 模塊完成虛擬機的中斷注入。


輕量化機型

在 virtio 設備激活階段,將中斷回調函數 interrupt_cb,作為 activate() 函數的入參傳入,保存在設備對應的 IO handler 中。當需要發送中斷時,會調用該中斷回調函數。activate() 函數聲明如下:

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

輕量機型架構下的設備使用 Virtio MMIO 協議,處理完 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 對應的全局中斷號,注入到虛擬機中。

標準機型

與輕量機型不同,標準機型架構下實現的設備使用 Virtio PCI 協議。因此,中斷方式也改為了 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() 填充 MSI message 結構的 address 和 data 成員。接着向 KVM 發送封裝好的 message。最後在內核 KVM 模塊,根據中斷路由表項,向虛擬機注入對應的中斷。

關注我們

StratoVirt 已經在 openEuler 社區開源。後續將開展一系列主題分享,如果您對 StratoVirt 的使用、實現感興趣,歡迎您圍觀和加入。

項目地址

http://gitee.com/openeuler/stratovirt

項目 wiki

http://gitee.com/openeuler/stratovirt/wikis

郵件列表

http://mailweb.openeuler.org/postorius/lists/virt.openeuler.org/

提交 issue

http://gitee.com/openeuler/stratovirt/issues

安裝指導

http://www.openeuler.org/zh/other/projects/stratovirt/

入羣

如果您對虛擬化技術感興趣,歡迎加入 Virt SIG 技術交流羣,討論 StratoVirt、KVM、QEMU 和 Libvirt 等虛擬化相關技術。您可以添加如下微信小助手,回覆 StratoVirt 入羣。




本文分享自微信公眾號 - openEuler(openEulercommunity)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閲讀的你也加入,一起分享。