TODO: 研讀 KVM: Linux 虛擬化基礎建設, 打造以 KVM 為基礎的精簡虛擬機器管理程式, 2022 年報告 和 2023 年報告,以理解 KVM 和 VirtIO 原理,針對電腦網路和 2D 裝置模擬提出方案
TODO: 將 semu 的部分 VirtIO 成果整合到 kvm-host
在這次專題中,將介紹 KVM 運作原理,並展示一個以 KVM 為基礎的精簡虛擬機器管理程式實作( kvm-host ),它能在 x86-64 及 arm64 平台上運作,提供了基本的 VirtIO 儲存裝置、 serial 裝置 (用來作為 console) ,並且能成功在上面執行 Linux 系統,而目標是在該虛擬機器上實作 Virtio-net 裝置。
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 呼叫 :
Virtio 是 IO 請求溝通的標準,架構如下圖所示,有一個前端和後端,前端通常作為驅動存在被 Guest OS 使用,後端則是在 Guest OS 被視為裝置的一種,後端可以是軟體模擬出來的裝置也可以是支援 Virtio 的實體裝置。
以 VirtIO 實現的裝置來說,前端將 IO 請求傳給後端,後端會將請求傳給實際的裝置,等 IO 處理完成後傳回給前端,後端的這過程也就是裝置的模擬。
前後端使用 Virtqueue
作為資料交換的機制
一個 virtIO 的設備需要有以下結構
Device status field 是一連串的位元,每個位元代表不同的意義,用於設備和驅動程式執行初始化的狀態,設置的可以是 Guest 或者是 Driver,所以我們可透過 status field 查看裝置狀態。
通常會依照以下步驟初始化裝置
若裝置運作的過程遇到錯誤,會主動設置 DEVICE_NEEDS_RESET (64)
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
來告知資訊來溝通,但傳輸的方式是 transport specific
。
對於 Virtio-pci 驅動到裝置的通知由寫入特定的記憶體區域觸發 vm-exit 完成,裝置到驅動的通知則由 interrupt 完成。
Virtqueue 是 guest 申請的一塊記憶體區域,共享給 host 的記憶體區塊。
一個 virtqueue 實際上就是一個存放了 guest 和 host 之間數據交換的緩衝區的佇列。guest 可以將需要發送給 host 的資料放入這些 buffers 中,host 則可以讀取這些 buffers 中的數據進行處理,或者將自己的數據寫入這些 buffers 中傳送給 guest。
目前 Virtio 1.1 有以下兩種 Virtqueue ,而在 KVM-Host 目前實作的是 Packed Virtqueues
Descriptor area 包含了許多 guest 的 buffers 還有他們的長度。
單個 descriptor 結構如下
在 flags 中的 0x2 bit 被設置,代表該設備只能寫,若為 0 則代表只能讀
這是 driver 要給 device 所使用的 buffers,在此處放置緩衝區並不意味設備需要立即使用,如 virtio-net 提供了一堆用於封包接收的描述符,這些描述符僅在封包到達時由設備使用,並且在那一刻之前為準備使用。
avail ring 結構如下
idx 和 flag 只能由驅動程式寫入,設備只能讀取
初始時, descriptor area 中有一個 buffer 長度為 2000 bytes,起始位置為 0x8000,而在 Avail area 中目前沒有可用的 buffer
將 buffer 加入到 avail area, ring[0] 存 descriptor area 中 buffer index,idx 指向下個 next descriptor entry(ring[1])
在 flag 中的 NEXT (0x1) 位元設定 1 ,代表這個 buffer 為 Chained buffer,再透過 Next 去找下個 Chained buffer。
設備會將已用的 buffer 返回給 driver,結構跟 avail ring 一樣,但是有額外一個結構,紀錄使用的 buffer 使用多少長度。
0x3000 為寫入總長度
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)
如果要讓 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 簡易流程如上圖
下圖為 Qemu 中 virtio-net 的架構圖
虛擬機中的前端為 Guest OS,後端為 Qemu process ,後端可以想成為 VMM ,負責模擬裝置。
前端及後端的資料交換方式(溝通)是採用兩個 Virtqueue
virtio-net device 和 Host OS 資料交換透過 TAP 裝置,TAP\TUN 為 linux 中的虛擬網路裝置
TUN 工作在 IP 層,無法與真實網卡作 bridge
TAP 工作在 MAC 層,可與真實網卡作 bridge
當 Userspace 程式往字符設備 /dev/net/tun 寫入資料時,寫入的資料會出現在 TUN 裝置上。當內核向 TUN 裝置發送資料時,通過讀取這個字符設備 /dev/net/tun 也可以拿到該資料。
一個 PCI 架構如下, Host bridge 負責連接 CPU 和管理所有的 PCI 裝置及 Bus 。一個 PCI 邏輯裝置提供 256 bytes 的 Configuration Space ,用以完成裝置的設定與初始化, CPU 不能直接存取這個空間,需要透過 PCI 的 Host Bridge 提供特殊的機制,讓 CPU 完成配置空間的存取。
PCI 配置空間的前 64 bytes 是每個裝置共通的
0xFFFF
為不合法值,當讀取不存在的裝置時,會返回該值,Virtio 為 0x1AF4
。virtio_net_dev
結構如下,它有兩個 virtqueue
在 vm 啟動前呼叫 virtio_net_init
,並取得 tap 的 file descriptor ,存入到 virtio-net 的結構中。
接下來需要將 virtio_net dev 註冊到 pci 上,其中 virtio-net 的pci configuration 為 device id
= 0x1041
及 class code
為 0x020000
(Ethernet controller),可參造 PCI
從以下 dmesg 中可以看出 virtio_net dev(0000:00:01.0: [1af4:1000]) 以被註冊到 pci 上,但它卻無法被正確識別成 virtio_net device,如 virtio_blk 那樣
發現 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
才能正確加入
現在 bit 1~3 皆為設定為 1 ,代表 driver 初始化完成。
在 RX Virtq 和 TX Virtq 的 virtq_ops 會有不同工作行為,當 Guest OS virtio-net driver 對 Virtqueue 讀寫時,會觸發 complete_request
的 callback function ,所以要有不同的實作。
以 RX Virtq 來說,virtio-net 裝置會從 Available Desc 中讀取一個可用的緩衝區,並且再從 TAP 裝置讀取從 Host OS 傳入的資料,寫入到該 Available Desc 所指向的緩衝區,更新該 Available Desc 為 Used Desc,並且通知 Guest OS
以 TX Virtq 來說,virtio-net 裝置會從 Available Desc 中讀取一個可用的緩衝區( virtio-net driver 會將要傳送的資料寫入到 Avail Desc 中),並且讀取該緩衝區的資料再將該資料寫入到 TAP 裝置,更新該 Available Desc 為 Used Desc,並且通知 Guest OS。
此外要注意的事情是不管是從 Virtqueue 讀取資料或寫入資料,資料前面都應該要包含 Virtio_header
的區塊,格式如下
在 virtio_net_enable_vq_rx
中會建立一個 rx_thread,他會去監聽 TAP device 有沒有寫入事件,若有事件發生則呼叫 virtq_handle_avail
執行它的 complete_request
callback
在 virtio_net_enable_vq_tx
中會建立一個 tx_thread,他會去監聽 TX Virtq 有沒有寫入事件,若有事件發生則呼叫 virtq_handle_avail
執行它的 complete_request
callback
在 Host OS 執行以下指令
在 Guest OS 執行以下指令
測試影片如下
不知道為何會噴出以下錯誤訊息,但是它不影響運作