--- tags: linux2022 --- # kvm-host 的改進 contributed by < `ray90514` > 本次的改進目標為將 Virtio-blk 引入 [kvm-host](https://github.com/sysprog21/kvm-host) ,實作參考 [kvmtool](https://github.com/kvmtool/kvmtool) 及 [tinybox](https://github.com/DaiZhiyuan/tinybox)。virtio 基於以下規範 [Virtual I/O Device (VIRTIO) Version 1.1](https://docs.oasis-open.org/virtio/virtio/v1.1/virtio-v1.1.pdf) 開發過程的程式碼在 [ray90514/kvm-host](https://github.com/ray90514/kvm-host) ## TODO - [x] bus - [x] pci - [x] virtio-pci - [x] virtqueue - [x] virtio-blk - [ ] ? ## 準備工作 1. 完成 `kvm-host` 的安裝與執行 2. 開啟 virtio-blk 根據 [How to use Virtio](https://www.linux-kvm.org/page/Virtio) 的指示,開啟以下 Linux Kernel 的編譯選項,重新編譯 bzImage - `CONFIG_VIRTIO_PCI=y` (Virtualization -> PCI driver for virtio devices) - `CONFIG_VIRTIO_BALLOON=y` (Virtualization -> Virtio balloon driver) - `CONFIG_VIRTIO_BLK=y` (Device Drivers -> Block -> Virtio block driver) - `CONFIG_VIRTIO_NET=y` (Device Drivers -> Network device support -> Virtio -network driver) - `CONFIG_VIRTIO=y` (automatically selected) - `CONFIG_VIRTIO_RING=y` (automatically selected) 3. 了解 `kvm-host` 目前的實作 參考 [KVM](https://hackmd.io/@RinHizakura/SJpFJ0mfF) ,在呼叫 `ioctl(v->vcpu_fd, KVM_RUN, 0)` 後讓 KVM 執行設定好的程式,直到事件發生, `kvm-host` 根據事件做對應的處理,再繼續執行,如以下程式碼所示,這也是 `kvm-host` 的主要部份 ```c while (1) { int err = ioctl(v->vcpu_fd, KVM_RUN, 0); if (err < 0 && (errno != EINTR && errno != EAGAIN)) { munmap(run, run_size); return throw_err("Failed to execute kvm_run"); } switch (run->exit_reason) { case KVM_EXIT_IO: if (run->io.port >= COM1_PORT_BASE && run->io.port < COM1_PORT_END) serial_handle(&v->serial, run); break; case KVM_EXIT_INTR: serial_console(&v->serial); break; case KVM_EXIT_SHUTDOWN: printf("shutdown\n"); munmap(run, run_size); return 0; default: printf("reason: %d\n", run->exit_reason); munmap(run, run_size); return -1; } } ``` 根據事件種類 `KVM_EXIT_IO` ,可以在 `struct kvm_run` 的變數可以得到以下資訊 ```c struct { __u8 direction; __u8 size; /* bytes */ __u16 port; __u32 count; __u64 data_offset; /* relative to kvm_run start */ } io; ``` 模擬 io 時會需要用到這些,目前 `kvm-host` 只模擬了 `COM1` 的 serial port,之後的改進也會從這裡開始 ## Virtio Virtio 是 IO 請求溝通的標準,架構如下圖所示,有一個前端和後端,前端通常作為驅動存在被 Guest OS 使用,後端則是在 Guest OS 被視為裝置的一種,後端可以是軟體模擬出來的裝置也可以是支援 Virtio 的實體裝置 以 Hypervisor 實現的後端來說,前端將 IO 請求傳給後端,後端會將請求傳給實際的裝置,等 IO 處理完成後傳回給前端,後端的這過程也就是裝置的模擬。前後端使用 Virtqueue 作為資料交換的機制 ![](https://i.imgur.com/yXnhsjm.gif) 對於前述 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 之上 :::warning Virtio-mmio 類似 PCI 但只用於虛擬裝置,可提供給不支援 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://i.imgur.com/4bImQit.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://i.imgur.com/cZgk93i.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://i.imgur.com/Vb9j4aX.png) 一個 PCI 邏輯裝置提供 256 bytes 的 Configuration Space ,用以完成裝置的設定與初始化, CPU 不能直接存取這個空間,需要透過 PCI 的 Host Bridge 提供特殊的機制,讓 CPU 完成配置空間的存取 這個機制藉由 CF8 、 CFC 這兩個 IO Port,先是在 CF8 寫入要存取的配置空間暫存器的位址,然後寫入或讀出 CFC 就可以完成對該暫存器的操作 ![](https://i.imgur.com/lFrzlRN.png) Bus Number 搭配 Device Number 可以用來識別實際上的 PCI 裝置,每個裝置可以提供不同的功能,每個功能被視為一個邏輯裝置,用 Bus Number : Device Number : Function Number 來分辨每一個邏輯裝置 256 bytes 的配置空間由 64 個 32 bits 的暫存器組成,以 Register Offset 來決定 ![](https://i.imgur.com/rncemPq.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://i.imgur.com/b7kC3sm.png) BAR 的組成如下, Base Address 是裝置內部的記憶體映射到 CPU 定址空間的起始位址,在裝置初始化階段由 Driver 寫入,最低位指示空間的種類,Type 指示位址長度為 32 bits 或 64 bits Base Address 對齊裝置內部空間的大小,根據這個大小低位不可寫 ![](https://i.imgur.com/SU0dpiK.gif) 自定義的部份由 Capability List 組成, Status 的 Bit 4 會告知該裝置有沒有 Capability List , Capability List​開頭的位址固定在 0x34 每一個 Capability 的第一個 Byte 為 規定好的 ID ,第二個 Byte 為下一個 Capability 的開頭位址,接下來是 Capability 的內容 ![](https://i.imgur.com/onO4nkh.jpg) ## 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 由各裝置定義 ## 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 會被分為三個 element , `type` 、 `reserved` 、 `sector` 一個, `data[]` 一個, `status` 一個 ```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/ray90514/kvm-host/blob/master/src/bus.c) 引入了一種結構,用來處理位址與裝置的映射關係,使用 linked list 管理裝置 ```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/ray90514/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); } ``` :::warning 執行 kvm-host 會發現以下錯誤訊息 ``` PCI: Fatal: No config space access function found ``` 追查程式碼發現錯誤原因在 [arch/x86/pci/direct.c](https://github.com/torvalds/linux/blob/master/arch/x86/pci/direct.c) 的 `pci_sanity_check` ,這個函式是用來檢查 PCI 機制的完整性 ```c /* * Before we decide to use direct hardware access mechanisms, we try to do some * trivial checks to ensure it at least _seems_ to be working -- we just test * whether bus 00 contains a host bridge (this is similar to checking * techniques used in XFree86, but ours should be more reliable since we * attempt to make use of direct access hints provided by the PCI BIOS). * * This should be close to trivial, but it isn't, because there are buggy * chipsets (yes, you guessed it, by Intel and Compaq) that have no class ID. */ static int __init pci_sanity_check(const struct pci_raw_ops *o) ``` [Epic kvmtool adventures](https://meta.it-syndikat.org/t/epic-kvmtool-adventures/2471) 有提到 kvmtool 是怎麼做的,我們將 `pci=conf1` 加入到 kvm-host 的 `KERNEL_OPTS` 以跳過檢查 重新執行後 Guest Linux 能探查及初始化我們模擬的 PCI 設備了 ``` pci 0000:00:00.0: [1af4:1042] type 00 class 0x010000 ``` ::: 當 Guest OS 寫入 pci 配置空間的 command 時,低兩位是用來開啟或關閉記憶體映射的,此時將 bar 的位址註冊到對應的 bus 上 ![](https://i.imgur.com/KV3RD6N.jpg) ```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/ray90514/kvm-host/blob/master/src/virtio-pci.c) [virtio-pci.c](https://) 負責初始化 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/ray90514/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/ray90514/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` 發起的讀寫請求也就是對該 file 讀寫 ## 測試 若要在 Guest Linux 使用 EXT4 ,得開啟以下編譯選項 ``` CONFIG_EXT4_FS=y ``` 在 Linux 使用以下命令建立一個 ext4 的 disk image ``` 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 掛載裝置 ``` 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 的編號 ``` sudo losetup -f virtio_blk.img mkdir /tmp/disk sudo mount /dev/loop19 /tmp/disk ``` 掛載成功後,使用 `ls /tmp/disk` 查看裡面的內容 ``` lost+found test ``` 至此確認 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/) 及 [qemu-kvm的ioeventfd机制](https://www.cnblogs.com/haiyonghao/p/14440743.html) 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); ``` 然後建立一個 thread 用來等待 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://i.imgur.com/M7OvMcp.png) 但目前 Linux Kernel 沒有 vhost-blk ,有人發過 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)