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 的使用、實現感興趣,歡迎您圍觀和加入。
專案地址
https://gitee.com/openeuler/stratovirt
專案 wiki
https://gitee.com/openeuler/stratovirt/wikis
郵件列表
https://mailweb.openeuler.org/postorius/lists/virt.openeuler.org/
提交 issue
https://gitee.com/openeuler/stratovirt/issues
安裝指導
https://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開放應用生態