執行人: jimmylu890303
專題解說錄影
Google 簡報連結
ollieni
請問在你的測試中可能會有大量連線的狀況嗎? 若是有的話,會不會出現甚麼問題? 以及要如何優化?
目前測試中沒有包含大量網路流量的測試,只有包含基本的傳送封包,但若有大量網路流量的問題,可以會導致 virtqueue 沒有 available 的狀態,可能會導致延遲的問題,因為要等待 available virtqueue 才能繼續動作,所以在 Virtio 規範中,它是定義 virtqueue 至少為一對,要應付這種狀況就要實作多對的 virtqueue 來解決。可參照規格書的 5.1.2 小節
Image Not Showing Possible ReasonsLearn More →
- The image was uploaded to a note which you don't have access to
- The note which the image was originally uploaded to has been deleted
nosba0957
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 裝置舉例來說,它會分成傳送和接收兩條路線
- Host OS 傳送到 Guest OS
虛擬裝置會從 Available Ring 找出可用的緩衝區(在 split virtq 中是在 Available Ring 找 idx ,用此 idx 可在 Descriptor Area 找到緩衝區的資訊),而虛擬裝置會將資料寫進該緩衝區,之後將這個緩衝區資訊記錄在 Used Ring 中。- Guest OS 傳送到 Host OS
Guest OS 中的驅動程式會將要傳送的資料寫入到一個緩衝區中,再將此緩衝區記錄到 Available Ring ,則虛擬裝置會從 Available Ring 取出該緩衝區的資料,在進一步進行處理。
已修正,註冊 PCI 的部分可參照此處實作,對於一個 PCI 裝置而言,都會有一個 256 bytes 的 Configuration Space ,在此 space 中要根據裝置的類型去填入對應的值,這樣才能使作業系統在探查 PCI 裝置時,才能明確知道裝置的類型並且正確載入對應的驅動程式。
aa860630
對於 Guest OS 而言,要如何通知 virtio-net 裝置進行傳送,並且 Host OS 要如何對 Guest OS 進行通知要進行接收?
- 在驅動程式通知裝置會使用 Virtio-PCI 裝置的 notify 區域,而裝置通知驅動則是使用 IRQ 。
- 對於 Guest OS 通知裝置是透過驅動程式會對 Virtio-PCI 裝置的 notify 區域進行寫入(可在此處參考),所以我的實作中 tx_thread 會監控此區域是否有寫入事件,當有寫入事件,則直接呼叫 virtqueue 中的 callback 將資料寫入到 TAP 裝置
- rx_thread 會監控 TAP 裝置是否有寫入事件,有寫入事件代表 Guest 要接收,所以有寫入事件發生時, rx_thread 會呼叫 rx_virtqueue 的 callback 將資料寫入,之後再使用 IRQ 通知 Guest 。
在 KVM 的基礎之上,建構精簡且得以運作 Linux 核心的虛擬機器,使其藉由 VirtIO 具備電腦網路連線能力。
參考資訊:
Hypervisor 是作業系統與硬體之間的中間層,這允許多個作業系統可以作為獨立的 virtual machine(VM),運行於一個實體的電腦之上。Hypervisor 則管理硬體資源使這些 VM 可以共享之。它會在邏輯上將 VM 彼此分開,然後為每個 VM 指派本身的一部分基礎運算處理能力、記憶體和儲存容量,防止 VM 之間相互干擾。
Hypervisor 可以分為兩大類型,其一是 type-1 hypervisor,其直接運行在硬體之上,如下圖是比較經典的設計。它的優點是效率很高,因為可以直接存取硬體。這也增加了安全和穩定性,因為 type-1 hypervisor 和 CPU 之間不存在額外的作業系統層,因此較為單純而不容易被介入。
Type-2 hypervisor 則不直接在硬體上執行,而是作為應用程式執行在主作業系統(host)環境上執行,如下圖所展示的。因為 type-2 hypervisor 必須透過 host 作業系統存取資源,因而會引發延遲問題而相對 type-1 效能較差。
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 呼叫 :
再回顧上述 type2 的 Hypervisor, KVM 是直接執行在 Linux Kernel space,因此可以有效地減少延遲的問題,所以這也是為什麼 KVM 可以成功的原因,在許多知名的大公司皆有採用 KVM 實作的專案如 Cloud-Hypervisor 等。
而 KVM 提供的 API 可在 Using the KVM API 找到。
sysprog21/kvm-host 展示一個使用 Linux 的 kernel-based virtual machine,達成可載入 Linux 核心的系統級虛擬機器 (system virtual machine)的極小化實作。
以下解析程式碼的行為:
以下為專案中虛擬機器的實作
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;
而啟動虛擬機器的流程如下:
vm_init(&vm)
: 初始化虛擬機器, CPU 虛擬化及記憶體虛擬化vm_load_image(&vm, kernel_file)
: 載入 Linux 核心vm_load_initrd(&vm, initrd_file)
: 載入 initramfsvm_load_diskimg(&vm, diskimg_file)
: 指定 disk image 並初始化 virtio-blkvm_late_init(&vm)
(直接在 ISA 專屬程式部份實作): 執行虛擬機器前的最終初始化vm_run(&vm)
: 開始執行虛擬機器vm_exit(&vm)
: 虛擬機器結束、資源釋放vm_init
首先需要開啟 /dev/kvm
取得 kvm file descriptor
if ((v->kvm_fd = open("/dev/kvm", O_RDWR)) < 0)
return throw_err("Failed to open /dev/kvm");
呼叫 ioctl 對 kvm_fd(KVM_CREATE_VM
) 操作建立虛擬機器
if ((v->vm_fd = ioctl(v->kvm_fd, KVM_CREATE_VM, 0)) < 0)
return throw_err("Failed to create vm");
使用 vm_arch_init
根據不同的平台做初始化,以 x86 為例
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;
}
KVM_SET_TSS_ADDR
: 定義 3 個 page 的 physical address 範圍以設定 Task State SegmentKVM_SET_IDENTITY_MAP_ADDR
: 定義 1 個 page 的 physical address 範圍以設定 identity map (page table)KVM_CREATE_IRQCHIP
: 建立虛擬的 PICKVM_CREATE_PIT2
: 建立虛擬的 PIT設定 vm 中的記憶體
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, ®ion) < 0)
return throw_err("Failed to set user memory region");
KVM_SET_USER_MEMORY_REGION
: 為 VM 建立記憶體,將 guest physical memory 通過 host OS 的一段在 virtual 連續的空間(在 host 的 physical 不一定連續)來進行模擬
KVM_CREATE_VCPU
: 為 VM 建立 CPU
if ((v->vcpu_fd = ioctl(v->vm_fd, KVM_CREATE_VCPU, 0)) < 0)
return throw_err("Failed to create vcpu");
Virtio 是 IO 請求溝通的標準,架構如下圖所示,有一個前端和後端,前端通常作為驅動存在被 Guest OS 使用,後端則是在 Guest OS 被視為裝置的一種,後端可以是軟體模擬出來的裝置也可以是支援 Virtio 的實體裝置。
前後端使用 Virtqueue
作為資料交換的機制
一個 virtIO 的設備需要有以下結構
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 ,來達成與上述的功能。
通常會依照以下步驟初始化裝置
若裝置運作的過程遇到錯誤,會主動設置 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 結構如下
struct virtq_desc {
le64 addr;
le32 len;
le16 flags;
le16 next; // for "Chained descriptors"
};
在 flags 中的 0x2 bit 被設置,代表該設備只能寫,若為 0 則代表只能讀
這是 driver 要給 device 所使用的 buffers,在此處放置緩衝區並不意味設備需要立即使用,如 virtio-net 提供了一堆用於封包接收的描述符,這些描述符僅在封包到達時由設備使用,並且在那一刻之前為準備使用。
avail ring 結構如下
struct virtq_avail {
le16 flags;
le16 idx;
le16 ring[ /* Queue Size */ ];
le16 used_event;
};
idx 和 flag 只能由驅動程式寫入,設備只能讀取
在 flag 中的 NEXT (0x1) 位元設定 1 ,代表這個 buffer 為 Chained buffer,再透過 Next 去找下個 Chained buffer。
設備會將已用的 buffer 返回給 driver,結構跟 avail ring 一樣,但是有額外一個結構,紀錄使用的 buffer 使用多少長度。
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;
};
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 也可以拿到該資料。
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 裝置進行互動。
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
kvm-host 實作週邊裝置是使用 PCI 的方法,所以我們需要知道一些關於 PCI 的先備知識
在 PCI 裝置中,都會有一個 256 bytes 的 Configuration Space ,此空間用於驅動的初始化及配置,並且作業系統可以透過訪問該 Configuration Space 得知裝置的類型
Configuration Space 結構如下圖
前 4 個 register 為 Common Header Fields
,其中
0xFFFF
為不合法值,當讀取不存在的裝置時,會返回該值例如,假設設備A 的「記憶體空間」標誌位元被設定為1,表示設備A 被分配了記憶體位址範圍[0x0000, 0xFFFF],並且允許對該範圍內的記憶體進行讀寫操作。進行讀寫操作。位址針對寫入資料的操作。
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 |
0x0
則代表 base register 為 32 位元,並且可以映射到 32 位元的記憶體位置。0x2
則代表 base register 為 64 位元,並且可以映射到 64 位元的記憶體位置。0x1
則代表 base register 為 16 位元,並且可以映射到 16 位元的記憶體位置。(早期版本)所以對於一個作業系統中,要正確的識別出裝置是使用 vendor ID, device ID, class code ,這會被定義在 PCI 規格中,正確的識別之後,作業系統才能去載入匹配的驅動程式
virtio_net_dev
結構如下,它有兩個 virtqueue
#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 的結構中。
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
#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 初始化完成。
從以下 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)
在 RX Virtq 和 TX Virtq 的 virtq_ops 會有不同工作行為,當 Guest OS virtio-net driver 對 Virtqueue 讀寫時,會觸發 complete_request
的 callback function ,所以要有不同的實作。
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,
};
以 RX Virtq 來說,virtio-net 裝置會從 Available Desc 中讀取一個可用的緩衝區,並且再從 TAP 裝置讀取從 Host OS 傳入的資料,寫入到該 Available Desc 所指向的緩衝區,更新該 Available Desc 為 Used Desc,並且通知 Guest OS
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。
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;
}
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 中查看
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;
};
在 virtio_net_enable_vq_rx
中會建立一個 rx_thread,他會去監聽 TAP device 有沒有寫入事件,若有事件發生則呼叫 virtq_handle_avail
執行它的 complete_request
callback
// 監控 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
// 監控 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);
}
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
測試影片如下
Learn More →