自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

實(shí)戰(zhàn)篇:在QEMU中編寫和調(diào)試VHost/Virtio驅(qū)動(dòng)

網(wǎng)絡(luò) 通信技術(shù)
有沒(méi)有一種方法能夠打破這種通信困境,讓虛擬設(shè)備之間的通信更加高效、流暢呢?答案就是 vhost/virtio 技術(shù)。它就像是虛擬世界中的通信加速器,為解決虛擬設(shè)備通信難題帶來(lái)了新的曙光。接下來(lái),就讓我們深入了解 vhost/virtio 技術(shù)的奧秘。

在云計(jì)算環(huán)境中,當(dāng)多個(gè)虛擬機(jī)同時(shí)進(jìn)行大規(guī)模數(shù)據(jù)傳輸時(shí),這種通信瓶頸就會(huì)變得尤為明顯,嚴(yán)重影響了云服務(wù)的性能和用戶體驗(yàn)。同樣,在大數(shù)據(jù)分析場(chǎng)景下,虛擬機(jī)需要頻繁地讀寫存儲(chǔ)設(shè)備,如果虛擬設(shè)備通信效率低下,就會(huì)導(dǎo)致數(shù)據(jù)分析的速度大幅下降,無(wú)法滿足實(shí)時(shí)性的要求。

那么,有沒(méi)有一種方法能夠打破這種通信困境,讓虛擬設(shè)備之間的通信更加高效、流暢呢?答案就是 vhost/virtio 技術(shù)。它就像是虛擬世界中的通信加速器,為解決虛擬設(shè)備通信難題帶來(lái)了新的曙光。接下來(lái),就讓我們深入了解 vhost/virtio 技術(shù)的奧秘。

一、手寫 Vhost/Virtio

1.1 前期準(zhǔn)備:搭建 “舞臺(tái)”

在開(kāi)始這場(chǎng)奇妙的手寫 vhost/virtio 之旅前,我們首先需要搭建一個(gè)合適的開(kāi)發(fā)環(huán)境,就如同搭建一個(gè)穩(wěn)固的舞臺(tái),為后續(xù)的精彩表演做好充分準(zhǔn)備。

我們要安裝 Qemu,它可是虛擬化的基石。安裝 Qemu 的方式有多種,對(duì)于追求便捷的開(kāi)發(fā)者來(lái)說(shuō),可以使用系統(tǒng)自帶的包管理器,比如在 Ubuntu 系統(tǒng)中,只需在終端輸入 “sudo apt - get install qemu - system - x86” 即可輕松完成安裝。但如果你想要更前沿的功能和性能優(yōu)化,從源代碼編譯安裝則是個(gè)不錯(cuò)的選擇。你可以從 Qemu 的官方網(wǎng)站下載最新的源代碼,然后按照官方文檔的指引進(jìn)行編譯和安裝 ,雖然這個(gè)過(guò)程可能稍微復(fù)雜一些,但能讓你獲得最適合自己需求的 Qemu 版本。

除了 Qemu,我們還需要一系列相關(guān)的開(kāi)發(fā)工具和庫(kù)文件。比如,GCC(GNU Compiler Collection)是必不可少的,它能將我們編寫的 C 代碼編譯成可執(zhí)行的程序。安裝 GCC 也很簡(jiǎn)單,在大多數(shù) Linux 系統(tǒng)中,通過(guò)包管理器就能快速完成安裝。另外,還需要安裝一些開(kāi)發(fā)庫(kù),如 libvirt 開(kāi)發(fā)庫(kù),它提供了與虛擬化管理相關(guān)的接口,方便我們?cè)诖a中對(duì)虛擬機(jī)進(jìn)行各種操作;以及 libpciaccess 庫(kù),它有助于我們?cè)L問(wèn) PCI 設(shè)備,在處理虛擬設(shè)備相關(guān)的功能時(shí)發(fā)揮著重要作用。在安裝這些庫(kù)文件時(shí),一定要注意它們的版本兼容性,不同版本之間可能存在接口差異,不兼容的版本可能會(huì)導(dǎo)致后續(xù)開(kāi)發(fā)過(guò)程中出現(xiàn)各種難以調(diào)試的問(wèn)題。

1.2 初窺門徑:理解關(guān)鍵數(shù)據(jù)結(jié)構(gòu)

當(dāng)我們搭建好開(kāi)發(fā)環(huán)境后,就如同踏入了一座神秘的城堡,首先要熟悉城堡中的各種機(jī)關(guān)和暗道,也就是 vhost/virtio 中的關(guān)鍵數(shù)據(jù)結(jié)構(gòu)。

在 vhost/virtio 的世界里,virtio_ring 數(shù)據(jù)隊(duì)列是最為核心的數(shù)據(jù)結(jié)構(gòu)之一,它就像是一座橋梁,連接著虛擬機(jī)(Guest)和宿主機(jī)(Host)之間的數(shù)據(jù)傳輸通道。virtio_ring 主要由描述符表(descriptor table)、可用環(huán)表(available ring)和已用環(huán)表(used ring)三部分組成。描述符表就像是一個(gè)貨物清單,里面存放著真正的數(shù)據(jù)報(bào)文信息,每個(gè)描述符都記錄了數(shù)據(jù)的起始地址、長(zhǎng)度以及一些標(biāo)志位等關(guān)鍵信息 ,這些信息就像是貨物的標(biāo)簽,告訴接收方如何正確地處理這些數(shù)據(jù)。

可用環(huán)表則是 Guest 用來(lái)告知 Host 有哪些數(shù)據(jù)是可供處理的,它就像是一個(gè)待處理任務(wù)列表,Guest 將數(shù)據(jù)描述符的索引放入可用環(huán)表中,Host 從這里獲取任務(wù)并進(jìn)行處理。已用環(huán)表則是 Host 用來(lái)通知 Guest 哪些數(shù)據(jù)已經(jīng)處理完成,Guest 可以回收相應(yīng)的資源,就像是完成任務(wù)后的反饋清單。

在網(wǎng)絡(luò)通信場(chǎng)景中,當(dāng) Guest 要發(fā)送網(wǎng)絡(luò)數(shù)據(jù)包時(shí),它會(huì)先將數(shù)據(jù)包的相關(guān)信息填充到描述符表中,然后將描述符的索引添加到可用環(huán)表中,Host 檢測(cè)到可用環(huán)表有新的任務(wù)后,就會(huì)從描述符表中獲取數(shù)據(jù)包并進(jìn)行發(fā)送處理,處理完成后,將描述符的索引放入已用環(huán)表中,Guest 看到已用環(huán)表的反饋后,就知道哪些數(shù)據(jù)包已經(jīng)成功發(fā)送,可以進(jìn)行后續(xù)的操作了。理解這些數(shù)據(jù)結(jié)構(gòu)的工作原理和相互之間的關(guān)系,是我們手寫 vhost/virtio 的關(guān)鍵,只有掌握了它們,我們才能在后續(xù)的代碼實(shí)現(xiàn)中得心應(yīng)手。

1.3 核心代碼實(shí)現(xiàn):構(gòu)建 “通信橋梁”

①創(chuàng)建共享內(nèi)存

共享內(nèi)存是 vhost/virtio 實(shí)現(xiàn)高效通信的基礎(chǔ),它就像是一個(gè)公共的倉(cāng)庫(kù),Guest 和 Host 都可以直接訪問(wèn),從而避免了數(shù)據(jù)的多次拷貝,大大提高了通信效率。

在創(chuàng)建共享內(nèi)存時(shí),我們可以使用操作系統(tǒng)提供的相關(guān)函數(shù),比如在 Linux 系統(tǒng)中,可以使用 shmget 函數(shù)來(lái)創(chuàng)建共享內(nèi)存段。首先,我們需要定義共享內(nèi)存的大小和一些權(quán)限標(biāo)志 ,然后調(diào)用 shmget 函數(shù),它會(huì)返回一個(gè)共享內(nèi)存標(biāo)識(shí)符,這個(gè)標(biāo)識(shí)符就像是倉(cāng)庫(kù)的鑰匙,后續(xù)我們對(duì)共享內(nèi)存的操作都需要使用這個(gè)標(biāo)識(shí)符。例如:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>

#define SHM_SIZE 1024 * 1024 // 共享內(nèi)存大小為1MB

int main() {
    key_t key;
    int shmid;
    // 生成一個(gè)唯一的鍵值
    key = ftok(".", 'a'); 
    if (key == -1) {
        perror("ftok");
        return 1;
    }
    // 創(chuàng)建共享內(nèi)存段
    shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666); 
    if (shmid == -1) {
        perror("shmget");
        return 1;
    }
    printf("共享內(nèi)存創(chuàng)建成功,標(biāo)識(shí)符為: %d\n", shmid);
    // 后續(xù)可以使用shmid進(jìn)行共享內(nèi)存的操作
    //...
    // 最后刪除共享內(nèi)存段
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl");
        return 1;
    }
    return 0;
}

創(chuàng)建好共享內(nèi)存后,還需要將其映射到進(jìn)程的地址空間中,這樣我們的代碼才能直接訪問(wèn)共享內(nèi)存中的數(shù)據(jù)。在 Linux 中,可以使用 shmat 函數(shù)來(lái)完成映射操作。映射完成后,就可以像訪問(wèn)普通內(nèi)存一樣對(duì)共享內(nèi)存進(jìn)行讀寫操作了。同時(shí),我們還需要注意在適當(dāng)?shù)臅r(shí)候通知內(nèi)核或者其他進(jìn)程共享內(nèi)存的相關(guān)信息,比如通過(guò)信號(hào)量或者其他同步機(jī)制,確保各個(gè)進(jìn)程對(duì)共享內(nèi)存的訪問(wèn)是安全和有序的。

②初始化 Virtio Ring

Virtio Ring 的初始化是確保數(shù)據(jù)能夠正確傳輸?shù)年P(guān)鍵步驟,它就像是為橋梁設(shè)置正確的交通規(guī)則,讓數(shù)據(jù)能夠在 Guest 和 Host 之間順暢地流動(dòng)。

初始化 Virtio Ring 時(shí),我們需要設(shè)置描述符、可用和已用索引等關(guān)鍵參數(shù)。首先,我們要為描述符表分配內(nèi)存空間,并初始化每個(gè)描述符的內(nèi)容,包括數(shù)據(jù)的起始地址、長(zhǎng)度和標(biāo)志位等。例如:

#include <stdint.h>

#define QUEUE_SIZE 256

// Virtio描述符結(jié)構(gòu)
typedef struct {
    uint64_t addr;
    uint32_t len;
    uint16_t flags;
    uint16_t next;
} virtio_desc;

// Virtio可用環(huán)結(jié)構(gòu)
typedef struct {
    uint16_t flags;
    uint16_t idx;
    uint16_t ring[QUEUE_SIZE];
} virtio_avail;

// Virtio已用環(huán)結(jié)構(gòu)
typedef struct {
    uint16_t flags;
    uint16_t idx;
    struct {
        uint32_t id;
        uint32_t len;
    } ring[QUEUE_SIZE];
} virtio_used;

// Virtio Ring結(jié)構(gòu)
typedef struct {
    virtio_desc *desc;
    virtio_avail *avail;
    virtio_used *used;
} virtio_ring;

// 初始化Virtio Ring
void init_virtio_ring(virtio_ring *ring) {
    // 分配描述符表內(nèi)存
    ring->desc = (virtio_desc *)malloc(QUEUE_SIZE * sizeof(virtio_desc));
    if (ring->desc == NULL) {
        // 處理內(nèi)存分配失敗的情況
        return;
    }
    // 分配可用環(huán)內(nèi)存
    ring->avail = (virtio_avail *)malloc(sizeof(virtio_avail));
    if (ring->avail == NULL) {
        free(ring->desc);
        return;
    }
    // 分配已用環(huán)內(nèi)存
    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 相關(guān)的結(jié)構(gòu),然后實(shí)現(xiàn)了一個(gè)初始化函數(shù) init_virtio_ring。在函數(shù)中,我們?yōu)槊枋龇怼⒖捎铆h(huán)和已用環(huán)分配內(nèi)存,并對(duì)它們進(jìn)行初始化。描述符的 next 字段形成了一個(gè)環(huán)形鏈表,方便數(shù)據(jù)的管理和訪問(wèn)??捎铆h(huán)和已用環(huán)的 idx 字段初始化為 0,表示當(dāng)前沒(méi)有數(shù)據(jù)待處理或已處理。通過(guò)這樣的初始化操作,Virtio Ring 就可以準(zhǔn)備好進(jìn)行數(shù)據(jù)的收發(fā)工作了。

③數(shù)據(jù)收發(fā)處理

數(shù)據(jù)的發(fā)送和接收是 vhost/virtio的核心功能,它就像是橋梁上車輛的行駛,實(shí)現(xiàn)了Guest 和Host之間的信息交互。

當(dāng) Guest 要發(fā)送數(shù)據(jù)時(shí),它會(huì)首先填充數(shù)據(jù)到共享內(nèi)存中,并將數(shù)據(jù)的相關(guān)信息(如數(shù)據(jù)長(zhǎng)度、內(nèi)存地址等)填充到 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;
    // 獲取一個(gè)可用的描述符
    virtio_desc *desc = &ring->desc[desc_idx];
    // 設(shè)置描述符的地址和長(zhǎng)度
    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ù)
    // 這里可以通過(guò)中斷或者其他機(jī)制通知Host
}

在發(fā)送數(shù)據(jù)的過(guò)程中,我們需要注意可用環(huán)表的索引管理,確保不會(huì)發(fā)生溢出。同時(shí),要及時(shí)通知 Host 有新的數(shù)據(jù)到來(lái),以便 Host 能夠及時(shí)處理。

當(dāng) Host 接收到 Guest 的通知后,它會(huì)從可用環(huán)表中獲取描述符的索引,然后根據(jù)索引從描述符表中獲取數(shù)據(jù)的相關(guān)信息,并從共享內(nèi)存中讀取數(shù)據(jù)進(jìn)行處理。處理完成后,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];
        // 從共享內(nèi)存中讀取數(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ù)已處理完成
    // 這里可以通過(guò)中斷或者其他機(jī)制通知Guest
}

在接收數(shù)據(jù)的過(guò)程中,Host 需要不斷地檢查可用環(huán)表和已用環(huán)表的索引,確保能夠及時(shí)處理新的數(shù)據(jù),并將處理結(jié)果反饋給 Guest。同時(shí),也要注意已用環(huán)表的索引管理,避免出現(xiàn)錯(cuò)誤。

④中斷處理機(jī)制

中斷處理在 vhost/virtio 中起著至關(guān)重要的作用,它就像是橋梁上的交通信號(hào)燈,能夠及時(shí)通知對(duì)方有重要事件發(fā)生,從而實(shí)現(xiàn)高效的通信。

在 vhost/virtio 中,中斷主要用于 Guest 通知 Host 有數(shù)據(jù)待處理,或者 Host 通知 Guest 數(shù)據(jù)已經(jīng)處理完成。當(dāng) Guest 填充數(shù)據(jù)到共享內(nèi)存并更新 Virtio Ring 后,它可以通過(guò)觸發(fā)中斷來(lái)通知 Host。在 Linux 系統(tǒng)中,可以使用 eventfd 來(lái)實(shí)現(xiàn)中斷通知機(jī)制。

首先,Guest 創(chuàng)建一個(gè) eventfd 對(duì)象,并將其與 Virtio Ring 的中斷關(guān)聯(lián)起來(lái)。當(dāng) Guest 需要通知 Host 時(shí),它向 eventfd 對(duì)象寫入一個(gè)值,這個(gè)值會(huì)觸發(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 在啟動(dòng)時(shí),會(huì)監(jiān)聽(tīng)這個(gè) eventfd 對(duì)象,當(dāng)接收到中斷信號(hào)時(shí),它會(huì)調(diào)用相應(yīng)的中斷處理函數(shù)來(lái)處理數(shù)據(jù)。例如:

#include <sys/eventfd.h>
#include <poll.h>

// Host監(jiān)聽(tīng)中斷
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 通過(guò) write 函數(shù)向 eventfd 對(duì)象寫入值來(lái)觸發(fā)中斷,Host 使用 poll 函數(shù)監(jiān)聽(tīng) eventfd 對(duì)象,當(dāng)接收到 POLLIN 事件時(shí),說(shuō)明有中斷發(fā)生,然后調(diào)用 host_receive_data 函數(shù)處理數(shù)據(jù)。通過(guò)這樣的中斷處理機(jī)制,Guest 和 Host 可以實(shí)現(xiàn)高效的數(shù)據(jù)交互,避免了不必要的輪詢操作,提高了系統(tǒng)的性能和響應(yīng)速度。

二、QEMU后端驅(qū)動(dòng)

VIRTIO設(shè)備的前端是GUEST的內(nèi)核驅(qū)動(dòng),后端由QEMU或者DPU實(shí)現(xiàn)。不論是原來(lái)的QEMU-VIRTIO框架還是現(xiàn)在的DPU,VIRTIO的控制面和數(shù)據(jù)面都是相對(duì)獨(dú)立的設(shè)計(jì)。本文主要針對(duì)QEMU的VIRTIO后端進(jìn)行分析。

控制面負(fù)責(zé)GUEST前端和VIRTIO設(shè)備的協(xié)商流程,主要用于前后端的feature協(xié)商匹配、向GUEST提供后端設(shè)備信息、建立前后端之間的數(shù)據(jù)通道。等控制面協(xié)商完成后,數(shù)據(jù)面啟動(dòng)前后端的數(shù)據(jù)交互流程;后面的流程中控制面負(fù)責(zé)一些配置信息的下發(fā)和通知,比如block設(shè)備capacity配置、net設(shè)備mac地址動(dòng)態(tài)修改等。

QEMU負(fù)責(zé)設(shè)備控制面的實(shí)現(xiàn),而數(shù)據(jù)面由VHOST框架接管。VHOST又分為用戶態(tài)的vhost-user和內(nèi)核態(tài)的vhost-kernel路徑,前者是由用戶態(tài)的dpdk接管數(shù)據(jù)路徑,將數(shù)據(jù)從用戶態(tài)OVS協(xié)議棧轉(zhuǎn)發(fā),后者是由內(nèi)核態(tài)的vhost驅(qū)動(dòng)接管數(shù)據(jù)路徑,將數(shù)據(jù)從內(nèi)核協(xié)議棧發(fā)送出去。本文主要針對(duì)vhost-user路徑,以net設(shè)備為例進(jìn)行描述。

如果要順利的看懂QEMU后端VIRTIO驅(qū)動(dòng)框架,需要具備QEMU的QOM基礎(chǔ)知識(shí),在這個(gè)基礎(chǔ)上將數(shù)據(jù)結(jié)構(gòu)、初始化流程理清楚,就可以更快的上手。如果只是對(duì)VIRTIO相關(guān)的設(shè)計(jì)感興趣,可直接看下一章原理性的內(nèi)容。

QEMU設(shè)備管理是非常重要的部分,后來(lái)引入了專門的設(shè)備樹(shù)管理機(jī)制。而其參照了C++的類、繼承的一些概念,但又不是完全一致,對(duì)于非科班出身的作者閱讀起來(lái)有些吃力。因?yàn)榭蚣芟嚓P(guān)的代碼中時(shí)常使用內(nèi)部數(shù)據(jù)指針cast的一些宏定義,非常影響可讀性。

2.1 VIRTIO設(shè)備創(chuàng)建流程

從實(shí)際的命令行示例入手,查看設(shè)備是如何創(chuàng)建的。

(1)virtio-net-pci設(shè)備命令行

首先從QEMU的命令行入手,創(chuàng)建一個(gè)使用virtio設(shè)備的虛擬機(jī),可使用如下命令行:

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)建一個(gè)虛擬硬件設(shè)備,都是通過(guò)-device來(lái)實(shí)現(xiàn)的,上面的命令行中創(chuàng)建了一個(gè)virtio-net-pci設(shè)備

-device virtio-net-pci,netdev=mynet3,id=net1,mac=00:00:00:00:00:03,disable-legacy=on

這個(gè)硬件設(shè)備的構(gòu)造依賴于qemu框架里的netdev設(shè)備(并不會(huì)獨(dú)立的對(duì)guest呈現(xiàn))

-netdev type=vhost-user,id=mynet3,chardev=char1,vhostforce,queues=$QNUM

上面的netdev設(shè)備又依賴于qemu框架里的字符設(shè)備(同樣不會(huì)獨(dú)立的對(duì)guest呈現(xiàn))

-chardev socket,id=char1,path=/tmp/vhostsock0,server

(2)命令行解析處理

QEMU的命令行解析在main函數(shù)進(jìn)行,解析后按照qemu標(biāo)準(zhǔn)格式存儲(chǔ)到本地。然后通過(guò)qemu_find_opts(“”)接口可以獲取本地結(jié)構(gòu)體中具有相應(yīng)關(guān)鍵字的所有命令列表,對(duì)解析后的命令列表使用qemu_opts_foreach依次執(zhí)行處理函數(shù)。

常用的用法,比如netdev的處理,qemu_find_opts找到所有的netdev的命令列表,qemu_opts_foreach則對(duì)列表里的所有元素依次執(zhí)行net_init_netdev,初始化相應(yīng)的netdev結(jié)構(gòu)。

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í)行相應(yīng)的net_init_vhost_user函數(shù)進(jìn)行初始化,并為每個(gè)隊(duì)列創(chuàng)建一個(gè)NetClientState結(jié)構(gòu),用于后續(xù)socket通信。對(duì)于"-device"參數(shù)的處理也是采用同樣的方式,依次執(zhí)行device_init_func,初始化相應(yīng)的DeviceState結(jié)構(gòu)。

if (qemu_opts_foreach(qemu_find_opts("device"),
                          device_init_func, NULL, NULL)) {
        exit(1);
    }

device后跟的第一個(gè)參數(shù)qemu稱為driver,其實(shí)就是根據(jù)不同的設(shè)備類型(我們的場(chǎng)景為“virtio-net-pci")匹配不同的處理。而device采用的是通用的設(shè)備類,根據(jù)驅(qū)動(dòng)的名字在device_init_func函數(shù)里調(diào)用qdev_device_add()接口,然后匹配到相應(yīng)的DeviceClass(就是virtio-net-pci對(duì)應(yīng)的DeviceClass)。

匹配到DeviceClass后,調(diào)用class里的instance_init接口,創(chuàng)建相應(yīng)的實(shí)例,即DeviceState。

備注:看到了DeviceClass和DeviceState,這個(gè)是QEMU設(shè)備管理框架里的重要元素。

1)Class后綴表示一類方法實(shí)現(xiàn),是相應(yīng)設(shè)備類型的一類實(shí)現(xiàn),對(duì)于同一設(shè)備類型的多個(gè)設(shè)備是通用的,不管創(chuàng)建幾個(gè)virtio-pci-net設(shè)備,只需要一份VirtioPciClass。

2)State后綴表示具體的instance實(shí)體,每創(chuàng)建一個(gè)設(shè)備都要實(shí)例化一個(gè)instance結(jié)構(gòu)。創(chuàng)建和初始化這個(gè)結(jié)構(gòu)是由object_new()接口完成的,初始化還會(huì)調(diào)用相應(yīng)的類定義的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)設(shè)備實(shí)例初始化

在qdev_device_add函數(shù)中,首先會(huì)調(diào)用object_new,創(chuàng)建object(object是所有instance實(shí)例的根結(jié)構(gòu)),最終是通過(guò)調(diào)用每個(gè)virtio-pci-net相應(yīng)DeviceClass里的instance_init創(chuàng)建實(shí)例。

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結(jié)構(gòu)體中包含其父類的實(shí)例VirtIOPCIProxy,其擁有的設(shè)備框架自定義的結(jié)構(gòu)是VirtIONet的實(shí)例。對(duì)于netdev來(lái)說(shuō),它也利用了qemu的class和device框架,但netdev不像-device一樣通過(guò)框架的qdev_device_add接口調(diào)用object_new完成。他的數(shù)據(jù)空間跟隨在virtio_net_pci的自定義結(jié)構(gòu)里,然后通過(guò)virtio_instance_init_com接口顯式的調(diào)用object_initialize()函數(shù)實(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設(shè)備realize流程

qdev_device_add接口中,還會(huì)調(diào)用realize接口,前面的instance_init只是實(shí)例的簡(jiǎn)單初始化,真實(shí)的設(shè)備相關(guān)的具體初始化動(dòng)作都是從設(shè)備realize之后進(jìn)行的。也就是相應(yīng)class的realize接口。

首先在qdev_device_add()接口中,置位設(shè)備的realized屬性,進(jìn)而調(diào)用每一層class的realize函數(shù)。大家想一下,類似于內(nèi)核驅(qū)動(dòng),設(shè)備肯定按照協(xié)議的分層從下向上識(shí)別的,先識(shí)別pci設(shè)備,然后是virtio,進(jìn)而識(shí)別到virtio-net設(shè)備。所以qemu的識(shí)別過(guò)程也是這樣,從最底層的realize層層調(diào)用至上層的realize接口。

參照VirtIO的Class結(jié)構(gòu),整個(gè)realize的流程整理如下:

圖片圖片

圖片圖片

在初始化的過(guò)程中,對(duì)數(shù)據(jù)結(jié)構(gòu)進(jìn)行一一初始化。在pci設(shè)備的realize之前插入virtio_pci_dc_realize函數(shù)的原因是,如果是modern模式的pci設(shè)備必須是pci-express協(xié)議,所以需要置位PCIDevice里的pcie-capability標(biāo)識(shí),強(qiáng)行令pci設(shè)備為pcie類型。然后再進(jìn)行pci層的設(shè)備初始化,初始化一個(gè)pcie設(shè)備。

virtio_pci_realize接口對(duì)VirtioPCIProxy數(shù)據(jù)結(jié)構(gòu)進(jìn)行了初始化,是virtio+pci需要的初始化。所以初始化了virtio設(shè)備的bar空間以及需要的pcie-capability。

virtio_net_pci_realize接口主要是觸發(fā)VirtIONet里的VirtIODevice的realize流程,這才是virtio設(shè)備的realize流程(virtio_device_realize接口)。

virtio_device_realize接口實(shí)現(xiàn)調(diào)用了virtio_net_device_realize,對(duì)于特定virtio設(shè)備(net類型)的初始化都是在這里進(jìn)行的。所以這部分是對(duì)VirtIONet及其包裹的VirtIODevice數(shù)據(jù)結(jié)構(gòu)進(jìn)行初始化,包括VirtIODevice結(jié)構(gòu)里的vq指針就是在這里根據(jù)隊(duì)列個(gè)數(shù)動(dòng)態(tài)申請(qǐng)空間的。

virtio_device_realize接口還執(zhí)行了virtio_bus_device_plugged接口,這是virtio總線上的virtio設(shè)備的plugged接口,這部分內(nèi)容脫離了virtio-pci框架,進(jìn)入到更上層的virtio框架。但virtio_bus派生了virtio_pci_bus,virtio_pci_bus將繼承的virtio_bus的接口都設(shè)置成了自己的接口。所以最終還是調(diào)用了virtio-pci下的virtio_pci_device_plugged函數(shù)。

virtio_pci_device_plugged接口是最核心的初始化接口,modern模式初始化pci設(shè)備的bar空間讀寫操作接口,因?yàn)榉侄鄩K讀寫,所以還引入了memory_region,然后添加相應(yīng)的capability;legacy模式初始化pci的bar空間讀寫操作接口,至此virtio設(shè)備的初始化流程完成,等待與host的接口操作。

2.2 VIRTIO設(shè)備實(shí)現(xiàn)

結(jié)合qemu的設(shè)備框架模型,分析了qemu從命令行到virtio設(shè)備創(chuàng)建的處理流程?,F(xiàn)在設(shè)備創(chuàng)建出來(lái)了,是如何與host進(jìn)行交互和操作的。本節(jié)主要講述這部分內(nèi)容,明確一點(diǎn),所有的數(shù)據(jù)和操作接口都會(huì)匯聚到一個(gè)結(jié)構(gòu)體,設(shè)備創(chuàng)建過(guò)程中的instance實(shí)例就承載了我們這個(gè)設(shè)備的所有數(shù)據(jù)和ops,所以分析這個(gè)VirtIONetPCI結(jié)構(gòu)及其衍生輻射的其他數(shù)據(jù)就可以了。

struct VirtIONetPCI {
    VirtIOPCIProxy parent_obj;    //VIRTIO-PCI數(shù)據(jù)
    VirtIONet vdev;    //VIRTIO-NET數(shù)據(jù)
};

對(duì)于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; 
};

其實(shí)控制面主要就是用于協(xié)商操作,而這部分操作是通過(guò)bar空間的讀寫實(shí)現(xiàn)的。所以對(duì)virtio-pci來(lái)說(shuō),bar空間的讀寫操作接口是主線的流程,其余的數(shù)據(jù)結(jié)構(gòu)都是圍繞這組讀寫操作接口展開(kāi)。

對(duì)于legacy設(shè)備,bar0用于virtio設(shè)備協(xié)商的空間。bar0在VirtIOPCIProxy中對(duì)應(yīng)的是bar結(jié)構(gòu)。bar0的讀寫接口對(duì)應(yīng)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,
};

對(duì)于modern設(shè)備,采用了更靈活的方式,將virtio設(shè)備協(xié)商的空間分成4個(gè)(common、isr、device、notify)區(qū)間,每個(gè)區(qū)間的bar index和bar內(nèi)offset由pci設(shè)備的capability指定。VIRTIO前端驅(qū)動(dòng)解析capability識(shí)別不同的區(qū)間,進(jìn)而與設(shè)備同步地址區(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。

注冊(cè)完上述ops后,GUEST對(duì)設(shè)備的bar空間進(jìn)行訪問(wèn),會(huì)進(jìn)入相應(yīng)的操作接口,VIRTIO控制面的初始化流程具體可以參見(jiàn)協(xié)議或者作者其他文章,文中主要是對(duì)代碼的實(shí)現(xiàn)框架梳理,摘抄legacy模式的write接口部分內(nèi)容示例說(shuō)明。后端設(shè)備根據(jù)前端驅(qū)動(dòng)寫入的地址和數(shù)據(jù)進(jìn)行相應(yīng)的處理。

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地址的,對(duì)于legacy模式,avail/used ring以及descriptor的空間連續(xù),所以傳入連續(xù)空間的首地址即可,實(shí)際傳入的是頁(yè)幀號(hào)。virtio_queue_set_addr接口負(fù)責(zé)將前端驅(qū)動(dòng)傳入的GPA記錄到VirtioDevice->vq結(jié)構(gòu)。

重點(diǎn)關(guān)注一下VIRTIO_PCI_STATUS,該位置置位VIRTIO_CONFIG_S_DRIVER_OK,說(shuō)明前端驅(qū)動(dòng)操作完成,可啟動(dòng)virtio-net設(shè)備的數(shù)據(jù)面操作。所以這個(gè)位置很重要,在virtio_set_status()接口中,會(huì)啟動(dòng)數(shù)據(jù)面的一系列操作,包括對(duì)VHOST的配置也是由這里觸發(fā)。

圖片圖片

上圖中就是從pcie---->virtio---->virtio-net----->vhost_net的調(diào)用關(guān)系。在設(shè)備初始化完成后,后續(xù)的操作都是由guest操作bar空間來(lái)觸發(fā),尤其是流程的推動(dòng)都是對(duì)bar空間的寫操作觸發(fā)的。比如VIRTIO_COFNIG_S_DRIVER_OK的寫入。

三、QEMU與VHOST接口描述

3.1 vhost-user類型netdev設(shè)備創(chuàng)建

我們知道指定vhost設(shè)備的命令行如下,是根據(jù)type=vhost-user定義:

-netdev type=vhost-user,id=mynet3,chardev=char1,vhostforce,queues=$QNUM

設(shè)備的命令行處理流程是統(tǒng)一的,也是在main函數(shù)里首先會(huì)解析命令行參數(shù)到本地的數(shù)組,然后進(jìn)入標(biāo)準(zhǔn)的設(shè)備創(chuàng)建流程:

if (qemu_opts_foreach(qemu_find_opts("netdev"),
                          net_init_netdev, NULL, errp)) {
        return -1;
    }

所以netdev的設(shè)備創(chuàng)建入口就是net_init_netdev函數(shù),在net_init_netdev函數(shù)里,根據(jù)type=vhost-user類型,執(zhí)行net_init_vhost_user()接口創(chuàng)建設(shè)備。

該接口首先執(zhí)行net_vhost_claim_chardev(),匹配其依賴的chardev,也就是命令行中的char1,找到相應(yīng)的Chardev*設(shè)備。

然后調(diào)用net_vhost_user_init()接口進(jìn)行真實(shí)的初始化操作。最終,vhost-user類型的netdev設(shè)備生成的結(jié)構(gòu)實(shí)體是什么呢?

圖片圖片

最終NetClientState被掛載到了全局變量net_clients鏈表中。另外,還有一組數(shù)據(jù)結(jié)構(gòu)是在檢測(cè)到vhost后端socket連接之后創(chuàng)建的。socket連接之后會(huì)調(diào)用上圖中的事件處理函數(shù)net_vhost_user_event,進(jìn)而調(diào)用vhost_user_start()接口。

圖片圖片

vhost-user的net設(shè)備申請(qǐng)的數(shù)據(jù)結(jié)構(gòu)和框架,其中vhost_net中vhost_dev結(jié)構(gòu)有一個(gè)重要的vhost_ops指針,對(duì)后端的接口都在這里。前面提到過(guò)vhost的后端有用戶態(tài)和內(nèi)核態(tài)兩種,所以VhostOps的實(shí)現(xiàn)也有兩種,因?yàn)槲覀冎付藇host_user類型的后端,實(shí)際vhost_ops會(huì)初始化為user_ops。VhostOps是與后端類型相關(guān)的不同的處理方式。

3.2 QEMU與VHOST通信

qemu與vhost的通信是通過(guò)socket的方式。具體的實(shí)現(xiàn)都在vhost-user的backend接口里,以VhostOps的封裝形式提供給virtio_net層使用。

(1)通信接口

VIRTIO_PCI_STATUS標(biāo)識(shí)控制面協(xié)商的狀態(tài),當(dāng)VIRTIO_CONFIG_S_DRIVER_OK置位時(shí),說(shuō)明控制面協(xié)商完成,此時(shí)啟動(dòng)數(shù)據(jù)面的傳輸。所以VHOST層面的交互是從這個(gè)狀態(tài)啟動(dòng)的。而該狀態(tài)通過(guò)virtio_set_status()接口調(diào)用到virtio_net_set_status()和virtio_net_vhost_status(),判斷是第一次VIRTIO_CONFIG_S_DRIVER_OK標(biāo)識(shí)置位,則此時(shí)認(rèn)為需要啟動(dòng)數(shù)據(jù)面了,會(huì)調(diào)用vhost_net_start()接口啟動(dòng)與VHOST后端的交互,對(duì)于數(shù)據(jù)面來(lái)說(shuō),最主要的是隊(duì)列相關(guān)的信息。如下圖示:

圖片圖片

其實(shí)VhostOps里的每個(gè)接口根據(jù)名字都可以直觀的推斷出實(shí)現(xiàn)的作用,比如:

  • vhost_set_vring_num是設(shè)置隊(duì)列的大小
  • vhost_set_vring_addr是設(shè)置ring的基地址(GPA)
  • vhost_set_vring_base設(shè)置last_avail_index
  • vhost_set_vring_kick是設(shè)置隊(duì)列kick需要的eventfd信息

vhost_set_mem_table接口,我們知道qemu里獲取的guest的地址都是GUEST地址空間的,一般是GPA,那么qemu在創(chuàng)建虛擬機(jī)的時(shí)候是知道GPA到HVA的映射關(guān)系的,所以根據(jù)GPA可以輕松獲得HVA,進(jìn)而操作進(jìn)程地址空間的VA即可。但作為獨(dú)立的進(jìn)程,VHOST-USER擁有獨(dú)立的地址空間,是不可以使用QEMU進(jìn)程的VA的。所以QEMU需要將自己記錄的一組或多組[GPA,HVA,size,memfd]通知給VHOST-USER。VHOST_USER收到這幾個(gè)信息后將這片內(nèi)存空間通過(guò)mmap映射到本地進(jìn)程的地址空間,然后一并記錄到記錄到本地的memory table。

mmap_addr = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE,
				 MAP_SHARED | populate, reg->fd, 0);

對(duì)于為何可以使用qemu里的文件句柄fd,具體原理沒(méi)有深究,看起來(lái)內(nèi)核是允許這樣一種共想文件句柄的方式的。注意需要在qemu的命令行里指定memory share=on的方式就可以。

-object memory-backend-file,id=mem,size=3072M,mem-path=/dev/hugepages,share=on

等后續(xù)接收到vring address的配置時(shí),VHOST-USER就通過(guò)傳入的GPA,查表找到memory table里的表項(xiàng),獲取自有進(jìn)程空間的VA,就可以訪問(wèn)真實(shí)的地址空間了。

(2)數(shù)據(jù)通信格式

具體的數(shù)據(jù)通信格式來(lái)說(shuō),QEMU與VHOST-USER的消息通過(guò)socket的方式,QEMU和VHOST-USER一個(gè)作為client,另一個(gè)作為server。socket文件路徑是在qemu命令行里傳入的。

數(shù)據(jù)通信的格式是固定的,都是一個(gè)標(biāo)準(zhǔn)的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字段是一個(gè)union類型,標(biāo)識(shí)具體的命令類型,所有的命令類型:

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框架設(shè)計(jì)

VHOST-USER是DPDK一個(gè)重要的功能,主要的代碼在lib/librte_vhost下。

VHOST的框架還是比較直接的,整體看起來(lái)DPDK的代碼框架比QEMU的簡(jiǎn)潔很多,更加容易理解。DPDK主要是作為一個(gè)接口庫(kù)使用,所以lib下面也是對(duì)外提供操作接口,供調(diào)用方使用。

4.1 VHOST初始化流程

VHOST模塊初始化流程的關(guān)鍵接口有三個(gè):

①rte_vhost_driver_register(const char *path, uint64_t flags)

申請(qǐng)并初始化vhost_user_socket結(jié)構(gòu),vsocket指針存入全局?jǐn)?shù)組vhost_user.vsockets[]中;
打開(kāi)path對(duì)應(yīng)的socket文件,fd存入vsocket->socket_fd中。

②rte_vhost_driver_callback_register(const char *path, struct vhost_device_ops const * const ops)

注冊(cè)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、申請(qǐng)vhost_user_connection結(jié)構(gòu)體;
    b、申請(qǐng)virtio_net結(jié)構(gòu)體,存儲(chǔ)到全局?jǐn)?shù)組vhost_devices,返回其在數(shù)組中的索引vid;
    c、conn配置,conn->fd =fd; conn->vsocket=vsocket; conn->vid =vid;
    d、注冊(cè)socket接口的處理函數(shù)

fdset_add(&vhost_user.fdset, fd, vhost_user_read_cb, NULL, conn);

整個(gè)初始化流程到這里就結(jié)束了,后面就開(kāi)始等待qemu的消息,對(duì)應(yīng)的消息處理函數(shù)就是vhost_user_read_cb函數(shù)。

4.2 VHOST與QEMU通信流程

初始化流程創(chuàng)建了vsocket結(jié)構(gòu)(與qemu的socket實(shí)體),創(chuàng)建了virtio_net結(jié)構(gòu)(設(shè)備實(shí)體),并注冊(cè)了socket消息的處理函數(shù),后面就可以監(jiān)聽(tīng)socket的信息,作為qemu的后端完善和建立整個(gè)流程。作為virtio設(shè)備的backend,vhost在初始化完成后,所有的動(dòng)作都是由前端的socket消息觸發(fā)的。

所有的消息處理函數(shù)以MSG的request請(qǐng)求類型字段作為索引,記錄在vhost_message_handlers數(shù)組中。具體的處理不詳細(xì)描述,主要就是將接收到的信息記錄到本地,供數(shù)據(jù)面啟動(dòng)后使用。

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”,說(shuō)明從這里開(kāi)始VHOST啟動(dòng)數(shù)據(jù)面的收發(fā)。查看上面的日志,是在兩個(gè)隊(duì)列的地址信息配置完成之后。對(duì)應(yīng)到QEMU的流程,就是QEMU里的VIRTIO設(shè)備device_status的VIRTIO_CONFIG_S_DRIVER_OK狀態(tài)標(biāo)志置位時(shí),調(diào)用virtio_net_start()接口做了這個(gè)動(dòng)作。

至此,整個(gè)數(shù)據(jù)通道完全建立。

一般的使用場(chǎng)景是在OVS添加一個(gè)端口,關(guān)聯(lián)到剛剛分析的VHOST-USER(指定與qemu的socket-path),實(shí)現(xiàn)對(duì)GUEST網(wǎng)口的數(shù)據(jù)收發(fā)。VHOST-USER作為client的場(chǎng)景,可以使用如下的命令行建立一個(gè)端口。

ovs-vsctl add-port br0 vhost-client-1 \
    -- set Interface vhost-client-1 type=dpdkvhostuserclient \
         options:vhost-server-path=$VHOST_USER_SOCKET_PATH

五、用QEMU實(shí)現(xiàn)的virtio網(wǎng)卡

virtio network device是一個(gè)虛擬以太網(wǎng)卡,它支持 TX/RX 多隊(duì)列??站彌_區(qū)放置在RX virtqueue中用于接收數(shù)據(jù)包,而輸出的數(shù)據(jù)包則放在TX virtqueue中進(jìn)行傳輸。另一個(gè) virtqueue 用于driver, device之間的管理通信,如設(shè)置mac 地址或修改隊(duì)列的數(shù)量。virtio network device支持許多卸載功能,如checksum計(jì)算,GSO/GRO,并讓真實(shí)物理網(wǎng)絡(luò)設(shè)備來(lái)做這些。

為了發(fā)送數(shù)據(jù)包,driver在available ring中填入一個(gè)描述符,其中包含卸載信息和數(shù)據(jù)幀緩沖區(qū), 數(shù)據(jù)幀可以以SG(scatter/gather)形式由多個(gè)描述串聯(lián)組成。Descriptor table/available ring/used ring這些數(shù)據(jù)結(jié)構(gòu)由guest中的driver分配并管理,但由于device實(shí)際位于qemu內(nèi),而qemu可以訪問(wèn)所有g(shù)uest內(nèi)存,所以device能夠定位緩沖區(qū)并讀取或?qū)懭胨鼈儭?/span>

發(fā)送一個(gè)報(bào)文時(shí) virtio-net device和 virtio-net driver的處理流程:

  • qemu啟動(dòng)guest后由guest內(nèi)部的pci總線機(jī)制發(fā)現(xiàn)virtio設(shè)備并加載virtio-net driver。
  • guest內(nèi)部的virtio-net driver分配好Descriptor table/available ring/used ring等核心數(shù)據(jù)結(jié)構(gòu),并寫入PCI MMIO寄存器,進(jìn)而后端virtio-net device也知道了virtqueue的內(nèi)存布局。
  • driver填充完要發(fā)送的數(shù)據(jù)包后,它會(huì)觸發(fā)“available ring notification”(寫PCI MMIO中的notification),將控制權(quán)返回給 QEMU。
  • virtio-net device通過(guò)sendmsg系統(tǒng)調(diào)用將數(shù)據(jù)包寫入到內(nèi)核tun設(shè)備上,在host kernel看來(lái),網(wǎng)絡(luò)協(xié)議棧從tun設(shè)備上收到一個(gè)來(lái)自VM的報(bào)文。進(jìn)一步的host kernel網(wǎng)絡(luò)協(xié)議??梢允褂肙VS等轉(zhuǎn)發(fā)機(jī)制轉(zhuǎn)發(fā)這個(gè)報(bào)文。
  • Qemu 中的virtio-net device通知guest緩沖區(qū)操作(讀取或?qū)懭耄┮淹瓿?,它通過(guò)將數(shù)據(jù)放入虛擬隊(duì)列并發(fā)送used ring notification來(lái)觸發(fā)guest vCPU 中的中斷。

接收數(shù)據(jù)包的過(guò)程與發(fā)送數(shù)據(jù)包的過(guò)程類似。唯一的區(qū)別是,在這種情況下,空緩沖區(qū)由guest預(yù)先分配并供device使用,以便它可以將傳入數(shù)據(jù)寫入它們。

Qemu實(shí)現(xiàn)virtio-net device的缺點(diǎn):

  • 性能開(kāi)銷:虛擬化技術(shù)本身會(huì)引入一定的性能開(kāi)銷。盡管virtio-net是為了提高虛擬網(wǎng)絡(luò)設(shè)備性能而設(shè)計(jì)的,但與原生網(wǎng)絡(luò)設(shè)備相比仍然會(huì)有一些額外的開(kāi)銷。
  • 虛擬化復(fù)雜性:QEMU是一個(gè)強(qiáng)大而復(fù)雜的虛擬化軟件,實(shí)現(xiàn)和配置virtio-net設(shè)備需要一定的技術(shù)和經(jīng)驗(yàn)。對(duì)于不熟悉QEMU或者虛擬化技術(shù)的人來(lái)說(shuō),可能會(huì)面臨一定的學(xué)習(xí)曲線和困難。
  • 驅(qū)動(dòng)兼容性:雖然virtio-net已經(jīng)得到了廣泛支持,并且常見(jiàn)操作系統(tǒng)都提供了對(duì)其驅(qū)動(dòng)程序的支持,但仍然可能存在某些情況下驅(qū)動(dòng)程序不完全兼容或出現(xiàn)問(wèn)題。這可能導(dǎo)致網(wǎng)絡(luò)功能不穩(wěn)定或無(wú)法正常工作。
  • 虛擬化依賴:使用virtio-net需要依賴QEMU等虛擬化軟件,在某些環(huán)境中可能存在限制或約束。例如,在嵌入式系統(tǒng)或特殊硬件平臺(tái)上,可能無(wú)法輕松地部署和運(yùn)行QEMU。

5.1 Vhost協(xié)議

為了解決qemu實(shí)現(xiàn)virtio-net device的限制,設(shè)計(jì)了 vhost 協(xié)議。vhost API 是一種基于消息的協(xié)議,它允許hypervisor (qemu)將數(shù)據(jù)平面卸載到另一個(gè)更有效地執(zhí)行數(shù)據(jù)轉(zhuǎn)發(fā)的組件(handler)。使用此協(xié)議, hypervisor向handler發(fā)送以下配置信息:

hypervisor的內(nèi)存布局。這樣,handler可以在hypervisor的內(nèi)存空間中定位虛擬隊(duì)列和緩沖區(qū)。

一對(duì)文件描述符(kick fd/call fd),用于handler發(fā)送和接收 virtio 規(guī)范中定義的通知。這些文件描述符在handler和 KVM 之間共享,因此它們可以直接通信而無(wú)需hypervisor的干預(yù)。請(qǐng)注意,每個(gè)虛擬隊(duì)列仍然可以動(dòng)態(tài)禁用此通知。

在此過(guò)程之后, hypervisor將不再處理數(shù)據(jù)包(從虛擬隊(duì)列讀取或?qū)懭?從虛擬隊(duì)列寫入)。相反,數(shù)據(jù)平面將完全卸載到handler,它現(xiàn)在可以直接訪問(wèn) virtqueues 的內(nèi)存區(qū)域以及直接向guest發(fā)送和接收通知。

vhost 消息可以在任何主機(jī)本地傳輸協(xié)議中交換,例如 Unix 套接字或字符設(shè)備,并且虛擬機(jī)管理程序可以充當(dāng)服務(wù)器或客戶端(在通信通道的上下文中)。hypervisor是協(xié)議的領(lǐng)導(dǎo)者,卸載設(shè)備是handler,它們中的任何一個(gè)都可以發(fā)送消息。Vhost protocol并沒(méi)有規(guī)定數(shù)據(jù)面一定要卸載至哪里,它既可以是host kernel(vhost-net), 也可以是host用戶態(tài)(vhost-user),還可以是硬件。以下主要針對(duì)卸載至host kernel即vhost-net。

5.2 vhost-net虛擬化網(wǎng)絡(luò)技術(shù)

vhost-net是一個(gè)內(nèi)核驅(qū)動(dòng)程序,它實(shí)現(xiàn)了 vhost 協(xié)議的handler,以實(shí)現(xiàn)高效的數(shù)據(jù)平面,即數(shù)據(jù)包轉(zhuǎn)發(fā)。在這個(gè)實(shí)現(xiàn)中,qemu 和 vhost-net 內(nèi)核驅(qū)動(dòng)程序(處理程序)使用 ioctls 來(lái)交換 vhost 消息,并且使用幾個(gè)稱為 irqfd 和 ioeventfd 的類似 eventfd 的文件描述符來(lái)與guest交換通知。

當(dāng)加載 vhost-net 內(nèi)核驅(qū)動(dòng)程序時(shí),它會(huì)創(chuàng)建/dev/vhost-net字符設(shè)備。當(dāng) qemu啟動(dòng)參數(shù)支持vhost-net 時(shí),它會(huì)打開(kāi)此字符設(shè)備并使用多個(gè) ioctl調(diào)用初始化 vhost-net 實(shí)例。這些對(duì)于將hypervisor與 vhost-net 實(shí)例相關(guān)聯(lián)、準(zhǔn)備 virtio 功能協(xié)商并將guest物理內(nèi)存映射傳遞給 vhost-net 驅(qū)動(dòng)程序是必需的。在初始化期間,vhost-net 內(nèi)核驅(qū)動(dòng)程序創(chuàng)建了一個(gè)名為 vhost-$pid 的內(nèi)核線程,其中 $pid 是管理程序進(jìn)程 pid。該線程稱為“vhost 工作線程”。

Tap 設(shè)備仍然用于guest將報(bào)文發(fā)送至host kernel,但現(xiàn)在是vhost工作線程處理 I/O 事件,即它輪詢guest drvier的通知或tap事件,并轉(zhuǎn)發(fā)數(shù)據(jù)。

Qemu 分配一個(gè)eventfd并將其注冊(cè)到 vhost 和 KVM 以實(shí)現(xiàn)通知繞過(guò)。vhost-$pid 內(nèi)核線程輪詢它,當(dāng)guest寫特定PCI MMIO地址時(shí)觸發(fā)vm-exit進(jìn)入KVM,此時(shí)KVM檢測(cè)到地址關(guān)聯(lián)了一個(gè)ioeventfd,所以寫入此fd。這種機(jī)制被命名為 ioeventfd。這樣,對(duì)特定guest內(nèi)存地址的簡(jiǎn)單讀/寫操作不需要經(jīng)過(guò)昂貴的 QEMU 進(jìn)程喚醒,可以直接路由到 vhost 工作線程。這樣做也有異步的好處,不需要 vCPU 停止(所以不需要立即進(jìn)行上下文切換)。

另一方面,qemu 分配另一個(gè) eventfd 并將其再次注冊(cè)到 KVM 和 vhost 以進(jìn)行直接 vCPU 中斷注入。這種機(jī)制稱為irqfd,它允許主機(jī)中的任何進(jìn)程通過(guò)寫入irqfd來(lái)將 vCPU 中斷注入guest,具有相同的優(yōu)點(diǎn)(異步、不需要立即上下文切換等)。

請(qǐng)注意,virtio 數(shù)據(jù)包處理后端中的此類更改對(duì)于仍然使用標(biāo)準(zhǔn) virtio 接口的guest來(lái)說(shuō)是完全透明的。

在QEMU中,virtio是一種用于虛擬機(jī)和宿主機(jī)之間進(jìn)行高性能數(shù)據(jù)傳輸?shù)臉?biāo)準(zhǔn)化接口。而virtio-net則是基于virtio標(biāo)準(zhǔn)實(shí)現(xiàn)的一種虛擬網(wǎng)絡(luò)設(shè)備,用于連接虛擬機(jī)和宿主機(jī)的網(wǎng)絡(luò)通信。

具體來(lái)說(shuō),在使用QEMU創(chuàng)建虛擬機(jī)時(shí),可以通過(guò)以下步驟實(shí)現(xiàn)virtio-net網(wǎng)卡:

  • 啟動(dòng)QEMU命令時(shí)添加參數(shù) "-device virtio-net" 或者 "-netdev user,id=net0 -device virtio-net,netdev=net0",其中"net0"為網(wǎng)絡(luò)設(shè)備的名稱。
  • QEMU將會(huì)創(chuàng)建一個(gè)名為"net0"的virtio-net網(wǎng)卡,并將其與虛擬機(jī)關(guān)聯(lián)起來(lái)。
  • 虛擬機(jī)內(nèi)部操作系統(tǒng)會(huì)將這個(gè)virtio-net網(wǎng)卡識(shí)別為一個(gè)正常的物理網(wǎng)卡,并加載相應(yīng)的驅(qū)動(dòng)程序。
  • 宿主機(jī)上運(yùn)行的QEMU負(fù)責(zé)處理從虛擬機(jī)發(fā)送過(guò)來(lái)的數(shù)據(jù)包,并轉(zhuǎn)發(fā)到宿主機(jī)上與該網(wǎng)卡對(duì)應(yīng)的物理網(wǎng)絡(luò)設(shè)備上,或者反向地將從物理網(wǎng)絡(luò)設(shè)備接收到的數(shù)據(jù)包傳遞給虛擬機(jī)。

通過(guò)使用virtio-net網(wǎng)卡,可以提供高性能、低延遲和可擴(kuò)展性好的網(wǎng)絡(luò)通信,在虛擬化環(huán)境中更加有效地利用計(jì)算資源,并提供與原生網(wǎng)絡(luò)設(shè)備相當(dāng)?shù)男阅堋?/span>

QEMU命令行參數(shù):

qemu-system-x86_64 -netdev user,id=net0 -device virtio-net,netdev=net0

啟動(dòng)腳本示例(bash):

#!/bin/bash
qemu-system-x86_64 \
  -netdev user,id=net0 \
  -device virtio-net,netdev=net0 \
  [其他QEMU參數(shù)]

請(qǐng)注意,上述代碼只是一個(gè)簡(jiǎn)單的示例,具體的QEMU參數(shù)和配置可能因?qū)嶋H情況而有所不同。你可以根據(jù)自己的需求進(jìn)行相應(yīng)的修改和調(diào)整。

此外,還需要確保在虛擬機(jī)內(nèi)部操作系統(tǒng)中加載了virtio-net驅(qū)動(dòng)程序,并正確配置網(wǎng)絡(luò)設(shè)置。具體步驟和操作方式取決于虛擬機(jī)所使用的操作系統(tǒng)。

責(zé)任編輯:武曉燕 來(lái)源: 深度Linux
相關(guān)推薦

2023-03-09 06:37:17

OKR

2009-06-15 16:05:30

設(shè)計(jì)AnnotatioJava

2021-07-02 10:10:55

SecurityJWT系統(tǒng)

2019-05-21 14:33:01

2021-09-09 08:55:50

Python項(xiàng)目驗(yàn)證碼

2021-07-05 08:41:49

RedisGEO系統(tǒng)

2017-11-08 13:31:34

分層架構(gòu)代碼DDD

2021-05-07 06:42:51

Vhost-NetLinux虛擬化

2018-05-08 18:26:49

數(shù)據(jù)庫(kù)MySQL性能

2021-04-29 09:40:32

測(cè)試IDEAirtest

2021-02-14 16:49:22

Linux虛擬化Virtio

2023-02-23 10:03:57

2010-11-09 10:03:26

2016-08-31 09:19:57

2016-12-09 13:45:21

RNN大數(shù)據(jù)深度學(xué)習(xí)

2021-09-08 09:48:39

數(shù)據(jù)庫(kù)工具技術(shù)

2021-03-30 05:58:01

JavascriptCss3轉(zhuǎn)盤小游戲

2023-02-23 10:11:15

OKR項(xiàng)目管理

2023-11-21 08:25:09

2022-01-06 06:23:49

Swagger參數(shù)解析器
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)