實戰(zhàn)篇:在QEMU中編寫和調試VHost/Virtio驅動
在云計算環(huán)境中,當多個虛擬機同時進行大規(guī)模數(shù)據(jù)傳輸時,這種通信瓶頸就會變得尤為明顯,嚴重影響了云服務的性能和用戶體驗。同樣,在大數(shù)據(jù)分析場景下,虛擬機需要頻繁地讀寫存儲設備,如果虛擬設備通信效率低下,就會導致數(shù)據(jù)分析的速度大幅下降,無法滿足實時性的要求。
那么,有沒有一種方法能夠打破這種通信困境,讓虛擬設備之間的通信更加高效、流暢呢?答案就是 vhost/virtio 技術。它就像是虛擬世界中的通信加速器,為解決虛擬設備通信難題帶來了新的曙光。接下來,就讓我們深入了解 vhost/virtio 技術的奧秘。
一、手寫 Vhost/Virtio
1.1 前期準備:搭建 “舞臺”
在開始這場奇妙的手寫 vhost/virtio 之旅前,我們首先需要搭建一個合適的開發(fā)環(huán)境,就如同搭建一個穩(wěn)固的舞臺,為后續(xù)的精彩表演做好充分準備。
我們要安裝 Qemu,它可是虛擬化的基石。安裝 Qemu 的方式有多種,對于追求便捷的開發(fā)者來說,可以使用系統(tǒng)自帶的包管理器,比如在 Ubuntu 系統(tǒng)中,只需在終端輸入 “sudo apt - get install qemu - system - x86” 即可輕松完成安裝。但如果你想要更前沿的功能和性能優(yōu)化,從源代碼編譯安裝則是個不錯的選擇。你可以從 Qemu 的官方網(wǎng)站下載最新的源代碼,然后按照官方文檔的指引進行編譯和安裝 ,雖然這個過程可能稍微復雜一些,但能讓你獲得最適合自己需求的 Qemu 版本。
除了 Qemu,我們還需要一系列相關的開發(fā)工具和庫文件。比如,GCC(GNU Compiler Collection)是必不可少的,它能將我們編寫的 C 代碼編譯成可執(zhí)行的程序。安裝 GCC 也很簡單,在大多數(shù) Linux 系統(tǒng)中,通過包管理器就能快速完成安裝。另外,還需要安裝一些開發(fā)庫,如 libvirt 開發(fā)庫,它提供了與虛擬化管理相關的接口,方便我們在代碼中對虛擬機進行各種操作;以及 libpciaccess 庫,它有助于我們訪問 PCI 設備,在處理虛擬設備相關的功能時發(fā)揮著重要作用。在安裝這些庫文件時,一定要注意它們的版本兼容性,不同版本之間可能存在接口差異,不兼容的版本可能會導致后續(xù)開發(fā)過程中出現(xiàn)各種難以調試的問題。
1.2 初窺門徑:理解關鍵數(shù)據(jù)結構
當我們搭建好開發(fā)環(huán)境后,就如同踏入了一座神秘的城堡,首先要熟悉城堡中的各種機關和暗道,也就是 vhost/virtio 中的關鍵數(shù)據(jù)結構。
在 vhost/virtio 的世界里,virtio_ring 數(shù)據(jù)隊列是最為核心的數(shù)據(jù)結構之一,它就像是一座橋梁,連接著虛擬機(Guest)和宿主機(Host)之間的數(shù)據(jù)傳輸通道。virtio_ring 主要由描述符表(descriptor table)、可用環(huán)表(available ring)和已用環(huán)表(used ring)三部分組成。描述符表就像是一個貨物清單,里面存放著真正的數(shù)據(jù)報文信息,每個描述符都記錄了數(shù)據(jù)的起始地址、長度以及一些標志位等關鍵信息 ,這些信息就像是貨物的標簽,告訴接收方如何正確地處理這些數(shù)據(jù)。
可用環(huán)表則是 Guest 用來告知 Host 有哪些數(shù)據(jù)是可供處理的,它就像是一個待處理任務列表,Guest 將數(shù)據(jù)描述符的索引放入可用環(huán)表中,Host 從這里獲取任務并進行處理。已用環(huán)表則是 Host 用來通知 Guest 哪些數(shù)據(jù)已經(jīng)處理完成,Guest 可以回收相應的資源,就像是完成任務后的反饋清單。
在網(wǎng)絡通信場景中,當 Guest 要發(fā)送網(wǎng)絡數(shù)據(jù)包時,它會先將數(shù)據(jù)包的相關信息填充到描述符表中,然后將描述符的索引添加到可用環(huán)表中,Host 檢測到可用環(huán)表有新的任務后,就會從描述符表中獲取數(shù)據(jù)包并進行發(fā)送處理,處理完成后,將描述符的索引放入已用環(huán)表中,Guest 看到已用環(huán)表的反饋后,就知道哪些數(shù)據(jù)包已經(jīng)成功發(fā)送,可以進行后續(xù)的操作了。理解這些數(shù)據(jù)結構的工作原理和相互之間的關系,是我們手寫 vhost/virtio 的關鍵,只有掌握了它們,我們才能在后續(xù)的代碼實現(xiàn)中得心應手。
1.3 核心代碼實現(xiàn):構建 “通信橋梁”
①創(chuàng)建共享內存
共享內存是 vhost/virtio 實現(xiàn)高效通信的基礎,它就像是一個公共的倉庫,Guest 和 Host 都可以直接訪問,從而避免了數(shù)據(jù)的多次拷貝,大大提高了通信效率。
在創(chuàng)建共享內存時,我們可以使用操作系統(tǒng)提供的相關函數(shù),比如在 Linux 系統(tǒng)中,可以使用 shmget 函數(shù)來創(chuàng)建共享內存段。首先,我們需要定義共享內存的大小和一些權限標志 ,然后調用 shmget 函數(shù),它會返回一個共享內存標識符,這個標識符就像是倉庫的鑰匙,后續(xù)我們對共享內存的操作都需要使用這個標識符。例如:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#define SHM_SIZE 1024 * 1024 // 共享內存大小為1MB
int main() {
key_t key;
int shmid;
// 生成一個唯一的鍵值
key = ftok(".", 'a');
if (key == -1) {
perror("ftok");
return 1;
}
// 創(chuàng)建共享內存段
shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget");
return 1;
}
printf("共享內存創(chuàng)建成功,標識符為: %d\n", shmid);
// 后續(xù)可以使用shmid進行共享內存的操作
//...
// 最后刪除共享內存段
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl");
return 1;
}
return 0;
}
創(chuàng)建好共享內存后,還需要將其映射到進程的地址空間中,這樣我們的代碼才能直接訪問共享內存中的數(shù)據(jù)。在 Linux 中,可以使用 shmat 函數(shù)來完成映射操作。映射完成后,就可以像訪問普通內存一樣對共享內存進行讀寫操作了。同時,我們還需要注意在適當?shù)臅r候通知內核或者其他進程共享內存的相關信息,比如通過信號量或者其他同步機制,確保各個進程對共享內存的訪問是安全和有序的。
②初始化 Virtio Ring
Virtio Ring 的初始化是確保數(shù)據(jù)能夠正確傳輸?shù)年P鍵步驟,它就像是為橋梁設置正確的交通規(guī)則,讓數(shù)據(jù)能夠在 Guest 和 Host 之間順暢地流動。
初始化 Virtio Ring 時,我們需要設置描述符、可用和已用索引等關鍵參數(shù)。首先,我們要為描述符表分配內存空間,并初始化每個描述符的內容,包括數(shù)據(jù)的起始地址、長度和標志位等。例如:
#include <stdint.h>
#define QUEUE_SIZE 256
// Virtio描述符結構
typedef struct {
uint64_t addr;
uint32_t len;
uint16_t flags;
uint16_t next;
} virtio_desc;
// Virtio可用環(huán)結構
typedef struct {
uint16_t flags;
uint16_t idx;
uint16_t ring[QUEUE_SIZE];
} virtio_avail;
// Virtio已用環(huán)結構
typedef struct {
uint16_t flags;
uint16_t idx;
struct {
uint32_t id;
uint32_t len;
} ring[QUEUE_SIZE];
} virtio_used;
// Virtio Ring結構
typedef struct {
virtio_desc *desc;
virtio_avail *avail;
virtio_used *used;
} virtio_ring;
// 初始化Virtio Ring
void init_virtio_ring(virtio_ring *ring) {
// 分配描述符表內存
ring->desc = (virtio_desc *)malloc(QUEUE_SIZE * sizeof(virtio_desc));
if (ring->desc == NULL) {
// 處理內存分配失敗的情況
return;
}
// 分配可用環(huán)內存
ring->avail = (virtio_avail *)malloc(sizeof(virtio_avail));
if (ring->avail == NULL) {
free(ring->desc);
return;
}
// 分配已用環(huán)內存
ring->used = (virtio_used *)malloc(sizeof(virtio_used));
if (ring->used == NULL) {
free(ring->desc);
free(ring->avail);
return;
}
// 初始化描述符
for (int i = 0; i < QUEUE_SIZE; i++) {
ring->desc[i].addr = 0;
ring->desc[i].len = 0;
ring->desc[i].flags = 0;
ring->desc[i].next = i + 1;
}
ring->desc[QUEUE_SIZE - 1].next = 0;
// 初始化可用環(huán)
ring->avail->flags = 0;
ring->avail->idx = 0;
// 初始化已用環(huán)
ring->used->flags = 0;
ring->used->idx = 0;
}
在上述代碼中,我們首先定義了 Virtio Ring 相關的結構,然后實現(xiàn)了一個初始化函數(shù) init_virtio_ring。在函數(shù)中,我們?yōu)槊枋龇怼⒖捎铆h(huán)和已用環(huán)分配內存,并對它們進行初始化。描述符的 next 字段形成了一個環(huán)形鏈表,方便數(shù)據(jù)的管理和訪問。可用環(huán)和已用環(huán)的 idx 字段初始化為 0,表示當前沒有數(shù)據(jù)待處理或已處理。通過這樣的初始化操作,Virtio Ring 就可以準備好進行數(shù)據(jù)的收發(fā)工作了。
③數(shù)據(jù)收發(fā)處理
數(shù)據(jù)的發(fā)送和接收是 vhost/virtio的核心功能,它就像是橋梁上車輛的行駛,實現(xiàn)了Guest 和Host之間的信息交互。
當 Guest 要發(fā)送數(shù)據(jù)時,它會首先填充數(shù)據(jù)到共享內存中,并將數(shù)據(jù)的相關信息(如數(shù)據(jù)長度、內存地址等)填充到 Virtio Ring 的描述符中。然后,Guest 將描述符的索引添加到可用環(huán)表中,并更新可用環(huán)表的 idx 索引,通知 Host 有新的數(shù)據(jù)需要處理。例如:
// Guest發(fā)送數(shù)據(jù)
void guest_send_data(virtio_ring *ring, const void *data, size_t len) {
uint16_t desc_idx = ring->avail->idx;
// 獲取一個可用的描述符
virtio_desc *desc = &ring->desc[desc_idx];
// 設置描述符的地址和長度
desc->addr = (uint64_t)data;
desc->len = len;
desc->flags = 0;
// 將描述符索引添加到可用環(huán)表中
ring->avail->ring[ring->avail->idx % QUEUE_SIZE] = desc_idx;
// 更新可用環(huán)表的idx索引
ring->avail->idx++;
// 通知Host有新數(shù)據(jù)
// 這里可以通過中斷或者其他機制通知Host
}
在發(fā)送數(shù)據(jù)的過程中,我們需要注意可用環(huán)表的索引管理,確保不會發(fā)生溢出。同時,要及時通知 Host 有新的數(shù)據(jù)到來,以便 Host 能夠及時處理。
當 Host 接收到 Guest 的通知后,它會從可用環(huán)表中獲取描述符的索引,然后根據(jù)索引從描述符表中獲取數(shù)據(jù)的相關信息,并從共享內存中讀取數(shù)據(jù)進行處理。處理完成后,Host 將描述符的索引添加到已用環(huán)表中,并更新已用環(huán)表的 idx 索引,通知 Guest 數(shù)據(jù)已經(jīng)處理完成。例如:
// Host接收數(shù)據(jù)
void host_receive_data(virtio_ring *ring) {
uint16_t used_idx = ring->used->idx;
while (used_idx < ring->avail->idx) {
uint16_t desc_idx = ring->avail->ring[used_idx % QUEUE_SIZE];
virtio_desc *desc = &ring->desc[desc_idx];
// 從共享內存中讀取數(shù)據(jù)并處理
// 這里省略具體的數(shù)據(jù)處理邏輯
// 將描述符索引添加到已用環(huán)表中
ring->used->ring[ring->used->idx % QUEUE_SIZE].id = desc_idx;
ring->used->ring[ring->used->idx % QUEUE_SIZE].len = desc->len;
// 更新已用環(huán)表的idx索引
ring->used->idx++;
}
// 通知Guest數(shù)據(jù)已處理完成
// 這里可以通過中斷或者其他機制通知Guest
}
在接收數(shù)據(jù)的過程中,Host 需要不斷地檢查可用環(huán)表和已用環(huán)表的索引,確保能夠及時處理新的數(shù)據(jù),并將處理結果反饋給 Guest。同時,也要注意已用環(huán)表的索引管理,避免出現(xiàn)錯誤。
④中斷處理機制
中斷處理在 vhost/virtio 中起著至關重要的作用,它就像是橋梁上的交通信號燈,能夠及時通知對方有重要事件發(fā)生,從而實現(xiàn)高效的通信。
在 vhost/virtio 中,中斷主要用于 Guest 通知 Host 有數(shù)據(jù)待處理,或者 Host 通知 Guest 數(shù)據(jù)已經(jīng)處理完成。當 Guest 填充數(shù)據(jù)到共享內存并更新 Virtio Ring 后,它可以通過觸發(fā)中斷來通知 Host。在 Linux 系統(tǒng)中,可以使用 eventfd 來實現(xiàn)中斷通知機制。
首先,Guest 創(chuàng)建一個 eventfd 對象,并將其與 Virtio Ring 的中斷關聯(lián)起來。當 Guest 需要通知 Host 時,它向 eventfd 對象寫入一個值,這個值會觸發(fā) Host 的中斷處理程序。例如:
#include <sys/eventfd.h>
#include <unistd.h>
// Guest觸發(fā)中斷通知Host
void guest_notify_host(int eventfd) {
uint64_t value = 1;
if (write(eventfd, &value, sizeof(value)) == -1) {
perror("write eventfd");
}
}
Host 在啟動時,會監(jiān)聽這個 eventfd 對象,當接收到中斷信號時,它會調用相應的中斷處理函數(shù)來處理數(shù)據(jù)。例如:
#include <sys/eventfd.h>
#include <poll.h>
// Host監(jiān)聽中斷
void host_listen_interrupt(int eventfd, virtio_ring *ring) {
struct pollfd fds[1];
fds[0].fd = eventfd;
fds[0].events = POLLIN;
while (1) {
int ret = poll(fds, 1, -1);
if (ret == -1) {
perror("poll");
break;
} else if (ret > 0 && (fds[0].revents & POLLIN)) {
uint64_t value;
if (read(eventfd, &value, sizeof(value)) == -1) {
perror("read eventfd");
continue;
}
// 處理數(shù)據(jù)
host_receive_data(ring);
}
}
}
在上述代碼中,Guest 通過 write 函數(shù)向 eventfd 對象寫入值來觸發(fā)中斷,Host 使用 poll 函數(shù)監(jiān)聽 eventfd 對象,當接收到 POLLIN 事件時,說明有中斷發(fā)生,然后調用 host_receive_data 函數(shù)處理數(shù)據(jù)。通過這樣的中斷處理機制,Guest 和 Host 可以實現(xiàn)高效的數(shù)據(jù)交互,避免了不必要的輪詢操作,提高了系統(tǒng)的性能和響應速度。
二、QEMU后端驅動
VIRTIO設備的前端是GUEST的內核驅動,后端由QEMU或者DPU實現(xiàn)。不論是原來的QEMU-VIRTIO框架還是現(xiàn)在的DPU,VIRTIO的控制面和數(shù)據(jù)面都是相對獨立的設計。本文主要針對QEMU的VIRTIO后端進行分析。
控制面負責GUEST前端和VIRTIO設備的協(xié)商流程,主要用于前后端的feature協(xié)商匹配、向GUEST提供后端設備信息、建立前后端之間的數(shù)據(jù)通道。等控制面協(xié)商完成后,數(shù)據(jù)面啟動前后端的數(shù)據(jù)交互流程;后面的流程中控制面負責一些配置信息的下發(fā)和通知,比如block設備capacity配置、net設備mac地址動態(tài)修改等。
QEMU負責設備控制面的實現(xiàn),而數(shù)據(jù)面由VHOST框架接管。VHOST又分為用戶態(tài)的vhost-user和內核態(tài)的vhost-kernel路徑,前者是由用戶態(tài)的dpdk接管數(shù)據(jù)路徑,將數(shù)據(jù)從用戶態(tài)OVS協(xié)議棧轉發(fā),后者是由內核態(tài)的vhost驅動接管數(shù)據(jù)路徑,將數(shù)據(jù)從內核協(xié)議棧發(fā)送出去。本文主要針對vhost-user路徑,以net設備為例進行描述。
如果要順利的看懂QEMU后端VIRTIO驅動框架,需要具備QEMU的QOM基礎知識,在這個基礎上將數(shù)據(jù)結構、初始化流程理清楚,就可以更快的上手。如果只是對VIRTIO相關的設計感興趣,可直接看下一章原理性的內容。
QEMU設備管理是非常重要的部分,后來引入了專門的設備樹管理機制。而其參照了C++的類、繼承的一些概念,但又不是完全一致,對于非科班出身的作者閱讀起來有些吃力。因為框架相關的代碼中時常使用內部數(shù)據(jù)指針cast的一些宏定義,非常影響可讀性。
2.1 VIRTIO設備創(chuàng)建流程
從實際的命令行示例入手,查看設備是如何創(chuàng)建的。
(1)virtio-net-pci設備命令行
首先從QEMU的命令行入手,創(chuàng)建一個使用virtio設備的虛擬機,可使用如下命令行:
gdb --args ./x86_64-softmmu/qemu-system-x86_64 \
-machine accel=kvm -cpu host -smp sockets=2,cores=2,threads=1 -m 3072M \
-object memory-backend-file,id=mem,size=3072M,mem-path=/dev/hugepages,share=on \
-hda /home/kvm/disk/vm0.img -mem-prealloc -numa node,memdev=mem \
-vnc 0.0.0.0:00 -monitor stdio --enable-kvm \
-netdev type=tap,id=eth0,ifname=tap30,script=no,downscript=no
-device e1000,netdev=eth0,mac=12:03:04:05:06:08 \
-chardev socket,id=char1,path=/tmp/vhostsock0,server \
-netdev type=vhost-user,id=mynet3,chardev=char1,vhostforce,queues=$QNUM
-device virtio-net-pci,netdev=mynet3,id=net1,mac=00:00:00:00:00:03,disable-legacy=on
其中,創(chuàng)建一個虛擬硬件設備,都是通過-device來實現(xiàn)的,上面的命令行中創(chuàng)建了一個virtio-net-pci設備
-device virtio-net-pci,netdev=mynet3,id=net1,mac=00:00:00:00:00:03,disable-legacy=on
這個硬件設備的構造依賴于qemu框架里的netdev設備(并不會獨立的對guest呈現(xiàn))
-netdev type=vhost-user,id=mynet3,chardev=char1,vhostforce,queues=$QNUM
上面的netdev設備又依賴于qemu框架里的字符設備(同樣不會獨立的對guest呈現(xiàn))
-chardev socket,id=char1,path=/tmp/vhostsock0,server
(2)命令行解析處理
QEMU的命令行解析在main函數(shù)進行,解析后按照qemu標準格式存儲到本地。然后通過qemu_find_opts(“”)接口可以獲取本地結構體中具有相應關鍵字的所有命令列表,對解析后的命令列表使用qemu_opts_foreach依次執(zhí)行處理函數(shù)。
常用的用法,比如netdev的處理,qemu_find_opts找到所有的netdev的命令列表,qemu_opts_foreach則對列表里的所有元素依次執(zhí)行net_init_netdev,初始化相應的netdev結構。
int net_init_clients(Error **errp)
{
QTAILQ_INIT(&net_clients);
if (qemu_opts_foreach(qemu_find_opts("netdev"),
net_init_netdev, NULL, errp)) {
return -1;
}
return 0;
}
net_init_netdev初始化函數(shù)中,根據(jù)type=vhost-user,執(zhí)行相應的net_init_vhost_user函數(shù)進行初始化,并為每個隊列創(chuàng)建一個NetClientState結構,用于后續(xù)socket通信。對于"-device"參數(shù)的處理也是采用同樣的方式,依次執(zhí)行device_init_func,初始化相應的DeviceState結構。
if (qemu_opts_foreach(qemu_find_opts("device"),
device_init_func, NULL, NULL)) {
exit(1);
}
device后跟的第一個參數(shù)qemu稱為driver,其實就是根據(jù)不同的設備類型(我們的場景為“virtio-net-pci")匹配不同的處理。而device采用的是通用的設備類,根據(jù)驅動的名字在device_init_func函數(shù)里調用qdev_device_add()接口,然后匹配到相應的DeviceClass(就是virtio-net-pci對應的DeviceClass)。
匹配到DeviceClass后,調用class里的instance_init接口,創(chuàng)建相應的實例,即DeviceState。
備注:看到了DeviceClass和DeviceState,這個是QEMU設備管理框架里的重要元素。
1)Class后綴表示一類方法實現(xiàn),是相應設備類型的一類實現(xiàn),對于同一設備類型的多個設備是通用的,不管創(chuàng)建幾個virtio-pci-net設備,只需要一份VirtioPciClass。
2)State后綴表示具體的instance實體,每創(chuàng)建一個設備都要實例化一個instance結構。創(chuàng)建和初始化這個結構是由object_new()接口完成的,初始化還會調用相應的類定義的instance_init()接口。
Breakpoint 2, virtio_net_pci_instance_init (obj=0x5555575b8740) at hw/virtio/virtio-pci.c:3364
3364 {
(gdb) bt
#0 0x0000555555ab0c10 in virtio_net_pci_instance_init (obj=0x5555575b8740) at hw/virtio/virtio-pci.c:3364
#1 0x0000555555b270bf in object_initialize_with_type (data=data@entry=0x5555575b8740, size=<optimized out>, type=type@entry=0x5555563c3070) at qom/object.c:384
#2 0x0000555555b271e1 in object_new_with_type (type=0x5555563c3070) at qom/object.c:546
#3 0x0000555555b27385 in object_new (typename=typename@entry=0x5555563d2310 "virtio-net-pci") at qom/object.c:556
#4 0x000055555593b5c5 in qdev_device_add (opts=0x5555563d22a0, errp=errp@entry=0x7fffffffddd0) at qdev-monitor.c:625
#5 0x000055555593db17 in device_init_func (opaque=<optimized out>, opts=<optimized out>, errp=<optimized out>) at vl.c:2289
#6 0x0000555555c1ab6a in qemu_opts_foreach (list=<optimized out>, func=func@entry=0x55555593daf0 <device_init_func>, opaque=opaque@entry=0x0, errp=errp@entry=0x0) at util/qemu-option.c:1106
#7 0x00005555557d85d6 in main (argc=<optimized out>, argv=<optimized out>, envp=<optimized out>) at vl.c:4593
(3)設備實例初始化
在qdev_device_add函數(shù)中,首先會調用object_new,創(chuàng)建object(object是所有instance實例的根結構),最終是通過調用每個virtio-pci-net相應DeviceClass里的instance_init創(chuàng)建實例。
static void virtio_net_pci_instance_init(Object *obj)
{
VirtIONetPCI *dev = VIRTIO_NET_PCI(obj);
virtio_instance_init_common(obj, &dev->vdev, sizeof(dev->vdev),
TYPE_VIRTIO_NET);
object_property_add_alias(obj, "bootindex", OBJECT(&dev->vdev),
"bootindex");
}
VirtioNetPci結構體中包含其父類的實例VirtIOPCIProxy,其擁有的設備框架自定義的結構是VirtIONet的實例。對于netdev來說,它也利用了qemu的class和device框架,但netdev不像-device一樣通過框架的qdev_device_add接口調用object_new完成。他的數(shù)據(jù)空間跟隨在virtio_net_pci的自定義結構里,然后通過virtio_instance_init_com接口顯式的調用object_initialize()函數(shù)實現(xiàn)“virtio-net-device”的instance初始化。
struct VirtIONetPCI {
VirtIOPCIProxy parent_obj; //virtio-pci類<----繼承pci-device<----繼承device
VirtIONet vdev; //virtio-net<----繼承virtio-device<----繼承device
};
(4)virtio-net-pci設備realize流程
qdev_device_add接口中,還會調用realize接口,前面的instance_init只是實例的簡單初始化,真實的設備相關的具體初始化動作都是從設備realize之后進行的。也就是相應class的realize接口。
首先在qdev_device_add()接口中,置位設備的realized屬性,進而調用每一層class的realize函數(shù)。大家想一下,類似于內核驅動,設備肯定按照協(xié)議的分層從下向上識別的,先識別pci設備,然后是virtio,進而識別到virtio-net設備。所以qemu的識別過程也是這樣,從最底層的realize層層調用至上層的realize接口。
參照VirtIO的Class結構,整個realize的流程整理如下:
圖片
圖片
在初始化的過程中,對數(shù)據(jù)結構進行一一初始化。在pci設備的realize之前插入virtio_pci_dc_realize函數(shù)的原因是,如果是modern模式的pci設備必須是pci-express協(xié)議,所以需要置位PCIDevice里的pcie-capability標識,強行令pci設備為pcie類型。然后再進行pci層的設備初始化,初始化一個pcie設備。
virtio_pci_realize接口對VirtioPCIProxy數(shù)據(jù)結構進行了初始化,是virtio+pci需要的初始化。所以初始化了virtio設備的bar空間以及需要的pcie-capability。
virtio_net_pci_realize接口主要是觸發(fā)VirtIONet里的VirtIODevice的realize流程,這才是virtio設備的realize流程(virtio_device_realize接口)。
virtio_device_realize接口實現(xiàn)調用了virtio_net_device_realize,對于特定virtio設備(net類型)的初始化都是在這里進行的。所以這部分是對VirtIONet及其包裹的VirtIODevice數(shù)據(jù)結構進行初始化,包括VirtIODevice結構里的vq指針就是在這里根據(jù)隊列個數(shù)動態(tài)申請空間的。
virtio_device_realize接口還執(zhí)行了virtio_bus_device_plugged接口,這是virtio總線上的virtio設備的plugged接口,這部分內容脫離了virtio-pci框架,進入到更上層的virtio框架。但virtio_bus派生了virtio_pci_bus,virtio_pci_bus將繼承的virtio_bus的接口都設置成了自己的接口。所以最終還是調用了virtio-pci下的virtio_pci_device_plugged函數(shù)。
virtio_pci_device_plugged接口是最核心的初始化接口,modern模式初始化pci設備的bar空間讀寫操作接口,因為分多塊讀寫,所以還引入了memory_region,然后添加相應的capability;legacy模式初始化pci的bar空間讀寫操作接口,至此virtio設備的初始化流程完成,等待與host的接口操作。
2.2 VIRTIO設備實現(xiàn)
結合qemu的設備框架模型,分析了qemu從命令行到virtio設備創(chuàng)建的處理流程。現(xiàn)在設備創(chuàng)建出來了,是如何與host進行交互和操作的。本節(jié)主要講述這部分內容,明確一點,所有的數(shù)據(jù)和操作接口都會匯聚到一個結構體,設備創(chuàng)建過程中的instance實例就承載了我們這個設備的所有數(shù)據(jù)和ops,所以分析這個VirtIONetPCI結構及其衍生輻射的其他數(shù)據(jù)就可以了。
struct VirtIONetPCI {
VirtIOPCIProxy parent_obj; //VIRTIO-PCI數(shù)據(jù)
VirtIONet vdev; //VIRTIO-NET數(shù)據(jù)
};
對于VirtIOPCIProxy,選取比較重要的部分摘抄如下:
struct VirtIOPCIProxy {
PCIDevice pci_dev;
MemoryRegion bar;
struct {
VirtIOPCIRegion common;
VirtIOPCIRegion isr;
VirtIOPCIRegion device;
VirtIOPCIRegion notify;
};
MemoryRegion modern_bar;
MemoryRegion io_bar;
uint32_t msix_bar_idx;
bool disable_modern;
uint32_t nvectors;
uint32_t guest_features[2];
VirtIOPCIQueue vqs[VIRTIO_QUEUE_MAX];
VirtIOIRQFD *vector_irqfd;
};
其實控制面主要就是用于協(xié)商操作,而這部分操作是通過bar空間的讀寫實現(xiàn)的。所以對virtio-pci來說,bar空間的讀寫操作接口是主線的流程,其余的數(shù)據(jù)結構都是圍繞這組讀寫操作接口展開。
對于legacy設備,bar0用于virtio設備協(xié)商的空間。bar0在VirtIOPCIProxy中對應的是bar結構。bar0的讀寫接口對應virtio_pci_config_ops。
memory_region_init_io(&proxy->bar, OBJECT(proxy),
&virtio_pci_config_ops,
proxy, "virtio-pci", size);
static const MemoryRegionOps virtio_pci_config_ops = {
.read = virtio_pci_config_read,
.write = virtio_pci_config_write,
.impl = {
.min_access_size = 1,
.max_access_size = 4,
},
.endianness = DEVICE_LITTLE_ENDIAN,
};
對于modern設備,采用了更靈活的方式,將virtio設備協(xié)商的空間分成4個(common、isr、device、notify)區(qū)間,每個區(qū)間的bar index和bar內offset由pci設備的capability指定。VIRTIO前端驅動解析capability識別不同的區(qū)間,進而與設備同步地址區(qū)間。
memory_region_init_io(&proxy->common.mr, OBJECT(proxy),
&common_ops,
proxy,
"virtio-pci-common",
proxy->common.size);
memory_region_init_io(&proxy->isr.mr, OBJECT(proxy),
&isr_ops,
proxy,
"virtio-pci-isr",
proxy->isr.size);
memory_region_init_io(&proxy->device.mr, OBJECT(proxy),
&device_ops,
virtio_bus_get_device(&proxy->bus),
"virtio-pci-device",
proxy->device.size);
memory_region_init_io(&proxy->notify.mr, OBJECT(proxy),
?ify_ops,
virtio_bus_get_device(&proxy->bus),
"virtio-pci-notify",
proxy->notify.size);
如上述代碼所示,common區(qū)間、isr區(qū)間、device區(qū)間、notify區(qū)間的操作接口分別是common_ops、isr_ops、device_ops、notify_ops。
注冊完上述ops后,GUEST對設備的bar空間進行訪問,會進入相應的操作接口,VIRTIO控制面的初始化流程具體可以參見協(xié)議或者作者其他文章,文中主要是對代碼的實現(xiàn)框架梳理,摘抄legacy模式的write接口部分內容示例說明。后端設備根據(jù)前端驅動寫入的地址和數(shù)據(jù)進行相應的處理。
static void virtio_ioport_write(void *opaque, uint32_t addr, uint32_t val)
{
VirtIOPCIProxy *proxy = opaque;
VirtIODevice *vdev = virtio_bus_get_device(&proxy->bus);
hwaddr pa;
switch (addr) {
case VIRTIO_PCI_GUEST_FEATURES:
/* Guest does not negotiate properly? We have to assume nothing. */
if (val & (1 << VIRTIO_F_BAD_FEATURE)) {
val = virtio_bus_get_vdev_bad_features(&proxy->bus);
}
virtio_set_features(vdev, val);
break;
case VIRTIO_PCI_QUEUE_PFN:
pa = (hwaddr)val << VIRTIO_PCI_QUEUE_ADDR_SHIFT;
if (pa == 0) {
virtio_pci_reset(DEVICE(proxy));
}
else
virtio_queue_set_addr(vdev, vdev->queue_sel, pa);
break;
case VIRTIO_PCI_QUEUE_NOTIFY:
if (val < VIRTIO_QUEUE_MAX) {
virtio_queue_notify(vdev, val);
}
break;
case VIRTIO_PCI_STATUS:
if (!(val & VIRTIO_CONFIG_S_DRIVER_OK)) {
virtio_pci_stop_ioeventfd(proxy);
}
virtio_set_status(vdev, val & 0xFF);
if (val & VIRTIO_CONFIG_S_DRIVER_OK) {
virtio_pci_start_ioeventfd(proxy);
}
if (vdev->status == 0) {
virtio_pci_reset(DEVICE(proxy));
}
break;
default:
error_report("%s: unexpected address 0x%x value 0x%x",
__func__, addr, val);
break;
}
}
VIRTIO_PCI_QUEUE_PFN字段是寫入queue地址的,對于legacy模式,avail/used ring以及descriptor的空間連續(xù),所以傳入連續(xù)空間的首地址即可,實際傳入的是頁幀號。virtio_queue_set_addr接口負責將前端驅動傳入的GPA記錄到VirtioDevice->vq結構。
重點關注一下VIRTIO_PCI_STATUS,該位置置位VIRTIO_CONFIG_S_DRIVER_OK,說明前端驅動操作完成,可啟動virtio-net設備的數(shù)據(jù)面操作。所以這個位置很重要,在virtio_set_status()接口中,會啟動數(shù)據(jù)面的一系列操作,包括對VHOST的配置也是由這里觸發(fā)。
圖片
上圖中就是從pcie---->virtio---->virtio-net----->vhost_net的調用關系。在設備初始化完成后,后續(xù)的操作都是由guest操作bar空間來觸發(fā),尤其是流程的推動都是對bar空間的寫操作觸發(fā)的。比如VIRTIO_COFNIG_S_DRIVER_OK的寫入。
三、QEMU與VHOST接口描述
3.1 vhost-user類型netdev設備創(chuàng)建
我們知道指定vhost設備的命令行如下,是根據(jù)type=vhost-user定義:
-netdev type=vhost-user,id=mynet3,chardev=char1,vhostforce,queues=$QNUM
設備的命令行處理流程是統(tǒng)一的,也是在main函數(shù)里首先會解析命令行參數(shù)到本地的數(shù)組,然后進入標準的設備創(chuàng)建流程:
if (qemu_opts_foreach(qemu_find_opts("netdev"),
net_init_netdev, NULL, errp)) {
return -1;
}
所以netdev的設備創(chuàng)建入口就是net_init_netdev函數(shù),在net_init_netdev函數(shù)里,根據(jù)type=vhost-user類型,執(zhí)行net_init_vhost_user()接口創(chuàng)建設備。
該接口首先執(zhí)行net_vhost_claim_chardev(),匹配其依賴的chardev,也就是命令行中的char1,找到相應的Chardev*設備。
然后調用net_vhost_user_init()接口進行真實的初始化操作。最終,vhost-user類型的netdev設備生成的結構實體是什么呢?
圖片
最終NetClientState被掛載到了全局變量net_clients鏈表中。另外,還有一組數(shù)據(jù)結構是在檢測到vhost后端socket連接之后創(chuàng)建的。socket連接之后會調用上圖中的事件處理函數(shù)net_vhost_user_event,進而調用vhost_user_start()接口。
圖片
vhost-user的net設備申請的數(shù)據(jù)結構和框架,其中vhost_net中vhost_dev結構有一個重要的vhost_ops指針,對后端的接口都在這里。前面提到過vhost的后端有用戶態(tài)和內核態(tài)兩種,所以VhostOps的實現(xiàn)也有兩種,因為我們指定了vhost_user類型的后端,實際vhost_ops會初始化為user_ops。VhostOps是與后端類型相關的不同的處理方式。
3.2 QEMU與VHOST通信
qemu與vhost的通信是通過socket的方式。具體的實現(xiàn)都在vhost-user的backend接口里,以VhostOps的封裝形式提供給virtio_net層使用。
(1)通信接口
VIRTIO_PCI_STATUS標識控制面協(xié)商的狀態(tài),當VIRTIO_CONFIG_S_DRIVER_OK置位時,說明控制面協(xié)商完成,此時啟動數(shù)據(jù)面的傳輸。所以VHOST層面的交互是從這個狀態(tài)啟動的。而該狀態(tài)通過virtio_set_status()接口調用到virtio_net_set_status()和virtio_net_vhost_status(),判斷是第一次VIRTIO_CONFIG_S_DRIVER_OK標識置位,則此時認為需要啟動數(shù)據(jù)面了,會調用vhost_net_start()接口啟動與VHOST后端的交互,對于數(shù)據(jù)面來說,最主要的是隊列相關的信息。如下圖示:
圖片
其實VhostOps里的每個接口根據(jù)名字都可以直觀的推斷出實現(xiàn)的作用,比如:
- vhost_set_vring_num是設置隊列的大小
- vhost_set_vring_addr是設置ring的基地址(GPA)
- vhost_set_vring_base設置last_avail_index
- vhost_set_vring_kick是設置隊列kick需要的eventfd信息
vhost_set_mem_table接口,我們知道qemu里獲取的guest的地址都是GUEST地址空間的,一般是GPA,那么qemu在創(chuàng)建虛擬機的時候是知道GPA到HVA的映射關系的,所以根據(jù)GPA可以輕松獲得HVA,進而操作進程地址空間的VA即可。但作為獨立的進程,VHOST-USER擁有獨立的地址空間,是不可以使用QEMU進程的VA的。所以QEMU需要將自己記錄的一組或多組[GPA,HVA,size,memfd]通知給VHOST-USER。VHOST_USER收到這幾個信息后將這片內存空間通過mmap映射到本地進程的地址空間,然后一并記錄到記錄到本地的memory table。
mmap_addr = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE,
MAP_SHARED | populate, reg->fd, 0);
對于為何可以使用qemu里的文件句柄fd,具體原理沒有深究,看起來內核是允許這樣一種共想文件句柄的方式的。注意需要在qemu的命令行里指定memory share=on的方式就可以。
-object memory-backend-file,id=mem,size=3072M,mem-path=/dev/hugepages,share=on
等后續(xù)接收到vring address的配置時,VHOST-USER就通過傳入的GPA,查表找到memory table里的表項,獲取自有進程空間的VA,就可以訪問真實的地址空間了。
(2)數(shù)據(jù)通信格式
具體的數(shù)據(jù)通信格式來說,QEMU與VHOST-USER的消息通過socket的方式,QEMU和VHOST-USER一個作為client,另一個作為server。socket文件路徑是在qemu命令行里傳入的。
數(shù)據(jù)通信的格式是固定的,都是一個標準的header后面跟著payload:
typedefstruct{
VhostUserRequestrequest;
uint32_tflags;
uint32_tsize;/* the following payload size */
}QEMU_PACKEDVhostUserHeader;
typedefunion{
uint64_tu64;
structvhost_vring_statestate;
structvhost_vring_addraddr;
VhostUserMemorymemory;
VhostUserLoglog;
structvhost_iotlb_msgiotlb;
VhostUserConfigconfig;
VhostUserCryptoSessionsession;
VhostUserVringAreaarea;
}VhostUserPayload;
typedefstructVhostUserMsg{
VhostUserHeaderhdr;
VhostUserPayloadpayload;
}QEMU_PACKEDVhostUserMsg;
其中request字段是一個union類型,標識具體的命令類型,所有的命令類型:
typedefenumVhostUserRequest{
VHOST_USER_NONE=0,
VHOST_USER_GET_FEATURES=1,
VHOST_USER_SET_FEATURES=2,
VHOST_USER_SET_OWNER=3,
VHOST_USER_RESET_OWNER=4,
VHOST_USER_SET_MEM_TABLE=5,
VHOST_USER_SET_LOG_BASE=6,
VHOST_USER_SET_LOG_FD=7,
VHOST_USER_SET_VRING_NUM=8,
VHOST_USER_SET_VRING_ADDR=9,
VHOST_USER_SET_VRING_BASE=10,
VHOST_USER_GET_VRING_BASE=11,
VHOST_USER_SET_VRING_KICK=12,
VHOST_USER_SET_VRING_CALL=13,
VHOST_USER_SET_VRING_ERR=14,
VHOST_USER_GET_PROTOCOL_FEATURES=15,
VHOST_USER_SET_PROTOCOL_FEATURES=16,
VHOST_USER_GET_QUEUE_NUM=17,
VHOST_USER_SET_VRING_ENABLE=18,
VHOST_USER_SEND_RARP=19,
VHOST_USER_NET_SET_MTU=20,
VHOST_USER_SET_SLAVE_REQ_FD=21,
VHOST_USER_IOTLB_MSG=22,
VHOST_USER_SET_VRING_ENDIAN=23,
VHOST_USER_GET_CONFIG=24,
VHOST_USER_SET_CONFIG=25,
VHOST_USER_CREATE_CRYPTO_SESSION=26,
VHOST_USER_CLOSE_CRYPTO_SESSION=27,
VHOST_USER_POSTCOPY_ADVISE=28,
VHOST_USER_POSTCOPY_LISTEN=29,
VHOST_USER_POSTCOPY_END=30,
VHOST_USER_MAX
}VhostUserRequest;
四、VHOST-USER框架設計
VHOST-USER是DPDK一個重要的功能,主要的代碼在lib/librte_vhost下。
VHOST的框架還是比較直接的,整體看起來DPDK的代碼框架比QEMU的簡潔很多,更加容易理解。DPDK主要是作為一個接口庫使用,所以lib下面也是對外提供操作接口,供調用方使用。
4.1 VHOST初始化流程
VHOST模塊初始化流程的關鍵接口有三個:
①rte_vhost_driver_register(const char *path, uint64_t flags)
申請并初始化vhost_user_socket結構,vsocket指針存入全局數(shù)組vhost_user.vsockets[]中;
打開path對應的socket文件,fd存入vsocket->socket_fd中。
②rte_vhost_driver_callback_register(const char *path, struct vhost_device_ops const * const ops)
注冊vhost_device_ops到vsocket->ops中。
③rte_vhost_driver_start(const char *path)
根據(jù)VHOST是client或者server,選擇vhost_user_start_client()/vhost_user_start_server()兩種路徑,看一下vhost作為client的路徑。vhost_user_start_client():
1)vhost_user_connect_nonblock,連接socket;
2)vhost_user_add_connection(int fd, struct vhost_user_socket *vsocket)
a、申請vhost_user_connection結構體;
b、申請virtio_net結構體,存儲到全局數(shù)組vhost_devices,返回其在數(shù)組中的索引vid;
c、conn配置,conn->fd =fd; conn->vsocket=vsocket; conn->vid =vid;
d、注冊socket接口的處理函數(shù)
fdset_add(&vhost_user.fdset, fd, vhost_user_read_cb, NULL, conn);
整個初始化流程到這里就結束了,后面就開始等待qemu的消息,對應的消息處理函數(shù)就是vhost_user_read_cb函數(shù)。
4.2 VHOST與QEMU通信流程
初始化流程創(chuàng)建了vsocket結構(與qemu的socket實體),創(chuàng)建了virtio_net結構(設備實體),并注冊了socket消息的處理函數(shù),后面就可以監(jiān)聽socket的信息,作為qemu的后端完善和建立整個流程。作為virtio設備的backend,vhost在初始化完成后,所有的動作都是由前端的socket消息觸發(fā)的。
所有的消息處理函數(shù)以MSG的request請求類型字段作為索引,記錄在vhost_message_handlers數(shù)組中。具體的處理不詳細描述,主要就是將接收到的信息記錄到本地,供數(shù)據(jù)面啟動后使用。
VHOST_CONFIG:/tmp/vhostsock0:connected
VHOST_CONFIG:newdevice,handleis0
VHOST_CONFIG:readmessageVHOST_USER_GET_FEATURES
VHOST_CONFIG:readmessageVHOST_USER_GET_PROTOCOL_FEATURES
VHOST_CONFIG:readmessageVHOST_USER_SET_PROTOCOL_FEATURES
VHOST_CONFIG:negotiatedVhost-userprotocolfeatures:0xcbf
VHOST_CONFIG:readmessageVHOST_USER_GET_QUEUE_NUM
VHOST_CONFIG:readmessageVHOST_USER_SET_SLAVE_REQ_FD
VHOST_CONFIG:readmessageVHOST_USER_SET_OWNER
VHOST_CONFIG:readmessageVHOST_USER_GET_FEATURES
VHOST_CONFIG:readmessageVHOST_USER_SET_VRING_CALL
VHOST_CONFIG:vringcallidx:0file:53
VHOST_CONFIG:readmessageVHOST_USER_SET_VRING_CALL
VHOST_CONFIG:vringcallidx:1file:54
VHOST_CONFIG:readmessageVHOST_USER_SET_VRING_ENABLE
VHOST_CONFIG:setqueueenable:1toqpidx:0
VHOST_CONFIG:readmessageVHOST_USER_SET_VRING_ENABLE
VHOST_CONFIG:setqueueenable:1toqpidx:1
VHOST_CONFIG:readmessageVHOST_USER_SET_VRING_ENABLE
VHOST_CONFIG:setqueueenable:1toqpidx:0
VHOST_CONFIG:readmessageVHOST_USER_SET_VRING_ENABLE
VHOST_CONFIG:setqueueenable:1toqpidx:1
VHOST_CONFIG:readmessageVHOST_USER_SET_FEATURES
VHOST_CONFIG:negotiatedVirtiofeatures:0x140408002
VHOST_CONFIG:readmessageVHOST_USER_SET_MEM_TABLE
VHOST_CONFIG:guestmemoryregion0,size:0xc0000000
guestphysicaladdr:0x0
guestvirtualaddr:0x7fff00000000
hostvirtualaddr:0x2aaac0000000
mmapaddr:0x2aaac0000000
mmapsize:0xc0000000
mmapalign:0x40000000
mmapoff:0x0
VHOST_CONFIG:readmessageVHOST_USER_SET_VRING_NUM
VHOST_CONFIG:readmessageVHOST_USER_SET_VRING_BASE
VHOST_CONFIG:readmessageVHOST_USER_SET_VRING_ADDR
VHOST_CONFIG:reallocatevqfrom0to1node
VHOST_CONFIG:reallocatedevfrom0to1node
VHOST_CONFIG:readmessageVHOST_USER_SET_VRING_KICK
VHOST_CONFIG:vringkickidx:0file:56
VHOST_CONFIG:readmessageVHOST_USER_SET_VRING_CALL
VHOST_CONFIG:vringcallidx:0file:57
VHOST_CONFIG:readmessageVHOST_USER_SET_VRING_NUM
VHOST_CONFIG:readmessageVHOST_USER_SET_VRING_BASE
VHOST_CONFIG:readmessageVHOST_USER_SET_VRING_ADDR
VHOST_CONFIG:reallocatevqfrom0to1node
VHOST_CONFIG:readmessageVHOST_USER_SET_VRING_KICK
VHOST_CONFIG:vringkickidx:1file:53
VHOST_CONFIG:virtioisnowreadyforprocessing.
VHOST_DATA:(0)devicehasbeenaddedtodatacore1
VHOST_CONFIG:readmessageVHOST_USER_SET_VRING_CALL
VHOST_CONFIG:vringcallidx:1file:58
VHOST_CONFIG:readmessageVHOST_USER_SET_VRING_ENABLE
VHOST_CONFIG:setqueueenable:1toqpidx:0
VHOST_CONFIG:readmessageVHOST_USER_SET_VRING_ENABLE
VHOST_CONFIG:setqueueenable:1toqpidx:1
VHOST_CONFIG:readmessageVHOST_USER_SET_VRING_ENABLE
VHOST_CONFIG:setqueueenable:1toqpidx:0
VHOST_CONFIG:readmessageVHOST_USER_SET_VRING_ENABLE
VHOST_CONFIG:setqueueenable:1toqpidx:1
VHOST_DATA:liufengTXvirtio_dev_tx_split:dev(0)queue_id(1),soc_queue(1)
從上面的日志中找到一條消息“virtio is now ready for processing”,說明從這里開始VHOST啟動數(shù)據(jù)面的收發(fā)。查看上面的日志,是在兩個隊列的地址信息配置完成之后。對應到QEMU的流程,就是QEMU里的VIRTIO設備device_status的VIRTIO_CONFIG_S_DRIVER_OK狀態(tài)標志置位時,調用virtio_net_start()接口做了這個動作。
至此,整個數(shù)據(jù)通道完全建立。
一般的使用場景是在OVS添加一個端口,關聯(lián)到剛剛分析的VHOST-USER(指定與qemu的socket-path),實現(xiàn)對GUEST網(wǎng)口的數(shù)據(jù)收發(fā)。VHOST-USER作為client的場景,可以使用如下的命令行建立一個端口。
ovs-vsctl add-port br0 vhost-client-1 \
-- set Interface vhost-client-1 type=dpdkvhostuserclient \
options:vhost-server-path=$VHOST_USER_SOCKET_PATH
五、用QEMU實現(xiàn)的virtio網(wǎng)卡
virtio network device是一個虛擬以太網(wǎng)卡,它支持 TX/RX 多隊列。空緩沖區(qū)放置在RX virtqueue中用于接收數(shù)據(jù)包,而輸出的數(shù)據(jù)包則放在TX virtqueue中進行傳輸。另一個 virtqueue 用于driver, device之間的管理通信,如設置mac 地址或修改隊列的數(shù)量。virtio network device支持許多卸載功能,如checksum計算,GSO/GRO,并讓真實物理網(wǎng)絡設備來做這些。
為了發(fā)送數(shù)據(jù)包,driver在available ring中填入一個描述符,其中包含卸載信息和數(shù)據(jù)幀緩沖區(qū), 數(shù)據(jù)幀可以以SG(scatter/gather)形式由多個描述串聯(lián)組成。Descriptor table/available ring/used ring這些數(shù)據(jù)結構由guest中的driver分配并管理,但由于device實際位于qemu內,而qemu可以訪問所有guest內存,所以device能夠定位緩沖區(qū)并讀取或寫入它們。
發(fā)送一個報文時 virtio-net device和 virtio-net driver的處理流程:
- qemu啟動guest后由guest內部的pci總線機制發(fā)現(xiàn)virtio設備并加載virtio-net driver。
- guest內部的virtio-net driver分配好Descriptor table/available ring/used ring等核心數(shù)據(jù)結構,并寫入PCI MMIO寄存器,進而后端virtio-net device也知道了virtqueue的內存布局。
- driver填充完要發(fā)送的數(shù)據(jù)包后,它會觸發(fā)“available ring notification”(寫PCI MMIO中的notification),將控制權返回給 QEMU。
- virtio-net device通過sendmsg系統(tǒng)調用將數(shù)據(jù)包寫入到內核tun設備上,在host kernel看來,網(wǎng)絡協(xié)議棧從tun設備上收到一個來自VM的報文。進一步的host kernel網(wǎng)絡協(xié)議棧可以使用OVS等轉發(fā)機制轉發(fā)這個報文。
- Qemu 中的virtio-net device通知guest緩沖區(qū)操作(讀取或寫入)已完成,它通過將數(shù)據(jù)放入虛擬隊列并發(fā)送used ring notification來觸發(fā)guest vCPU 中的中斷。
接收數(shù)據(jù)包的過程與發(fā)送數(shù)據(jù)包的過程類似。唯一的區(qū)別是,在這種情況下,空緩沖區(qū)由guest預先分配并供device使用,以便它可以將傳入數(shù)據(jù)寫入它們。
Qemu實現(xiàn)virtio-net device的缺點:
- 性能開銷:虛擬化技術本身會引入一定的性能開銷。盡管virtio-net是為了提高虛擬網(wǎng)絡設備性能而設計的,但與原生網(wǎng)絡設備相比仍然會有一些額外的開銷。
- 虛擬化復雜性:QEMU是一個強大而復雜的虛擬化軟件,實現(xiàn)和配置virtio-net設備需要一定的技術和經(jīng)驗。對于不熟悉QEMU或者虛擬化技術的人來說,可能會面臨一定的學習曲線和困難。
- 驅動兼容性:雖然virtio-net已經(jīng)得到了廣泛支持,并且常見操作系統(tǒng)都提供了對其驅動程序的支持,但仍然可能存在某些情況下驅動程序不完全兼容或出現(xiàn)問題。這可能導致網(wǎng)絡功能不穩(wěn)定或無法正常工作。
- 虛擬化依賴:使用virtio-net需要依賴QEMU等虛擬化軟件,在某些環(huán)境中可能存在限制或約束。例如,在嵌入式系統(tǒng)或特殊硬件平臺上,可能無法輕松地部署和運行QEMU。
5.1 Vhost協(xié)議
為了解決qemu實現(xiàn)virtio-net device的限制,設計了 vhost 協(xié)議。vhost API 是一種基于消息的協(xié)議,它允許hypervisor (qemu)將數(shù)據(jù)平面卸載到另一個更有效地執(zhí)行數(shù)據(jù)轉發(fā)的組件(handler)。使用此協(xié)議, hypervisor向handler發(fā)送以下配置信息:
hypervisor的內存布局。這樣,handler可以在hypervisor的內存空間中定位虛擬隊列和緩沖區(qū)。
一對文件描述符(kick fd/call fd),用于handler發(fā)送和接收 virtio 規(guī)范中定義的通知。這些文件描述符在handler和 KVM 之間共享,因此它們可以直接通信而無需hypervisor的干預。請注意,每個虛擬隊列仍然可以動態(tài)禁用此通知。
在此過程之后, hypervisor將不再處理數(shù)據(jù)包(從虛擬隊列讀取或寫入/從虛擬隊列寫入)。相反,數(shù)據(jù)平面將完全卸載到handler,它現(xiàn)在可以直接訪問 virtqueues 的內存區(qū)域以及直接向guest發(fā)送和接收通知。
vhost 消息可以在任何主機本地傳輸協(xié)議中交換,例如 Unix 套接字或字符設備,并且虛擬機管理程序可以充當服務器或客戶端(在通信通道的上下文中)。hypervisor是協(xié)議的領導者,卸載設備是handler,它們中的任何一個都可以發(fā)送消息。Vhost protocol并沒有規(guī)定數(shù)據(jù)面一定要卸載至哪里,它既可以是host kernel(vhost-net), 也可以是host用戶態(tài)(vhost-user),還可以是硬件。以下主要針對卸載至host kernel即vhost-net。
5.2 vhost-net虛擬化網(wǎng)絡技術
vhost-net是一個內核驅動程序,它實現(xiàn)了 vhost 協(xié)議的handler,以實現(xiàn)高效的數(shù)據(jù)平面,即數(shù)據(jù)包轉發(fā)。在這個實現(xiàn)中,qemu 和 vhost-net 內核驅動程序(處理程序)使用 ioctls 來交換 vhost 消息,并且使用幾個稱為 irqfd 和 ioeventfd 的類似 eventfd 的文件描述符來與guest交換通知。
當加載 vhost-net 內核驅動程序時,它會創(chuàng)建/dev/vhost-net字符設備。當 qemu啟動參數(shù)支持vhost-net 時,它會打開此字符設備并使用多個 ioctl調用初始化 vhost-net 實例。這些對于將hypervisor與 vhost-net 實例相關聯(lián)、準備 virtio 功能協(xié)商并將guest物理內存映射傳遞給 vhost-net 驅動程序是必需的。在初始化期間,vhost-net 內核驅動程序創(chuàng)建了一個名為 vhost-$pid 的內核線程,其中 $pid 是管理程序進程 pid。該線程稱為“vhost 工作線程”。
Tap 設備仍然用于guest將報文發(fā)送至host kernel,但現(xiàn)在是vhost工作線程處理 I/O 事件,即它輪詢guest drvier的通知或tap事件,并轉發(fā)數(shù)據(jù)。
Qemu 分配一個eventfd并將其注冊到 vhost 和 KVM 以實現(xiàn)通知繞過。vhost-$pid 內核線程輪詢它,當guest寫特定PCI MMIO地址時觸發(fā)vm-exit進入KVM,此時KVM檢測到地址關聯(lián)了一個ioeventfd,所以寫入此fd。這種機制被命名為 ioeventfd。這樣,對特定guest內存地址的簡單讀/寫操作不需要經(jīng)過昂貴的 QEMU 進程喚醒,可以直接路由到 vhost 工作線程。這樣做也有異步的好處,不需要 vCPU 停止(所以不需要立即進行上下文切換)。
另一方面,qemu 分配另一個 eventfd 并將其再次注冊到 KVM 和 vhost 以進行直接 vCPU 中斷注入。這種機制稱為irqfd,它允許主機中的任何進程通過寫入irqfd來將 vCPU 中斷注入guest,具有相同的優(yōu)點(異步、不需要立即上下文切換等)。
請注意,virtio 數(shù)據(jù)包處理后端中的此類更改對于仍然使用標準 virtio 接口的guest來說是完全透明的。
在QEMU中,virtio是一種用于虛擬機和宿主機之間進行高性能數(shù)據(jù)傳輸?shù)臉藴驶涌凇6鴙irtio-net則是基于virtio標準實現(xiàn)的一種虛擬網(wǎng)絡設備,用于連接虛擬機和宿主機的網(wǎng)絡通信。
具體來說,在使用QEMU創(chuàng)建虛擬機時,可以通過以下步驟實現(xiàn)virtio-net網(wǎng)卡:
- 啟動QEMU命令時添加參數(shù) "-device virtio-net" 或者 "-netdev user,id=net0 -device virtio-net,netdev=net0",其中"net0"為網(wǎng)絡設備的名稱。
- QEMU將會創(chuàng)建一個名為"net0"的virtio-net網(wǎng)卡,并將其與虛擬機關聯(lián)起來。
- 虛擬機內部操作系統(tǒng)會將這個virtio-net網(wǎng)卡識別為一個正常的物理網(wǎng)卡,并加載相應的驅動程序。
- 宿主機上運行的QEMU負責處理從虛擬機發(fā)送過來的數(shù)據(jù)包,并轉發(fā)到宿主機上與該網(wǎng)卡對應的物理網(wǎng)絡設備上,或者反向地將從物理網(wǎng)絡設備接收到的數(shù)據(jù)包傳遞給虛擬機。
通過使用virtio-net網(wǎng)卡,可以提供高性能、低延遲和可擴展性好的網(wǎng)絡通信,在虛擬化環(huán)境中更加有效地利用計算資源,并提供與原生網(wǎng)絡設備相當?shù)男阅堋?/span>
QEMU命令行參數(shù):
qemu-system-x86_64 -netdev user,id=net0 -device virtio-net,netdev=net0
啟動腳本示例(bash):
#!/bin/bash
qemu-system-x86_64 \
-netdev user,id=net0 \
-device virtio-net,netdev=net0 \
[其他QEMU參數(shù)]
請注意,上述代碼只是一個簡單的示例,具體的QEMU參數(shù)和配置可能因實際情況而有所不同。你可以根據(jù)自己的需求進行相應的修改和調整。
此外,還需要確保在虛擬機內部操作系統(tǒng)中加載了virtio-net驅動程序,并正確配置網(wǎng)絡設置。具體步驟和操作方式取決于虛擬機所使用的操作系統(tǒng)。