# KVM: Linux 虛擬化基礎建設 > 貢獻者: RinHizakura, ray90514, yanjiew1, jserv ==[直播錄影](https://youtu.be/EF6CD4vKBb0)== ## 簡介 KVM (Kernel-based Virtual Machine) 是 Linux 核心提供的系統虛擬機器基礎建設,它是個 Linux 核心模組,能讓 Linux 系統成為一個 Type-2 Hypervisor 。KVM 透過硬體虛擬化支援 (Intel VT, 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](http://www-archive.xenproject.org/files/xensummit_germany09/IanPratt.pdf), 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) :::warning Xen problem: support for running as Host (Dom0) or Guest (DomU) was out-of-tree ::: 2017 年第四季,AWS 支援 KVM 作為虛擬化架構,自此 KVM 可說是席捲 Linux 虛擬化技術生態。在 AWS 啟動的 2006 年,Xen 是當時最成熟、開放原始碼的 Linux 虛擬化引擎技術。因此,AWS 在早期採用 Xen,成為其 [EC2](https://aws.amazon.com/ec2/) 的底層基礎。 KVM 全名是 Kernel-based Virtual Machine,最早是以色列新創公司 [Qumranet](https://en.wikipedia.org/wiki/Qumranet) (創立於 2005 年) 發表的開放原始碼專案,在 2006 年下半收錄於 Linux 核心,自此大幅強化 KVM 的影響力。 2008 年,Red Hat 以 1 億 7 百萬美元收購 KVM 背後的 Qumranet 公司,藉此擴展 Red Hat 對於虛擬技術的支援。得益於 KVM 的活躍發展,2011 年啟動的 Google Cloud 從一開始就採用 KVM 引擎。Qumranet 核心人物 Avi Kivity 在公司成功賣給 Red Hat 後,開創新一代的分散式資料庫技術公司 [ScyllaDB](https://www.scylladb.com/)。 世界上前幾大的資訊科技公司不約而同,選擇在以色列設立研發中心,且成果卓著,例如,Intel x86 家族中為人所知的 CPU 微架構: Sandy Bridge 和 Ivy Bridge 都由 Intel 以色列的研發中心研發,藍色巨人 IBM 也早在 1970 年代就在以色列設立龐大的研發中心,成果斐然。 虛擬化技術歷史悠久,早在 1967 年,第一代的硬體虛擬化技術就由 IBM 提出。在大型主機中實作以 CP/CMS 為代表的虛擬化技術,直到 2006 年,Intel 和 AMD 才分別在各自的處理器中加入「有限」的硬體虛擬化特性,分別稱為,Intel VT-x 和 AMD-V。與大型機所採用的專為虛擬化設計的處理器不同,從 PC 起家者以 Intel 為代表的 x86 家族的處理器生來就不是為虛擬化設計。要在 x86 家族處理器上完全向後相容的同時加入硬體虛擬化特性,無疑成為一個挑戰,硬體層面實作較為困難,導致軟體層面的實作複雜度也隨之水漲船高。 Linux 核心中,虛擬化相關程式碼,x86 架構部分的程式碼數量是 S390 架構的 7 倍、PPC 架構的 8 倍、Arm 架構的 4 倍,其複雜度之高從中可見一斑。 Avi Kivity 提出的方案非常清晰且巧妙:聚焦於 Linux 核心,至於 User space 部分交給穩定可靠的 QEMU,採用後者作為其使用者層級的入口,而且 KVM 僅實作 HVM 功能。以 Avi Kivity 為主的工程師僅僅用不到一年時間,就讓 Linux 社群接受 KVM 的設計方案並通過程式碼審查,最終於 2006 年 10 月合併進入 Linux `v2.6.20`。 :::warning why did KVM succeed? 1. KVM had a better, more flexible, and future-proof design built into Linux, not underneath it 2. reuse things already there that suited Linux more and left it in control which obviously is in the interest of Linux developers ::: > 註: underneath 語意與 under 相近,但更強烈 [Cloud-Hypervisor](https://github.com/cloud-hypervisor/cloud-hypervisor) 是 Intel 以 KVM 為基礎,開發出來的嶄新 Virtual Machine Monitor (VMM),這項虛擬化建設著眼於資源配置侷限但又廣泛部署的雲端運算環境,採用 Rust 程式語言打造。Cloud-Hypervisor 由於自身定位,不考慮舊有通行的硬體支援,相反地,主要支援 VirtIO 為基礎的 para-virtualized device。 [Confidential Computing](https://confidentialcomputing.io/) 旨在建立一套可用於資料中心、雲端和邊緣運算的安全信賴環境標準和規範,以 AMD EPYC 處理器來說,利用其安全加密虛擬化 SEV (Secure Encrypted Virtualization),可藉由虛擬層來建立安全的虛擬機器,只要是在這個受信賴的虛擬機器內執行的任何程式,都可取得金鑰來解開遭加密記憶體上的資料,進行處理。因為虛擬機器所用的加密金鑰,是處理器內建的硬體加密機制隨機亂數產生,且每個虛擬機器都不一樣,因為在虛擬機器外部,無法取得這個金鑰來解開記憶體內處理中的資料,就達到阻絕外部瀏覽的保護效果。從虛擬機器層面來建立加密保護的優點是,應用程式不用修改,只要能放入該虛擬環境來執行,即可具有機密運算的作用。 其他實作: * [xvisor](https://wiki.csie.ncku.edu.tw/embedded/xvisor) * [bao hypervisor](https://github.com/bao-project/bao-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)環境上執行,如下圖所展示的。因為 type-2 hypervisor 必須透過 host 作業系統存取資源,因而會引發延遲問題而相對 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) ### QEMU QEMU 本身也是為一個 hypervisor,不過其主要都是用軟體去模擬指令的執行、一些 device 的 IO,並沒有使用 Hardware-assisted virtualization,因此執行速度相較緩慢。然而他的好處是擴充性高,使用者只需要使用 Qemu 提供的 API,就能迅速模擬出一個 device 讓 guest OS 做存取, QEMU 使用的軟體模擬技術為 DBT (Dynamic Binary Translation),QEMU 本身實作出的工具稱作 TCG (Tiny Code Generator) DBT 的輸入為要模擬的一整塊 basic block 的指令,且指令可以是 hypervisor 本身所支援的指令集;輸出為 host 本身的指令集的指令。 TCG 會在輸入與輸出中間加上一層 IR,亦即 input $\to$ IR $\to$ output,這樣可以減去指令轉換的成本 QEMU-KVM 結合 KVM 以及 QEMU 的優點,首先 KVM 已處理 memory mapping 及 instruction emulation,之後由 QEMU 來提供硬體 I/O 虛擬化,並透過 ioctl() /dev/kvm 裝置和 KVM 互動。 而 QEMU-KVM 相比原生 QEMU 的改動: * 原生的 QEMU 藉由 DBT 實作 CPU 的 full-virtualization,但修改後的 QEMU-KVM 會呼叫 ioctl() 來運用 KVM 模組 * 原本的 QEMU 是單執行緒,QEMU-KVM 採多執行緒 ### KVM [KVM](https://en.wikipedia.org/wiki/Kernel-based_Virtual_Machine) 是個 Linux 核心模組/子系統,可將 Linux 轉為 type-2 hypervisor,結合硬體的虛擬化支援,使得 host machine 上可以執行多個獨立的虛擬環境,稱為 guest 或者 virtual machine。由於 KVM 直接提供 CPU 和記憶體的虛擬化,guest os 的 CPU 指令不需要額外經過軟體的 decode,而是直接交給硬體進行處理,因此可以有效的提高運行速度。而結合軟體(例如 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 * 支援 PCI device 直接分配和單根 I/O 虛擬化 (SR-IOV) SR-IOV - single-root IO virtualization * 支援 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 的採納 AWS: [The Nitro System journey](https://docs.aws.amazon.com/whitepapers/latest/security-design-of-aws-nitro-system/the-nitro-system-journey.html) [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/) AWS: [Firecracker](https://firecracker-microvm.github.io/) ![image](https://hackmd.io/_uploads/H1_2Tq3QA.png) [FreeBSD on Firecracker](https://www.usenix.org/publications/loginonline/freebsd-firecracker) > FreeBSD boots under Firecracker — and does so extremely quickly. Including uncommitted patches (to FreeBSD and also to Firecracker), on a virtual machine with 1 CPU and 128 MB of RAM, the FreeBSD kernel can boot in under 20 ms GCP: [About nested virtualization](https://cloud.google.com/compute/docs/instances/nested-virtualization/overview) Android: [AVF architecture](https://source.android.com/docs/core/virtualization/architecture?hl=en) > [在 Google Pixel 6 成功跑起 Win11 虛擬機器](https://www.techbang.com/posts/94185-android-13-meritorious-service-google-pixel-6-successfully) --- ## 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; void *priv; } vm_t; ``` 其中 `priv` 在 arm64 會指向下面的結構體: ```c typedef struct { uint64_t entry; size_t initrdsz; int gic_fd; /* 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_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 的一段在 virtual 連續的空間(在 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 的方式與 User space 互動。並且都是用 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 ,可以把 User space 的記憶體空間分配給虛擬機器。這裡就是把剛才 `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 指令,因此需指向核心被擺放的記憶體位置 * 設定 `rsp`: `rsp` 則需設定在 boot paramters 的位置 > 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. 最後設定虛擬的 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/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` 表示 availible,簡單的把 `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` 使 booting 可以正確運作 ### 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 移植的配置: * 0 - 64K I/O port * 1M - 16M GIC * 1GB - 2GB PCI MMIO * 2GB - DRAM 跟 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 deivce fd 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; /* 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 實作的私有結構體: ```diff 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; + void *priv; } vm_t; ``` ### 建立與初始化中斷控制器 > 參考資料: > - [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 核心中針對 vGICv3 的說明](https://www.kernel.org/doc/Documentation/virtual/kvm/devices/arm-vgic-v3.txt) > - [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 提供的虛擬化中斷控制器,我們可以不必在 User space 自行實作中斷控制器的模擬,可以直接使用 Linux 核心提供的實作。 [eMAG 8180](https://en.wikichip.org/wiki/ampere_computing/emag/8180) 主機僅能支援建立虛擬 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` 指令來讀寫暫存器。而 Redistributer 為每個 CPU Core 各一個, Distributer 則是整個系統共用一個。 Redistributer 和 Distributer 都是用 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` 中。 接下來要設定 Redistributer 和 Distributer 的 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 。一旦初始化 GICv3 後,就不能再建立新的 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 核心的說明文件。`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),而是設為下面的格式: ``` bits: | 31 ... 24 | 23 ... 16 | 15 ... 0 | field: | irq_type | vcpu_index | irq_id | The irq_type field has the following values: - irq_type[0]: out-of-kernel GIC: irq_id 0 is IRQ, irq_id 1 is FIQ - irq_type[1]: in-kernel GIC: SPI, irq_id between 32 and 1019 (incl.) (the vcpu_index field is ignored) - irq_type[2]: in-kernel GIC: PPI, irq_id between 16 and 31 (incl.) (The irq_id field thus corresponds nicely to the IRQ ID in the ARM GIC specs) ``` 因為我們要發送的是 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 初始化相關的說明請見後續。 #### IO 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 deivce 定義。 ```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 virtual cpu。關於指令的解碼、執行,和對記憶體的存取操作將會在 KVM 中進行,我們所設計的虛擬機器不需要實作相關的模擬。我們只需要處理會使得 `KVM_RUN` ioctl 返回的特殊事件(大部分是 I/O 設備操作的模擬)。 ### Linux 核心啟動流程 > [The Linux/x86 Boot Protocol](https://www.kernel.org/doc/html/latest/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/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/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_begin_node()` 來新增節點。一開始需要新增根節點,故需先呼叫 `fdt_begin_node(fdt, "")` 。 3. 節點中,可以用 `fdt_property()`, `fdt_property_cell()`, `fdt_property_u64()` 等來加入屬性。其中若使用 `fdt_property()`,libfdt 會原封不動的把內容放置進入,但 device tree 內的數值採用 big-endian 表示,故 `fdt_property()` 要搭配 `cpu_to_fdt32()` 或 `cpu_to_fdt64()` 來轉換 endianness 。 4. 節點的屬性都新增完後,用 `fdt_end_node()` 來關閉節點。 5. 最後用 `fdt_finish()` ,來完成 device tree 。 `fdt_finish()` 呼叫完成後,只要上述 `fdt_begin_node()` 和 `fdt_end_node()` 都有正確配對,緩衝區的內容即為合法的 device tree。 ### 設定 vCPU 暫存器初始值 > 參考資料: [Booting arm64 Linux](https://docs.kernel.org/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) * [include/uapi/linux/serial_reg.h](https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/serial_reg.h) --- ## Virtio Virtio 是 IO 請求溝通的標準,架構如下圖所示,有一個前端和後端,前端通常作為驅動存在被 Guest OS 使用,後端則是在 Guest OS 被視為裝置的一種,後端可以是軟體模擬出來的裝置也可以是支援 Virtio 的實體裝置。 以 Hypervisor 實作的後端來說,前端將 IO 請求傳給後端,後端會將請求傳給實際的裝置,等 IO 處理完成後傳回給前端,後端的這過程也就是裝置的模擬。前後端使用 Virtqueue 作為資料交換的機制 ![](https://hackmd.io/_uploads/ryjVCU3vh.png) 對於前述 kvm 對 io 的處理流程,Virtio 的機制使得模式切換的次數減少,以及若 Virtqueue 用共享記憶體實作,則可以減少資料複製的開銷,提升虛擬化下 IO 的效能。 一個 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 用來指示裝置所支援的特性 > 0 to 23 Feature bits for the specific device type 24 to 37 Feature bits reserved for extensions to the queue and feature negotiation mechanisms 38 and above Feature bits reserved for future extensions. ### Notification 通知包含以下三種,第一種是配置改變時由裝置發起的通知,後兩種與 Virtqueue 相關。 通知的實作為 transport specific ,對於 Virtio-pci 驅動到裝置的通知由寫入特定的記憶體區域觸發 vm-exit 完成,裝置到驅動的通知則由 interrupt 完成 - configuration change notification - available buffer notification - used buffer notification ### Device Configuration Space Virtio 支援三種 Bus 來探查及初始化 Virtio 裝置, PCI, MMIO, Channel I/O ,裝置會有根據 bus 定義的配置空間,其中包含上述所提到的 Feature bits 和 Device Status Field。 Virtio-pci 也就是基於 PCI 實作的 Virtio ### Virtqueues 裝置和驅動共享的資料結構,用於驅動與裝置 IO 請求的溝通 目前 Virtio 1.1 有以下兩種 Virtqueue ,目前實作的是 Packed Virtqueues - Split Virtqueues - Packed Virtqueues ### Device Initialization 通常會依照以下步驟初始化裝置 1. 重設裝置 2. Guest OS 探查到裝置後會設定 `ACNOWLEDGE (1)` 3. 確立裝置的驅動後設定 `DRIVER (2)` 4. 讀取裝置的 Feature bits ,根據驅動所能提供的特性寫入一個裝置 Feature 的 subset 5. 設定 `FEATURES_OK (8)` ,裝置在此之後不再接受 Feature 6. 讀取裝置的狀態,若仍是 `FEATURES_OK (8)` 代表 Feature 協商成功,若不是則裝置不可用 7. 執行特定於裝置的初始化 8. 一切正常就設定 `DRIVER_OK (4)` 否則設定 `FAILED (128)` 若裝置運作的過程遇到錯誤,會主動設定 `DEVICE_NEEDS_RESET (64)` ### Virtio-blk Virtio-blk 也就是支援 Virtio 的 block device ,我們要引入的 Virtio-blk 建構於 Virtio-pci 之上 ### Packed Virtqueues Split Virtqueues 的結構如下圖,驅動要發起 IO 請求會將 descriptors 填入 Descriptor Table 完成後會將 descriptors 的位置寫入 Avail Ring ,然後向裝置發起 vailable buffer notification 裝置收到通知後,會根據 Avail Ring 找出對應的 descriptors,然後依照 buffer 內容處理 IO 請求,完成後會寫入 Used Ring 及向驅動發起 used buffer notification ![](https://hackmd.io/_uploads/ryfi0UhP3.png) Packed Virtqueues 是 Virtio 1.1 提出的新的 Virtqueue 結構 基本概念與 Split Virtqueues 相似,只是將 Descriptor Table 、 Avail Ring 、 Used Ring 合併為一個 Descriptor Ring ,以有效利用 Cache。 除此之外,原本的 Device Area 與 Driver Area 變為由 Device Event Suppression 和 Driver Event Suppression 來使用, Event Suppression 用來讓裝置或驅動抑制對方的通知 這三個 Area 都位於 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; }; ``` 每一個 IO 請求關聯一個 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 的最後一個 - 若讀入的是 Decriptor Ring 最後一個,則將 wrap counter 反轉,然後從頭讀入 - 處理完 IO 請求後寫入 used descriptor ,其 used flag 設定為與 wrap counter 相同 - 若有 used buffer 可以發起 used buffer notification 裝置讀取 buffer 依照驅動寫入的順序,若有多個請求則依照請求完成的順序寫入 Descriptor Ring 同一次通知內的請求,第一個 buffer 的 used flag 要最後寫入 若 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 這兩個 IO 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 是每個裝置共通的,而剩下的 128 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-pci 的 Capibility 定義如下,每個 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 的結構如下 完整的 Feature bits 是 64 bits ,目前實作的 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 1 | Bits | 0 | 1 | 2 to 31 | | ------- | --------------- | ----------------------------- | -------- | | Purpose | Queue Interrupt | Device Configuration Interrup | 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 - IO 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` 發起 IO 請求,根據 `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` 的事件,以及一個 `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_config common_config; struct virtio_pci_isr_cap isr_cap; struct virtio_pci_notify_data notify_data; void *dev_config; }; ``` 裝置驅動程式通知裝置的方式是透過寫入某個位址,實作中是寫入 `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 ,並對實際的裝置發起 IO 請求 - `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 union virtq_desc *virtq_get_avail(struct virtq *vq) { union virtq_desc *desc_ring = (union virtq_desc *) vq->info.desc_addr; union virtq_desc *desc = &desc_ring[vq->next_avail_idx]; uint16_t flags = desc->pdesc.flags; bool avail = flags & VIRTQ_DESC_F_AVAIL; bool used = flags & VIRTQ_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) `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 通知 IO 事件發生的機制 當 Guest 進行 IO 操作觸發 vm-exit 時, kvm 會先判斷該位址是否有註冊 eventfd ,若有則透過 eventfd 通知,然後讓 Guest 繼續執行 用於某些實際上不傳輸資料僅作為通知的 IO 請求,可以減少時間的開銷,如 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 與實際發送 IO 請求的這部分 若 vhost 實作在 kernel space ,可以減少從 kernel space 來回切換到 user space 的開銷 ![](https://hackmd.io/_uploads/HkbaePnv2.png) 但目前 Linux Kernel 沒有 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-box](https://github.com/cppcoffee/kvm-box): minimalist VMM that uses the Linux KVM to create and run microVM, capable of running Linux kernel partially. * [Alioth](https://github.com/google/alioth) is an experimental KVM-based type-2 hypervisor in Rust implemented from scratch.