# Linux 核心專題: 打造具備網路連線的精簡虛擬機器 > 執行人: jimmylu890303 > [專題解說錄影](https://youtu.be/i-hnX1dV5M4?si=v-aEGutuferoOJwu) > [Google 簡報連結](https://docs.google.com/presentation/d/1zavyECppgu1Z_GGvkVhS_FcaDxt-Jwve8jJbdZWqzlw/edit?usp=sharing) ### Reviewed by `ollieni` 請問在你的測試中可能會有大量連線的狀況嗎? 若是有的話,會不會出現甚麼問題? 以及要如何優化? > 目前測試中沒有包含大量網路流量的測試,只有包含基本的傳送封包,但若有大量網路流量的問題,可以會導致 virtqueue 沒有 available 的狀態,可能會導致延遲的問題,因為要等待 available virtqueue 才能繼續動作,所以在 Virtio 規範中,它是定義 virtqueue 至少為一對,要應付這種狀況就要實作多對的 virtqueue 來解決。可參照[規格書](https://docs.oasis-open.org/virtio/virtio/v1.1/csprd01/virtio-v1.1-csprd01.pdf)的 5.1.2 小節 ![image](https://hackmd.io/_uploads/r14Rwlmv0.png) ### Reviewed by `nosba0957` 1. split virtqueues 中, device area 就是下方描述的 used ring 嗎? > Device Area: data supplied by device to driver. Also called used virtqueue > Device area 稱為 Used virtqueue 也叫做 used ring,三者形容的東西是相同的概念,這裡 suppiled by 我會將它理解成是被裝置維護。 如果是,那依據這段描述的意思,裝置也可以透過 used ring 傳資料給 driver 嗎? > Used Ring 是用於記錄哪些緩衝區是被裝置消費使用過的。 以 virtio-net 裝置舉例來說,它會分成傳送和接收兩條路線 > 1. Host OS 傳送到 Guest OS 虛擬裝置會從 Available Ring 找出可用的緩衝區(在 split virtq 中是在 Available Ring 找 idx ,用此 idx 可在 Descriptor Area 找到緩衝區的資訊),而虛擬裝置會將資料寫進該緩衝區,之後將這個緩衝區資訊記錄在 Used Ring 中。 > 2. Guest OS 傳送到 Host OS Guest OS 中的驅動程式會將要傳送的資料寫入到一個緩衝區中,再將此緩衝區記錄到 Available Ring ,則虛擬裝置會從 Available Ring 取出該緩衝區的資料,在進一步進行處理。 2. 解釋註冊到 PCI 的部分,Configuration Space 和 Command Register 的圖片無法顯示,需要修正。 > 已修正,註冊 PCI 的部分可參照此處[實作](https://github.com/sysprog21/kvm-host/blob/0f31f1bb69fe815f7c547a9e051d1667af77986d/src/pci.c#L176),對於一個 PCI 裝置而言,都會有一個 256 bytes 的 Configuration Space ,在此 space 中要根據裝置的類型去填入對應的值,這樣才能使作業系統在探查 PCI 裝置時,才能明確知道裝置的類型並且正確載入對應的驅動程式。 ### Reviewed by `aa860630` 對於 Guest OS 而言,要如何通知 virtio-net 裝置進行傳送,並且 Host OS 要如何對 Guest OS 進行通知要進行接收? > 0. 在驅動程式通知裝置會使用 Virtio-PCI 裝置的 notify 區域,而裝置通知驅動則是使用 IRQ 。 > 1. 對於 Guest OS 通知裝置是透過驅動程式會對 Virtio-PCI 裝置的 notify 區域進行寫入(可在此處[參考](https://github.com/sysprog21/kvm-host/blob/0f31f1bb69fe815f7c547a9e051d1667af77986d/src/virtio-pci.c#L214)),所以我的實作中 tx_thread 會監控此區域是否有寫入事件,當有寫入事件,則直接呼叫 virtqueue 中的 callback 將資料寫入到 TAP 裝置 > 2. rx_thread 會監控 TAP 裝置是否有寫入事件,有寫入事件代表 Guest 要接收,所以有寫入事件發生時, rx_thread 會呼叫 rx_virtqueue 的 callback 將資料寫入,之後再使用 IRQ 通知 Guest 。 ## 任務簡介 在 KVM 的基礎之上,建構精簡且得以運作 Linux 核心的虛擬機器,使其藉由 VirtIO 具備電腦網路連線能力。 參考資訊: * [2022 年報告](https://hackmd.io/@ray90514/kvm-host) * [2023 年報告](https://hackmd.io/@sysprog/rkro_FeSh) ## TODO: 研讀 [KVM: Linux 虛擬化基礎建設](https://hackmd.io/@sysprog/linux-kvm) 和 [打造以 KVM 為基礎的精簡虛擬機器管理程式](https://pretalx.coscup.org/coscup-2023/talk/JSGKMQ/),摘錄 kvm-host 運作原理 ### 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) ### KVM [KVM](https://en.wikipedia.org/wiki/Kernel-based_Virtual_Machine) (Kernel-based Virtual Machine) 是 Linux 核心提供的系統虛擬機器基礎建設,它是個 Linux 核心模組,能讓 Linux 系統成為一個 Type-2 Hypervisor 。KVM 透過硬體虛擬化支援 (Intel VT, AMD-V) 來提供 CPU 和記憶體虛擬化功能。藉由硬體虛擬化技術,客體作業系統 (Guest OS) 不必經由軟體模擬或轉換指令,即可高效率且安全地直接執行在硬體上。使用者空間的管理程式只要負責模擬周邊裝置、呼叫 KVM API ,即可建立並高效率地執行虛擬機器。 KVM 提供 API 供 User space 呼叫 : - CPU 虛擬化 (透過硬體支援) - 記憶體虛擬化 (包含 MMU) - 中斷控制器虛擬化 ![image](https://hackmd.io/_uploads/S1zwbd9I0.png) 再回顧上述 type2 的 Hypervisor, KVM 是直接執行在 Linux Kernel space,因此可以有效地減少延遲的問題,所以這也是為什麼 KVM 可以成功的原因,在許多知名的大公司皆有採用 KVM 實作的專案如 [Cloud-Hypervisor](https://github.com/cloud-hypervisor/cloud-hypervisor) 等。 而 KVM 提供的 API 可在 [Using the KVM API](https://lwn.net/Articles/658511/) 找到。 ### KVM-host [sysprog21/kvm-host](https://github.com/sysprog21/kvm-host) 展示一個使用 Linux 的 kernel-based virtual machine,達成可載入 Linux 核心的系統級虛擬機器 (system virtual machine)的極小化實作。 以下解析程式碼的行為: 以下為專案中虛擬機器的實作 ```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; ``` 而啟動虛擬機器的流程如下: 1. `vm_init(&vm)`: 初始化虛擬機器, CPU 虛擬化及記憶體虛擬化 2. `vm_load_image(&vm, kernel_file)`: 載入 Linux 核心 3. `vm_load_initrd(&vm, initrd_file)`: 載入 initramfs 4. `vm_load_diskimg(&vm, diskimg_file)`: 指定 disk image 並初始化 virtio-blk 5. `vm_late_init(&vm)` (直接在 ISA 專屬程式部份實作): 執行虛擬機器前的最終初始化 6. `vm_run(&vm)`: 開始執行虛擬機器 7. `vm_exit(&vm)`: 虛擬機器結束、資源釋放 #### `vm_init` 首先需要開啟 `/dev/kvm` 取得 kvm file descriptor ```c if ((v->kvm_fd = open("/dev/kvm", O_RDWR)) < 0) return throw_err("Failed to open /dev/kvm"); ``` 呼叫 ioctl 對 kvm_fd(`KVM_CREATE_VM`) 操作建立虛擬機器 ```c if ((v->vm_fd = ioctl(v->kvm_fd, KVM_CREATE_VM, 0)) < 0) return throw_err("Failed to create vm"); ``` 使用 `vm_arch_init` 根據不同的平台做初始化,以 x86 為例 ```c int vm_arch_init(vm_t *v) { if (ioctl(v->vm_fd, KVM_SET_TSS_ADDR, 0xffffd000) < 0) return throw_err("Failed to set TSS addr"); __u64 map_addr = 0xffffc000; if (ioctl(v->vm_fd, KVM_SET_IDENTITY_MAP_ADDR, &map_addr) < 0) return throw_err("Failed to set identity map address"); if (ioctl(v->vm_fd, KVM_CREATE_IRQCHIP, 0) < 0) return throw_err("Failed to create IRQ chip"); struct kvm_pit_config pit = {.flags = 0}; if (ioctl(v->vm_fd, KVM_CREATE_PIT2, &pit) < 0) return throw_err("Failed to create i8254 interval timer"); return 0; } ``` 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) 設定 vm 中的記憶體 ```c v->mem = mmap(NULL, RAM_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (!v->mem) return throw_err("Failed to mmap vm memory"); struct kvm_userspace_memory_region region = { .slot = 0, .flags = 0, .guest_phys_addr = RAM_BASE, .memory_size = RAM_SIZE, .userspace_addr = (__u64) v->mem, }; if (ioctl(v->vm_fd, KVM_SET_USER_MEMORY_REGION, &region) < 0) return throw_err("Failed to set user memory region"); ``` `KVM_SET_USER_MEMORY_REGION`: 為 VM 建立記憶體,將 guest physical memory 通過 host OS 的一段在 virtual 連續的空間(在 host 的 physical 不一定連續)來進行模擬 ![](https://hackmd.io/_uploads/ryfwqL3wn.png) `KVM_CREATE_VCPU`: 為 VM 建立 CPU ```c if ((v->vcpu_fd = ioctl(v->vm_fd, KVM_CREATE_VCPU, 0)) < 0) return throw_err("Failed to create vcpu"); ``` ## TODO: 理解 KVM 和 VirtIO 原理 ### VirtIO Virtio 是 IO 請求溝通的標準,架構如下圖所示,有一個前端和後端,前端通常作為驅動存在被 Guest OS 使用,後端則是在 Guest OS 被視為裝置的一種,後端可以是軟體模擬出來的裝置也可以是支援 Virtio 的實體裝置。 ![image](https://hackmd.io/_uploads/Bkng4xQ8A.png) 以 VirtIO 實現的裝置來說,前端將 IO 請求傳給後端,後端會將請求傳給實際的裝置,等 IO 處理完成後傳回給前端,後端的這過程也就是裝置的模擬。 前後端使用 `Virtqueue` 作為資料交換的機制 一個 virtIO 的設備需要有以下結構 - Device status field - Feature bits - Notifications - virtqueue (One or more) #### Device status field Device status field 是一連串的位元,每個位元代表不同的意義,用於設備和驅動程式執行初始化的狀態,設置的可以是 Guest 或者是 Driver,所以我們可透過 status field 查看裝置狀態。 ``` ACKNOWLEDGE (0x1) // 確認有該裝置存在 DRIVER (0x2) // Driver 正在初始化 DRIVER_OK (0x4) FEATURES_OK (0x8) DEVICE_NEEDS_RESET (0x40) // 如果設備故障,可以設定此 bit (為設備設置的) FAILED (0x80) // driver 透過設定此 bit ,來達成與上述的功能。 ``` 通常會依照以下步驟初始化裝置 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) #### 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. #### Notifications 設備和驅動必須使用 `Notifications` 來告知資訊來溝通,但傳輸的方式是 `transport specific` 。 對於 Virtio-pci 驅動到裝置的通知由寫入特定的記憶體區域觸發 vm-exit 完成,裝置到驅動的通知則由 interrupt 完成。 - configuration change notification - available buffer notification - used buffer notification #### virtqueue Virtqueue 是 guest 申請的一塊記憶體區域,共享給 host 的記憶體區塊。 一個 virtqueue 實際上就是一個存放了 guest 和 host 之間數據交換的緩衝區的佇列。guest 可以將需要發送給 host 的資料放入這些 buffers 中,host 則可以讀取這些 buffers 中的數據進行處理,或者將自己的數據寫入這些 buffers 中傳送給 guest。 目前 Virtio 1.1 有以下兩種 Virtqueue ,而在 KVM-Host 目前實作的是 Packed Virtqueues - Split Virtqueues - Packed Virtqueues ##### Split Virtqueues - Descriptor Area: used for describing buffers. - Driver Area: data supplied by driver to the device. Also called avail virtqueue. - Device Area: data supplied by device to driver. Also called used virtqueue. ###### Descriptor ring (area) Descriptor area 包含了許多 guest 的 buffers 還有他們的長度。 單個 descriptor 結構如下 ```c struct virtq_desc { le64 addr; le32 len; le16 flags; le16 next; // for "Chained descriptors" }; ``` > 在 flags 中的 0x2 bit 被設置,代表該設備只能寫,若為 0 則代表只能讀 ![image](https://hackmd.io/_uploads/SJ0svxQLA.png) ###### Avail ring: Supplying data to the device 這是 driver 要給 device 所使用的 buffers,在此處放置緩衝區並不意味設備需要立即使用,如 virtio-net 提供了一堆用於封包接收的描述符,這些描述符僅在封包到達時由設備使用,並且在那一刻之前為準備使用。 avail ring 結構如下 ```c struct virtq_avail { le16 flags; le16 idx; le16 ring[ /* Queue Size */ ]; le16 used_event; }; ``` > idx 和 flag 只能由驅動程式寫入,設備只能讀取 - idx : where the driver would put the ==next descriptor entry== in the avail ring (modulo the queue size). - ring : an array of integers of the same length as the descriptors ring ![image](https://hackmd.io/_uploads/BkR4zFe40.png) 初始時, descriptor area 中有一個 buffer 長度為 2000 bytes,起始位置為 0x8000,而在 Avail area 中目前沒有可用的 buffer ![image](https://hackmd.io/_uploads/Hy8BGYlEC.png) 將 buffer 加入到 avail area, ring[0] 存 descriptor area 中 buffer index,idx 指向下個 next descriptor entry(ring[1]) ###### Chained descriptors: Supplying large data to the device 在 flag 中的 NEXT (0x1) 位元設定 1 ,代表這個 buffer 為 Chained buffer,再透過 Next 去找下個 Chained buffer。 ![image](https://hackmd.io/_uploads/r1X0box4A.png) ###### Used ring: When the device is done with the data 設備會將已用的 buffer 返回給 driver,結構跟 avail ring 一樣,但是有額外一個結構,紀錄使用的 buffer 使用多少長度。 ```c struct virtq_used_elem { /* Index of start of used descriptor chain. */ le32 id; /* Total length of the descriptor chain which was used (written to) */ le32 len; }; struct virtq_used { le16 flags; le16 idx; struct virtq_used_elem ring[ /* Queue Size */]; le16 avail_event; }; ``` ![image](https://hackmd.io/_uploads/ryVFUjgNC.png) > 0x3000 為寫入總長度 ##### Packed Virtqueue Packed Virtqueue 是 Virtio 1.1 提出的新的 Virtqueue 結構。 基本概念與 Split Virtqueues 相似,只是將 Descriptor Table 、 Avail Ring 、 Used Ring 合併為一個 Descriptor Ring。 而 Packed Virtqueue 主要為以下兩個變更 在 Device 和 Driver 中內部會維護各自的 `wrap counter` ,初始皆為 1 ,當走訪完 Descriptor Table 中最後一個元素時, 會將各自的 `wrap counter` 進行反轉,所以 `wrap counter` 不是為 0 就是為 1 。 在 Packed Descriptors 中新增了兩項 flags - `AVAIL(0x7)` - `USED(0x15)` ![image](https://hackmd.io/_uploads/SJXQrpQ8A.png) 如果要讓 descriptor 為 available , 驅動程式會讓 `AVAIL(0x7)` flag 與驅動程式內部的 `wrap counter` 相同,並且 `USED(0x15)` flag 與 `wrap counter` 相反。 | Address | Length | ID | Flags | | ---------- | ------ | --- |:-----:| | 0x80000000 | 0x1000 | 0 | WA | 當裝置開始搜尋驅動程式提供的第一個描述符時,他會查詢是否 `AVAIL(0x7)` flag 與裝置的 wrap counter 相同,當相同時,代表這個 descriptor 可用。 在裝置使用完後,會將 `USED(0x15)` flag 和 `AVAIL(0x7)` flag 設定成和 `wrap counter` 相同的值,並且和 Split Virtqueue 中的 Used ring 一樣會回傳 id 和 written length (如果有回傳的話)。 | Address | Length | ID | Flags | | ---------- | ------ | --- |:-----:| | 0x80000000 | 0x1000 | 0 | WAU | ## TODO: 針對電腦網路裝置模擬提出方案 ### Virtio-Net ![image](https://hackmd.io/_uploads/HytJhp1v0.png) Virtio-net 簡易流程如上圖 1. Guest OS 中 userspace 的程式想要發送封包,所以它調用 syscall 發送到 guest 中的 network stack ,之後傳到 virtio-net driver 中 2. virtio-net driver 會將資料寫入記憶體內存的區塊 3. KVM 通知 virtio-net device 要進行處理 4. virtio-net device 從 rx virtqueue 中讀出資料寫入至 TAP 5. TAP 會將要傳送的資料傳送到 Host OS 中的 network stack ,最後在作實際的傳送。 下圖為 Qemu 中 virtio-net 的架構圖 ![image](https://hackmd.io/_uploads/H1qbsXQS0.png) 虛擬機中的前端為 Guest OS,後端為 Qemu process ,後端可以想成為 VMM ,負責模擬裝置。 前端及後端的資料交換方式(溝通)是採用兩個 Virtqueue - Virtqueue[0](Receive Queue,RX):用於從 TAP 裝置接收資料包,然後傳遞給 Guest OS。 - Virtqueue[1] (Transmit Queue,TX):用於從 Guest OS 接收資料,並將其發送到 TAP 裝置。 virtio-net device 和 Host OS 資料交換透過 TAP 裝置,[TAP/TUN](https://docs.kernel.org/networking/tuntap.html) 為 linux 中的虛擬網路裝置 TUN 工作在 IP 層,無法與真實網卡作 bridge TAP 工作在 MAC 層,可與真實網卡作 bridge ![image](https://hackmd.io/_uploads/HJWrXnILR.png) 當 Userspace 程式往字符設備 /dev/net/tun 寫入資料時,寫入的資料會出現在 TUN 裝置上。當內核向 TUN 裝置發送資料時,通過讀取這個字符設備 /dev/net/tun 也可以拿到該資料。 ```c int tapfd = open("/dev/net/tun", O_RDWR); if(tapfd < 0){ printf("failed to open TUN device\n"); return false; } // Set flag to create tap struct ifreq ifreq = {.ifr_flags = IFF_TAP | IFF_NO_PI}; strncpy(ifreq.ifr_name, TAP_INTERFACE, sizeof(ifreq.ifr_name)); if (ioctl(tapfd, TUNSETIFF, &ifreq) < 0) { printf("failed to open TAP device\n"); return false; } ``` #### 總結專案目標 實作下圖中的紅色方框區塊,需要註冊 virtio-net 裝置,並且它要能夠與 virtqueue 及 TAP 裝置進行互動。 ![image](https://hackmd.io/_uploads/SJ35oTkDC.png) ### 實作 > [Pull Request](https://github.com/sysprog21/kvm-host/pull/34) #### 0. 重新編譯 Linux Kernel `virtio-net driver` 是由 Guest OS 提供,所以在編譯 kernel 的時候,需要開啟必要的選項,這樣 Guest OS 中才會有 `virtio-net driver`,並且也要在 busybox 中開啟一些關於網路的命令支援 ``` CONFIG_NET=y CONFIG_INET=y CONFIG_NETDEVICES=y CONFIG_VIRTIO=y CONFIG_VIRTIO_NET=y CONFIG_VIRTIO_RING=y ``` 詳細的資訊在 [commit - 2448d1](https://github.com/sysprog21/kvm-host/pull/34/commits/2448d1f346db285d8ff7a431e7047a265e0296ca) #### 1. 將 Virtio-net 裝置註冊到 PCI 上 ##### PCI kvm-host 實作週邊裝置是使用 PCI 的方法,所以我們需要知道一些關於 PCI 的先備知識 在 PCI 裝置中,都會有一個 256 bytes 的 Configuration Space ,此空間用於驅動的初始化及配置,並且作業系統可以透過訪問該 Configuration Space 得知裝置的類型 Configuration Space 結構如下圖 ![image](https://hackmd.io/_uploads/rkIq4wGD0.png) ###### Common Header Fields 前 4 個 register 為 `Common Header Fields` ,其中 - Device ID : 辨別特定的裝置 - Vendor ID : 辨別設備的製造商,其中有效的 ID 由 [PCI-SIG](https://pcisig.com/membership/member-companies) 分配,`0xFFFF` 為不合法值,當讀取不存在的裝置時,會返回該值 - Status : 記錄 PCI 事件的狀態資訊寄存器。 - Command : 提供對設備生成和回應 PCI 週期的控制能力。 - Class Code : 指定設備執行功能類型的 read-only register ##### Command Register ![image](https://hackmd.io/_uploads/H14pVvfwC.png) - Interrupt Disable : If set to 1 the assertion of the devices INTx# signal is disabled; otherwise, assertion of the signal is enabled. - Memory Space : If set to 1 the device can respond to Memory Space accesses; otherwise, the device's response is disabled. > 例如,假設設備A 的「記憶體空間」標誌位元被設定為1,表示設備A 被分配了記憶體位址範圍[0x0000, 0xFFFF],並且允許對該範圍內的記憶體進行讀寫操作。進行讀寫操作。位址針對寫入資料的操作。 - I/O Space : If set to 1 the device can respond to I/O Space accesses; otherwise, the device's response is disabled. ##### Base Addresss Register (BAR) Base Addresss Register 保存設備所使用的記憶體位址或者為 port address 的偏移量,BAR 分為兩種類型, Memory Space BAR 或者 I/O Space BAR。 Memory Space BAR Layout | Bits 31-4 | Bit 3 | Bits 2-1 | Bit 0 | | ---------------------------- | ------------ | -------- |:--------:| | 16-Byte Aligned Base Address | Prefetchable | Type | Always 0 | - Type : 指定 BAR 的大小及在記憶體中映射的位置 - 若 Type 值為 `0x0` 則代表 base register 為 32 位元,並且可以映射到 32 位元的記憶體位置。 - 若 Type 值為 `0x2` 則代表 base register 為 64 位元,並且可以映射到 64 位元的記憶體位置。 - 若 Type 值為 `0x1` 則代表 base register 為 16 位元,並且可以映射到 16 位元的記憶體位置。(早期版本) 所以對於一個作業系統中,要正確的識別出裝置是使用 vendor ID, device ID, class code ,這會被定義在 PCI 規格中,正確的識別之後,作業系統才能去載入匹配的驅動程式 ##### 實作 virtio-net `virtio_net_dev` 結構如下,它有兩個 virtqueue ```c #define VIRTIO_NET_VIRTQ_NUM 2 struct virtio_net_dev { struct virtio_pci_dev virtio_pci_dev; struct virtio_net_config config; struct virtq vq[VIRTIO_NET_VIRTQ_NUM]; int tapfd; int irqfd; int rx_ioeventfd; int tx_ioeventfd; int irq_num; pthread_t rx_thread; pthread_t tx_thread; bool enable; }; ``` 在 vm 啟動前呼叫 `virtio_net_init`,並取得 tap 的 file descriptor ,存入到 virtio-net 的結構中。 ```c bool virtio_net_init(struct virtio_net_dev *virtio_net_dev){ memset(virtio_net_dev, 0x00, sizeof(struct virtio_net_dev)); virtio_net_dev->tapfd = open("/dev/net/tun", O_RDWR); if(virtio_net_dev->tapfd < 0){ return false; } struct ifreq ifreq = {.ifr_flags = IFF_TAP | IFF_NO_PI}; strncpy(ifreq.ifr_name, TAP_INTERFACE, sizeof(ifreq.ifr_name)); if (ioctl(virtio_net_dev->tapfd, TUNSETIFF, &ifreq) < 0) { fprintf(stderr, "failed to allocate TAP device: %s\n", strerror(errno)); return false; } assert(fcntl(virtio_net_dev->tapfd, F_SETFL, fcntl(virtio_net_dev->tapfd, F_GETFL, 0) | O_NONBLOCK) >= 0); return true; } } ``` 接下來需要將 virtio_net dev 註冊到 pci 上,其中 virtio-net 的pci configuration 為 `device id` 為 `0x1041` 及 `class code` 為 `0x020000` (Ethernet controller),可參照 [PCI](https://pcisig.com/sites/default/files/files/PCI_Code-ID_r_1_11__v24_Jan_2019.pdf) ```c #define VIRTIO_PCI_VENDOR_ID 0x1AF4 #define VIRTIO_PCI_DEVICE_ID_NET 0x1041 #define VIRTIO_NET_PCI_CLASS 0x020000 void virtio_net_init_pci(struct virtio_net_dev *virtio_net_dev, struct pci *pci, struct bus *io_bus, struct bus *mmio_bus){ struct virtio_pci_dev *dev = &virtio_net_dev->virtio_pci_dev; virtio_net_setup(virtio_net_dev); virtio_pci_init(dev, pci, io_bus, mmio_bus); virtio_pci_set_dev_cfg(dev, &virtio_net_dev->config, sizeof(virtio_net_dev->config)); virtio_pci_set_pci_hdr(dev, VIRTIO_PCI_DEVICE_ID_NET, VIRTIO_NET_PCI_CLASS, virtio_net_dev->irq_num); virtio_pci_set_virtq(dev, virtio_net_dev->vq, VIRTIO_NET_VIRTQ_NUM); virtio_pci_add_feature(dev, 0); virtio_pci_enable(dev); pthread_create(&virtio_net_dev->worker_thread, NULL, (void *) virtio_net_thread, (void *) virtio_net_dev); } ``` ``` $ /sys/class/pci_bus/0000:00/device/0000:00:01.0/virtio1 # cat status 0x00000001 ``` 發現 status bit 只有 bit 0 為 1, bit 1 為0 ,代表 OS 知道裝置的存在,但不知道去哪裡 loading driver? 原先在 config 檔案中有開啟 `CONFIG_VIRTIO_NET=y`,但是在編譯 kernel image 時 .config 中沒有 `CONFIG_VIRTIO_NET=y` 的選項,後來發現有相依關係,加入以下選項 `CONFIG_NETDEVICES=y` 才能正確加入 ``` CONFIG_VIRTIO_NET=y CONFIG_NETDEVICES=y ``` ``` $ /sys/class/pci_bus/0000:00/device/0000:00:01.0/virtio1 # cat status 0x0000000f ``` 現在 bit 1~3 皆為設定為 1 ,代表 driver 初始化完成。 :::warning 從以下 dmesg 中可以看出 virtio_net dev(0000:00:01.0: [1af4:1000]) 以被註冊到 pci 上,但它卻無法被正確識別成 virtio_net device,如 virtio_blk 那樣 ::: ``` PCI host bridge to bus 0000:00 pci_bus 0000:00: root bus resource [io 0x0000-0xffff] pci_bus 0000:00: root bus resource [mem 0x00000000-0xffffffffffff] pci_bus 0000:00: No busn resource found for root bus, will use [bus 00-ff] pci 0000:00:00.0: [1af4:1042] type 00 class 0x018000 pci 0000:00:00.0: reg 0x10: [mem 0x00000000-0x000000ff] pci 0000:00:01.0: [1af4:1000] type 00 class 0x020000 pci 0000:00:01.0: reg 0x10: [mem 0x00000000-0x000000ff] pci 0000:00:00.0: BAR 0: assigned [mem 0x40000000-0x400000ff] pci 0000:00:01.0: BAR 0: assigned [mem 0x40000100-0x400001ff] pci_bus 0000:00: resource 4 [io 0x0000-0xffff] pci_bus 0000:00: resource 5 [mem 0x00000000-0xffffffffffff] virtio-pci 0000:00:00.0: enabling device (0000 -> 0002) virtio-pci 0000:00:01.0: enabling device (0000 -> 0002) virtio_blk virtio0: 1/0/0 default/read/poll queues virtio_blk virtio0: [vda] 4800 512-byte logical blocks (2.46 MB/2.34 MiB) ``` #### 2. 實作 Virtqueue Callback 在 RX Virtq 和 TX Virtq 的 virtq_ops 會有不同工作行為,當 Guest OS virtio-net driver 對 Virtqueue 讀寫時,會觸發 `complete_request` 的 callback function ,所以要有不同的實作。 ```c static struct virtq_ops virtio_net_rx_ops = { .enable_vq = virtio_net_enable_vq_rx, .complete_request = virtio_net_complete_request_rx, .notify_used = virtio_net_notify_used_rx, }; static struct virtq_ops virtio_net_tx_ops = { .enable_vq = virtio_net_enable_vq_tx, .complete_request = virtio_net_complete_request_tx, .notify_used = virtio_net_notify_used_tx, }; ``` ##### complete_request 以 RX Virtq 來說,virtio-net 裝置會從 Available Desc 中讀取一個可用的緩衝區,並且再從 TAP 裝置讀取從 Host OS 傳入的資料,寫入到該 Available Desc 所指向的緩衝區,更新該 Available Desc 為 Used Desc,並且通知 Guest OS ```c void virtio_net_complete_request_rx(struct virtq *vq) { struct virtio_net_dev *dev = (struct virtio_net_dev *)vq->dev; vm_t *v = container_of(dev, vm_t, virtio_net_dev); struct vring_packed_desc *desc; while((desc = virtq_get_avail(vq)) != NULL){ uint8_t *data = vm_guest_to_host(v, desc->addr); struct virtio_net_hdr_v1 *virtio_hdr = (struct virtio_net_hdr_v1 *)data; memset(virtio_hdr, 0, sizeof(struct virtio_net_hdr_v1)); virtio_hdr->num_buffers = 1; size_t virtio_header_len = sizeof(struct virtio_net_hdr_v1); ssize_t read_bytes = read(dev->tapfd, data + virtio_header_len, desc->len - virtio_header_len); if (read_bytes < 0) { vq->guest_event->flags = VRING_PACKED_EVENT_FLAG_DISABLE; return; } desc->len = virtio_header_len + read_bytes; desc->flags ^= (1ULL << VRING_PACKED_DESC_F_USED); dev->virtio_pci_dev.config.isr_cap.isr_status |= VIRTIO_PCI_ISR_QUEUE; return; } vq->guest_event->flags = VRING_PACKED_EVENT_FLAG_DISABLE; return; } ``` 以 TX Virtq 來說,virtio-net 裝置會從 Available Desc 中讀取一個可用的緩衝區( virtio-net driver 會將要傳送的資料寫入到 Avail Desc 中),並且讀取該緩衝區的資料再將該資料寫入到 TAP 裝置,更新該 Available Desc 為 Used Desc,並且通知 Guest OS。 ```C void virtio_net_complete_request_tx(struct virtq *vq) { struct virtio_net_dev *dev = (struct virtio_net_dev *)vq->dev; vm_t *v = container_of(dev, vm_t, virtio_net_dev); struct vring_packed_desc *desc; while((desc = virtq_get_avail(vq)) != NULL){ uint8_t *data = vm_guest_to_host(v, desc->addr); size_t virtio_header_len = sizeof(struct virtio_net_hdr_v1); if (desc->len < virtio_header_len) { vq->guest_event->flags = VRING_PACKED_EVENT_FLAG_DISABLE; return; } uint8_t *actual_data = data + virtio_header_len; size_t actual_data_len = desc->len - virtio_header_len; struct iovec iov[1]; iov[0].iov_base = actual_data; iov[0].iov_len = actual_data_len; ssize_t write_bytes = writev(dev->tapfd, iov, 1); if (write_bytes < 0) { vq->guest_event->flags = VRING_PACKED_EVENT_FLAG_DISABLE; return; } desc->flags ^= (1ULL << VRING_PACKED_DESC_F_USED); dev->virtio_pci_dev.config.isr_cap.isr_status |= VIRTIO_PCI_ISR_QUEUE; return; } vq->guest_event->flags = VRING_PACKED_EVENT_FLAG_DISABLE; return; } ``` ```c static void virtio_net_setup(struct virtio_net_dev *dev) { vm_t *v = container_of(dev, vm_t, virtio_net_dev); dev->enable = true; dev->irq_num = VIRTIO_NET_IRQ; dev->rx_ioeventfd = eventfd(0, EFD_CLOEXEC); dev->tx_ioeventfd = eventfd(0, EFD_CLOEXEC); dev->irqfd = eventfd(0, EFD_CLOEXEC); vm_irqfd_register(v, dev->irqfd, dev->irq_num, 0); for (int i = 0; i < VIRTIO_NET_VIRTQ_NUM; i++){ struct virtq_ops *ops = (i == 0) ? &virtio_net_rx_ops : &virtio_net_tx_ops; dev->vq[i].info.notify_off = i; virtq_init(&dev->vq[i], dev, ops); } } ``` 此外要注意的事情是不管是從 Virtqueue 讀取資料或寫入資料,資料前面都應該要包含 `Virtio_header` 的區塊,格式如下,可在 [linux/virtio_net.h](https://github.com/torvalds/linux/blob/22a40d14b572deb80c0648557f4bd502d7e83826/include/uapi/linux/virtio_net.h#L130) 中查看 ```c struct virtio_net_hdr_v1 { uint8_t flags; uint8_t gso_type; uint16_t hdr_len; uint16_t gso_size; uint16_t csum_start; uint16_t csum_offset; uint16_t num_buffers; }; ``` ##### enable_vq 在 `virtio_net_enable_vq_rx` 中會建立一個 rx_thread,他會去監聽 TAP device 有沒有寫入事件,若有事件發生則呼叫 `virtq_handle_avail` 執行它的 `complete_request` callback ```c // 監控 TAP device 有沒有寫入事件 static int virtio_net_virtq_available_rx(struct virtio_net_dev *dev, int timeout) { struct pollfd pollfd = (struct pollfd){ .fd = dev->tapfd, .events = POLLIN, }; return (poll(&pollfd, 1, timeout) > 0) && (pollfd.revents & POLLIN); } static void *virtio_net_vq_avail_handler_rx(void *arg){ struct virtq *vq = (struct virtq *)arg; struct virtio_net_dev *dev = (struct virtio_net_dev *)vq->dev; struct sigaction sa; sa.sa_handler = sigusr1_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGUSR1, &sa, NULL); while (!__atomic_load_n(&thread_stop, __ATOMIC_RELAXED)){ vq->guest_event->flags = VRING_PACKED_EVENT_FLAG_ENABLE; if (virtio_net_virtq_available_rx(dev, -1)) virtq_handle_avail(vq); } return NULL; } static void virtio_net_enable_vq_rx(struct virtq *vq) { struct virtio_net_dev *dev = (struct virtio_net_dev *) vq->dev; vm_t *v = container_of(dev, vm_t, virtio_net_dev); if (vq->info.enable) return; vq->info.enable = true; vq->desc_ring = (struct vring_packed_desc *) vm_guest_to_host(v, vq->info.desc_addr); vq->device_event = (struct vring_packed_desc_event *) vm_guest_to_host( v, vq->info.device_addr); vq->guest_event = (struct vring_packed_desc_event *) vm_guest_to_host( v, vq->info.driver_addr); uint64_t addr = virtio_pci_get_notify_addr(&dev->virtio_pci_dev, vq); vm_ioeventfd_register(v, dev->rx_ioeventfd, addr, 2, 0); pthread_create(&dev->rx_thread, NULL, virtio_net_vq_avail_handler_rx, (void *) vq); } ``` 在 `virtio_net_enable_vq_tx` 中會建立一個 tx_thread,他會去監聽 TX Virtq 有沒有寫入事件,若有事件發生則呼叫 `virtq_handle_avail` 執行它的 `complete_request` callback ```c // 監控 tx_virtq 有寫入事件 static int virtio_net_virtq_available_tx(struct virtio_net_dev *dev, int timeout) { struct pollfd pollfds[2]; pollfds[0].fd = dev->tx_ioeventfd; pollfds[0].events = POLLIN; pollfds[1].fd = dev->tapfd; pollfds[1].events = POLLOUT; int ret = poll(pollfds, 2, timeout); return ret > 0 && (pollfds[0].revents & POLLIN) && (pollfds[1].revents & POLLOUT); } static void *virtio_net_vq_avail_handler_tx(void *arg) { struct virtq *vq = (struct virtq *) arg; struct virtio_net_dev *dev = (struct virtio_net_dev *) vq->dev; struct sigaction sa; sa.sa_handler = sigusr1_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGUSR1, &sa, NULL); while (!__atomic_load_n(&thread_stop, __ATOMIC_RELAXED)) { vq->guest_event->flags = VRING_PACKED_EVENT_FLAG_ENABLE; if (virtio_net_virtq_available_tx(dev, -1)) virtq_handle_avail(vq); } return NULL; } static void virtio_net_enable_vq_tx(struct virtq *vq) { struct virtio_net_dev *dev = (struct virtio_net_dev *) vq->dev; vm_t *v = container_of(dev, vm_t, virtio_net_dev); if (vq->info.enable) return; vq->info.enable = true; vq->desc_ring = (struct vring_packed_desc *) vm_guest_to_host(v, vq->info.desc_addr); vq->device_event = (struct vring_packed_desc_event *) vm_guest_to_host( v, vq->info.device_addr); vq->guest_event = (struct vring_packed_desc_event *) vm_guest_to_host( v, vq->info.driver_addr); uint64_t addr = virtio_pci_get_notify_addr(&dev->virtio_pci_dev, vq); vm_ioeventfd_register(v, dev->tx_ioeventfd, addr, 2, 0); pthread_create(&dev->tx_thread, NULL, virtio_net_vq_avail_handler_tx, (void *) vq); } ``` ### 測試 Virtio-Net 裝置 TAP 裝置存在於 Host OS 中,而 virtio-net 裝置存在於 Guest OS 中。 測試 Virtio-net 裝置的方式是通過 Bridging 的方式將 TAP 和 virtio-net 裝置連接起來。 這樣做可以建立一個橋接(Bridge),使得 Host OS 和 Guest OS 中的虛擬機器可以在同一個網路範圍內。 在這個設定下,就可以使用 ping 命令來測試 Host OS 和 Guest OS 中的虛擬機器是否能夠互相通訊 在 Host OS 執行以下命令 ``` sudo ip link delete br0 sudo brctl addbr br0 sudo ip addr add 10.0.0.1/24 dev br0 sudo ip route add default via 10.0.0.1 dev br0 sudo ip link set br0 up sudo ip link set tap0 master br0 sudo ip link set tap0 up ``` 在 Guest OS 執行以下命令 ``` ip addr add 10.0.0.2/24 dev eth0 ip link set eth0 up ip route add default via 10.0.0.1 ``` 測試影片如下 {%youtube Gy7lSCbV_1A %} ## Ref - [Using the KVM API](https://lwn.net/Articles/658511/) - [KVM Document](https://www.kernel.org/doc/Documentation/virtual/kvm/api.txt) - [VirtIO 1.1](https://docs.oasis-open.org/virtio/virtio/v1.1/csprd01/virtio-v1.1-csprd01.pdf) - [PCI](https://wiki.osdev.org/PCI) - [Redhat - Virtio devices and drivers overview](https://www.redhat.com/en/blog/virtio-devices-and-drivers-overview-headjack-and-phone) - [Redhat - Virtqueue](https://www.redhat.com/en/blog/virtqueues-and-virtio-ring-how-data-travels) - [Redhat - Packed Virtqueue ](https://www.redhat.com/en/blog/packed-virtqueue-how-reduce-overhead-virtio)