# Linux 核心期末專題 : KVM-host 實作 Virtio-Net - [x] TODO: 研讀 [KVM: Linux 虛擬化基礎建設](https://hackmd.io/@sysprog/linux-kvm), [打造以 KVM 為基礎的精簡虛擬機器管理程式](https://pretalx.coscup.org/coscup-2023/talk/JSGKMQ/), [2022 年報告](https://hackmd.io/@ray90514/kvm-host) 和 [2023 年報告](https://hackmd.io/@sysprog/rkro_FeSh),以理解 KVM 和 VirtIO 原理,針對電腦網路和 2D 裝置模擬提出方案 - [x] TODO: 將 semu 的部分 VirtIO 成果整合到 [kvm-host](https://github.com/sysprog21/kvm-host) * semu 已有 virtio-net 和 [virtio-gpu](https://github.com/sysprog21/semu/pull/34),而這些成果若可整合到系統模擬效率更高的 [kvm-host](https://github.com/sysprog21/kvm-host),可顯著提高 Linux 系統模擬的可用性 > [實作 virtio-net](https://hackmd.io/@fewletter/kvm-host) ## 任務簡述 在這次專題中,將介紹 KVM 運作原理,並展示一個以 KVM 為基礎的精簡虛擬機器管理程式實作( [kvm-host](https://github.com/sysprog21/kvm-host) ),它能在 x86-64 及 arm64 平台上運作,提供了基本的 VirtIO 儲存裝置、 serial 裝置 (用來作為 console) ,並且能成功在上面執行 Linux 系統,而目標是在該虛擬機器上實作 Virtio-net 裝置。 ## KVM KVM (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) ### vm_t structure ```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; int vm_init(vm_t *v) { // 取得 kvm desc,建立 VM if ((v->kvm_fd = open("/dev/kvm", O_RDWR)) < 0) return throw_err("Failed to open /dev/kvm"); if ((v->vm_fd = ioctl(v->kvm_fd, KVM_CREATE_VM, 0)) < 0) return throw_err("Failed to create vm"); if (vm_arch_init(v) < 0) return -1; 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"); // 初始化VCPU if ((v->vcpu_fd = ioctl(v->vm_fd, KVM_CREATE_VCPU, 0)) < 0) return throw_err("Failed to create vcpu"); if (vm_arch_cpu_init(v) < 0) return -1; bus_init(&v->io_bus); bus_init(&v->mmio_bus); if (vm_arch_init_platform_device(v) < 0) return -1; return 0; } ``` ## 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 { le16 flags; le16 idx; struct virtq_used_elem ring[ /* Queue Size */]; le16 avail_event; }; 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; }; ``` ![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 | ## Virtio-Net ![image](https://hackmd.io/_uploads/HJ89goIIR.png) Virtio-net 簡易流程如上圖 1. Guest OS 中 userspace 的程式想要發送封包,所以它調用 syscall 發送到 guest 中的 network stack ,之後傳到 virtio-net driver 中 2. virtio-net driver 會將資料寫入記憶體內存的區塊 3. KVM 通知 virtio-net device 要進行處理 4. (第4-6 步) 和 Guest OS 發送步驟相同,通過 syscall 發送到 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 為 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; } ``` ## PCI 一個 PCI 架構如下, Host bridge 負責連接 CPU 和管理所有的 PCI 裝置及 Bus 。一個 PCI 邏輯裝置提供 256 bytes 的 Configuration Space ,用以完成裝置的設定與初始化, CPU 不能直接存取這個空間,需要透過 PCI 的 Host Bridge 提供特殊的機制,讓 CPU 完成配置空間的存取。 ![image](https://hackmd.io/_uploads/r1ali5ILR.png) ### Configuration Space 結構 ![image](https://hackmd.io/_uploads/Hk6Uiq8U0.png) #### Common Header Fields PCI 配置空間的前 64 bytes 是每個裝置共通的 - Device ID : 辨別特定的裝置,對於 Virtio-net 是 0x1041 - Vendor ID : 辨別設備的製造商,其中有效的 ID 由 [PCI-SIG](https://pcisig.com/membership/member-companies) 分配,`0xFFFF` 為不合法值,當讀取不存在的裝置時,會返回該值,Virtio 為 `0x1AF4` 。 - Status : 記錄 PCI 事件的狀態資訊寄存器。 - Command : 提供對設備生成和回應 PCI 週期的控制能力。 - Class Code : 指定設備執行功能類型的 read-only register ## 實作 Virtio-Net > [Branch - Implement-virtio-net](https://github.com/sysprog21/kvm-host/compare/master...jimmylu890303:kvm-host:Implement-virtio-net) ### 1. 將 Virtio-net 裝置註冊到 PCI 上 `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); } ``` :::warning 從以下 dmesg 中可以看出 virtio_net dev(0000:00:01.0: [1af4:1000]) 以被註冊到 pci 上,但它卻無法被正確識別成 virtio_net device,如 virtio_blk 那樣 ::: ``` $ /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 初始化完成。 ``` 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; if ((desc = virtq_get_avail(vq)) != NULL){ uint8_t *data = vm_guest_to_host(v, desc->addr); struct virtio_net_hdr_kvm *virtio_hdr = (struct virtio_net_hdr_kvm *)data; memset(virtio_hdr, 0, sizeof(struct virtio_net_hdr_kvm)); virtio_hdr->num_buffers = 1; size_t virtio_header_len = sizeof(struct virtio_net_hdr_kvm); 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_ENABLE; return; } uint8_t *actual_data = data + virtio_header_len; size_t actual_data_len = read_bytes; 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; pthread_kill((pthread_t) dev->tx_thread, SIGUSR1); return; } vq->guest_event->flags &= ~VRING_PACKED_EVENT_FLAG_ENABLE; return NULL; } ``` 以 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; if ((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_kvm); if (desc->len < virtio_header_len) { fprintf(stderr, "Descriptor length too small to contain Virtio header\n"); return; } struct virtio_net_hdr_kvm *virtio_hdr = (struct virtio_net_hdr_kvm *)data; 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) { return; } desc->flags ^= (1ULL << VRING_PACKED_DESC_F_USED); dev->virtio_pci_dev.config.isr_cap.isr_status |= VIRTIO_PCI_ISR_QUEUE; pthread_kill((pthread_t) dev->rx_thread, SIGUSR1); return; } return NULL; } ``` ```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` 的區塊,格式如下 ```c struct virtio_net_hdr_kvm { 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; uint64_t n; 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); } } 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; uint64_t n; 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)) { if (virtio_net_virtq_available_tx(dev, -1)) virtq_handle_avail(vq); } } 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); } ``` ## 測試 在 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 gEZp1rUH5y4 %} :::warning 不知道為何會噴出以下錯誤訊息,但是它不影響運作 ``` ~ # irq 14: nobody cared (try booting with the "irqpoll" option) CPU: 0 PID: 0 Comm: swapper Not tainted 6.1.35 #1 Call Trace: <IRQ> ? 0xffffffff81250590 ? 0xffffffff8124e4ae ? 0xffffffff81046408 ? 0xffffffff8104475a ? 0xffffffff8104477b ? 0xffffffff81046c0f ? 0xffffffff8100f46d ? 0xffffffff812510df </IRQ> <TASK> ? 0xffffffff81400b62 ? 0xffffffff81254da7 ? 0xffffffff81254da5 ? 0xffffffff81254e53 ? 0xffffffff8103d2ee ? 0xffffffff8103d3b9 ? 0xffffffff81252120 ? 0xffffffff81acd965 ? 0xffffffff81acde90 ? 0xffffffff81000132 </TASK> handlers: [<000000007fd7b76b>] 0xffffffff81154b43 Disabling IRQ #14 ``` ::: ## Ref - [KVM ppt](https://docs.google.com/presentation/d/1uWZO4_NVt3z76j-_pmXYf9cQwS-0WoyfbqkTb2B-5Oc/edit#slide=id.g2592c2f93cb_0_538) - [Using the KVM API](https://lwn.net/Articles/658511/) - [KVM Document](https://www.kernel.org/doc/Documentation/virtual/kvm/api.txt) - [virt-net](https://tinylab.org/virtio-vring/) - [virtqueue](https://nxw.name/2023/virtqueue) - [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) - [github ref](https://github.com/sysprog21/kvm-host/compare/master...fewletter:kvm-host:master) - [virtio-net 基礎篇](https://www.51cto.com/article/660499.html)