StratoVirt vCPU管理Rust執行緒同步的實現

語言: CN / TW / HK

StratoVirt是開源在openEuler社群的輕量級虛擬化平臺,具備輕量低噪、強安全性的行業競爭力。

StratoVirt程序執行在使用者態,在虛擬機器啟動之前,StratoVirt會完成啟動之前的準備工作,包括虛擬機器記憶體的初始化、CPU暫存器初始化、裝置初始化等,啟動,CPU暫存器初始化和虛擬機器在執行過程中vCPU陷出事件的處理,都是由StratoVirt的vCPU管理模組CPU完成。如下是StratoVirt中vCPU管理模組的組成,以及其在StratoVirt中的位置。

stratovirt
├── acpi
├── address_space
├── boot_loader
├── Cargo.lock
├── Cargo.toml
├── cpu
│   ├── Cargo.toml
│   └── src
│       ├── aarch64
│       │   ├── caps.rs
│       │   ├── core_regs.rs
│       │   └── mod.rs
│       ├── lib.rs
│       └── x86_64
│           ├── caps.rs
│           ├── cpuid.rs
│           └── mod.rs
├── devices
├── hypervisor
├── machine
├── machine_manager
├── migration
├── migration_derive
├── ozone
├── pci
├── src
│   └── main.rs
├── sysbus
├── util
├── vfio
└── virtio

StratoVirt vCPU模組的整體設計

StratoVirt的虛擬化解決方案也是一套軟硬結合的硬體輔助虛擬化解決方案,它的運作依賴於硬體輔助虛擬化的能力(如VT-X或Kunpeng-V)。vCPU模組的實現也是緊密依賴於這一套硬體輔助虛擬化的解決方案的。
對於物理機的CPU而言,硬體輔助虛擬化為CPU增加了一種新的模式:Non-Root模式,在該模式下,CPU執行的並不是物理機的指令,而是虛擬機器的指令。這種指令執行方式消除了大部分效能開銷,非常高效。但是特權指令(如I/O指令)不能通過這種方式執行,還是會強制將CPU退出到普通模式(即ROOT模式)下交給核心KVM模組和使用者態StratoVirt去處理,處理完再重新回到Non-Root模式下執行下一條指令。
而StratoVirt中的vCPU模組主要圍繞著KVM模組中對vCPU的模擬來實現,為了支援KVM模組中對CPU的模擬,CPU子系統主要負責處理退出到普通模式的事件,以及根據在GuestOS核心開始執行前對vCPU暫存器等虛擬硬體狀態的初始化。整個vCPU模組的設計模型如下圖所示:
StratoVirt通過第三方庫 kvm_ioctls來完成和KVM模組的互動,通過匹配 vcpu_fd.run()函式的返回值來處理退出到ROOT模式的事件,該函式的返回值是一個名為 VcpuExit的列舉,定義了退出到ROOT模式的事件型別,包括I/O的下發、系統關機事件、系統異常事件等,根據事件的型別vCPU將對不同的事件作出各自的處理。以上的整個過程都被包含在一個獨立的vCPU執行緒中,使用者可以自己通過對vCPU執行緒進行綁核等方式讓虛擬機器的vCPU獲取物理機CPU近似百分之百的效能。
同時,對vCPU暫存器虛擬硬體狀態資訊的初始化則是和StratoVirt的另一個模組BootLoader相互結合,在BootLoader中實現了一種根據Linux啟動協議快速引導啟動Linux核心映象的方法,在這套啟動流程中,BootLoader將主動完成傳統BIOS對一些硬體資訊的獲取,將對應的硬體表儲存在虛擬機器記憶體中,同時將提供一定的暫存器設定資訊,這些暫存器設定資訊將傳輸給vCPU模組,通過設定vCPU結構中的暫存器值,讓虛擬機器CPU跳過真實模式直接進入保護模式執行,這樣Linux核心就能直接從保護模式的入口開始執行,這種方式讓StratoVirt的啟動流程變得輕量快速。
在整個vCPU模組中,因為涉及到核心的KVM模組,少不了與C語言程式碼做互動。作為系統程式語言,Rust對FFI有非常完善的支援,讓vCPU中和KVM模組互動的部分高效且安全。

vCPU執行緒模型同步

vCPU模組還有一大職責就是管理vCPU的生命週期,包括new(建立),realize(使能),run(執行),pause(暫停),resume(恢復),destroy(銷燬)。New和realize的過程就是結構體建立和暫存器初始化的流程,run的過程即是實現KVM中vCPU運作和 VCPU_EXIT退出事件處理的流程。
另外的三種生命週期的實現則涉及到對執行緒同步的精密控制,例如在虛擬機器destroy的過程中,一般只有某一個vCPU接收到 VCPU_EXIT中的 SHUTDOWN事件,該vCPU執行緒需要把該事件傳遞到所有的vCPU執行緒,同步所有vCPU執行緒的狀態,完成虛擬機器的優雅關機。在這種場景下,我們就需要考慮在Rust中如何實現在多執行緒中進行狀態同步。

Rust中通過條件變數來實現同步

Rust多執行緒程式設計中,有一類用於同步的機制叫做屏障(Barrier),用於讓多執行緒來同步一些流程開始的位置,它相當於一個閘口,使用wait方法,將該執行緒放進臨界區並阻塞住,只有每個Barrier都到達wait方法呼叫的點,閘口才會開啟,所有的執行緒同步往下執行。

而在比較複雜的同步場景中,Rust還提供了另一個同步機制條件變數(Condition Variable)來支援更復雜的同步場景,它和屏障的功能類似,但是它並不阻塞全部程序,而是在滿足指定的條件之前阻塞某個得到互斥鎖的程序。也就是說,通過條件變數,我們可以在達到某種條件之前阻塞某個執行緒,這個特性可以讓我們很好得對執行緒進行同步。

為了支援各種場景的同步控制,條件變數還提供了三個方法:

  • notify_one(): 用來通知一次阻塞執行緒,如果有複數個執行緒被阻塞住, notify_one會被一個阻塞的執行緒所消耗,不會傳遞到別的阻塞執行緒去。
  • notify_all(): 用來通知所有的阻塞執行緒。
  • wait_timeout(): 將當前執行緒置入臨界區阻塞住並等待通知,可以設定一個 timeout來設定阻塞的最大時間,以免造成永久的阻塞導致程式卡死。

需要注意的一點是條件變數需要和鎖一起使用,而在程式執行中,每個條件變數每次只能和一個互斥體(被Mutex等鎖包裹都可稱為互斥體)進行使用。

vCPU生命週期控制和執行緒同步

CPU資料結構初始化時,建立一個互斥的生命週期列舉(CpuLifecycleState)和一個條件變數。

pub fn new(
        vcpu_fd: Arc<VcpuFd>,
        id: u8,
        arch_cpu: Arc<Mutex<ArchCPU>>,
        vm: Arc<Mutex<dyn MachineInterface + Send + Sync>>,
    ) -> Self {
        CPU {
            id,
            fd: vcpu_fd,
            arch_cpu,
            state: Arc::new((Mutex::new(CpuLifecycleState::Created), Condvar::new())),
            work_queue: Arc::new((Mutex::new(0), Condvar::new())),
            task: Arc::new(Mutex::new(None)),
            tid: Arc::new(Mutex::new(None)),
            vm: Arc::downgrade(&vm),
        }
    }

以destory生命週期為例,在x86_64架構下,當某個vCPU執行緒接收到VcpuExit::Shutdown事件後,會將該執行緒的CpuLifecycleState修改為Stopped,並呼叫儲存在CPU資料結構中一個指向上層結構的虛擬機器destroy方法,該方法能遍歷一個儲存著所有CPU資料結構的陣列,執行陣列中每一個CPUdestory()方法,該函式的實現如下:

fn destory(&self) -> Result<()> {
    let (cpu_state, cvar) = &*self.state;
    if *cpu_state.lock().unwrap() == CpuLifecycleState::Running {
        *cpu_state.lock().unwrap() = CpuLifecycleState::Stopping;
    } else {
        *cpu_state.lock().unwrap() = CpuLifecycleState::Stopped;
    }
    
    /* 省略具體的關機邏輯 */
    
    let mut cpu_state = cpu_state.lock().unwrap();
    cpu_state = cvar
            .wait_timeout(cpu_state, Duration::from_millis(32))
            .unwrap()
            .0;

    if *cpu_state == CpuLifecycleState::Stopped {
        *cpu_state = CpuLifecycleState::Nothing;
        Ok(())
    } else {
        Err(ErrorKind::DestroyVcpu(format!("VCPU still in {:?} state", *cpu_state)).into())
    }
}

作為CPU的成員方法,destory函式能獲取到每個CPU資料結構的互斥狀態和條件變數,此時將除觸發vCPU外所有的CPU資料的互斥狀態解鎖,並將狀態從執行時的Running修改為vCPU關機時的Stopping。這裡要注意一點,此時所有CPUdestroy函式都是在觸發關機事件的vCPU程序中進行的,而不是在每個vCPU各自的程序中進行。

緊接著進入Stopping狀態後,destroy函式會執行每個vCPU各自的關機邏輯,包括觸發vCPU,這部分主要還是與KVM模組進行互動,進行一些退出狀態的變更等。在執行完vCPU的關機邏輯後,條件變數會進入到wait_timeout的等待狀態,它的引數為每個vCPU的CpuLifecycleState生命週期狀態列舉和等待超時時間,也就是說在該生命週期列舉狀態變化前,該執行緒都會進入阻塞狀態。

此時除觸發vCPU外的vCPU執行緒中,CpuLifecycleState都已經進入了Stopping狀態,在所有vCPU執行緒中,vCPU的指令模擬函式kvm_vcpu_exec()都執行在一個迴圈中,對於每次迴圈的入口,都會執行ready_for_running()函式進入是否繼續模擬的判斷,在該函式中會對每個vCPU對應的CpuLifecycleState進行監控,當發現CpuLifecycleState已經變成Stopping時,vCPU將會退出迴圈,不繼續進行vCPU的模擬,退出模擬的迴圈後,將會修改CpuLifecycleStateStopped:

// The vcpu thread is about to exit, marking the state of the CPU state as Stopped.
let (cpu_state, _) = &*self.thread_cpu.state;
*cpu_state.lock().unwrap() = CpuLifecycleState::Stopped;

修改vCPU執行緒中互斥的生命週期狀態列舉後,將會觸發阻塞執行緒中對應的wait_timeout()函式,同時,該vCPU執行緒的生命週期結束。而對於阻塞執行緒,當其餘vCPU執行緒的狀態都已經變成Stopped後,阻塞解除,此時,所有的vCPU執行緒都已經狀態都已經同步到了Stopped,執行緒狀態同步成功。

用類似思路也可以實現pause(暫停)和resume(恢復)的生命週期控制。

關注我們

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

入群

如果您對虛擬化技術感興趣,可以進入 openEuler StratoVirt 主頁檢視相關資源點選閱讀原文進入專案主頁,包括安裝指導、虛擬機器配置、程式碼倉庫、學習資料等。也歡迎加入 Virt SIG 技術交流群,討論 StratoVirt、KVM、QEMU 和 Libvirt 等相關虛擬化技術。感興趣的同學可以新增如下微信小助手,回覆 StratoVirt 入群。


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