# KVM: Linux 虛擬化基礎建設 > 貢獻者: RinHizakura, ray90514, yanjiew1, jserv ==[直播錄影](https://youtu.be/F7kakRiWdBM)== ## 簡介 KVM (Kernel-based Virtual Machine) 是 Linux 核心提供的系統虛擬機器基礎建設,它是個 Linux 核心模組,能讓 Linux 系統成為 hypervisor。KVM 透過硬體虛擬化支援 (Intel VT-x, AMD-V) 來提供 CPU 和記憶體虛擬化功能。藉由硬體虛擬化技術,客體作業系統 (guest OS) 不必經由軟體模擬或轉換指令,即可高效率且安全地直接在硬體上執行。使用者空間的管理程式只要負責模擬週邊裝置、呼叫 KVM API,即可建立並執行虛擬機器。 本講座將介紹 KVM 運作原理,並展示一個以 KVM 為基礎的精簡虛擬機器管理程式實作,它能在 x86-64 及 Arm64 平台上運作,提供基本的硬體周邊和 virtio 裝置,並可在其上執行 Linux 系統。 ### 用詞 virtual machine 應翻譯為「虛擬機器」,而非簡略的「虛擬機」,用來區分 motor/engine (發動機) 在漢語常見的翻譯詞「機」。 ## 虛擬化概況 > 簡報:〈[Embedded Virtualization applied in Mobile Devices](https://www.slideshare.net/jserv/mobile-virtualization)〉 ![image](https://hackmd.io/_uploads/ryRNagACT.png) > 出處: Xen Directions, 2009 ![image](https://hackmd.io/_uploads/B1FHag0AT.png) > 出處: [Overview of Xen for ARM Servers](https://www.slideshare.net/linaroorg/lcu14-308-xen-project-for-arm-servers) Xen 由劍橋大學的 Ian Pratt 和 Keir Fraser 於 2003 年發表 (SOSP 論文〈[Xen and the Art of Virtualization](https://www.cl.cam.ac.uk/research/srg/netos/papers/2003-xensosp.pdf)〉),是 Type 1 hypervisor。2007 年 Citrix 以約 5 億美元收購 XenSource,2013 年 Xen 專案移至 Linux Foundation。 :::warning Xen 的問題: dom0/DomU 支援長期在 out-of-tree 狀態。DomU (paravirt guest) 至 Linux v2.6.23 (2007 年 10 月) 才進入 mainline;dom0 更晚,核心功能自 v2.6.37 (2011 年 1 月) 起逐步合併,至 v3.0 (2011 年 7 月) 加入 block backend (`xen-blkback`) 後才算完整可用 (network backend `xen-netback` 於 v2.6.39 合併)。 ::: 2017 年第四季,AWS 宣布其新一代 [C5 instance](https://aws.amazon.com/blogs/aws/now-available-compute-intensive-c5-instances-for-amazon-ec2/) 改採 KVM 作為虛擬化架構,自此 KVM 可說是席捲 Linux 虛擬化技術生態。在 AWS [EC2](https://aws.amazon.com/ec2/) 於 2006 年 8 月公開測試時,KVM 尚未問世 (v2.6.20 於 2007 年 2 月才釋出),Xen 是當時最成熟的開放原始碼完整虛擬化方案。因此,AWS 在早期採用 Xen 作為 EC2 的底層基礎。 KVM 全名是 Kernel-based Virtual Machine,最早是以色列新創公司 [Qumranet](https://en.wikipedia.org/wiki/Qumranet) (創立於 2005 年) 發表的開放原始碼專案。KVM 於 2006 年 10 月提交,收錄於 Linux v2.6.20 (2007 年 2 月釋出),自此大幅強化 KVM 的影響力。 2008 年,Red Hat 以 1.07 億美元收購 KVM 背後的 Qumranet 公司,藉此擴充 Red Hat 對於虛擬化技術的支援。得益於 KVM 的活躍發展,Google Compute Engine 於 2012 年推出預覽版時即採用 KVM 為基礎的 hypervisor。Qumranet 核心人物 Avi Kivity 在公司成功賣給 Red Hat 後,開創新一代的分散式資料庫技術公司 [ScyllaDB](https://www.scylladb.com/)。 世界上前幾大的資訊科技公司不約而同,選擇在以色列設立研發中心,且成果卓著,例如,Intel x86 家族中為人所知的 CPU 微架構: Sandy Bridge 和 Ivy Bridge 都由 Intel 以色列的研發中心研發,藍色巨人 IBM 也早在 1970 年代就在以色列設立龐大的研發中心,成果斐然。 虛擬化技術歷史悠久,早在 1967 年,IBM 的 CP-40/CMS 即在大型主機上實作虛擬化,後續 CP-67/CMS (1968) 延續該架構。直到 2005-2006 年,Intel 和 AMD 才分別在各自的處理器中加入「有限」的硬體虛擬化特性: Intel VT-x 於 2005 年 11 月隨 Pentium 4 出貨,AMD-V 於 2006 年推出。與大型機所採用的專為虛擬化設計的處理器不同,從 PC 起家者以 Intel 為代表的 x86 家族的處理器生來就不是為虛擬化設計。要在 x86 家族處理器上完全向後相容的同時加入硬體虛擬化特性,無疑成為一個挑戰,硬體層面實作較為困難,導致軟體層面的實作複雜度也隨之水漲船高。 Linux 核心中,虛擬化相關程式碼以 x86 架構最為龐大,約為 Arm 架構的 4 倍、S390 的 7 倍、PPC 的 8 倍 (依核心版本而異),其複雜度之高從中可見一斑。 Avi Kivity 提出的方案清晰且巧妙: KVM 僅聚焦於 Linux 核心中的 CPU 和記憶體虛擬化,至於使用者空間的裝置模擬交給穩定可靠的 QEMU 作為 VMM。以 Avi Kivity 為主的工程師在不到一年時間內,就讓 Linux 社群接受 KVM 的設計方案並通過程式碼審查。 :::warning KVM 為何勝出? 1. KVM 直接融入 Linux 核心,而非墊在 Linux 之下 (underneath);設計更靈活、更具前瞻性 2. 重用 Linux 既有的排程器、記憶體管理、驅動程式等基礎設施,讓 Linux 核心開發者保有掌控權 ::: KVM 的發展由 Linux Foundation 每年舉辦的 [KVM Forum](https://kvm-forum.qemu.org/) 及 [Linux Plumbers Conference](https://lpc.events/) 的 KVM microconference 所驅動,來自 Red Hat、Google、Intel、AMD、Arm 等公司的核心維護者在此協調設計方向。 [Cloud Hypervisor](https://github.com/cloud-hypervisor/cloud-hypervisor) 最初由 Intel 以 KVM 為基礎開發 (2019 年釋出開放原始碼),現由 Linux Foundation 治理,採用 Rust 程式語言打造。Cloud Hypervisor 著眼於 microVM 情境,不考慮舊有硬體支援,主要支援以 virtio 為基礎的 paravirtualized device,支援 CPU/記憶體熱插拔、VFIO 裝置直通、nested virtualization 及 Windows guest。所謂 microVM,是指採用最小化裝置模型和 direct kernel boot (略過 BIOS/UEFI) 的輕量虛擬機器,具備極快的啟動時間 (通常低於數百毫秒)、低記憶體開銷和小攻擊面 (attack surface,在未經授權的情況下存取企業系統與資料的漏洞、存取點以及攻擊管道的集合),適用於 serverless 函式和容器沙箱等短生命週期工作負載。 [Confidential Computing](https://confidentialcomputing.io/) 旨在建立一套可用於資料中心、雲端和邊緣運算的安全信賴環境標準和規範。以 AMD EPYC 處理器為例,利用其安全加密虛擬化 SEV (Secure Encrypted Virtualization),可藉由虛擬化層建立安全的虛擬機器。SEV 的加密對 guest 軟體完全透明: 記憶體控制器內建 AES 引擎,在寫入時加密、讀取時解密,guest 程式無需任何修改。每個虛擬機器的加密金鑰由 AMD Secure Processor (PSP) 以硬體亂數產生,且從不暴露給任何軟體 (包含 hypervisor 和 guest OS 本身);在虛擬機器外部無法取得該金鑰,從而達到阻絕外部窺視的保護效果。SEV 技術歷經三代演進: SEV (EPYC 7001 Naples, 2017) 提供記憶體加密,SEV-ES (EPYC 7002 Rome, 2019) 加入暫存器狀態加密,SEV-SNP (EPYC 7003 Milan, 2021) 再加入以 Reverse Map Table (RMP) 實現的記憶體完整性保護。各大雲端業者 (AWS、GCP、Azure) 已陸續推出基於 AMD SEV-SNP 的 confidential VM 服務,Intel TDX 和 Arm CCA 的 KVM 支援也在積極推進中,詳見後述「KVM 在雲端基礎建設的採用」一節。 其他實作: * [Xvisor](https://wiki.csie.ncku.edu.tw/embedded/xvisor): 嵌入式 Type 1 hypervisor * [Bao hypervisor](https://github.com/bao-project/bao-hypervisor): 針對 Arm 和 RISC-V 的輕量靜態分割 hypervisor ### Hypervisor 的分類 [Hypervisor](https://en.wikipedia.org/wiki/Hypervisor) 是作業系統與硬體之間的中間層,允許多個作業系統作為獨立的 virtual machine (VM) 在同一部實體電腦上執行。hypervisor 管理硬體資源使這些 VM 可以共享之,在邏輯上將 VM 彼此隔離,為每個 VM 指派一部分運算處理能力、記憶體和儲存容量,防止 VM 之間相互干擾。 Hypervisor 可以分為兩大類型,其一是 Type 1 hypervisor,直接在硬體之上執行,如下圖是比較經典的設計。它的優點是效率高,因為可以直接存取硬體。這也增加了安全和穩定性,因為 Type 1 hypervisor 和 CPU 之間不存在額外的作業系統層,因此較為單純而不容易被介入。 ![](https://hackmd.io/_uploads/HyqOY83w2.png =500x) Type 2 hypervisor 則不直接在硬體上執行,而是作為應用程式在主作業系統 (host OS) 環境上執行,如下圖所展示。因為 Type 2 hypervisor 必須透過 host OS 存取資源,因而會引發延遲問題,相對 Type 1 效能較差。 ![](https://hackmd.io/_uploads/HkAFt82w2.png =500x) > [Using Linux as Hypervisor with KVM](https://indico.cern.ch/event/39755/attachments/797208/1092716/slides.pdf) KVM 的分類存在討論: 傳統上 KVM 被歸為 Type 2 hypervisor (因為 KVM 模組載入於 Linux 核心之上),但一旦 KVM 模組載入,Linux 核心本身即成為直接掌控硬體的 hypervisor,具備 Type 1 的特徵。IBM 和 Red Hat 的文件傾向將 KVM 歸類為 Type 1。實務上 KVM 模糊了 Popek-Goldberg 分類法的界線,這也正是其設計優勢: 既享有 Type 1 的效能,又重用 Linux 核心既有的基礎設施。 ### QEMU [QEMU](https://www.qemu.org/) 由 Fabrice Bellard 於 2003 年開發,是一個開放原始碼的機器模擬器和虛擬化工具 (emulator / VMM)。在純軟體模擬模式下,QEMU 以軟體模擬指令的執行及裝置 I/O,不依賴硬體輔助虛擬化,因此可跨架構模擬 (例如在 x86 host 上模擬 Arm guest),但執行速度相對緩慢。其好處是擴充性高,使用者只需要使用 QEMU 提供的 API,就能迅速模擬出一個裝置讓 guest OS 存取。 QEMU 的軟體模擬技術為 DBT (Dynamic Binary Translation),其實作稱作 TCG (Tiny Code Generator)。TCG 以 translation block (對應 basic block) 為單位,將 guest 指令集 (可為任何 QEMU 支援的架構) 翻譯成 host 指令集,中間經過一層 TCG IR (intermediate representation): guest instructions $\to$ TCG IR $\to$ host instructions,藉此降低跨架構轉換的成本。 搭配 KVM 使用時 (透過 `qemu-system-*` 加上 `-accel kvm` 參數啟用),QEMU 作為 VMM 負責裝置模擬和 VM 生命週期管理,KVM 負責 CPU 虛擬化 (透過 VT-x/AMD-V) 和記憶體虛擬化 (透過 EPT/NPT)。QEMU 透過 `ioctl()` 操作 `/dev/kvm` (system fd)、VM fd 和 vCPU fd 三層 file descriptor 與 KVM 互動。 相比純 TCG 模式,搭配 KVM 的主要差異: * TCG 模式以軟體翻譯 guest 指令,KVM 模式下 guest 指令直接在硬體上執行 * KVM 模式下每個 vCPU 各為獨立執行緒 (各自呼叫 `KVM_RUN` ioctl);TCG 模式自 QEMU v2.10 (2017) 起也支援多執行緒 (MTTCG) QEMU 自 v4.2 (2019) 起提供 [`-machine microvm`](https://www.qemu.org/docs/master/system/i386/microvm.html) machine type,以最小化裝置模型和 direct kernel boot 達成類似 Firecracker 的 microVM 啟動速度,同時保留 QEMU 既有的裝置模擬生態系統。 ### KVM [KVM](https://en.wikipedia.org/wiki/Kernel-based_Virtual_Machine) 是個 Linux 核心模組/子系統,結合硬體的虛擬化支援,使得 host OS 上可以執行多個獨立的虛擬環境,稱為 guest 或 virtual machine。由於 KVM 直接提供 CPU 和記憶體的虛擬化,guest OS 的 CPU 指令不需要額外經由軟體解碼,而是直接交給硬體處理,因此可有效提升執行速度。結合軟體 (例如 KVM 搭配 QEMU) 模擬 CPU 和記憶體以外的裝置後,guest OS 便可完整地在 host OS 上載入並良好地執行。 KVM 提供的功能有: * 支援 CPU 和 memory Overcommit: Overcommit 代表可以要求超過實際可以 handle 的 memory,實際上在使用到才會為其分配 physical memory * 支援半虛擬化 I/O (virtio) * 支援熱插拔 (cpu, block device, network device 等) * 支援對稱多處理 (Symmetric Multi-Processing,縮寫為 SMP) * 支援 Live Migration * 透過 VFIO (Virtual Function I/O) 支援 PCI 裝置直通 (device passthrough) 和單根 I/O 虛擬化 (SR-IOV),需搭配 IOMMU (Intel VT-d / AMD-Vi) 提供 DMA 隔離 * 支援 KSM (kernel samepage merging) * 支援 NUMA 對於 Linux KVM 相關 API 的基本操作,可參見: * [sysprog21/kvm-host](https://github.com/sysprog21/kvm-host) * [Using the KVM API](https://lwn.net/Articles/658511/) * [KVM API Documentation](https://www.kernel.org/doc/Documentation/virtual/kvm/api.txt) ## KVM 在雲端基礎建設的採用 KVM 已成為公有和私有雲端服務及行動裝置虛擬化的共通基礎。各大雲端業者不僅採用 KVM 作為 hypervisor,也積極回饋 Linux 核心的 KVM 子系統,推動 confidential computing、大規模 live migration 和 microVM 等關鍵特性的演進。 ### AWS: Nitro System 與 Firecracker AWS 的 [Nitro System](https://docs.aws.amazon.com/whitepapers/latest/security-design-of-aws-nitro-system/the-nitro-system-journey.html) 將網路、儲存和安全功能卸載 (offload) 到專用硬體,讓 KVM hypervisor 僅需處理 CPU 和記憶體虛擬化,大幅降低虛擬化開銷。Nitro 的演進歷程: - 2013: 首個 Nitro 網路卡,卸載 VPC 網路處理 - 2017: Nitro hypervisor (基於 KVM) 上線,[C5 instance](https://aws.amazon.com/blogs/aws/now-available-compute-intensive-c5-instances-for-amazon-ec2/) 為首批採用者 - Nitro Enclaves: 提供隔離的運算環境,處理敏感隱私資料 - 2023 起: EC2 支援 [AMD SEV-SNP](https://aws.amazon.com/about-aws/whats-new/2023/04/amazon-ec2-amd-sev-snp/) confidential instance (R6a、M6a、C6a),利用 KVM 的 SEV-SNP hypervisor 支援保護 guest 記憶體 > [Deep dive on the AWS Nitro System](https://d1.awsstatic.com/events/Summits/reinvent2022/CMP301_Powering-Amazon-EC2-Deep-dive-on-the-AWS-Nitro-System.pdf) / [video](https://youtu.be/jAaqfeyvvSE) (2022) | [中文介紹](https://aws.amazon.com/tw/ec2/nitro/) [Firecracker](https://firecracker-microvm.github.io/) 是 AWS 以 Rust 開發的輕量 VMM,2018 年 11 月於 re:Invent 以 Apache 2.0 授權釋出開放原始碼,專為 serverless 和容器工作負載設計,啟動時間約 125ms (含 API 呼叫開銷,參見 [NSDI 2020 論文](https://www.usenix.org/conference/nsdi20/presentation/agache))。Firecracker 實作最小化的裝置模型 (virtio-net、virtio-block、virtio-vsock、virtio-balloon、virtio-rng、serial console),減少攻擊面。Firecracker 驅動 AWS Lambda 的底層隔離,2025 年因 AI agent 沙箱需求 (如 [E2B](https://e2b.dev/)) 而獲得更廣泛採用。 ![image](https://hackmd.io/_uploads/H1_2Tq3QA.png) [FreeBSD on Firecracker](https://www.usenix.org/publications/loginonline/freebsd-firecracker): FreeBSD 在 1 vCPU / 128 MB RAM 的 Firecracker 虛擬機器上,核心啟動時間可低於 20ms。 ### GCP: Confidential VM 與大規模 live migration [Google Compute Engine](https://cloud.google.com/compute/docs/instances/nested-virtualization/overview) 自 2012 年推出即採用 KVM,並支援 nested virtualization。GCP 是 Linux 核心 KVM 子系統的主要貢獻者之一,TDP MMU 的效能改進即由 Google 工程師 Sean Christopherson 和 David Matlack 等人主導。 GCP 於 2020 年率先推出基於 AMD SEV 的 [Confidential VM](https://cloud.google.com/confidential-computing/confidential-vm/docs/about-cvm) 服務 (N2D instance),後續升級至 SEV-SNP (2023 年,C2D/N2D with Milan/Genoa),讓使用者無需修改應用程式即可在加密的虛擬機器中執行工作負載。隨著 Linux v6.16 合併 Intel TDX 支援,GCP 也準備將 TDX-based confidential VM 納入產品線。 在 live migration 方面,Google 報告 TDP MMU 達成 416 vCPU / 12 TiB 虛擬機器的 live migration,測試時間縮減 89%,這直接受益於 KVM 的 TDP MMU 最佳化 (v5.10 引入,v6.9/v6.15 持續改進)。 ### Azure: SEV-SNP confidential VM Microsoft Azure 於 2022 年 7 月率先推出基於 AMD SEV-SNP 的 confidential VM ([DCasv5/ECasv5 系列](https://learn.microsoft.com/en-us/azure/confidential-computing/confidential-vm-overview)),早於 AWS (2023/04) 和 GCP。Azure 的 hypervisor 為 Hyper-V (非 KVM),但 Microsoft 積極為 Linux 核心貢獻 Hyper-V guest 驅動程式 (`drivers/hv/`) 和 confidential computing 路徑 (`arch/x86/hyperv/`),使 Linux 能在 Hyper-V 的 confidential VM 內良好運作。這些跨廠商的合作也催生 KVM VM planes 抽象層的提案,旨在為 AMD VMPL、Intel TDX partition、Hyper-V VTL 等不同廠商的虛擬特權層級提供統一的 KVM 介面。 ### Android: pKVM 與 AVF [Android Virtualization Framework](https://source.android.com/docs/core/virtualization/architecture?hl=en) (AVF) 在 Arm64 裝置上提供輕量保護虛擬機器 (protected VM, pVM)。AVF 的 hypervisor 後端以 pKVM 為主要參考實作,部分廠商 (如 Qualcomm) 亦可使用自有 hypervisor (如 Gunyah)。pKVM 在 Arm EL2 以保護模式執行 KVM,透過 stage-2 page table 強制隔離 guest 記憶體,使 host 核心 (EL1) 無法存取。 AVF 的 guest 端以 [Microdroid](https://source.android.com/docs/core/virtualization/microdroid) 為標準映像檔,這是一個最小化的 Android 執行環境,透過 vsock-based binder RPC 與 host Android 通訊。主要應用情境包含 [CompOS](https://source.android.com/docs/core/virtualization/usecases) (在 pVM 內編譯安全敏感程式碼以維護 Verified Boot 完整性)、隔離的金鑰管理,以及取代部分 TrustZone 工作負載。 pKVM 自 Android 13 (GKI 核心 5.10/5.15) 起出貨。upstream Linux 核心的 pKVM 功能持續演進: v6.12 (2024 年 11 月) 加入 guest 端偵測 pKVM 保護模式的支援,v6.14 (2025 年 3 月) 進一步縮小保護模式與非保護模式之間的功能差距。 > 注: [在 Google Pixel 6 上執行 Win11 虛擬機器](https://www.techbang.com/posts/94185-android-13-meritorious-service-google-pixel-6-successfully) 係使用標準 KVM (非 AVF/pKVM),展示 Arm64 裝置上 KVM 的完整虛擬化能力。 --- ## kvm-host [sysprog21/kvm-host](https://github.com/sysprog21/kvm-host) 展示一個使用 Linux 的 kernel-based virtual machine,達成可載入 Linux 核心的系統級虛擬機器 (system virtual machine) 的極小化實作,適合作為入門 Linux KVM 相關 API 的材料。以下解析程式碼的行為: #### `vm_t` 結構體 保存 VM 的狀態,如下: ```c typedef struct { int kvm_fd, vm_fd, vcpu_fd; void *mem; serial_dev_t serial; struct bus mmio_bus; struct bus io_bus; struct pci pci; struct diskimg diskimg; struct virtio_blk_dev virtio_blk_dev; struct virtio_net_dev virtio_net_dev; void *priv; } vm_t; ``` 其中 `priv` 在 arm64 會指向下面的結構體: ```c typedef struct { uint64_t entry; size_t initrdsz; int gic_fd; int gic_type; /* This device is a bridge between mmio_bus and io_bus */ struct dev iodev; } vm_arch_priv_t; ``` ### 啟動虛擬機器的流程 `main.c` 中包含 `main` 函式,即為程式的進入點。它會依序呼叫下面幾個函式建立虛擬機器: - `vm_init(&vm)`: 初始化虛擬機器 - `vm_load_image(&vm, kernel_file)`: 載入 Linux 核心 - `vm_load_initrd(&vm, initrd_file)`: 載入 initramfs (選用) - `vm_load_diskimg(&vm, diskimg_file)`: 指定 disk image 並初始化 virtio-blk (選用) - `vm_enable_net(&vm)`: 啟用 virtio-net 網路裝置 (選用) - `vm_late_init(&vm)`: 執行虛擬機器前的最終初始化 (ISA 專屬實作) - `vm_run(&vm)`: 開始執行虛擬機器 - `vm_exit(&vm)`: 虛擬機器結束、資源釋放 ### `vm_init` 使用 KVM 時,要先開啟 `/dev/kvm` 並透過 `KVM_CREATE_VM` 建立之後,初始化時分別需要設定: 1. `KVM_SET_TSS_ADDR`: 定義 3 個 page 的 physical address 範圍以設定 [Task State Segment](https://en.wikipedia.org/wiki/Task_state_segment) 2. `KVM_SET_IDENTITY_MAP_ADDR`: 定義 1 個 page 的 physical address 範圍以設定 identity map (page table) 3. `KVM_CREATE_IRQCHIP`: 建立虛擬的 [PIC](https://en.wikipedia.org/wiki/Programmable_interrupt_controller) 4. `KVM_CREATE_PIT2`: 建立虛擬的 [PIT](https://en.wikipedia.org/wiki/Programmable_interval_timer) 5. `KVM_SET_USER_MEMORY_REGION`: 為 VM 建立記憶體,將 guest physical memory 透過 host OS 的一段虛擬連續空間 (在 host 的 physical 不一定連續) 來模擬 ![](https://hackmd.io/_uploads/ryfwqL3wn.png) > [KVM MMU Virtualization](https://events.static.linuxfound.org/slides/2011/linuxcon-japan/lcj2011_guangrong.pdf) 6. `KVM_CREATE_VCPU`: 為 VM 建立 CPU `vm_init()` 會依序呼叫下列函式: - `open("/dev/kvm", O_RDWR)`: 開啟 KVM 裝置。 KVM 是以裝置檔案及 fd 的方式與使用者空間互動。並且都是用 ioctl 系統呼叫,傳入 KVM 相關的 fd 來操作 KVM。 - `ioctl(v->kvm_fd, KVM_CREATE_VM, 0)`: 使用 `KVM_CREATE_VM` ioctl 來建立新的 VM 。此時 Linux 核心內的 KVM 模組會建立虛擬機器,並回傳此 VM 的 fd 。 - `vm_arch_init(v)`: 呼叫各 ISA 專屬的 VM 初始化程式碼。 - 用 `mmap` 系統呼叫配置 guest 記憶體空間 - `ioctl(v->vm_fd, KVM_SET_USER_MEMORY_REGION, &region)`: 用 `KVM_SET_USER_MEMORY_REGION` ioctl ,可以把使用者空間的記憶體空間分配給虛擬機器。這裡就是把剛才 `mmap` 拿到的空間給 VM 使用。 - `ioctl(v->vm_fd, KVM_CREATE_VCPU, 0)` : 使用 `KVM_CREATE_VCPU` ioctl 來建立 vCPU 。 - `vm_arch_cpu_init(v)` : 呼叫 ISA 自己專屬的 vCPU 初始化程式。 - `bus_init(&v->io_bus)` 、 `bus_init(&v->mmio_bus)` : 初始化 `vm_t` 中的 `io_bus` 和 `mmio_bus` 。 - `vm_arch_init_platform_device(v)` :Platform device 指的是 PCI Bus 或 serial device 。各個 ISA 有不同的初始化方式,透過此函式來進行。 `vm_late_init()`:不同 ISA 有自己獨立的實作。 x86-64 不做任何事,而在 arm64 會建立 device tree 和設定 CPU 暫存器。 暫存器也該依據需求進行相應的初始化: 1. `KVM_GET_SREGS` / `KVM_SET_SREGS`: 設定 x86 中的 [segment register](https://en.wikipedia.org/wiki/X86_memory_segmentation),被用來指定 code / data / stack 等記憶體內容的定址範圍,包含: * Code Segment (CS): Pointer to the code * Data Segment (DS): Pointer to the data. * Extra Segment (ES). Pointer to extra data ('E' stands for 'Extra'). * F Segment (FS): Pointer to more extra data ('F' comes after 'E'). * G Segment (GS): Pointer to still more extra data ('G' comes after 'F'). * Stack Segment (SS): Pointer to the stack. > ![](https://i.imgur.com/JmBx8Th.png) > [x86 Segmentation for the 15-410 Student](https://www.cs.cmu.edu/~410/doc/segments/segments.html) > * [x86 Assembly/X86 Architecture](https://en.wikibooks.org/wiki/X86_Assembly/X86_Architecture) 2. 設定 `cr0` 以開啟 protected mode 3. `KVM_GET_REGS` / `KVM_SET_REGS`: * 設定 `rflags`: bit 1 恆為 1 * 設定 `rip`: `rip` 儲存下個要執行的 CPU 指令,因此需指向核心被擺放的記憶體位置 * 設定 `rsi`: `rsi` 則需設定在 boot parameters 的位置 > When using bzImage, the protected-mode kernel was relocated to 0x100000 (“high memory”), and the kernel real-mode block (boot sector, setup, and stack/heap) was made relocatable to any address between 0x10000 and end of low memory. > Reference: [The Linux/x86 Boot Protocol](https://docs.kernel.org/arch/x86/boot.html#memory-layout) > %rsi must hold the base address of the struct boot_params. > Reference: [The Linux/x86 Boot Protocol 1.15](https://www.kernel.org/doc/html/next/x86/boot.html#id1) 最後設定虛擬的 CPU (亦即 vCPU): 1. `KVM_GET_SUPPORTED_CPUID`: 取得一個 `struct kvm_cpuid2` 的結構單元,準備用來建立 cpuid information 2. 對於 `function` 為 `KVM_CPUID_SIGNATURE` 的 entry 進行設定 * [KVM CPUID bits](https://www.kernel.org/doc/html/latest/virt/kvm/x86/cpuid.html) 3. 最後使用 `KVM_SET_CPUID2` 將 entry 設定回 KVM 之中 ```c struct kvm_cpuid_entry { __u32 function; __u32 eax; __u32 ebx; __u32 ecx; __u32 edx; __u32 padding; }; struct kvm_cpuid2 { __u32 nent; __u32 padding; struct kvm_cpuid_entry2 entries[0]; }; ``` ### `vm_load_image` / `vm_load_initrd` 要載入 bzImage / initrd 到模擬的記憶體中,我們需要先對 Linux 開機流程的 memory map 有足夠的認識,以及 bzImage / initrd 應該放在相應的哪個位置 (在此範例程式之中,`X == 0x10000`): ``` Reg PhysAddr Binary image │ Protected-mode kernel │ RIP 100000 ├────────────────────────┤ bzImage [+ (setup_sects + 1) * 512] │ I/O memory hole │ 0A0000 ├────────────────────────┤ │ Reserved for BIOS │ ┆ ┆ │ Command line │ X+10000 ├────────────────────────┤ │ Stack/heap │ X+08000 ├────────────────────────┤ │ Kernel setup │ │ Kernel boot sector │ RSI X ├────────────────────────┤ bzImage [+ 0] │ Boot loader │ 001000 ├────────────────────────┤ │ Reserved for MBR/BIOS │ 000800 ├────────────────────────┤ │ Typically used by MBR │ 000600 ├────────────────────────┤ │ BIOS use only │ 000000 └────────────────────────┘ ... where the address X is as low as the design of the boot loader permits. ``` `vm_load_image` 所做即為設定一些必要的 `boot_params` 後,按照此 memory map 將 bzImage 載入到相對應的位置。此外,還需要設定 e820 table 以確保 initrd 所在位址是可以被合法使用的。需要設定哪些 memory address 可以為 OS 所用,而哪些要留給 bios / memory-mapped device 等: * 設定兩個 `struct boot_e820_entry`,type 皆為 `E820_RAM` 表示 available,簡單的把 `ISA_START_ADDRESS` 到 `ISA_END_ADDRESS` 以外的位置都設定成可用 `vm_load_initrd` 則載入 initrd。`boot_params` 的 `initrd_addr_max` 會記錄 initrd 會允許載入的最高位置,我們可以根據 `initrd_addr_max`、initrd 本身的大小、以及我們所模擬的 VM 擁有的記憶體大小來找到合適的載入位置。 * 並透過設定 `ramdisk_image` 和 `ramdisk_size` 使開機程序正確運作 ### guest VM 中 MMIO 和記憶體配置 > 記憶體配置位址定義在此 [`src/arch/arm64/vm-arch.h`](https://github.com/sysprog21/kvm-host/blob/master/src/arch/arm64/vm-arch.h) 跟 x86 不同,在 arm64 中,可自己自訂每一個裝置 MMIO 位址還有可用記憶體的位址。之後再透過 device tree 告訴 kernel 是如何配置的。 其中有參考 [kvmtool 的配置方式](https://github.com/kvmtool/kvmtool/blob/master/arm/include/arm-common/kvm-arch.h),但不是完全一致 以下是 kvm-host arm64 移植的配置: ``` ┌──────────────┬────────────────────────┐ │ 0x00000000 │ I/O port (64 KB) │ ├──────────────┼────────────────────────┤ │ 0x00100000 │ GIC (1M ─ 16M) │ ├──────────────┼────────────────────────┤ │ 0x40000000 │ PCI CFG + MMIO (1 GB) │ ├──────────────┼────────────────────────┤ │ 0x80000000 │ DRAM (RAM_BASE) │ └──────────────┴────────────────────────┘ ``` 跟 x86-64 不一樣的是,DRAM 的起始位址,在 x86-64 為 0,但在 arm64 內為 2GB 。故在各個架構的 `desc.h` 檔裡,定義對應該架構的 RAM 起始位址的巨集 `RAM_BASE` 。 在 x86-64 定義為 ```c #define RAM_BASE 0 ``` 而在 arm64 定義為 ```c #define RAM_BASE (1UL << 31) ``` ### arm64 專屬結構體建立 arm64 需要額外的空間記錄以下: 1. initramfs 大小 2. GIC device fd 和 GIC 類型 3. kernel entry point 4. Port I/O bus 和 MMIO bus 的 bridge 故在 `src/arch/arm64/vm.c` 加入了一個私有的結構體: ```c typedef struct { uint64_t entry; size_t initrdsz; int gic_fd; int gic_type; /* This device is a bridge between mmio_bus and io_bus */ struct dev iodev; } vm_arch_priv_t; static vm_arch_priv_t vm_arch_priv; ``` 並在 `src/vm.h` 中的 `vm_t` 透過 `priv` 指標指向不同 ISA 實作的私有結構體。 ### 建立與初始化中斷控制器 > 參考資料: > - [GICv3 and GICv4 Software Overview](https://developer.arm.com/documentation/dai0492/latest/) > - [Linux KVM API](https://www.kernel.org/doc/Documentation/virtual/kvm/api.txt) > - [Linux KVM API Alternative Page](https://docs.kernel.org/virt/kvm/api.html) > - [Linux 核心中針對 vGICv3 的說明](https://www.kernel.org/doc/Documentation/virtual/kvm/devices/arm-vgic-v3.txt) > - [Linux 核心中針對 vGICv3 的說明 Alternative Page](https://docs.kernel.org/virt/kvm/devices/arm-vgic-v3.html) > - [kvmtool 相關程式](https://github.com/kvmtool/kvmtool/blob/master/arm/gic.c) 實作在 [`src/arch/arm64/vm.c`](https://github.com/sysprog21/kvm-host/blob/master/src/arch/arm64/vm.c) 中的 `create_irqchip()` 和 `finalize_irqchip()` 二個函式中。`create_irqchip()` 會被 `vm_arch_init()` 函式呼叫;而 `finalize_irqchip()` 會被 `vm_init_platform_device()` 呼叫。 KVM 提供虛擬化中斷控制器,支援 GICv2 和 GICv3。取決於 host 硬體的支援,在 host 為 GICv3 的硬體上,如果 GICv3 硬體不支援 GICv2 模擬功能,那麼只能建立虛擬 GICv3 中斷控制器。若 host 的中斷控制器為 GICv2,則只能建立 GICv2 虛擬中斷控制器。 透過 KVM 提供的虛擬化中斷控制器,不必在使用者空間自行實作中斷控制器的模擬,可以直接使用 Linux 核心提供的實作。 [eMAG 8180](https://en.wikipedia.org/wiki/Ampere_Computing) 主機僅能支援建立虛擬 GICv3 的中斷控制器,故下面以 GICv3 來說明。 GICv3 中斷控制器架構: ![](https://hackmd.io/_uploads/SJHJc1Xuh.png) > 取自 [GICv3 and GICv4 Software Overview](https://developer.arm.com/documentation/dai0492/latest/) 在 GICv3 中, CPU Interface 是以系統暫存器的方式存取,使用 `msr` 和 `mrs` 指令來讀寫暫存器。而 Redistributor 為每個 CPU Core 各一個, Distributor 則是整個系統共用一個。 Redistributor 和 Distributor 都是用 MMIO 來存取。 使用 `KVM_CREATE_VM` ioctl 建立 VM 後,就可以用 `KVM_CREATE_DEVICE` VM ioctl 來建立中斷控制器。 `KVM_CREATE_DEVICE` ioctl 要傳入 `struct kvm_create_device` 結構。如下: ```c struct kvm_create_device gic_device = { .type = KVM_DEV_TYPE_ARM_VGIC_V3, }; ioctl(v->vm_fd, KVM_CREATE_DEVICE, &gic_device); ``` 裡面的 .type 屬性為 `KVM_DEV_TYPE_ARM_VGIC_V3` ,代表我們要建立一個 GICv3 的虛擬中斷控制器。 用 `ioctl(v->vm_fd, KVM_CREATE_DEVICE, &gic_device)` 來建立 GICv3 的中斷控制器。建立後,在 `struct kvm_create_device` 內的 fd 屬性可取得此 GICv3 中斷控制器的 file descriptor,並把它存在 arm64 的私有資料結構 `vm_arch_priv_t` 中。 接下來要設定 Redistributor 和 Distributor 的 MMIO 位址。使用 `KVM_SET_DEVICE_ATTR` ioctl 來設定,其中 file descriptor 是剛才取得的 GICv3 的 fd 。此 ioctl 傳入的參數為 `struct kvm_device_attr` ,故流程如下: ```c uint64_t dist_addr = ARM_GIC_DIST_BASE; uint64_t redist_addr = ARM_GIC_REDIST_BASE; struct kvm_device_attr dist_attr = { .group = KVM_DEV_ARM_VGIC_GRP_ADDR, .attr = KVM_VGIC_V3_ADDR_TYPE_DIST, .addr = (uint64_t) &dist_addr, }; struct kvm_device_attr redist_attr = { .group = KVM_DEV_ARM_VGIC_GRP_ADDR, .attr = KVM_VGIC_V3_ADDR_TYPE_REDIST, .addr = (uint64_t) &redist_addr, }; ioctl(gic_fd, KVM_SET_DEVICE_ATTR, &redist_attr); ioctl(gic_fd, KVM_SET_DEVICE_ATTR, &dist_attr); ``` 因為 `.addr` 要放一個指向 uint64_t 的指標,故我們先宣告區域變數,再透過 `&` 取得此區域變數的指標。 最後再透過 `ioctl(gic_fd, KVM_SET_DEVICE_ATTR, &redist_attr)` 和 `ioctl(gic_fd, KVM_SET_DEVICE_ATTR, &dist_attr)` 來設定 GICv3 的 MMIO 位址。 GICv3 建立完後,再建立所有要使用的 vCPU 後,還需要進行初始化,才能讓 VM 順利執行。需對剛才建立的 GICv3 的 fd 執行 `KVM_SET_DEVICE_ATTR` ioctl ,也是傳入 `struct kvm_device_attr` 結構,內容如下: ```c struct kvm_device_attr vgic_init_attr = { .group = KVM_DEV_ARM_VGIC_GRP_CTRL, .attr = KVM_DEV_ARM_VGIC_CTRL_INIT, }; ioctl(gic_fd, KVM_SET_DEVICE_ATTR, &vgic_init_attr); ``` 呼叫 `ioctl(gic_fd, KVM_SET_DEVICE_ATTR, &vgic_init_attr)` 即可初始化 GICv3。依據核心文件,此呼叫必須在所有 vCPU 建立完成後進行 ("Must be called after all VCPUs have been created"),因為 redistributor 區域的配置取決於 vCPU 數量。 這一節所提到的建立 GICv3 實作在 [`src/arch/arm64/vm.c`](https://github.com/sysprog21/kvm-host/blob/master/src/arch/arm64/vm.c) 檔案中的 `create_irqchip()` 函式中,被 `vm_arch_init()` 呼叫;而初始化 GICv3 則是在同樣檔案的 `finalize_irqchip()` 函式中,被 `vm_arch_init_platform_device()` 呼叫。 ### 初始化 vCPU 在 `vm_init()` ,會透過 `ioctl(v->vm_fd, KVM_CREATE_VCPU, 0)` 建立 vCPU ,得到 vcpu_fd,之後就會呼叫 `vm_arch_cpu_init()` 來初始化 vCPU 。 在 arm64 中,可以對 vcpu_fd 呼叫 `KVM_ARM_VCPU_INIT` ioctl 初始化 vCPU ,其中需傳入指向 `struct kvm_vcpu_init` 結構的指標作為參數。而 `struct kvm_vcpu_init` 可直接透過在 vm_fd 上呼叫 `KVM_ARM_PREFERRED_TARGET` ioctl 得到。故完整程式碼如下: ```c int vm_arch_cpu_init(vm_t *v) { struct kvm_vcpu_init vcpu_init; if (ioctl(v->vm_fd, KVM_ARM_PREFERRED_TARGET, &vcpu_init) < 0) return throw_err("Failed to find perferred CPU type\n"); if (ioctl(v->vcpu_fd, KVM_ARM_VCPU_INIT, &vcpu_init)) return throw_err("Failed to initialize vCPU\n"); return 0; } ``` ### 中斷處理 在 arm64 架構中, IRQ 號碼分配如下: - 0 - 15: SGI (Software generated interrupt) - 16 - 31: PPI (Private peripheral interrupt) - 32 - 1019: SPI (Shared peripheral interrupt) 週邊裝置的中斷通常會是 SPI 中斷。 在 KVM 中,也有另外一組中斷編號為 GSI (Global System Interrupt) ,在 arm64 , GSI + 32 即為 arm64 的中斷編號。 在 arm64 向 guest 發送中斷比較特別,雖然也是對 vm_fd 呼叫 `KVM_IRQ_LINE` ioctl ,會傳入指向 `struct kvm_irq_level` 的指標。 以下取自 [Linux 核心的說明文件](https://www.kernel.org/doc/Documentation/virtual/kvm/api.txt)。`struct kvm_irq_level` 定義如下: ```c struct kvm_irq_level { union { __u32 irq; /* GSI */ __s32 status; /* not used for KVM_IRQ_LEVEL */ }; __u32 level; /* 0 or 1 */ }; ``` 而結構中的 irq 就是用來設定要發的中斷編號。但在 arm64 下, irq 不是設為 GSI (x86-64 下為 GSI),而是設為下面的格式: ``` ┌───────────┬────────────┬────────────────┐ │ 31 ... 24 │ 23 ... 16 │ 15 ... 0 │ ├───────────┼────────────┼────────────────┤ │ irq_type │ vcpu_index │ irq_id │ └───────────┴────────────┴────────────────┘ irq_type 的值: - 0: out-of-kernel GIC: irq_id 0 = IRQ, irq_id 1 = FIQ - 1: in-kernel GIC: SPI, irq_id 介於 32 到 1019 (vcpu_index 忽略) - 2: in-kernel GIC: PPI, irq_id 介於 16 到 31 ``` 因為我們要發送的是 SPI ,故要把 irq_type 設為 1 ,然後 irq_id 設為 GSI + 32 。 根據上面的說明,我們可以寫出 arm64 專屬的發送中斷函式 (位於 [`src/arch/arm64/vm.c`](https://github.com/sysprog21/kvm-host/blob/master/src/arch/arm64/vm.c) 中): ```c int vm_irq_line(vm_t *v, int irq, int level) { struct kvm_irq_level irq_level = { .level = level, }; irq_level.irq = (KVM_ARM_IRQ_TYPE_SPI << KVM_ARM_IRQ_TYPE_SHIFT) | ((irq + ARM_GIC_SPI_BASE) & KVM_ARM_IRQ_NUM_MASK); if (ioctl(v->vm_fd, KVM_IRQ_LINE, &irq_level) < 0) return throw_err("Failed to set the status of an IRQ line, %llx\n", irq_level.irq); return 0; } ``` 另外我們也為 serial device 和 virtio-blk 指定 GSI 中斷編號,定義在 [`desc.h`](https://github.com/sysprog21/kvm-host/blob/master/src/arch/arm64/desc.h) 中: ```c #define SERIAL_IRQ 0 #define VIRTIO_BLK_IRQ 1 ``` ### 平台相關裝置 以下是 arm64 的 `vm_init_platform_device()` 的程式碼: ```c int vm_arch_init_platform_device(vm_t *v) { vm_arch_priv_t *priv = (vm_arch_priv_t *) v->priv; /* Initial system bus */ bus_init(&v->io_bus); bus_init(&v->mmio_bus); dev_init(&priv->iodev, ARM_IOPORT_BASE, ARM_IOPORT_SIZE, v, pio_handler); bus_register_dev(&v->mmio_bus, &priv->iodev); /* Initialize PCI bus */ pci_init(&v->pci); v->pci.pci_mmio_dev.base = ARM_PCI_CFG_BASE; bus_register_dev(&v->mmio_bus, &v->pci.pci_mmio_dev); /* Initialize serial device */ if (serial_init(&v->serial, &v->io_bus)) return throw_err("Failed to init UART device"); if (finalize_irqchip(v) < 0) return -1; return 0; } ``` 裡面包含 Bus 初始化、 serial device 初始化、 PCI Bus 初始化,遇有對 GICv3 的初始化。關於 Bus 和 PCI Bus 初始化相關的說明請見後續。 #### I/O Bus 在 x86-64 中,有 port I/O 指令,而 Port I/O 指令會呼叫 kvm-host 的 I/O Bus handler 處理,但在 arm64 中,所有的 I/O 操作都是 MMIO 。為了相容原本 serial device 的程式碼,故在 arm64 的實作中,定義了一個 bridge device ,用來把對前 64K 的 MMIO 操作導向 I/O bus 。 在 `vm_arch_priv_t` 內,用來做 mmio_bus 和 io_bus bridge device 定義。 ```c typedef struct { .... /* This device is a bridge between mmio_bus and io_bus*/ struct dev iodev; } vm_arch_priv_t; ``` 其處理函式: ```c static void pio_handler(void *owner, void *data, uint8_t is_write, uint64_t offset, uint8_t size) { vm_t *v = (vm_t *) owner; bus_handle_io(&v->io_bus, data, is_write, offset, size); } ``` 在 `vm_arch_init_platform_device()` 中的相關程式: ```c dev_init(&priv->iodev, ARM_IOPORT_BASE, ARM_IOPORT_SIZE, v, pio_handler); bus_register_dev(&v->mmio_bus, &priv->iodev); ``` 故 serial device 就不用特別對 I/O Port 的部份進行修改。 ### `vm_run` 在進行必要的設定並且載入 image 後,即可透過 `KVM_RUN` ioctl 來執行 guest vCPU。指令的解碼、執行及記憶體存取操作由 KVM 處理,我們的虛擬機器不需要實作相關模擬,只需要處理使得 `KVM_RUN` ioctl 返回的特殊事件 (主要是 I/O 裝置操作的模擬)。 ### Linux 核心啟動流程 > [The Linux/x86 Boot Protocol](https://www.kernel.org/doc/html/latest/arch/x86/boot.html) 相傳在十八世紀,德國 Baron Münchhausen 男爵常誇大吹噓自己的英勇事蹟,其中一項是「拉著自己的頭髮,將自己從受陷的沼澤中提起」,此事後來收錄於德國《吹牛大王歷險記》,則改寫為「用拔靴帶把自己從海中拉起來」,這裡的「拔靴帶」(bootstrap) 指的是長統靴靴筒頂端後方的小環帶,是用以輔助穿長統靴。這種有違物理原理的誇大動作,卻讓不同領域的人們獲得靈感,Robert A. Heinlein 發表於 1941 年的短文〈[By His Bootstraps](https://en.wikipedia.org/wiki/By_His_Bootstraps)〉收錄典故並給予多種延伸想法;滑鼠發明人 Doug Engelbart 博士甚至在 1989 年以此命名其研究機構「Bootstrap 學院」,並擔任該院主任。在商業上,bootstrapping 則被引申為一種創業模式,也就是初期投入少量的啟動資本,然後在創業過程中主要依靠從客戶得來的銷售收入,形成一個良好的正現金流。在電腦資訊領域,因為開機過程是環環相扣,先透過簡單的程式讀入記憶體,執行後又載入更多磁區、程式碼來執行,直到作業系統完全載入為止,所以開機過程也被稱為 bootstrapping,簡稱 "boot"。 > boot loader 也可很精簡,例如 [afboot-stm32](https://github.com/afaerber/afboot-stm32) 僅用 400 餘行,就能在 STM32 微控制器上載入 Linux 核心 > 延伸閱讀:〈[Qi -- Lightweight Boot Loader Applied in Mobile and Embedded Devices](https://www.slideshare.net/jserv/qi-lightweight-boot-loader-applied-in-mobile-and-embedded-devices)〉 要讓 kvm-host 可以啟動 Linux 並開啟與使用者互動的介面,涉及幾個關鍵的設施。 - [ ] bzImage 引用自 [vmlinux](https://en.wikipedia.org/wiki/Vmlinux): > As the Linux kernel matured, the size of the kernels generated by users grew beyond the limits imposed by some architectures, where the space available to store the compressed kernel code is limited. The bzImage (big zImage) format was developed to overcome this limitation by splitting the kernel over non-contiguous memory regions 因應 Linux 核心的空間日益增加,bzImage 因此被設計以將 kernel 切割成多段不連續的記憶體區塊,而得以分段載入記憶體之中,包含 object file `bootsect.o` + `setup.o` + `misc.o` + `piggy.o`,其中 `piggy.o` 的 data section 中就放著壓縮後的 vmlinux 檔(編譯出的原始 kernel image)。 - [ ] initrd 檔案系統在 UNIX-based 的作業系統具有關鍵的地位。其中,為了讓 kernel 在完成開機後,可以進入 user mode 執行使用者程式,或者新增新的 file system,[root filesystem](https://en.wikipedia.org/wiki/Root_directory) 的存在是必要的。 而 initrd 是 [initial ramdisk](https://en.wikipedia.org/wiki/Initial_ramdisk) 的縮寫,它借用 RAM 空間建立暫時的 root file system (簡稱 rootfs)。當 Linux 核心載入並執行時,由於 initrd 所在的空間存在檔案系統,且通常會包含 init 等程式,故可用以掛入某些特別的驅動程式,比方說 SCSI,完成階段性目標後,kernel 會將真正的 rootfs 掛載,並執行 /sbin/init 程式。總而言之,initrd 提供「兩階段開機」的支援。 為何需要此等迂迴的開機途徑呢?原因是,rootfs 可能所存在於非本機的外部儲存裝置。另一方面,如果要將所有裝置驅動程式直接編譯到核心中,儲存裝置很可能極難尋找。比方說 SCSI 裝置就需要複雜且耗時的程序,若用 RAID 系統更是需要看配置情況而定,同樣的問題也發生在 USB storage 上,因為 kernel 得花上更長的等待與配置時間,或說遠端掛載 rootfs,不僅得處理網路裝置的問題,甚至還得考慮相關的伺服器認證、通訊往返時間等議題。我們會希望 rootfs 的裝置驅動程式能具有像是 udevd 的工具可以實作自動載入。但是這裡就存在矛盾,udevd 是一個 executable file,因此在 rootfs 被掛載前,是不可能執行 udevd 的,但是如果 udevd 沒有啟動,那就無法自動載入 rootfs 所在裝置的驅動程式。 initrd 的出現得以解決這個矛盾。第一階段啟動的 initrd 提供初步啟動 user space 程式的環境,然後其中可以確定最後真正 rootfs 所在裝置究竟為何,因此 initrd 可以把負責啟動該 rootfs 的 driver 載入。最後再掛載真正的 root,完成其餘 init 程序。 更重要的是,我們可在 initrd 放置基礎的工具,一來作為掛載 rootfs 作準備,比方說硬體初始化、解密、解壓縮等等,二來提示使用者或系統管理員目前的狀態,這對於消費性電子產品來說,有很大的意義。 ### 載入 Linux 核心和 initrd 檔案 > 參考資料: [Booting arm64 Linux](https://docs.kernel.org/arch/arm64/booting.html) > 實作: [`src/arch/arm64/vm.c`](https://github.com/sysprog21/kvm-host/blob/master/src/arch/arm64/vm.c) 中的 `vm_arch_load_image` 和 `vm_arch_load_initrd` 函式。 根據 [Linux 核心文件](https://docs.kernel.org/arch/arm64/booting.html)提供的資料,一個 arm64 的 Linux kernel image , header 的內容如下: ```c typedef struct { uint32_t code0; /* Executable code */ uint32_t code1; /* Executable code */ uint64_t text_offset; /* Image load offset, little endian */ uint64_t image_size; /* Effective Image size, little endian */ uint64_t flags; /* kernel flags, little endian */ uint64_t res2; /* reserved */ uint64_t res3; /* reserved */ uint64_t res4; /* reserved */ uint32_t magic; /* Magic number, little endian, "ARM\x64" */ uint32_t res5; /* reserved (used for PE COFF offset) */ } arm64_kernel_header_t; ``` 文件提到 ```! The Image must be placed text_offset bytes from a 2MB aligned base address anywhere in usable system RAM and called there. ``` 故要選定一個 2MB 對齊的記憶體位址,然後再加上 header 中 `text_offset` 的值,這個位址就是核心 Image 放置的位置。而 `image_size` 是從核心 Image 放置的起始位置算起,要留給核心的可用記憶體空間。 載入核心後,要啟動核心執行第一個指令位址是 Image 載入的位址,即 `code0` 的位址,故也把此位址記錄下來,放到 vm_arch_priv_t 內的 entry 成員變數中。 除了載入核心外,還要載入 initramfs 作為開機時的第一個檔案系統。文件中提到: > If an initrd/initramfs is passed to the kernel at boot, it must reside entirely within a 1 GB aligned physical memory window of up to 32 GB in size that fully covers the kernel Image as well. 代表 initramfs 的位置跟 Linux 核心必須要在同一個 1GB aligned 下的 32 GB 的 window 內,但沒提到 initramfs 要不要對齊。 在 `src/arch/arm64/vm.c` 中的 `vm_arch_load_image` 和 `vm_arch_load_initrd` 二個函式,包含載入 Linux 核心和 initramfs 的程式。 `vm_arch_load_image` 會檢查核心的 header 。檢查完成後,把核心載入預先定義好的位址( `ARM_KERNEL_BASE` ),再加上 `text_offset` 。 ```c int vm_arch_load_image(vm_t *v, void *data, size_t datasz) { vm_arch_priv_t *priv = (vm_arch_priv_t *) v->priv; arm64_kernel_header_t *header = data; if (header->magic != 0x644d5241U) return throw_err("Invalid kernel image\n"); uint64_t offset; if (header->image_size == 0) offset = 0x80000; else offset = header->text_offset; if (offset + datasz >= ARM_KERNEL_SIZE || offset + header->image_size >= ARM_KERNEL_SIZE) { return throw_err("Image size too large\n"); } void *dest = vm_guest_to_host(v, ARM_KERNEL_BASE + offset); memmove(dest, data, datasz); priv->entry = ARM_KERNEL_BASE + offset; return 0; } ``` 而 initramfs 也是直接載入到固定位址(`ARM_INITRD_BASE`): ```c int vm_arch_load_initrd(vm_t *v, void *data, size_t datasz) { vm_arch_priv_t *priv = (vm_arch_priv_t *) v->priv; void *dest = vm_guest_to_host(v, ARM_INITRD_BASE); memmove(dest, data, datasz); priv->initrdsz = datasz; return 0; } ``` ### Device Tree 實作 > [Device Tree Specification](https://github.com/devicetree-org/devicetree-specification/releases/download/v0.3/devicetree-specification-v0.3.pdf) > [kvmtool 的實作](https://github.com/kvmtool/kvmtool/blob/master/arm/fdt.c) 啟動核心前需要把 device tree 所在的實體記憶體位址透過 `x0` 暫存器傳給核心。 [kvmtool](https://github.com/kvmtool/kvmtool) 透過 libfdt 來產生 device tree ,因此我們也可以採用同樣的作法。[libfdt](https://git.kernel.org/pub/scm/utils/dtc/dtc.git) 是包含在 dtc 套件內的函式庫。 實作 device tree 的過程,參考 kvmtool 的實作。這是 [kvmtool 的 DTB Dump](https://gist.github.com/yanjiew1/53be2d03430d187e61fff48eca3b6591) 。 Device Tree 主要定義以下: - Machine Type - CPU - Memory - Initramfs Address - Bootargs - Interrupt Controller - 16550 UART Addresses - PCI Addresses 使用 libfdt 來產生 device tree 流程: 1. 利用 `fdt_create()` 函式指定欲放置 device tree 的緩衝區及其空間,即新建空的 device tree。 2. 呼叫 `fdt_finish_reservemap()` 結束 memory reservation map 區段 (若無需保留特定記憶體區域,直接呼叫即可),之後才能進入結構建立階段。 3. 使用 `fdt_begin_node()` 來新增節點。一開始需要新增根節點,故需先呼叫 `fdt_begin_node(fdt, "")`。 4. 節點中,可以用 `fdt_property()`、`fdt_property_cell()`、`fdt_property_u64()` 等來加入屬性。其中 `fdt_property_cell()` 和 `fdt_property_u64()` 會自動處理 endianness 轉換;若使用 `fdt_property()` 則須自行以 `cpu_to_fdt32()` 或 `cpu_to_fdt64()` 轉換,因為 device tree 內的數值採用 big-endian 表示。 5. 節點的屬性都新增完後,用 `fdt_end_node()` 來關閉節點。 6. 最後用 `fdt_finish()` 來完成 device tree。`fdt_finish()` 呼叫完成後,只要上述 `fdt_begin_node()` 和 `fdt_end_node()` 都有正確配對,緩衝區的內容即為合法的 device tree。 ### 設定 vCPU 暫存器初始值 > 參考資料: [Booting arm64 Linux](https://docs.kernel.org/arch/arm64/booting.html) > [kvmtool arm/aarch64/kvm-cpu.c](https://github.com/kvmtool/kvmtool/blob/master/arm/aarch64/kvm-cpu.c) 設定 vCPU 暫存器的程式在 `src/arch/arm64/vm.c` 中的 `init_reg()`,由 `vm_late_init()` 呼叫。 `init_reg()` 內的程式,會把 x0 設定為 device tree 的位址, x1 ~ x3 設為 0 。把 Program Counter 設成 Linux 核心的起始位址。 ```c /* Initialize the vCPU registers according to Linux arm64 boot protocol * Reference: https://www.kernel.org/doc/Documentation/arm64/booting.txt */ static int init_reg(vm_t *v) { vm_arch_priv_t *priv = (vm_arch_priv_t *) v->priv; struct kvm_one_reg reg; uint64_t data; reg.addr = (uint64_t) &data; #define __REG(r) \ (KVM_REG_ARM_CORE_REG(r) | KVM_REG_ARM_CORE | KVM_REG_ARM64 | \ KVM_REG_SIZE_U64) /* Clear x1 ~ x3 */ for (int i = 0; i < 3; i++) { data = 0; reg.id = __REG(regs.regs[i]); if (ioctl(v->vcpu_fd, KVM_SET_ONE_REG, &reg) < 0) return throw_err("Failed to set x%d\n", i); } /* Set x0 to the address of the device tree */ data = ARM_FDT_BASE; reg.id = __REG(regs.regs[0]); if (ioctl(v->vcpu_fd, KVM_SET_ONE_REG, &reg) < 0) return throw_err("Failed to set x0\n"); /* Set program counter to the begining of kernel image */ data = priv->entry; reg.id = __REG(regs.pc); if (ioctl(v->vcpu_fd, KVM_SET_ONE_REG, &reg) < 0) return throw_err("Failed to set program counter\n"); #undef __REG return 0; } ``` 裡面定義 `__REG` 巨集,它可以用來產生 `KVM_SET_ONE_REG` ioctl 中,傳入的 `struct kvm_one_reg` 中,裡面的暫存器 ID。 ### UART * [Serial UART information](https://www.lammertbies.nl/comm/info/serial-uart) * [arch/x86/include/asm/msr-index.h](https://elixir.bootlin.com/linux/latest/source/arch/x86/include/asm/msr-index.h) * [include/uapi/linux/serial_reg.h](https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/serial_reg.h) --- ## virtio > [Virtual I/O Device (VIRTIO) Version 1.2](https://docs.oasis-open.org/virtio/virtio/v1.2/virtio-v1.2.html) (OASIS 標準) > 延伸: [Virtio and the chamber of secrets](https://www.youtube.com/watch?v=MY-PklsW1GQ) (KVM Forum 2024, Michael S. Tsirkin) -- 涵蓋 virtio 近期發展方向 virtio 是 I/O 請求溝通的標準,架構如下圖所示,有前端和後端。前端通常作為驅動程式存在,供 guest OS 使用;後端則是在 guest OS 中被視為裝置,後端可以是軟體模擬出來的裝置,也可以是支援 virtio 的實體裝置。 以 hypervisor 實作的後端來說,前端將 I/O 請求傳給後端,後端會將請求傳給實際的裝置,待 I/O 處理完成後傳回給前端,後端的這個過程即裝置模擬。前後端使用 virtqueue 作為資料交換的機制。 ![](https://hackmd.io/_uploads/ryjVCU3vh.png) 對於前述 KVM 對 I/O 的處理流程,virtio 的機制使得模式切換的次數減少;若 virtqueue 以共享記憶體實作,則可減少資料複製的開銷,提升虛擬化下 I/O 的效能。 一個 virtio 裝置包含以下幾個部分: - Device status field - Feature bits - Notifications - Device Configuration space - One or more virtqueues ### Device Status Field 用來指示裝置的狀態,通常由驅動程式寫入,在初始化階段使用: ``` ACKNOWLEDGE (1) DRIVER (2) FAILED (128) FEATURES_OK (8) DRIVER_OK (4) DEVICE_NEEDS_RESET (64) ``` ### Feature Bits 用來指示裝置所支援的特性 (virtio 1.2): > 0 to 23, 50 to 127: Feature bits for the specific device type > 24 to 40: Feature bits reserved for extensions to the queue and feature negotiation mechanisms > 41 to 49, 128 and above: Feature bits reserved for future extensions ### Notification 通知包含以下三種,第一種是配置改變時由裝置發起的通知,後兩種與 Virtqueue 相關。 通知的實作為 transport specific。對於 virtio-pci,驅動程式到裝置的通知 (available buffer notification) 由寫入特定記憶體位址完成;裝置到驅動程式的通知 (used buffer notification) 則由 interrupt 完成。在軟體模擬的裝置後端 (如 kvm-host),驅動程式的寫入會觸發 VM exit 由 VMM 處理;實體 virtio 裝置則直接由硬體處理,不涉及 VM exit。 - configuration change notification - available buffer notification - used buffer notification ### Device Configuration Space virtio 支援三種 bus 來探查及初始化裝置: PCI、MMIO、Channel I/O。裝置會有根據 bus 定義的配置空間,其中包含上述 feature bits 和 device status field。virtio-pci 即基於 PCI 實作的 virtio transport。 ### Virtqueues 裝置和驅動程式共享的資料結構,用於驅動程式與裝置之間 I/O 請求的溝通。virtio 1.1 規格定義以下兩種 virtqueue,kvm-host 實作的是 packed virtqueue: - split virtqueue - packed virtqueue ### Device Initialization 通常會依照以下步驟初始化裝置 1. 重設裝置 2. guest OS 探查到裝置後會設定 `ACKNOWLEDGE (1)` 3. 確立裝置的驅動程式後設定 `DRIVER (2)` 4. 讀取裝置的 feature bits,根據驅動程式所能提供的特性寫入一個裝置 feature 的 subset 5. 驅動程式設定 `FEATURES_OK (8)`,表示 feature 協商完成 6. 重新讀取 device status,若 `FEATURES_OK` 仍然存在代表裝置接受了驅動程式選定的 feature subset;若裝置清除了此旗標,則代表裝置不支援該組合,裝置不可用 7. 執行特定於裝置的初始化 8. 一切正常就設定 `DRIVER_OK (4)` 否則設定 `FAILED (128)` 若裝置運作的過程遇到錯誤,應設定 `DEVICE_NEEDS_RESET (64)` 並發送 configuration change notification ### virtio-blk virtio-blk 即支援 virtio 的 block device,kvm-host 引入的 virtio-blk 建構於 virtio-pci 之上。 ### Packed Virtqueues Split virtqueue 的結構如下圖。驅動程式要發起 I/O 請求會將 descriptor 填入 Descriptor Table,完成後將 descriptor 的位置寫入 Avail Ring,然後向裝置發起 available buffer notification。裝置收到通知後,根據 Avail Ring 找出對應的 descriptor,依照 buffer 內容處理 I/O 請求,完成後寫入 Used Ring 並向驅動程式發起 used buffer notification。 ![](https://hackmd.io/_uploads/ryfi0UhP3.png) Packed virtqueue 是 virtio 1.1 提出的新結構,基本概念與 split virtqueue 相似,但將 Descriptor Table、Avail Ring、Used Ring 合併為單一 Descriptor Ring,以有效利用 cache。原本的 Device Area 與 Driver Area 改由 Device Event Suppression 和 Driver Event Suppression 使用,Event Suppression 用來讓裝置或驅動程式抑制對方的通知。這三個區域都位於 guest OS 的 physical memory 上。 ![](https://hackmd.io/_uploads/Hkl30LnPn.png) Descriptor 結構體如下 ```c struct pvirtq_desc { /* Element Address. */ le64 addr; /* Element Length. */ le32 len; /* Buffer ID. */ le16 id; /* The flags depending on descriptor type. */ le16 flags; }; ``` 每個 I/O 請求關聯一個 buffer,每個 buffer 可由多段連續記憶體組成,每段為一個 element,由一個 descriptor 表示。每次發起通知可包含多組 buffer。 ::: warning 在 Specification , "buffer" 有時是指一段連續的記憶體也就是 element,有時是指多個 element 組合後的整體, "request" 則是指發起一次通知 ::: 裝置及其驅動程式內部會各自維護一個值為 1 或 0 的 wrap counter ,初始值為 1 , wrap counter 設計使得要從頭存取時,不會存取到該次通知已經存取過的。 write flag 的作用是標示該段記憶體是 write-only 還是 read-only indirect flag 標示該 descriptor 指向的是一個 descriptor table ,需要協商 Feature VIRTQ_DESC_F_INDIRECT 裝置接收 avail buffer 的流程如下 - 收到 avail buffer notification - 接續上一次讀取的位置,讀取 descriptor - 若 flags 的 avail 與裝置 wrap counter 相同且 used 與裝置 wrap counter 相反則該 descriptor avail - 若 flags 的 next 被設定,則 buffer 由多個 element 組成,繼續讀取下一個 descriptor ,直到 next 被設定為 0 就是 buffer 的最後一個 - 若讀入的是 Descriptor Ring 最後一個,則將 wrap counter 反轉,然後從頭讀入 - 處理完 I/O 請求後寫入 used descriptor,其 `VIRTQ_DESC_F_USED` 和 `VIRTQ_DESC_F_AVAIL` 兩個 flag 皆設定為與裝置的 wrap counter 相同 - 若有 used buffer 可發起 used buffer notification 裝置讀取 buffer 依照驅動程式寫入的順序;若有多個請求,則依照請求完成的順序寫入 Descriptor Ring。若一個 request 由多個 buffer 組成 (chained descriptor),第一個 descriptor 的 used flag 要最後寫入,確保驅動程式不會看到部分完成的 request。若 buffer 由多個 element 組成,則只需寫入第一個 descriptor。 ### PCI 因為要實作 virtio-pci,我們得先了解 PCI,參考 [PCI](https://wiki.osdev.org/PCI)。 一個 PCI 架構如下, Host bridge 負責連接 CPU 和管理所有的 PCI 裝置及 Bus ,裝置又分為一般裝置和 Bridge , Bridge 用來連接兩個 Bus ![](https://hackmd.io/_uploads/BkFCA82Pn.png) 一個 PCI 邏輯裝置提供 256 bytes 的 Configuration Space ,用以完成裝置的設定與初始化, CPU 不能直接存取這個空間,需要透過 PCI 的 Host Bridge 提供特殊的機制,讓 CPU 完成配置空間的存取 這個機制在 Intel 架構下可藉由 CF8、CFC 這兩個 I/O port,先在 CF8 寫入要存取的配置空間暫存器位址,然後寫入或讀出 CFC 即可完成對該暫存器的操作。 ![](https://hackmd.io/_uploads/rJXJyDnwn.png) Bus Number 搭配 Device Number 可以用來識別實際上的 PCI 裝置,每個裝置可以提供不同的功能,每個功能被視為一個邏輯裝置,用 Bus Number : Device Number : Function Number 來分辨每一個邏輯裝置 256 bytes 的配置空間由 64 個 32 bits 的暫存器組成,以 Register Offset 來決定 ![](https://hackmd.io/_uploads/HJnJywnwh.png) PCI 配置空間的前 64 bytes 是每個裝置共通的,而剩下的 192 bytes 則由裝置各自定義,共通的部份如下圖所示 * Vendor ID: 識別製造商的 ID,virtio 為 0x1AF4 * Device ID: 裝置的 ID,對於 virtio-blk 是 0x1042 * Command: 用於操作裝置的設定,可寫 * Status: 裝置的狀態 * Class Code: 裝置的種類 * Base Address Register (BAR): 裝置內部空間所映射到的位址,可寫 ![](https://hackmd.io/_uploads/BkcxkD2Ph.png) BAR 的組成如下, Base Address 是裝置內部的記憶體映射到 CPU 定址空間的起始位址,在裝置初始化階段由 Driver 寫入,最低位址是空間的種類,Type 指示位址長度為 32 bits 或 64 bits Base Address 對齊裝置內部空間的大小,根據這個大小低位不可寫 ![](https://hackmd.io/_uploads/HyBbyP2D3.png) 自訂的部份由 Capability List 組成, Status 的 Bit 4 會告知該裝置有沒有 Capability List , Capability List​開頭的位址固定在 0x34 每一個 Capability 的第一個 Byte 為 規定好的 ID ,第二個 Byte 為下一個 Capability 的開頭位址,接下來是 Capability 的內容 ![](https://hackmd.io/_uploads/rJA-1v2v2.png) ### virtio-pci > 注: 以下結構體定義依據 [virtio 1.1 規格](https://docs.oasis-open.org/virtio/virtio/v1.1/virtio-v1.1.html),virtio 1.2 新增 `id` 欄位並調整 padding。kvm-host 實作以 virtio 1.1 為目標。 virtio-pci 的 Capability 定義如下,每個 cap 對應裝置的一種配置空間。該空間在 guest OS 映射的開頭位址可由 `base_address_reg[cap.bar] + cap.offset` 得知: ```c struct virtio_pci_cap { u8 cap_vndr; /* Generic PCI field: PCI_CAP_ID_VNDR */ u8 cap_next; /* Generic PCI field: next ptr. */ u8 cap_len; /* Generic PCI field: capability length */ u8 cfg_type; /* Identifies the structure. */ u8 bar; /* Where to find it. */ u8 padding[3]; /* Pad to full dword. */ le32 offset; /* Offset within bar. */ le32 length; /* Length of the structure, in bytes. */ }; ``` `cfg_type` 為 cap 的種類 ```c /* Common configuration */ #define VIRTIO_PCI_CAP_COMMON_CFG 1 /* Notifications */ #define VIRTIO_PCI_CAP_NOTIFY_CFG 2 /* ISR Status */ #define VIRTIO_PCI_CAP_ISR_CFG 3 /* Device specific configuration */ #define VIRTIO_PCI_CAP_DEVICE_CFG 4 /* PCI configuration access */ #define VIRTIO_PCI_CAP_PCI_CFG 5 ``` Common configuration 的結構如下 (virtio 1.1 版本;virtio 1.2 新增 `queue_notif_config_data` 和 `queue_reset` 欄位)。完整的 feature bits 為 64 bits,kvm-host 實作的 feature 為 `VIRTIO_F_VERSION_1` (32) 和 `VIRTIO_F_RING_PACKED` (34): - `feature_select` 用來選擇呈現的 Feature 是高 32 位還是低 32位 - `device_status` 為前述之 Device Status Field - `num_queues` 為 Virtqueue 的數量 - `queue_select` 寫入後,存取帶有 `queue_` 的欄位會存取對應的 queue 的資訊,若`queue_select` 無效則 `queue_size` 必須為 0 - `queue_desc` Descriptor Area 在 guest OS 的位址 - `queue_driver` Driver Area 在 guest OS 的位址 - `queue_device` Device Area 在 guest OS 的位址 ```c struct virtio_pci_common_cfg { /* About the whole device. */ le32 device_feature_select; /* read-write */ le32 device_feature; /* read-only for driver */ le32 driver_feature_select; /* read-write */ le32 driver_feature; /* read-write */ le16 msix_config; /* read-write */ le16 num_queues; /* read-only for driver */ u8 device_status; /* read-write */ u8 config_generation; /* read-only for driver */ /* About a specific virtqueue. */ le16 queue_select; /* read-write */ le16 queue_size; /* read-write */ le16 queue_msix_vector; /* read-write */ le16 queue_enable; /* read-write */ le16 queue_notify_off; /* read-only for driver */ le64 queue_desc; /* read-write */ le64 queue_driver; /* read-write */ le64 queue_device; /* read-write */ }; ``` notification cap 會在共通的 cap 結構後附加資料 ```c struct virtio_pci_notify_cap { struct virtio_pci_cap cap; le32 notify_off_multiplier; /* Multiplier for queue_notify_off. */ }; ``` 裝置驅動程式發起 avail buffer notification 時會寫入一個記憶體位置,用這個 cap 先計算相對於 BAR 的偏移量,偏移量由以下算式取得 每個 queue 有各自的 `queue_notify_off` ,由 Common configuration 提供 ``` cap.offset + queue_notify_off * notify_off_multiplier ``` ISR status cap 用來指示 INT#x interrupt 的狀態,要求至少 single byte 的空間。當驅動程式讀取此空間時會將值重設為 0;裝置發起 virtqueue 相關的通知時會設定 bit 0。 | Bits | 0 | 1 | 2 to 31 | | ------- | --------------- | ----------------------------- | -------- | | Purpose | Queue Interrupt | Device Configuration Interrupt | Reserved | PCI configuration access capability 也是直接在原有的 cap 後面附加資料,提供額外存取 BAR 空間的機制 - `cap.bar`: 指示要存取的 BAR 空間 - `cap.offset`: 指示要存取的位址對於 BAR 的偏移量 - `cap.length`: 為存取的長度 - `pci_cfg_data`: 為對應的資料 ```c struct virtio_pci_cfg_cap { struct virtio_pci_cap cap; u8 pci_cfg_data[4]; /* Data for BAR access. */ }; ``` Device-specific configuration 由各裝置定義 ### PCI Bus 目前的 virtio-blk 裝置是放在 PCI 上,故需把 PCI 支援移植到 arm64 平台。 PCI 有三個 address space : - Configuration Space - MMIO Space - I/O Space 其中,Configuration Space 在 x86 上是透過 port I/O 存取。故要再修改 PCI 實作讓它支援用 MMIO 存取才能在 arm64 上使用。另外 I/O Space 部份,因為 virtio-blk 不使用,故就先不實作。 原本 `pci_init()` 函式會把 PCI bus 存取 PCI Configuration Space 的 `struct dev` 註冊到 `vm_t` 內的 `io_bus` ,但是在 arm64 因為是用 MMIO 存取 Configuration Space ,故這樣的作法不適用。所以把 `struct dev` 註冊的程式從 `pci_init()` 拿掉,改在 `vm_arch_platform_device()` 進行。 此外,也在 `struct pci` 內加入 `pci_mmio_dev` 型態為 `struct dev` ,它的 callback 函式會被初始化為 `pci_mmio_io()` ,`pci_mmio_io()` 就是針對 PCI Configuration Space 的 MMIO 處理函式。把 `struct pci` 內的 `pci_mmio_dev` 註冊到 `vm_t` 內的 `mmio_bus` ,就能使用 MMIO 存取 PCI Configuration Space 。 在 arm64 中的 `vm_arch_platform_device()` 註冊 PCI bus 程式碼如下: ```c= /* Initialize PCI bus */ pci_init(&v->pci); v->pci.pci_mmio_dev.base = ARM_PCI_CFG_BASE; bus_register_dev(&v->mmio_bus, &v->pci.pci_mmio_dev); ``` 其中第 3 行是指定 MMIO 的位址,第 4 行就是把 `pci_mmio_dev` 註冊到 `vm_t` 上的 `mmio_bus` 。 隨後 `pci_mmio_io` 用來處理 PCI Configuration Space MMIO Handler 的程式: ```c static void pci_mmio_io(void *owner, void *data, uint8_t is_write, uint64_t offset, uint8_t size) { struct pci *pci = (struct pci *) owner; bus_handle_io(&pci->pci_bus, data, is_write, offset, size); } ``` 會直接把 MMIO 對 Configuration Space 的讀寫轉發到 `struct pci` 內的 `pci_bus` 中。 PCI device 在使用 line based interrupt 時,通常會是 level-triggered 。但是因為原本的 virtio-blk 實作,使用 irqfd 來送中斷,會是 edge-triggered 故會無法正常運作。解決方式是,在 device tree 中,描述此中斷為 edge-triggered 即可。 ### virtio-blk virtio-blk 的 device configuration 如下,除了 `capacity` 其他欄位需要對應的 feature 才會使用到: - `capacity` : Block Device 的大小,以 512 bytes 的 sector 為單位 ```c struct virtio_blk_config { le64 capacity; le32 size_max; le32 seg_max; struct virtio_blk_geometry { le16 cylinders; u8 heads; u8 sectors; } geometry; le32 blk_size; struct virtio_blk_topology { // # of logical blocks per physical block (log2) u8 physical_block_exp; // offset of first aligned logical block u8 alignment_offset; // suggested minimum I/O size in blocks le16 min_io_size; // optimal (suggested maximum) I/O size in blocks le32 opt_io_size; } topology; u8 writeback; u8 unused0[3]; le32 max_discard_sectors; le32 max_discard_seg; le32 discard_sector_alignment; le32 max_write_zeroes_sectors; le32 max_write_zeroes_seg; u8 write_zeroes_may_unmap; u8 unused1[3]; }; ``` ### Feature bits - [ ] Device Operation request 的格式如下,一個 request buffer 會被分為以下: ```c struct virtio_blk_req { le32 type; le32 reserved; le64 sector; u8 data[]; u8 status; }; ``` - [ ] `type`: request 的種類 ```c #define VIRTIO_BLK_T_IN 0 #define VIRTIO_BLK_T_OUT 1 #define VIRTIO_BLK_T_FLUSH 4 #define VIRTIO_BLK_T_DISCARD 11 #define VIRTIO_BLK_T_WRITE_ZEROES 13 ``` - [ ] `sector`: 相對於裝置開頭的偏移量,以 sector 為單位 - [ ] `data`: request 需要的資料 - [ ] `status`: request 的狀態,由裝置寫入 ```c #define VIRTIO_BLK_S_OK 0 #define VIRTIO_BLK_S_IOERR 1 #define VIRTIO_BLK_S_UNSUPP 2 ``` 對於 `VIRTIO_BLK_T_IN` 和 `VIRTIO_BLK_T_OUT` , `data[]` 是讀寫操作資料的存取處 ### 實作 - [ ] bus > [bus.c](https://github.com/sysprog21/kvm-host/blob/master/src/bus.c) 用來處理位址與裝置的映射關係,使用鏈結串列管理裝置 ```c struct dev { uint64_t base; uint64_t len; void *owner; dev_io_fn do_io; struct dev *next; }; struct bus { uint64_t dev_num; struct dev *head; }; ``` 使用以下函式將 `dev` 註冊到 `bus` 上 ```c void bus_register_dev(struct bus *bus, struct dev *dev); ``` 使用以下函式對 `bus` 發起 I/O 請求,根據 `dev` 的 `base` 和 `len` 找出目標裝置,然後呼叫 `do_io` callback: `owner` 是指向擁有這個 `dev` 的結構體,用於 callback 的參數 ```c void bus_handle_io(struct bus *bus, void* data, uint8_t is_write, uint64_t addr, uint8_t size); ``` 實作中有 `io_bus` 和 `mmio_bus` 分別處理 `KVM_EXIT_IO` 和 `KVM_EXIT_MMIO` 事件,以及一個 `pci_bus` 處理 PCI 裝置的配置空間 - [ ] pci > [pci.c](https://github.com/sysprog21/kvm-host/blob/master/src/pci.c) 首先註冊這兩個裝置到 io_bus , `pci_addr_dev` 位於 CF8 ,而 `pci_bus_dev` 位於 CFC ``` bus_register_dev(io_bus, &pci->pci_addr_dev); bus_register_dev(io_bus, &pci->pci_bus_dev); ``` `pci_bus_dev` 的 callback 會去 `pci_bus` 找到我們註冊的 virtio-blk 裝置,完成對 PCI裝置配置空間的讀寫操作 ```c static void pci_data_io(void *owner, void *data, uint8_t is_write, uint64_t offset, uint8_t size) { struct pci *pci = (struct pci *) owner; uint64_t addr = pci->pci_addr.value | offset; bus_handle_io(&pci->pci_bus, data, is_write, addr, size); } ``` 當 guest OS 寫入 pci 配置空間的 command 時,低兩位是用來開啟或關閉記憶體映射的,此時將 bar 的位址註冊到對應的 bus 上 ![](https://hackmd.io/_uploads/HJinkwnv3.png) ```c static inline void pci_activate_bar(struct pci_dev *dev, uint8_t bar, struct bus *bus) { if (!dev->bar_active[bar] && dev->hdr.bar[bar]) bus_register_dev(bus, &dev->space_dev[bar]); dev->bar_active[bar] = true; } static void pci_command_bar(struct pci_dev *dev) { bool enable_io = dev->hdr.command & PCI_COMMAND_IO; bool enable_mem = dev->hdr.command & PCI_COMMAND_MEMORY; for (int i = 0; i < PCI_CFG_NUM_BAR; i++) { struct bus *bus = dev->bar_is_io_space[i] ? dev->io_bus : dev->mmio_bus; bool enable = dev->bar_is_io_space[i] ? enable_io : enable_mem; if(enable) pci_activate_bar(dev, i, bus); else pci_deactivate_bar(dev, i, bus); } } ``` 此時 guest OS 可以存取到 virtio-pci 的配置空間 ``` pci 0000:00:00.0: BAR 0: assigned [mem 0x40000000-0x400000ff] ``` ### virtio-pci > [virtio-pci.c](https://github.com/sysprog21/kvm-host/blob/master/src/virtio-pci.c) 負責初始化 pci 裝置的配置空間及 virtio-pci 裝置的配置空間 之前註冊到 `mmio_bus` 或 `io_bus` 的 bar 裝置也是由 virtio-pci 負責初始化,其 callback 除了普通的讀寫操作,還有完成 feature select 、 queue select 等機制 ``` virtio-pci 0000:00:00.0: enabling device (0000 -> 0002) ``` 目前的實作將 virtio-pci 的四個 capability 的配置空間放在同一個 bar ```c struct virtio_pci_config { struct virtio_pci_common_cfg common_cfg; struct virtio_pci_isr_cap isr_cap; struct virtio_pci_notify_data notify_data; void *dev_cfg; }; ``` 裝置驅動程式通知裝置的方式是透過寫入某個位址,實作中是寫入 `notify_data` 收到通知後使用 virtq 的 `virtq_handle_avail` 處理通知 ```c if (offset == offsetof(struct virtio_pci_config, notify_data)) virtq_handle_avail(&dev->vq[dev->config.notify_data.vqn]); ``` ### virtq > [virtq.c](https://github.com/sysprog21/kvm-host/blob/master/src/virtq.c) `virtq_handle_avail` 實作如下 ```c void virtq_handle_avail(struct virtq *vq) { struct virtq_event_suppress *driver_event_suppress = (struct virtq_event_suppress *) vq->info.driver_addr; if (!vq->info.enable) return; virtq_complete_request(vq); if (driver_event_suppress->flags == RING_EVENT_FLAGS_ENABLE) virtq_notify_used(vq); } ``` 目前共有三種由裝置實作的操作,包括 `virtq_complete_request` 和 `virtq_notify_used` - `enable_vq` 用於 guest OS 寫入 common config 的 `queue_enable` 時,啟用 virtqueue - `complete_request` 組裝收到的 element,並對實際的裝置發起 I/O 請求 - `virtq_notify_used` 則是通知驅動程式有 used buffer 可用 ```c struct virtq_ops { void (*complete_request)(struct virtq *vq); void (*enable_vq)(struct virtq *vq); void (*notify_used)(struct virtq *vq); }; ``` `virtq` 的核心在 `virtq_get_avail` ,這個函式會根據前述之 packed virtqueues 的規定,回傳下一個 avail 的 descriptor ```c struct vring_packed_desc *virtq_get_avail(struct virtq *vq) { struct vring_packed_desc *desc = &vq->desc_ring[vq->next_avail_idx]; uint16_t flags = desc->flags; bool avail = flags & (1ULL << VRING_PACKED_DESC_F_AVAIL); bool used = flags & (1ULL << VRING_PACKED_DESC_F_USED); if (avail != vq->used_wrap_count || used == vq->used_wrap_count) { return NULL; } vq->next_avail_idx++; if (vq->next_avail_idx >= vq->info.size) { vq->next_avail_idx -= vq->info.size; vq->used_wrap_count ^= 1; } return desc; } ``` ### virtio-blk > [virtio-blk.c](https://github.com/sysprog21/kvm-host/blob/master/src/virtio-blk.c) > 延伸: [IOThread Virtqueue Mapping](https://www.youtube.com/watch?v=tVIRDdf79-0) (KVM Forum 2024, Stefan Hajnoczi) -- QEMU 中 virtio-blk 的 SMP 擴展性改進 `virtio-blk` 負責 `virtq` 與 `virtio-pci` 的初始化,以及提供 device-specific config 和實作 `virtq_ops`: ```c static struct virtq_ops ops = { .enable_vq = virtio_blk_enable_vq, .complete_request = virtio_blk_complete_request, .notify_used = virtio_blk_notify_used, }; ``` - `virtio_blk_enable_vq` 將 guest OS 填入的 virtq 位址轉成 kvm-host 可存取的位址 - `virtio_blk_complete_request` 使用 `virtq_get_avail` 取得 element 並根據 `struct virtio_blk_req` 組合 element ,然後依照 request 的內容發起對 block device 的讀寫 - `virtio_blk_notify_used` 目前僅透過 `vm_irq_trigger` 經由 KVM 對 guest OS 發起 interrupt 實作使用 disk image 作為 block device , `virtio-blk` 發起的讀寫請求也就是對該檔案的讀寫 ### 改進 - [ ] eventfd 參考 [eventfd(2)](https://man7.org/linux/man-pages/man2/eventfd.2.html) eventfd 用於程式間的溝通機制,對 eventfd 寫入相當於發起通知,對 eventfd 讀取相當於等待通知 - [ ] irqfd 參考 [kvm: add support for irqfd](https://lwn.net/Articles/332924/) 及 [qemu-kvm的irqfd机制](https://www.cnblogs.com/haiyonghao/p/14440723.html) irqfd 是 kvm 提供用於透過 eventfd 發起 interrupt 的機制 首先透過 `ioctl` 將 eventfd 跟 interrupt 綁定 ```c void vm_ioeventfd_register(vm_t *v, int fd, unsigned long long addr, int len, int flags) { struct kvm_ioeventfd ioeventfd = { .fd = fd, .addr = addr, .len = len, .flags = flags}; if (ioctl(v->vm_fd, KVM_IOEVENTFD, &ioeventfd) < 0) throw_err("Failed to set the status of IOEVENTFD"); } ``` 原先使用 `ioctl` 發送 interrupt 現在改為 `write` ```c static void virtio_blk_notify_used(struct virtq *vq) { struct virtio_blk_dev *dev = (struct virtio_blk_dev *) vq->dev; uint64_t n = 1; if (write(dev->irqfd, &n, sizeof(n)) < 0) throw_err("Failed to write the irqfd"); } ``` - [ ] ioeventfd 參考 [KVM: add ioeventfd support](https://patchwork.kernel.org/project/kvm/patch/1251028605-31977-23-git-send-email-avi@redhat.com/) ioeventfd 為 KVM 提供透過 eventfd 通知 I/O 事件發生的機制。 當 guest 進行 I/O 操作觸發 VM exit 時,KVM 會先判斷該位址是否有註冊 eventfd,若有則透過 eventfd 通知,然後讓 guest 繼續執行。 用於某些不傳輸資料而僅作為通知的 I/O 請求,可減少時間開銷,如 virtio-pci 的 avail buffer notification。 首先將 eventfd 註冊為 ioeventfd ```c uint64_t addr = virtio_pci_get_notify_addr(&dev->virtio_pci_dev, vq); vm_ioeventfd_register(v, dev->ioeventfd, addr, dev->virtio_pci_dev.notify_len, 0); ``` 然後建立一個執行緒,用以等待 ioeventfd 的通知 ```c pthread_create(&dev->vq_avail_thread, NULL, virtio_blk_vq_avail_handler, (void *) vq); static void *virtio_blk_vq_avail_handler(void *arg) { struct virtq *vq = (struct virtq *) arg; struct virtio_blk_dev *dev = (struct virtio_blk_dev *) vq->dev; uint64_t n; while (read(dev->ioeventfd, &n, sizeof(n))) { virtq_handle_avail(vq); } return NULL; } ``` 完成後當 guest 寫入該位址, kvm 會通知這個 ioeventfd ,然後執行 `virtq_handle_avail` ### Linux Kernel vhost > 原始程式碼: [linux/drivers/vhost/](https://github.com/torvalds/linux/tree/master/drivers/vhost) irqfd 與 ioeventfd 實際上多用於 vhost 與 KVM 的溝通。vhost 負責處理 virtqueue 的 avail buffer 與實際傳送 I/O 請求。若 vhost 實作在 kernel space,可減少在 kernel space 和 user space 之間來回切換的開銷。 ![](https://hackmd.io/_uploads/HkbaePnv2.png) 截至目前 Linux 核心 mainline 沒有 vhost-blk,Asias He 發過 patch 但未被採納: [vhost-blk: Add vhost-blk support v6](https://patchwork.ozlabs.org/project/netdev/patch/1354412033-32372-1-git-send-email-asias@redhat.com/)。 可參考 [linux/drivers/vhost/blk.c](https://github.com/asias/linux/blob/blk.vhost-blk/drivers/vhost/blk.c) ### virtio 測試 若要在 guest Linux 使用 ext4,須開啟以下編譯選項: ``` CONFIG_EXT4_FS=y ``` 在 Linux 使用以下命令建立一個 ext4 的 disk image ```shell $ dd if=/dev/zero of=./virtio_blk.img bs=1M count=8 mkfs.ext4 ./virtio_blk.img ``` 使用參數 `-d virtio_blk.img` 執行 kvm-host,確認 guest Linux 有以下訊息: ``` virtio_blk virtio0: [vda] 16384 512-byte logical blocks (8.39 MB/8.00 MiB) ``` 使用以下命令在 guest Linux 掛載裝置: ```shell $ mkdir disk $ mount /dev/vda disk ``` 使用 `$ cat /proc/mounts` 確認是否有掛載成功 ``` /dev/vda /disk ext4 rw,relatime 0 0 ``` 嘗試在 `/disk` 建立檔案,並確保檔案有被寫入 ``` $ touch /disk/test $ sync ``` 使用 `ls /disk` 可以看到 `test` 已被建立 ``` lost+found test ``` 退出 kvm-host ,嘗試掛載 virtio_blk.img ,使用 `losetup` 可以查看 loop device 的編號 ```shell $ sudo losetup -f virtio_blk.img $ mkdir /tmp/disk $ sudo mount /dev/loop19 /tmp/disk ``` 掛載成功後,使用 `ls /tmp/disk` 查看裡面的內容 ``` lost+found test ``` 至此確認 virtio-blk 的存取正常運作。 ## 近期 KVM 核心發展 上述雲端業者的 confidential VM、大規模 live migration 及 microVM 需求,直接推動 Linux 核心 KVM 子系統的演進。以下整理 v6.6 以來的關鍵特性。 ### guest_memfd: 機密虛擬機器的記憶體基礎 Linux v6.8 (2024 年 3 月) 合併 `guest_memfd` 機制,該機制新增 `KVM_CREATE_GUEST_MEMFD` ioctl,建立一個綁定於特定虛擬機器的匿名檔案描述子,host 的使用者空間無法映射該記憶體。搭配 `KVM_SET_MEMORY_ATTRIBUTES` ioctl 可按 page 指定共享 (shared) 或私有 (private) 屬性。 `guest_memfd` 是 AMD SEV-SNP、Intel TDX 及 pKVM 等機密虛擬機器實作的共同基礎,也是上述各雲端業者 confidential VM 產品的核心依賴。後續增強包含: - `GUEST_MEMFD_FLAG_NO_DIRECT_MAP`: 將 guest page 從 host 核心的 direct map 中移除 - NUMA mempolicy 支援 (因 `guest_memfd` page 無法使用 `mbind()`) - pKVM/Arm64 整合: page 在 fault 進 guest 時自動從 host 取消映射 > 參考: [KVM guest_memfd() and per-page attributes](https://lwn.net/Articles/950433/) (LWN, 2023)、[guest_memfd roadmap](https://lpc.events/event/18/contributions/1767/) (LPC 2024, Paolo Bonzini)、[Guest private memory for software-based hypervisors](https://lpc.events/event/18/contributions/1758/) (LPC 2024, Fuad Tabba) ### 硬體 confidential computing 支援 Confidential computing 是近年 KVM Forum 的核心議題。Paolo Bonzini 在 [KVM Forum 2024 的 keynote](https://www.youtube.com/watch?v=vad405tU8rk) 中以 "Rivers, dams and kernel development" 為題,闡述 TDX 合併策略及 confidential computing 在 KVM 中的長期發展路線。 - AMD SEV: KVM 支援自 Linux v4.16 (2018) 起合併 (`KVM_X86_SEV_VM`);SEV-ES 於 v5.11 (2021) 合併 (`KVM_X86_SEV_ES_VM`);SEV-SNP 於 v6.11 (2024 年 9 月) 合併 (`KVM_X86_SNP_VM`)。SNP 在既有的 SEV 加密記憶體保護之上,以 Reverse Map Table (RMP) 加入記憶體完整性保護,防止 hypervisor 竄改、重映射或重送 guest 記憶體內容。後續發展包含 SVSM (Secure VM Service Module) 及 PCI TSM (TEE Security Manager) 為 confidential VM 的裝置直通提供基礎建設。SEV-SNP 的 live migration 方案 (mirror VM 架構) 於 [KVM Forum 2024](https://www.youtube.com/watch?v=RYIjUweFysA) 由 AMD 工程師 Tom Lendacky 發表。 > 參考: [KVM: Add AMD SEV-SNP Hypervisor Support](https://lwn.net/Articles/973282/) (LWN, 2024) - Intel TDX: Linux v6.16 (2025 年 7 月) 合併初始支援,涵蓋 TDX 初始化、vCPU/VM 生命週期管理及透過 `guest_memfd` 的加密 guest 記憶體處理。v6.16 同時引入統一的 TSM-MR ABI,為 TDX 和 SEV-SNP 提供標準化的量測介面。 > 參考: [TDX MMU prep series](https://lwn.net/Articles/982565/) (LWN, 2024) - Arm CCA: 仍在開發中 (截至 2026 年初),以 RME (Realm Management Extension) 為硬體基礎,搭配 RMM (Realm Management Monitor) 規格 v1.0-rel0 為目標。CCA 透過 RMI (Realm Management Interface) 讓 host 與 RMM 溝通,RSI (Realm Services Interface) 讓 Realm guest 與 RMM 溝通。Google 的 Will Deacon 在 [KVM Forum 2024](https://www.youtube.com/watch?v=6Hdq9npRMeI) 分享 Android confidential guest 端的變更。 > 參考: [arm64: Support for Arm CCA in KVM](https://lwn.net/Articles/1009564/) (LWN, 2024) ### Rust VMM 生態系統 > 參考: [FOSDEM 2026: rust-vmm evolution](https://fosdem.org/2026/schedule/event/WEHLEY-rust-vmm_evolution_on_ecosystem_and_monorepo/) [rust-vmm](https://github.com/rust-vmm) 專案 (2018 年由 Amazon 和 Intel 於 KVM Forum 發起) 持續整合共用 crate (`kvm-ioctls`、`vm-memory`、`virtio-queue`、`vhost`、`linux-loader` 等),主要消費者包含 Cloud Hypervisor、Firecracker、[Dragonball](https://github.com/kata-containers/kata-containers) (Alibaba, 現整合至 Kata Containers) 和 [libkrun](https://github.com/containers/libkrun): - Cloud Hypervisor: 超過十萬行 Rust 程式碼,direct kernel boot 啟動時間低於 100ms - Firecracker: 超過十萬行 Rust 程式碼,啟動時間約 125ms (含 API 呼叫開銷) - rust-vmm 已加入 RISC-V 架構支援 (`kvm-bindings` 含 `riscv64` 繫結),並逐步將各 crate 整合至 monorepo ## 延伸閱讀 ### 入門材料 * [Using the KVM API](https://lwn.net/Articles/658511/) (LWN, 2015) * [KVM: the Linux Virtual Machine Monitor](https://www.kernel.org/doc/ols/2007/ols2007v1-pages-225-230.pdf) (OLS, 2007, Avi Kivity et al.) ### 實作參考 * [kvm-box](https://github.com/cppcoffee/kvm-box): 以 Linux KVM 建立和執行 microVM 的極簡 VMM (x86-64, Rust) * [Alioth](https://github.com/google/alioth): Google 以 Rust 從零打造的實驗性 KVM-based Type 2 hypervisor ### KVM Forum 錄影 * [KVM Forum 2024 playlist](https://www.youtube.com/playlist?list=PLW3ep1uCIRfwqKJYHxXsjIvG-Jy3nO2si) -- 涵蓋 TDX 策略、SEV-SNP live migration、virtio 發展、Android confidential guest、IOThread virtqueue mapping 等議題 * [KVM Forum 2023 playlist](https://www.youtube.com/playlist?list=PLW3ep1uCIRfx5ApKGg41ARULGswMOCGBj) -- 涵蓋 guest_memfd 記憶體管理、COCONUT SVSM、KVM/arm64 nested virtualization、eBPF guest exit 處理