eventfd是內核實現的高效線程通信機制,還適合於內核與用戶態的通信,KVM模塊利用eventfd實現了KVM和qemu的高效通信機制ioeventfd,這一節主要將ioeventfd的實現,分KVM的實現和qemu的使用兩部分。
KVM ioeventfd
數據結構
- ioeventfd是內核kvm模塊向qemu提供的一個vm ioctl命令字
KVM_IOEVENTFD
,對應調用kvm_vm_ioctl函數,原型如下:
int kvm_ioeventfd(struct kvm *kvm, struct kvm_ioeventfd *args)
- 這個命令字的功能是將一個eventfd綁定到一段客戶機的地址空間,這個空間可以是mmio,也可以是pio。當guest寫這段地址空間時,會觸發
EPT_MISCONFIGURATION
缺頁異常,KVM處理時如果發現這段地址落在了已註冊的ioeventfd地址區間裏,會通過寫關聯eventfd通知qemu,從而節約一次內核態到用戶態的切換開銷。用戶態傳入的參數如下:
struct kvm_ioeventfd {
__u64 datamatch; /* 1 */
__u64 addr; /* legal pio/mmio address */
__u32 len; /* 0, 1, 2, 4, or 8 bytes */
__s32 fd; /* 2 */
__u32 flags;
__u8 pad[36];
};
1. 如果flags設置了KVM_IOEVENTFD_FLAG_DATAMATCH,只有當客戶機向addr地址寫入的值時datamatch,纔會觸發event
2. eventfd關聯的fd,eventfd_ctx的結構在初始化時被放在了file結構的private_data中,首先通過fd從進程的fd表中可以找到file結構,順藤摸瓜可
以找到eventfd_ctx,從這裏能夠看出,eventfd的用法是用戶態註冊好evenfd並獲得fd,然後將fd和感興趣的地址區間封裝成kvm_ioeventfd結構體
作爲參數,調用ioctl KVM_IOEVENTFD命令字,註冊到kvm
- 用戶態信息kvm_ioeventfd需要轉化成內核態存放,當guest寫地址時找到對應的結構體,觸發event,ioeventfd內核態結構體基於eventfd,如下:
/*
* --------------------------------------------------------------------
* ioeventfd: translate a PIO/MMIO memory write to an eventfd signal.
*
* userspace can register a PIO/MMIO address with an eventfd for receiving
* notification when the memory has been touched.
* --------------------------------------------------------------------
*/
struct _ioeventfd {
struct list_head list;
u64 addr; /* 3 */
int length; /* 4 */
struct eventfd_ctx *eventfd; /* 5 */
u64 datamatch; /* 6 */
struct kvm_io_device dev; /* 7 */
u8 bus_idx; /* 8 */
};
3. 客戶機PIO/MMIO地址,當客戶機向該地址寫入數據時觸發event
4. 客戶機向該地址寫入數據時數據的長度,當length爲0時,忽略寫入數據的長度
5. 關聯的eventfd
6. 用戶態程序設置的match data,當ioeventfd被設置了KVM_IOEVENTFD_FLAG_DATAMATCH,只有滿足客戶機寫入的值等於datamatch的條件時才觸發event
7. VM-Exit退出時調用dev->ops的write操作,對應ioeventfd_write
8. 客戶機的地址被分爲了4類,MMIO,PIO,VIRTIO_CCW_NOTIFY,FAST_MMIO,bus_idx用來區分註冊的地址是哪一類
- 用戶下發
KVM_IOEVENTFD
命令字最終會生成_ioeventfd結構體,存放在內核中,由於它是和一個虛機關聯的,因此被放到了kvm結構體中維護,kvm結構體中有兩個字段,存放了ioeventfd相關的信息,如下:
struct kvm {
......
struct kvm_io_bus *buses[KVM_NR_BUSES]; /* 9 */
struct list_head ioeventfds; /* 10 */
......
}
struct kvm_io_bus {
int dev_count;
int ioeventfd_count;
struct kvm_io_range range[];
};
enum kvm_bus {
KVM_MMIO_BUS,
KVM_PIO_BUS,
KVM_VIRTIO_CCW_NOTIFY_BUS,
KVM_FAST_MMIO_BUS,
KVM_NR_BUSES
};
9. kvm中將ioeventfd註冊的地址分爲4類,每類地址可以認爲有獨立的地址空間,它們被抽象成4個bus上的地址。分別是kvm_bus所列出的
MMIO,PIO,VIRTIO_CCW_NOTIFY,FAST_MMIO,MMIO和FAST_MMIO的區別是,MMIO需要檢查寫入地址的值長度是否和ioeventfd
指定的長度相等,FAST_MMIO不需要檢查。ioeventfd的地址信息被放在kvm_io_bus的range中
10. ioeventfd的整個信息通過list成員被鏈接到了ioeventfds中
註冊流程
-KVM_IOEVENTFD
ioctl命令字的主要功能是在kvm上註冊這個ioevent,最終目的是將ioevenfd信息添加到kvm結構的buses和ioeventfds兩個成員中,註冊流程如下:
static int kvm_assign_ioeventfd(struct kvm *kvm, struct kvm_ioeventfd *args)
{
......
bus_idx = ioeventfd_bus_from_flags(args->flags); /* 1 */
kvm_assign_ioeventfd_idx(kvm, bus_idx, args); /* 2 */
......
}
1. 首先根據地址所屬總線類型,找到總線索引,方便在kvm->buses數組中找到kvm_io_bus結構
2. 註冊ioeventfd,將用戶態的信息拆解,封裝成內核態的kvm_io_bus和_ioeventfd結構,保存到kvm結構體的對應成員
- 跟蹤
kvm_assign_ioeventfd_idx
主要流程:
static int kvm_assign_ioeventfd_idx(struct kvm *kvm,
enum kvm_bus bus_idx,
struct kvm_ioeventfd *args)
{
......
eventfd = eventfd_ctx_fdget(args->fd); /* 3 */
p = kzalloc(sizeof(*p), GFP_KERNEL); /* 4 */
INIT_LIST_HEAD(&p->list);
p->addr = args->addr;
p->bus_idx = bus_idx;
p->length = args->len;
p->eventfd = eventfd;
kvm_iodevice_init(&p->dev, &ioeventfd_ops); /* 5 */
kvm_io_bus_register_dev(kvm, bus_idx, p->addr, p->length, &p->dev); /* 6 */
kvm->buses[bus_idx]->ioeventfd_count++; /* 7 */
list_add_tail(&p->list, &kvm->ioeventfds);
......
}
static const struct kvm_io_device_ops ioeventfd_ops = {
.write = ioeventfd_write,
.destructor = ioeventfd_destructor,
};
3. 根據fd從進程的描述符表中找到struct file結構,從file->private_data取出eventfd_ctx
4. 使用用戶態傳入的kvm_ioeventfd初始化一個_ioeventfd結構
5. 設置_ioeventfd的鉤子函數,當虛機寫內存缺頁時,KVM首先嚐試觸發p->dev中的write函數,檢查缺頁地址是否滿足ioeventfd觸發條件
6. 將_ioeventfd中的地址信息和鉤子函數封裝成kvm_io_range,放到kvm->buses的range[]數組中。之後kvm在處理缺頁就可以查詢到缺頁地址是否在已註冊的ioeventfd的地址區間
7. 更新ioeventfd的計數,將ioeventfd放到kvm的ioeventfds鏈表中,維護起來
- 註冊ioeventfd的數據結構如下圖所示:
觸發流程
- 當kvm檢查VM-Exit退出原因,如果是缺頁引起的退出並且原因是
EPT misconfiguration
,首先檢查缺頁的物理地址是否落在已註冊ioeventfd的物理區間,如果是,調用對應區間的write函數。虛機觸發缺頁的流程如下:
vmx_handle_exit
kvm_vmx_exit_handlers[exit_reason](vcpu)
handle_ept_misconfig
- 分析缺頁流程處理函數:
static int handle_ept_misconfig(struct kvm_vcpu *vcpu)
{
......
gpa = vmcs_read64(GUEST_PHYSICAL_ADDRESS); /* 1 */
if (!kvm_io_bus_write(vcpu->kvm, KVM_FAST_MMIO_BUS, gpa, 0, NULL)) { /* 2 */
skip_emulated_instruction(vcpu);
trace_kvm_fast_mmio(gpa);
return 1;
}
handle_mmio_page_fault(vcpu, gpa, true); /* 3 */
......
}
1. 從VMCS結構中讀取引發缺頁的虛機物理地址
2. 首先嚐試觸發ioeventfd,如果成功,eventfd會通知用戶態qemu,因此不需要退到用戶態,觸發ioeventfd之後再次進入客戶態就行了
3. 如果引發缺頁的地址不再ioeventfd監聽範圍內,進行缺頁處理,這裏的實現我們跳過,重點分析觸發ioeventfd的情形
- 分析如何觸發eventfd流程:
int kvm_io_bus_write(struct kvm *kvm, enum kvm_bus bus_idx, gpa_t addr,
int len, const void *val)
{
......
range = (struct kvm_io_range) { /* 4 */
.addr = addr,
.len = len,
};
bus = srcu_dereference(kvm->buses[bus_idx], &kvm->srcu); /* 5 */
r = __kvm_io_bus_write(bus, &range, val); /* 6 */
......
4. 將引發缺頁的物理地址封裝成kvm_io_range格式,方便與以在總線上註冊的range進行對比
5. 根據物理地址類型找到該類型所在的地址總線,這個總線上記錄這所有在該總線上註冊的ioeventfd的地址區間
6. 對比總線上的地址區間和引發缺頁的物理地址區間,如果缺頁地址區間落在的總線上的地址區間裏,調用對應的write函數觸發eventfd
- 最終,客戶機的缺頁流程會調用kvm_io_device->write()函數觸發eventfd,看下它的具體實現:
/* MMIO/PIO writes trigger an event if the addr/val match */
static int ioeventfd_write(struct kvm_vcpu *vcpu, struct kvm_io_device *this, gpa_t addr,
int len, const void *val)
{
struct _ioeventfd *p = to_ioeventfd(this); /* 7 */
if (!ioeventfd_in_range(p, addr, len, val)) /* 8 */
return -EOPNOTSUPP;
eventfd_signal(p->eventfd, 1); /* 9 */
return 0;
}
7. 根據從bus總線上取下的range[],取出其dev成員,由於dev結構體是_ioeventfd的一個成員,通過container轉化可以取出_ioeventfd
8. 檢查缺頁物理地址和range中註冊的地址是否匹配
9. 取出_ioeventfd中的eventfd_ctx結構體,往它維護的計數器中加1
- eventfd_signal實現如下:
/**
* eventfd_signal - Adds @n to the eventfd counter.
* @ctx: [in] Pointer to the eventfd context.
* @n: [in] Value of the counter to be added to the eventfd internal counter.
* The value cannot be negative.
*
* This function is supposed to be called by the kernel in paths that do not
* allow sleeping. In this function we allow the counter to reach the ULLONG_MAX
* value, and we signal this as overflow condition by returning a POLLERR
* to poll(2).
*
* Returns the amount by which the counter was incremented. This will be less
* than @n if the counter has overflowed.
*/
__u64 eventfd_signal(struct eventfd_ctx *ctx, __u64 n)
{
......
if (ULLONG_MAX - ctx->count < n) /* 10 */
n = ULLONG_MAX - ctx->count;
ctx->count += n; /* 11 */
if (waitqueue_active(&ctx->wqh)) /* 12 */
wake_up_locked_poll(&ctx->wqh, POLLIN);
......
}
10. 首先判斷下計數器是否即將溢出。如果計數器加上1之後溢出了,讓計數器直接等於最大值,內核態寫eventfd與用戶態有所區別,它不允許阻
塞,因此當計數器溢出時直接設置其爲最大值
11. 增加計數器的值
12. 喚醒阻塞在eventfd上的讀線程,如果計數器原來爲0,有讀線程阻塞在這個eventfd上,那麼此時計數器加1後,就可以喚醒這些線程
QEMU ioeventfd
- 傳統的QEMU設備模擬,當虛機訪問到pci的配置空間或者BAR空間時,會觸發缺頁異常而VM-Exit,kvm檢查到虛機訪問的是用戶QEMU模擬的用戶態設備的空間,這是IO操作,會退出到用戶態交給QEMU處理。梳理整個流程,需要經過兩次cpu模式切換,一次是非根模式到根模式,一次是內核態到用戶態。分析內核態切換到用戶態的原因,是kvm沒有模擬這個設備,處理不了這種情況才退出到用戶態讓QEMU處理,但還有一種解決方法就是讓kvm通知QEMU,把要處理IO這件事情通知到QEMU就可以了,這樣就節省了一個內核態到用戶態的開銷。
- 如果以這樣的方式實現,假設虛機所有IO操作kvm都一股腦兒通知QEMU,那QEMU也不知道到底是哪個設備需要處理,所以需要一個東西作爲QEMU和KVM之間通知的載體,當QEMU模擬一個設備時,首先將這個設備的物理地址空間信息摘出來,對應關聯一個回調函數,然後傳遞給KVM,其目的是告知KVM,當虛機內部有訪問該物理地址空間的動作時,KVM調用QEMU關聯的回調函數通知QEMU,這樣就能實現針對具體物理區間的通知。這個實現就是ioeventfd。
數據結構
- 以virtio磁盤爲例,virtio磁盤是一個pci設備,它有pci空間,這些空間的內存都是QEMU模擬的,當虛機寫這些pci空間時,QEMU需要做對應的處理,在virtio磁盤初始化成功後,它就會將自己的地址空間信息提取出來,封裝成ioeventfd,通過ioctl命令字傳遞給KVM,ioeventfd中包含一個QEMU提前通過eventfd創建好的fd,KVM通知QEMU是就往這個fd中寫1。以下就是這個ioeventfd的結構,具體含義見前面的分析。
struct kvm_ioeventfd {
__u64 datamatch;
__u64 addr; /* legal pio/mmio address */
__u32 len; /* 0, 1, 2, 4, or 8 bytes */
__s32 fd;
__u32 flags;
__u8 pad[36];
};
- QEMU是通過MemoryRegion來進行虛機內存管理的,一個MR可以對應一段虛機的內存區間,MR結構中有兩個成員與ioeventfd相關:
struct MemoryRegion {
......
unsigned ioeventfd_nb; /* 1 */
MemoryRegionIoeventfd *ioeventfds; /* 2 */
......
}
1. 表示MR對應的地址區間中有多少個ioeventfd
2. MR中包含的ioeventfds數組,每註冊一個ioeventfd,不只KVM會有記錄,QEMU也會有記錄
MemoryRegionIoeventfd
具體結構如下:
struct MemoryRegionIoeventfd {
AddrRange addr; /* 3 */
bool match_data; /* 4 */
uint64_t data;
EventNotifier *e; /* 5 */
};
3. 虛機物理地址空間,用戶告知KVM,當虛機寫這個空間時通知QEMU
4. 是否匹配寫入的值,如果match_data這個爲真,只要當寫入addr地址的值爲data時,KVM才通知QEMU
5. 當滿足上面的所有條件後,KVM通過增加EventNotifier->wfd對應的內核計數器,通知QEMU
註冊流程
- QEMU註冊ioeventfd的時間點是在virtio磁盤驅動初始化成功之後,流程如下:
static void virtio_ioport_write(void *opaque, uint32_t addr, uint32_t val)
{
switch (addr) {
case VIRTIO_PCI_STATUS:
if (val & VIRTIO_CONFIG_S_DRIVER_OK) { /* 前端驅動想device_status字段寫入DRIVER_OK,表明驅動初始化完成 */
virtio_pci_start_ioeventfd(proxy);
}
......
}
virtio_pci_start_ioeventfd
之後走到virtio_blk_data_plane_start
,這個函數會針對virtio磁盤的每個隊列,都設置一個ioeventfd,從這裏可以看出,virtio數據到達的通知針對每個隊列,都是獨立的:
virtio_pci_start_ioeventfd
virtio_bus_start_ioeventfd(&proxy->bus)
vdc->start_ioeventfd(vdev)
vdc->start_ioeventfd = virtio_blk_data_plane_start
int virtio_blk_data_plane_start(VirtIODevice *vdev)
{
......
/* Set up virtqueue notify */
for (i = 0; i < nvqs; i++) { /* 爲virtio磁盤的每個隊列都設置一個ioeventfd */
r = virtio_bus_set_host_notifier(VIRTIO_BUS(qbus), i, true);
}
......
}
virtio_bus_set_host_notifier
具體實現如下:
int virtio_bus_set_host_notifier(VirtioBusState *bus, int n, bool assign)
{
......
EventNotifier *notifier = virtio_queue_get_host_notifier(vq); /* 1 */
r = event_notifier_init(notifier, 1); /* 2 */
k->ioeventfd_assign(proxy, notifier, n, true); /* 3 */
......
}
1. 每個virtio隊列關聯一個eventfd,eventfd的fd被存放到VirtQueue->host_notifier中,這裏把它取出來,用於初始化並傳遞給KVM
2. 初始化EventNotifier,將eventfd對應的計數器設置爲1
3. 註冊ioeventfd,最終會通過ioctl命令字KVM_IOEVENTFD註冊到KVM
- ioeventfd關聯一段內存空間,由於QEMU每有地址空間變化,都會影響全部的地址空間,QEMU要統一更新地址空間的視圖,包括flatview和ioeventfd的更新。
ioeventfd_assign
對應virtio_pci_ioeventfd_assign
,流程如下:
virtio_pci_ioeventfd_assign
memory_region_add_eventfd
void memory_region_add_eventfd(MemoryRegion *mr,
hwaddr addr,
unsigned size,
bool match_data,
uint64_t data,
EventNotifier *e)
{
......
MemoryRegionIoeventfd mrfd = { /* 4 */
.addr.start = int128_make64(addr),
.addr.size = int128_make64(size),
.match_data = match_data,
.data = data,
.e = e,
};
mr->ioeventfds[i] = mrfd;
memory_region_transaction_commit() /* 5 */
......
}
4. 封裝地址信息並將其添加到MR的ioeventfds數組上
5. 檢查根MR下面的每個子MR,蒐集所有的ioeventfd,統一註冊
memory_region_transaction_commit
最終走到系統調用,下發ioctl命令字給KVM,註冊ioeventfd
memory_region_transaction_commit
address_space_update_ioeventfds
address_space_add_del_ioeventfds
MEMORY_LISTENER_CALL(as, eventfd_add, Reverse, §ion, fd->match_data, fd->data, fd->e);
kvm_io_ioeventfd_add
kvm_set_ioeventfd_pio
kvm_vm_ioctl(kvm_state, KVM_IOEVENTFD, &kick)