Linux上的Virtio

簡介

Virtio 是一個開放標準,它定義了一個協議,用於在不同型別的驅動程式和裝置之間進行通訊,請參閱 virtio 規範的第 5 章(“裝置型別”)([1])。 它最初是作為虛擬機器監控程式實現的半虛擬化裝置的標準而開發的,但它可以用於將任何相容裝置(真實的或模擬的)與驅動程式連線。

為了便於說明,本文件將重點介紹在虛擬機器中執行的 Linux 核心的常見情況,該核心使用虛擬機器監控程式提供的半虛擬化裝置,虛擬機器監控程式透過 PCI 等標準機制將它們公開為 virtio 裝置。

裝置-驅動程式通訊:virtqueues

雖然 virtio 裝置實際上是虛擬機器監控程式中的一個抽象層,但它們向客戶機公開,就好像它們是使用特定傳輸方法(PCI、MMIO 或 CCW)的物理裝置,這與裝置本身是正交的。 virtio 規範詳細定義了這些傳輸方法,包括裝置發現、功能和中斷處理。

客戶機作業系統中的驅動程式和虛擬機器監控程式中的裝置之間的通訊是透過共享記憶體完成的(這就是使 virtio 裝置如此高效的原因),使用稱為 virtqueues 的專用資料結構,實際上是類似於網路裝置中使用的緩衝區描述符的環形緩衝區 [1]

struct vring_desc

Virtio 環形描述符,16 位元組長。 這些可以透過 next 連線在一起。

定義:

struct vring_desc {
    __virtio64 addr;
    __virtio32 len;
    __virtio16 flags;
    __virtio16 next;
};

成員

addr

緩衝區地址(客戶機物理地址)

len

緩衝區長度

flags

描述符標誌

next

如果設定了 VRING_DESC_F_NEXT 標誌,則表示鏈中下一個描述符的索引。 我們也透過此標誌連結未使用的描述符。

描述符指向的所有緩衝區都由客戶機分配,並由主機用於讀取或寫入,但不能同時用於兩者。

有關 virtqueue 的參考定義,請參閱 virtio 規範的第 2.5 章(“Virtqueues”)([1]),有關主機裝置和客戶機驅動程式如何通訊的圖解概述,請參閱 “Virtqueues and virtio ring: How the data travels” 部落格文章 ([2])。

vring_virtqueue 結構對 virtqueue 進行建模,包括環形緩衝區和管理資料。 嵌入到此結構中的是 virtqueue 結構,它是 virtio 驅動程式最終使用的資料結構

struct virtqueue

一個用於註冊緩衝區以進行傳送或接收的佇列。

定義:

struct virtqueue {
    struct list_head list;
    void (*callback)(struct virtqueue *vq);
    const char *name;
    struct virtio_device *vdev;
    unsigned int index;
    unsigned int num_free;
    unsigned int num_max;
    bool reset;
    void *priv;
};

成員

list

此裝置的 virtqueue 鏈

callback

緩衝區被消耗時要呼叫的函式(可以為 NULL)。

name

此 virtqueue 的名稱(主要用於除錯)

vdev

為此佇列建立的 virtio 裝置。

index

此佇列的從零開始的序號。

num_free

我們希望能夠容納的元素數量。

num_max

裝置支援的最大元素數量。

reset

vq 是否處於重置狀態。

priv

一個指標,供 virtqueue 實現使用。

描述

關於 num_free 的說明:對於間接緩衝區,每個緩衝區在佇列中需要一個元素,否則每個緩衝區將需要每個 sg 元素一個元素。

當裝置消耗了驅動程式提供的緩衝區時,將觸發此結構指向的回撥函式。 更具體地說,觸發器將是虛擬機器監控程式發出的中斷(請參閱 vring_interrupt())。 在 virtqueue 設定過程中(特定於傳輸方式),會為 virtqueue 註冊中斷請求處理程式。

irqreturn_t vring_interrupt(int irq, void *_vq)

在中斷時通知 virtqueue

引數

int irq

IRQ 號(已忽略)

void *_vq

要通知的 struct virtqueue

描述

呼叫 _vq 的回撥函式來處理 virtqueue 通知。

裝置發現和探測

在核心中,virtio 核心包含 virtio 匯流排驅動程式和特定於傳輸方式的驅動程式,如 virtio-pcivirtio-mmio。 然後,有針對特定裝置型別的各個 virtio 驅動程式註冊到 virtio 匯流排驅動程式。

核心如何查詢和配置 virtio 裝置取決於虛擬機器監控程式如何定義它。 以 QEMU virtio-console 裝置為例。 當使用 PCI 作為傳輸方法時,該裝置將以供應商 0x1af4(Red Hat, Inc.)和裝置 ID 0x1003(virtio 控制檯)的形式出現在 PCI 總線上,如規範中所定義,因此核心將像檢測任何其他 PCI 裝置一樣檢測到它。

在 PCI 列舉過程中,如果發現某個裝置與 virtio-pci 驅動程式匹配(根據 virtio-pci 裝置表,任何供應商 ID = 0x1af4 的 PCI 裝置)

/* Qumranet donated their vendor ID for devices 0x1000 thru 0x10FF. */
static const struct pci_device_id virtio_pci_id_table[] = {
        { PCI_DEVICE(PCI_VENDOR_ID_REDHAT_QUMRANET, PCI_ANY_ID) },
        { 0 }
};

然後探測 virtio-pci 驅動程式,如果探測順利,則將設備註冊到 virtio 匯流排

static int virtio_pci_probe(struct pci_dev *pci_dev,
                            const struct pci_device_id *id)
{
        ...

        if (force_legacy) {
                rc = virtio_pci_legacy_probe(vp_dev);
                /* Also try modern mode if we can't map BAR0 (no IO space). */
                if (rc == -ENODEV || rc == -ENOMEM)
                        rc = virtio_pci_modern_probe(vp_dev);
                if (rc)
                        goto err_probe;
        } else {
                rc = virtio_pci_modern_probe(vp_dev);
                if (rc == -ENODEV)
                        rc = virtio_pci_legacy_probe(vp_dev);
                if (rc)
                        goto err_probe;
        }

        ...

        rc = register_virtio_device(&vp_dev->vdev);

當設備註冊到 virtio 匯流排時,核心將在匯流排中查詢可以處理該裝置的驅動程式,並呼叫該驅動程式的 probe 方法。

此時,將透過呼叫適當的 virtio_find 幫助函式(如 virtio_find_single_vq() 或 virtio_find_vqs())來分配和配置 virtqueue,這些函式最終將呼叫特定於傳輸方式的 find_vqs 方法。

參考

[1] Virtio 規範 v1.2:https://docs.oasis-open.org/virtio/virtio/v1.2/virtio-v1.2.html

[2] Virtqueues and virtio ring: How the data travels https://#/en/blog/virtqueues-and-virtio-ring-how-data-travels

腳註