StratoVirt 的中斷處理是如何實現的?
中斷是外部設備向操作系統發起請求,打斷 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源創計劃”,歡迎正在閲讀的你也加入,一起分享。
- 玩轉機密計算從 secGear 開始
- openEuler資源利用率提升之道06:虛擬機混部OpenStack調度
- openGauss Cluster Manager RTO Test
- JVM 鎖 bug 導致 G1 GC 掛起問題分析和解決【畢昇JDK技術剖析 · 第 2 期】
- 手把手帶你玩轉 openEuler | openEuler 的使用
- 681名學生中選!暑期2021開啟火熱“開源之夏”!
- 手把手帶你玩轉 openEuler | 初識 openEuler
- StratoVirt 中的 PCI 設備熱插拔實現
- 使用 NMT 和 pmap 解決 JVM 資源泄漏問題
- JNI 中錯誤的信號處理導致 JVM 崩潰問題分析
- Java Flight Recorder - 事件機制詳解
- 畢昇 JDK 8u292、11.0.11 發佈!
- StratoVirt 中的虛擬網卡是如何實現的?
- openEuler結合ebpf提升ServiceMesh服務體驗的探索
- 我的openEuler社區參與之旅
- StratoVirt 的中斷處理是如何實現的?
- 看看畢昇 JDK 團隊是如何解決 JVM 中 CMS 的 Crash
- 使用 perf 解決 JDK8 小版本升級後性能下降的問題【畢昇JDK技術剖析 · 第 1 期】
- 2021年畢昇 JDK 的第一個重要更新來了
- 漏洞盒子 × openEuler | 廣邀白帽共築安全的Linux開放應用生態