contributed by < ray90514
>
本次的改進目標為將 Virtio-blk 引入 kvm-host ,實作參考 kvmtool 及 tinybox。virtio 基於以下規範 Virtual I/O Device (VIRTIO) Version 1.1
開發過程的程式碼在 ray90514/kvm-host
kvm-host
的安裝與執行根據 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)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 是 IO 請求溝通的標準,架構如下圖所示,有一個前端和後端,前端通常作為驅動存在被 Guest OS 使用,後端則是在 Guest OS 被視為裝置的一種,後端可以是軟體模擬出來的裝置也可以是支援 Virtio 的實體裝置
以 Hypervisor 實現的後端來說,前端將 IO 請求傳給後端,後端會將請求傳給實際的裝置,等 IO 處理完成後傳回給前端,後端的這過程也就是裝置的模擬。前後端使用 Virtqueue 作為資料交換的機制
對於前述 kvm 對 io 的處理流程, Virtio 的機制使得模式切換的次數減少,以及若 Virtqueue 用共享內存實現,則可以減少資料複製的開銷,提升虛擬化下 IO 的效能
一個 Virtio 裝置包含以下幾個部分
用來指示裝置的狀態,通常由驅動寫入,在初始化階段使用
ACKNOWLEDGE (1)
DRIVER (2)
FAILED (128)
FEATURES_OK (8)
DRIVER_OK (4)
DEVICE_NEEDS_RESET (64)
用來指示裝置所支援的特性
0 to 23 Feature bits for the specific device type
24 to 37 Feature bits reserved for extensions to the queue and feature negotiation mechanisms
38 and above Feature bits reserved for future extensions.
通知包含以下三種,第一種是配置改變時由裝置發起的通知,後兩種與 Virtqueue 相關
通知的實作為 transport specific ,對於 Virtio-pci 驅動到裝置的通知由寫入特定的記憶體區域觸發 vm-exit 完成,裝置到驅動的通知則由 interrupt 完成
Virtio 支援三種 Bus 來探查及初始化 Virtio 裝置, PCI 、 MMIO 、 Channel I/O ,裝置會有根據 bus 定義的配置空間,其中包含上述所提到的 Feature bits 和 Device Status Field
Virtio-pci 也就是基於 PCI 實現的 Virtio
裝置和驅動共享的資料結構,用於驅動與裝置 IO 請求的溝通
目前 Virtio 1.1 有以下兩種 Virtqueue ,目前實作的是 Packed Virtqueues
通常會依照以下步驟初始化裝置
ACNOWLEDGE (1)
DRIVER (2)
FEATURES_OK (8)
,裝置在此之後不再接受 FeatureFEATURES_OK (8)
代表 Feature 協商成功,若不是則裝置不可用DRIVER_OK (4)
否則設置 FAILED (128)
若裝置運作的過程遇到錯誤,會主動設置 DEVICE_NEEDS_RESET (64)
Virtio-blk 也就是支援 Virtio 的 block device ,我們要引入的 Virtio-blk 建構於 Virtio-pci 之上
Virtio-mmio 類似 PCI 但只用於虛擬裝置,可提供給不支援 PCI 的環境,實作上較精簡
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 的流程如下
裝置讀取 buffer 依照驅動寫入的順序,若有多個請求則依照請求完成的順序寫入 Descriptor Ring
同一次通知內的請求,第一個 buffer 的 used flag 要最後寫入
若 buffer 由多個 element 組成,則只需寫入第一個 descriptor
因為要實作 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 則由裝置各自定義,共通的部份如下圖所示
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 的 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 Fieldnum_queues
為 Virtqueue 的數量queue_select
寫入後,存取帶有 queue_
的欄位會存取對應的 queue 的資訊,若queue_select
無效則 queue_size
必須為 0queue_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 的 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];
};
request 的格式如下,一個 request buffer 會被分為三個 element , type
、 reserved
、 sector
一個, 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_IN
和 VIRTIO_BLK_T_OUT
, data[]
是讀寫操作資料的存取處
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 請求,根據 dev
的 base
和 len
找出目標裝置,然後呼叫 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_bus
和 mmio_bus
處理 KVM_EXIT
的事件,以及一個 pci_bus
處理 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.c 的 pci_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.c 負責初始化 pci 裝置的配置空間及 virtio-pci 裝置的配置空間
之前註冊到 mmio_bus
或 io_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.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_request
和 virtq_notify_used
enable_vq
用於 Guest OS 寫入 common config 的 queue_enable
時,啟用 virtqueuecomplete_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.c
virtio-blk
負責 virtq
與 virtio-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(2)
eventfd 用於程式間的溝通機制,對 eventfd 寫入相當於發起通知,對 eventfd 讀取相當於等待通知
參考 kvm: add support for irqfd 及 qemu-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");
}
參考 KVM: add ioeventfd support 及 qemu-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
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