Try   HackMD

kvm-host 的改進

contributed by < ray90514 >

本次的改進目標為將 Virtio-blk 引入 kvm-host ,實作參考 kvmtooltinybox。virtio 基於以下規範 Virtual I/O Device (VIRTIO) Version 1.1

開發過程的式碼在 ray90514/kvm-host

TODO

  • bus
  • pci
  • virtio-pci
  • virtqueue
  • virtio-blk
  • ?

準備工作

  1. 完成 kvm-host 的安裝與執行
  2. 開啟 virtio-blk

根據 How to use Virtio 的指示,開啟以下 Linux Kernel 的編譯選項,重新編譯 bzImage

  • CONFIG_VIRTIO_PCI=y (Virtualization -> PCI driver for virtio devices)
  • CONFIG_VIRTIO_BALLOON=y (Virtualization -> Virtio balloon driver)
  • CONFIG_VIRTIO_BLK=y (Device Drivers -> Block -> Virtio block driver)
  • CONFIG_VIRTIO_NET=y (Device Drivers -> Network device support -> Virtio -network driver)
  • CONFIG_VIRTIO=y (automatically selected)
  • CONFIG_VIRTIO_RING=y (automatically selected)
  1. 了解 kvm-host 目前的實作

參考 KVM ,在呼叫 ioctl(v->vcpu_fd, KVM_RUN, 0) 後讓 KVM 執行設定好的程式,直到事件發生, kvm-host 根據事件做對應的處理,再繼續執行,如以下程式碼所示,這也是 kvm-host 的主要部份

while (1) {
    int err = ioctl(v->vcpu_fd, KVM_RUN, 0);
    if (err < 0 && (errno != EINTR && errno != EAGAIN)) {
        munmap(run, run_size);
            return throw_err("Failed to execute kvm_run");
        }
    switch (run->exit_reason) {
        case KVM_EXIT_IO:
            if (run->io.port >= COM1_PORT_BASE && run->io.port < COM1_PORT_END)
                serial_handle(&v->serial, run);
            break;
        case KVM_EXIT_INTR:
            serial_console(&v->serial);
            break;
        case KVM_EXIT_SHUTDOWN:
            printf("shutdown\n");
            munmap(run, run_size);
            return 0;
        default:
            printf("reason: %d\n", run->exit_reason);
            munmap(run, run_size);
            return -1;
    }
}

根據事件種類 KVM_EXIT_IO ,可以在 struct kvm_run 的變數可以得到以下資訊

struct {
	__u8 direction;
	__u8 size; /* bytes */
	__u16 port;
	__u32 count;
	__u64 data_offset; /* relative to kvm_run start */
} io;

模擬 io 時會需要用到這些,目前 kvm-host 只模擬了 COM1 的 serial port,之後的改進也會從這裡開始

Virtio

Virtio 是 IO 請求溝通的標準,架構如下圖所示,有一個前端和後端,前端通常作為驅動存在被 Guest OS 使用,後端則是在 Guest OS 被視為裝置的一種,後端可以是軟體模擬出來的裝置也可以是支援 Virtio 的實體裝置
以 Hypervisor 實現的後端來說,前端將 IO 請求傳給後端,後端會將請求傳給實際的裝置,等 IO 處理完成後傳回給前端,後端的這過程也就是裝置的模擬。前後端使用 Virtqueue 作為資料交換的機制

對於前述 kvm 對 io 的處理流程, Virtio 的機制使得模式切換的次數減少,以及若 Virtqueue 用共享內存實現,則可以減少資料複製的開銷,提升虛擬化下 IO 的效能
一個 Virtio 裝置包含以下幾個部分

  • Device status field
  • Feature bits
  • Notifications
  • Device Configuration space
  • One or more virtqueues

Device Status Field

用來指示裝置的狀態,通常由驅動寫入,在初始化階段使用

ACKNOWLEDGE (1)
DRIVER (2)
FAILED (128)
FEATURES_OK (8)
DRIVER_OK (4)
DEVICE_NEEDS_RESET (64)

Feature Bits

用來指示裝置所支援的特性

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.

Notification

通知包含以下三種,第一種是配置改變時由裝置發起的通知,後兩種與 Virtqueue 相關
通知的實作為 transport specific ,對於 Virtio-pci 驅動到裝置的通知由寫入特定的記憶體區域觸發 vm-exit 完成,裝置到驅動的通知則由 interrupt 完成

  • configuration change notification
  • available buffer notification
  • used buffer notification

Device Configuration Space

Virtio 支援三種 Bus 來探查及初始化 Virtio 裝置, PCI 、 MMIO 、 Channel I/O ,裝置會有根據 bus 定義的配置空間,其中包含上述所提到的 Feature bits 和 Device Status Field
Virtio-pci 也就是基於 PCI 實現的 Virtio

Virtqueues

裝置和驅動共享的資料結構,用於驅動與裝置 IO 請求的溝通
目前 Virtio 1.1 有以下兩種 Virtqueue ,目前實作的是 Packed Virtqueues

  • Split Virtqueues
  • Packed Virtqueues

Device Initialization

通常會依照以下步驟初始化裝置

  1. 重設裝置
  2. Guest OS 探查到裝置後會設置 ACNOWLEDGE (1)
  3. 確立裝置的驅動後設置 DRIVER (2)
  4. 讀取裝置的 Feature bits ,根據驅動所能提供的特性寫入一個裝置 Feature 的 subset
  5. 設置 FEATURES_OK (8) ,裝置在此之後不再接受 Feature
  6. 讀取裝置的狀態,若仍是 FEATURES_OK (8) 代表 Feature 協商成功,若不是則裝置不可用
  7. 執行特定於裝置的初始化
  8. 一切正常就設置 DRIVER_OK (4) 否則設置 FAILED (128)

若裝置運作的過程遇到錯誤,會主動設置 DEVICE_NEEDS_RESET (64)

Virtio-blk

Virtio-blk 也就是支援 Virtio 的 block device ,我們要引入的 Virtio-blk 建構於 Virtio-pci 之上

Virtio-mmio 類似 PCI 但只用於虛擬裝置,可提供給不支援 PCI 的環境,實作上較精簡

Packed Virtqueues

Split Virtqueues 的結構如下圖,驅動要發起 IO 請求會將 descriptors 填入 Descriptor Table
完成後會將 descriptors 的位置寫入 Avail Ring ,然後向裝置發起 vailable buffer notification
裝置收到通知後,會根據 Avail Ring 找出對應的 descriptors,然後依照 buffer 內容處理 IO 請求,完成後會寫入 Used Ring 及向驅動發起 used buffer notification

Packed Virtqueues 是 Virtio 1.1 提出的新的 Virtqueue 結構
基本概念與 Split Virtqueues 相似,只是將 Descriptor Table 、 Avail Ring 、 Used Ring 合併為一個 Descriptor Ring ,以有效利用 Cache
除此之外,原本的 Device Area 與 Driver Area 變為由 Device Event Suppression 和 Driver Event Suppression 來使用, Event Suppression 用來讓裝置或驅動抑制對方的通知
這三個 Area 都位於 Guest OS 的 physical memory 上

Descriptor 結構如下

struct pvirtq_desc {
    /* Element Address. */
    le64 addr;
    /* Element Length. */
    le32 len;
    /* Buffer ID. */
    le16 id;
    /* The flags depending on descriptor type. */
    le16 flags;
};

每一個 IO 請求關聯一個 Buffer ,每一個 Buffer 可以由多段連續的記憶體組成,每一段為一個 Element ,由一個 Descriptor 表示,每次發起通知可以有多組 Buffer

在 Specification , "buffer" 有時是指一段連續的記憶體也就是 element,有時是指多個 element 組合後的整體, "request" 則是指發起一次通知

裝置和驅動內部會各自維護一個值為 1 或 0 的 wrap counter ,初始值為 1 , wrap counter 設計使得要從頭存取時,不會存取到該次通知已經存取過的
write flag 的作用是標示該段記憶體是 write-only 還是 read-only
indirect flag 標示該 descriptor 指向的是一個 descriptor table ,需要協商 Feature VIRTQ_DESC_F_INDIRECT
裝置接收 avail buffer 的流程如下

  • 收到 avail buffer notification
  • 接續上一次讀取的位置,讀取 descriptor
  • 若 flags 的 avail 與裝置 wrap counter 相同且 used 與裝置 wrap counter 相反則該 descriptor avail
  • 若 flags 的 next 被設置,則 buffer 由多個 element 組成,繼續讀取下一個 descriptor ,直到 next 被設置為 0 就是 buffer 的最後一個
  • 若讀入的是 Decriptor Ring 最後一個,則將 wrap counter 反轉,然後從頭讀入
  • 處理完 IO 請求後寫入 used descriptor ,其 used flag 設置為與 wrap counter 相同
  • 若有 used buffer 可以發起 used buffer notification

裝置讀取 buffer 依照驅動寫入的順序,若有多個請求則依照請求完成的順序寫入 Descriptor Ring
同一次通知內的請求,第一個 buffer 的 used flag 要最後寫入
若 buffer 由多個 element 組成,則只需寫入第一個 descriptor

PCI

因為要實作 Virtio-pci ,我們得先了解 PCI ,參考 PCI

一個 PCI 架構如下, Host bridge 負責連接 CPU 和管理所有的 PCI 裝置及 Bus ,裝置又分為一般裝置和 Bridge , Bridge 用來連接兩個 Bus

一個 PCI 邏輯裝置提供 256 bytes 的 Configuration Space ,用以完成裝置的設定與初始化, CPU 不能直接存取這個空間,需要透過 PCI 的 Host Bridge 提供特殊的機制,讓 CPU 完成配置空間的存取
這個機制藉由 CF8 、 CFC 這兩個 IO Port,先是在 CF8 寫入要存取的配置空間暫存器的位址,然後寫入或讀出 CFC 就可以完成對該暫存器的操作

Bus Number 搭配 Device Number 可以用來識別實際上的 PCI 裝置,每個裝置可以提供不同的功能,每個功能被視為一個邏輯裝置,用 Bus Number : Device Number : Function Number 來分辨每一個邏輯裝置
256 bytes 的配置空間由 64 個 32 bits 的暫存器組成,以 Register Offset 來決定

PCI 配置空間的前 64 bytes 是每個裝置共通的,而剩下的 128 bytes 則由裝置各自定義,共通的部份如下圖所示

  • Vendor ID: 識別製造商的 ID, Virtio 為 0x1AF4
  • Device ID: 裝置的 ID, 對於 Virtio-blk 是 0x1042
  • Command: 用於操作裝置的設定,可寫
  • Status: 裝置的狀態
  • Class Code: 裝置的種類
  • Base Address Register (BAR): 裝置內部空間所映射到的位址,可寫

BAR 的組成如下, Base Address 是裝置內部的記憶體映射到 CPU 定址空間的起始位址,在裝置初始化階段由 Driver 寫入,最低位指示空間的種類,Type 指示位址長度為 32 bits 或 64 bits
Base Address 對齊裝置內部空間的大小,根據這個大小低位不可寫

自定義的部份由 Capability List 組成, Status 的 Bit 4 會告知該裝置有沒有 Capability List , Capability List​開頭的位址固定在 0x34
每一個 Capability 的第一個 Byte 為 規定好的 ID ,第二個 Byte 為下一個 Capability 的開頭位址,接下來是 Capability 的內容

Virtio-pci

Virtio-pci 的 Capibility 定義如下,每一個 cap 對應裝置的一種配置空間
該空間在 Guest OS 映射的開頭位址可由 base_address_reg[cap.bar] + cap.offset 得知

struct virtio_pci_cap {
    u8 cap_vndr; /* Generic PCI field: PCI_CAP_ID_VNDR */
    u8 cap_next; /* Generic PCI field: next ptr. */
    u8 cap_len; /* Generic PCI field: capability length */
    u8 cfg_type; /* Identifies the structure. */
    u8 bar; /* Where to find it. */
    u8 padding[3]; /* Pad to full dword. */
    le32 offset; /* Offset within bar. */
    le32 length; /* Length of the structure, in bytes. */
};

cfg_type 為 cap 的種類

/* Common configuration */
#define VIRTIO_PCI_CAP_COMMON_CFG 1
/* Notifications */
#define VIRTIO_PCI_CAP_NOTIFY_CFG 2
/* ISR Status */
#define VIRTIO_PCI_CAP_ISR_CFG 3
/* Device specific configuration */
#define VIRTIO_PCI_CAP_DEVICE_CFG 4
/* PCI configuration access */
#define VIRTIO_PCI_CAP_PCI_CFG 5

Common configuration 的結構如下
完整的 Feature bits 是 64 bits ,目前實作的 Feature 為 VIRTIO_F_VERSION_1(32)VIRTIO_F_RING_PACKED(34)

  • feature_select 用來選擇呈現的 Feature 是高 32 位還是低 32位
  • device_status 為前述之 Device Status Field
  • num_queues 為 Virtqueue 的數量
  • queue_select 寫入後,存取帶有 queue_ 的欄位會存取對應的 queue 的資訊,若queue_select 無效則 queue_size 必須為 0
  • queue_desc Descriptor Area 在 Guest OS 的位址
  • queue_driver Driver Area 在 Guest OS 的位址
  • queue_device Device Area 在 Guest OS 的位址
struct virtio_pci_common_cfg {
    /* About the whole device. */
    le32 device_feature_select; /* read-write */
    le32 device_feature; /* read-only for driver */
    le32 driver_feature_select; /* read-write */
    le32 driver_feature; /* read-write */
    le16 msix_config; /* read-write */
    le16 num_queues; /* read-only for driver */
    u8 device_status; /* read-write */
    u8 config_generation; /* read-only for driver */
    /* About a specific virtqueue. */
    le16 queue_select; /* read-write */
    le16 queue_size; /* read-write */
    le16 queue_msix_vector; /* read-write */
    le16 queue_enable; /* read-write */
    le16 queue_notify_off; /* read-only for driver */
    le64 queue_desc; /* read-write */
    le64 queue_driver; /* read-write */
    le64 queue_device; /* read-write */
};

notification cap 會在共通的 cap 結構後附加資料

struct virtio_pci_notify_cap {
    struct virtio_pci_cap cap;
    le32 notify_off_multiplier; /* Multiplier for queue_notify_off. */
};

驅動發起 avail buffer notification 時會寫入一個記憶體位置,用這個 cap 先計算相對於 BAR 的偏移量,偏移量由以下算式取得
每個 queue 有各自的 queue_notify_off ,由 Common configuration 提供

cap.offset + queue_notify_off * notify_off_multiplier

ISR status cap 用來指示 INT#x interrupt 的狀態,要求至少 single byte 的空間,當驅動讀取這個空間時會將值重設為 0 ,裝置發起 virtqueue 相關的通知時會設置 bit 1

Bits 0 1 2 to 31
Purpose Queue Interrupt Device Configuration Interrup Reserved

PCI configuration access capability 也是直接在原有的 cap 後面附加資料,提供額外存取 BAR 空間的機制

  • cap.bar: 指示要存取的 BAR 空間
  • cap.offset: 指示要存取的位址對於 BAR 的偏移量
  • cap.length: 為存取的長度
  • pci_cfg_data: 為對應的資料
struct virtio_pci_cfg_cap {
    struct virtio_pci_cap cap;
    u8 pci_cfg_data[4]; /* Data for BAR access. */
};

Device-specific configuration 由各裝置定義

Virtio-Blk

Virtio-Blk 的 Device configuration 如下,除了 capacity 其他欄位需要對應的 Feature 才會使用到

  • capacity : Block Device 的大小,以 512 bytes 的 sector 為單位
struct virtio_blk_config {
    le64 capacity;
    le32 size_max;
    le32 seg_max;
    struct virtio_blk_geometry {
        le16 cylinders;
        u8 heads;
        u8 sectors;
    } geometry;
    le32 blk_size;
    struct virtio_blk_topology {
        // # of logical blocks per physical block (log2)
        u8 physical_block_exp;
        // offset of first aligned logical block
        u8 alignment_offset;
        // suggested minimum I/O size in blocks
        le16 min_io_size;
        // optimal (suggested maximum) I/O size in blocks
        le32 opt_io_size;
    } topology;
    u8 writeback;
    u8 unused0[3];
    le32 max_discard_sectors;
    le32 max_discard_seg;
    le32 discard_sector_alignment;
    le32 max_write_zeroes_sectors;
    le32 max_write_zeroes_seg;
    u8 write_zeroes_may_unmap;
    u8 unused1[3];
};

Feature bits

Device Operation

request 的格式如下,一個 request buffer 會被分為三個 element , typereservedsector 一個, data[] 一個, status 一個

struct virtio_blk_req {
    le32 type;
    le32 reserved;
    le64 sector;
    u8 data[];
    u8 status;
};
  • type: request 的種類
#define VIRTIO_BLK_T_IN 0
#define VIRTIO_BLK_T_OUT 1
#define VIRTIO_BLK_T_FLUSH 4
#define VIRTIO_BLK_T_DISCARD 11
#define VIRTIO_BLK_T_WRITE_ZEROES 13
  • sector: 相對於裝置開頭的偏移量,以 sector 為單位
  • data: request 需要的資料
  • status: request 的狀態,由裝置寫入
#define VIRTIO_BLK_S_OK 0
#define VIRTIO_BLK_S_IOERR 1
#define VIRTIO_BLK_S_UNSUPP 2

對於 VIRTIO_BLK_T_INVIRTIO_BLK_T_OUTdata[] 是讀寫操作資料的存取處

實作

bus

bus.c
引入了一種結構,用來處理位址與裝置的映射關係,使用 linked list 管理裝置

struct dev {
    uint64_t base;
    uint64_t len;
    void *owner;
    dev_io_fn do_io;
    struct dev *next;
};

struct bus {
    uint64_t dev_num;
    struct dev *head;
};

使用以下函式將 dev 註冊到 bus

void bus_register_dev(struct bus *bus, struct dev *dev);

使用以下函式對 bus 發起 IO 請求,根據 devbaselen 找出目標裝置,然後呼叫 do_io 這個 callback
owner 是指向擁有這個 dev 的結構體,用於 callback 的參數

void bus_handle_io(struct bus *bus, void* data, uint8_t is_write, uint64_t addr, uint8_t size);

實作中有 io_busmmio_bus 處理 KVM_EXIT 的事件,以及一個 pci_bus 處理 pci 裝置的配置空間

pci

pci.c
首先註冊這兩個裝置到 io_bus , pci_addr_dev 位於 CF8 ,而 pci_bus_dev 位於 CFC

bus_register_dev(io_bus, &pci->pci_addr_dev);
bus_register_dev(io_bus, &pci->pci_bus_dev);

pci_bus_dev 的 callback 會去 pci_bus 找到我們註冊的 virtio-blk 裝置,完成對 PCI裝置配置空間的讀寫操作

static void pci_data_io(void *owner, void *data, uint8_t is_write, uint64_t offset, uint8_t size)
{
    struct pci *pci = (struct pci *) owner;
    uint64_t addr = pci->pci_addr.value | offset;
    bus_handle_io(&pci->pci_bus, data, is_write, addr, size);
}

執行 kvm-host 會發現以下錯誤訊息

PCI: Fatal: No config space access function found

追查程式碼發現錯誤原因在 arch/x86/pci/direct.cpci_sanity_check ,這個函式是用來檢查 PCI 機制的完整性

/*
 * Before we decide to use direct hardware access mechanisms, we try to do some
 * trivial checks to ensure it at least _seems_ to be working -- we just test
 * whether bus 00 contains a host bridge (this is similar to checking
 * techniques used in XFree86, but ours should be more reliable since we
 * attempt to make use of direct access hints provided by the PCI BIOS).
 *
 * This should be close to trivial, but it isn't, because there are buggy
 * chipsets (yes, you guessed it, by Intel and Compaq) that have no class ID.
 */
static int __init pci_sanity_check(const struct pci_raw_ops *o)

Epic kvmtool adventures 有提到 kvmtool 是怎麼做的,我們將 pci=conf1 加入到 kvm-host 的 KERNEL_OPTS 以跳過檢查
重新執行後 Guest Linux 能探查及初始化我們模擬的 PCI 設備了

pci 0000:00:00.0: [1af4:1042] type 00 class 0x010000

當 Guest OS 寫入 pci 配置空間的 command 時,低兩位是用來開啟或關閉記憶體映射的,此時將 bar 的位址註冊到對應的 bus 上

static inline void pci_activate_bar(struct pci_dev *dev, uint8_t bar, struct bus *bus)
{
    if (!dev->bar_active[bar] && dev->hdr.bar[bar])
        bus_register_dev(bus, &dev->space_dev[bar]);
    dev->bar_active[bar] = true;
}

static void pci_command_bar(struct pci_dev *dev)
{
    bool enable_io = dev->hdr.command & PCI_COMMAND_IO;
    bool enable_mem = dev->hdr.command & PCI_COMMAND_MEMORY;
    for (int i = 0; i < PCI_CFG_NUM_BAR; i++) {
        struct bus *bus = dev->bar_is_io_space[i] ? dev->io_bus : dev->mmio_bus;
        bool enable = dev->bar_is_io_space[i] ? enable_io : enable_mem;

        if(enable)
            pci_activate_bar(dev, i, bus);
        else
            pci_deactivate_bar(dev, i, bus);
    }
}

此時 Guest OS 可以存取到 virtio-pci 的配置空間

pci 0000:00:00.0: BAR 0: assigned [mem 0x40000000-0x400000ff]

virtio-pci

virtio-pci.c

virtio-pci.c 負責初始化 pci 裝置的配置空間及 virtio-pci 裝置的配置空間
之前註冊到 mmio_busio_bus 的 bar 裝置也是由 virtio-pci 負責初始化,其 callback 除了普通的讀寫操作,還有完成 feature select 、 queue select 等機制

virtio-pci 0000:00:00.0: enabling device (0000 -> 0002)

目前的實作將 virtio-pci 的四個 capability 的配置空間放在同一個 bar

struct virtio_pci_config {
    struct virtio_pci_common_config common_config;
    struct virtio_pci_isr_cap isr_cap;
    struct virtio_pci_notify_data notify_data;
    void *dev_config;
};

驅動通知裝置的方式是透過寫入某個位址,實作中是寫入 notify_data
收到通知後使用 virtq 的 virtq_handle_avail 處理通知

if (offset == offsetof(struct virtio_pci_config, notify_data))
    virtq_handle_avail(&dev->vq[dev->config.notify_data.vqn]);

virtq

virtq.c
virtq_handle_avail 實作如下

void virtq_handle_avail(struct virtq *vq)
{
    struct virtq_event_suppress *driver_event_suppress =
        (struct virtq_event_suppress *) vq->info.driver_addr;

    if (!vq->info.enable)
        return;
    virtq_complete_request(vq);
    if (driver_event_suppress->flags == RING_EVENT_FLAGS_ENABLE)
        virtq_notify_used(vq);
}

目前共有三種由裝置實作的操作,包括 virtq_complete_requestvirtq_notify_used

  • enable_vq 用於 Guest OS 寫入 common config 的 queue_enable 時,啟用 virtqueue
  • complete_request 組裝收到的 element ,並對實際的裝置發起 IO 請求
  • virtq_notify_used 則是通知驅動有 used buffer 可用
struct virtq_ops {
    void (*complete_request)(struct virtq *vq);
    void (*enable_vq)(struct virtq *vq);
    void (*notify_used)(struct virtq *vq);
};

virtq 的核心在 virtq_get_avail ,這個函式會根據前述之 packed virtqueues 的規定,回傳下一個 avail 的 descriptor

union virtq_desc *virtq_get_avail(struct virtq *vq)
{
    union virtq_desc *desc_ring = (union virtq_desc *) vq->info.desc_addr;
    union virtq_desc *desc = &desc_ring[vq->next_avail_idx];
    uint16_t flags = desc->pdesc.flags;
    bool avail = flags & VIRTQ_DESC_F_AVAIL;
    bool used = flags & VIRTQ_DESC_F_USED;

    if (avail != vq->used_wrap_count || used == vq->used_wrap_count) {
        return NULL;
    }
    vq->next_avail_idx++;
    if (vq->next_avail_idx >= vq->info.size) {
        vq->next_avail_idx -= vq->info.size;
        vq->used_wrap_count ^= 1;
    }
    return desc;
}

virtio-blk

virtio-blk.c
virtio-blk 負責 virtqvirtio-pci 的初始化,以及提供 device-specific config 和實作 virtq_ops

static struct virtq_ops ops = {
    .enable_vq = virtio_blk_enable_vq,
    .complete_request = virtio_blk_complete_request,
    .notify_used = virtio_blk_notify_used,
};
  • virtio_blk_enable_vq 將 Guest OS 填入的 virtq 位址轉成 kvm-host 可存取的位址
  • virtio_blk_complete_request 使用 virtq_get_avail 取得 element 並根據 struct virtio_blk_req 組合 element ,然後依照 request 的內容發起對 block device 的讀寫
  • virtio_blk_notify_used 目前僅是使用 vm_irq_trigger 透過 kvm 對 Guest OS 發起 interrupt

實作使用 disk image 作為 block device , virtio-blk 發起的讀寫請求也就是對該 file 讀寫

測試

若要在 Guest Linux 使用 EXT4 ,得開啟以下編譯選項

CONFIG_EXT4_FS=y

在 Linux 使用以下命令建立一個 ext4 的 disk image

dd if=/dev/zero of=./virtio_blk.img bs=1M count=8
mkfs.ext4 ./virtio_blk.img

使用參數 -d virtio_blk.img 執行 kvm-host ,確認 Guest Linux 有以下訊息

virtio_blk virtio0: [vda] 16384 512-byte logical blocks (8.39 MB/8.00 MiB)

使用以下命令在 Guest Linux 掛載裝置

mkdir disk
mount /dev/vda disk

使用 cat /proc/mounts 確認是否有掛載成功

/dev/vda /disk ext4 rw,relatime 0 0

嘗試在 /disk 建立檔案,並確保檔案有被寫入

touch /disk/test
sync

使用 ls /disk 可以看到 test 已被建立

lost+found  test

退出 kvm-host ,嘗試掛載 virtio_blk.img ,使用 losetup 可以查看 loop device 的編號

sudo losetup -f virtio_blk.img
mkdir /tmp/disk
sudo mount /dev/loop19 /tmp/disk

掛載成功後,使用 ls /tmp/disk 查看裡面的內容

lost+found  test

至此確認 virtio-blk 的存取正常運作

改進

eventfd

參考 eventfd(2)
eventfd 用於程式間的溝通機制,對 eventfd 寫入相當於發起通知,對 eventfd 讀取相當於等待通知

irqfd

參考 kvm: add support for irqfdqemu-kvm的irqfd机制
irqfd 是 kvm 提供用於透過 eventfd 發起 interrupt 的機制
首先透過 ioctl 將 eventfd 跟 interrupt 綁定

void vm_ioeventfd_register(vm_t *v,
                           int fd,
                           unsigned long long addr,
                           int len,
                           int flags)
{
    struct kvm_ioeventfd ioeventfd = {
        .fd = fd, .addr = addr, .len = len, .flags = flags};

    if (ioctl(v->vm_fd, KVM_IOEVENTFD, &ioeventfd) < 0)
        throw_err("Failed to set the status of IOEVENTFD");
}

原先使用 ioctl 發送 interrupt 現在改為 write

static void virtio_blk_notify_used(struct virtq *vq)
{
    struct virtio_blk_dev *dev = (struct virtio_blk_dev *) vq->dev;
    uint64_t n = 1;

    if (write(dev->irqfd, &n, sizeof(n)) < 0)
        throw_err("Failed to write the irqfd");
}

ioeventfd

參考 KVM: add ioeventfd supportqemu-kvm的ioeventfd机制

ioeventfd 為 kvm 提供用於透過 eventfd 通知 IO 事件發生的機制
當 Guest 進行 IO 操作觸發 vm-exit 時, kvm 會先判斷該位址是否有註冊 eventfd ,若有則透過 eventfd 通知,然後讓 Guest 繼續執行
用於某些實際上不傳輸資料僅作為通知的 IO 請求,可以減少時間的開銷,如 virtio-pci 的 avail buffer notification
首先將 eventfd 註冊為 ioeventfd

uint64_t addr = virtio_pci_get_notify_addr(&dev->virtio_pci_dev, vq);
vm_ioeventfd_register(v, dev->ioeventfd, addr,
                      dev->virtio_pci_dev.notify_len, 0);

然後建立一個 thread 用來等待 ioeventfd 的通知

pthread_create(&dev->vq_avail_thread, NULL, virtio_blk_vq_avail_handler,
               (void *) vq);

static void *virtio_blk_vq_avail_handler(void *arg)
{
    struct virtq *vq = (struct virtq *) arg;
    struct virtio_blk_dev *dev = (struct virtio_blk_dev *) vq->dev;
    uint64_t n;
    while (read(dev->ioeventfd, &n, sizeof(n))) {
        virtq_handle_avail(vq);
    }
    return NULL;
}

完成後當 Guest 寫入該位址, kvm 會通知這個 ioeventfd ,然後執行 virtq_handle_avail

Linux Kernel vhost

linux/drivers/vhost/

irqfd 與 ioeventfd 實際上多用於 vhost 與 kvm 的溝通, vhost 也就是負責處理 virtqueue 的 avail buffer 與實際發送 IO 請求的這部分
若 vhost 實現在 kernel space ,可以減少從 kernel space 來回切換到 user space 的開銷

但目前 Linux Kernel 沒有 vhost-blk ,有人發過 patch 但未被接受 vhost-blk: Add vhost-blk support v6
可以參考 linux/drivers/vhost/blk.c