kernel version: 4.18
qemu version: 4.1.0
一 前言
1 eventfd是什么?
eventfd可以用于线程或者父子进程间通信,内核通过eventfd也可以向用户空间进程发消息。其核心实现是在内核空间维护一个计数器,向用户空间暴露一个与之关联的匿名fd。不同线程通过读写该fd通知或等待对方,内核通过写该fd通知用户程序。
2 ioeventfd是什么?
当QEMU模拟一个设备时,首先将这个设备的物理地址空间信息摘出来,对应关联一个回调函数,然后传递给KVM,其目的是告知KVM,当虚机内部有访问该物理地址空间的动作时,KVM调用QEMU关联的回调函数通知QEMU,这样就能实现针对具体物理区间的通知。这个实现就是ioeventfd。
3 guest kvm qemu 三者之间如何交互?
当guest写ioeventfd所在的地址空间,exit到kvm后,kvm触发一次pollin事件,QEMU监听到后调用回调函数,进行io操作。
4 ioeventfd如何对应guest内具体的设备?
qemu拉起进程时,初始化virtio设备,virtio设备拥有自己的地址空间,qemu将地址空间信息提取出来,封装成ioeventfd,通过ioctl命令字向KVM注册这段特定地址。
二 qemu 和 ioveventfd
传统的QEMU设备模拟,当虚机访问到pci的配置空间或者BAR空间时,会触发缺页异常而VM-Exit,kvm检查到虚机访问的是用户QEMU模拟的用户态设备的空间,这是I0操作,会退出到用户态交给QEMU处理。有一种解决方法就是让kvm通知QEMU,把要处理io这件事情通知到QEMU就可以了,这样就节省了一个内核态到用户态的开销。当QEMU模拟一个设备时,首先将这个设备的物理地址空间信息摘出来,对应关联一个回调函数,然后传递给KVM,其目的是告知KVM,当虚机内部有访问该物理地址空间的动作时,KVM调用QEMU关联的回调函数通知QEMU,这样就能实现针对具体物理区间的通知。这个实现就是ioeventfd。
1 数据结构
以virtio磁盘为例,virtio磁盘是一个pci设备,它有pci空间,这些空间的内存都是QEMU模拟的,当虚机写这些pci空间时,QEMU需要做对应的处理,在virtio磁盘初始化成功后,它就会将自己的地址空间信息提取出来,封装成ioeventfd,通过ioct1命令字传递给KVM,ioeventfd中包含一个QEMU提前通过eventfd创建好的fd,KVM通知QEMU是就往这个fd中写1。
struct kvm_ioeventfd {
__u64 datamatch;
__u64 addr; /* legal pio/mmio address */
__u32 len; /* 1, 2, 4, or 8 bytes; or 0 to ignore length */
__s32 fd;
__u32 flags;
__u8 pad[36];
};
QEMU是通过MemoryRegion来进行虚机内存管理的,一个MR可以对应一段虚机的内存区间,MR结构中有两个成员与ioeventfd相关:
struct MemoryRegion {
...
unsigned ioeventfd_nb;
MemoryRegionIoeventfd *ioeventfds;
...
};
struct MemoryRegionIoeventfd {
AddrRange addr;
bool match_data;
uint64_t data;
EventNotifier *e;
};
2 注册流程
QEMU注册ioeventfd的时间点是在virtio磁盘驱动初始化成功之后:
virtio_pci_common_write
virtio_pci_start_ioeventfd
virtio_bus_start_ioeventfd
vdc->start_ioeventfd
virtio_blk_data_plane_start
virtio_blk_data_plane_start
for (i = 0; i < nvqs; i++) { //为virtio磁盘的每个队列都设置一个ioeventfd
virtio_bus_set_host_notifier(VIRTIO_BUS(qbus), i, true);
k->ioeventfd_assign(proxy, notifier, n, true);
virtio_pci_ioeventfd_assign
memory_region_add_eventfd
memory_region_transaction_commit
...
kvm_vm_ioctl
3 触发流程
qemu依靠事件循环机制,轮询ioeventfd,当检查到ioeventfd事件,调用相应的回调函数去处理io请求。
main_loop_wait
os_host_main_loop_wait
if g_main_context_check //检查到ioeventfd事件,dispatch。
g_main_context_dispatch
aio_ctx_dispatch
aio_ctx_dispatch
aio_dispatch
aio_dispatch_handlers
node->io_write(node->opaque);
virtio_queue_host_notifier_aio_read
virtio_queue_notify_aio_vq
vq->handle_aio_output
virtio_blk_data_plane_handle_output
...
submit_requests //处理io请求
三 kvm 和 ioeventfd
1 数据结构
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;
__u64 addr; /* legal pio/mmio address */
__u32 len; /* 1, 2, 4, or 8 bytes; or 0 to ignore length */
__s32 fd;
__u32 flags;
__u8 pad[36];
};
内核态
struct _ioeventfd {
struct list_head list;
u64 addr;
int length;
struct eventfd_ctx *eventfd;
u64 datamatch;
struct kvm_io_device dev;
u8 bus_idx;
bool wildcard;
};
2 注册流程
KVM IOEVENTFD ioctl命令字的主要功能是在kvm上注册这个ioevent,最终目的是将ioevenfd信息添加到kvm结构的buses和ioeventfds两个成员中,注册流程:
kvm_vm_ioctl
case KVM_IOEVENTFD:
kvm_ioeventfd
kvm_assign_ioeventfd
ioeventfd_bus_from_flags
kvm_assign_ioeventfd_idx //注册ioeventfd,将用户态的信息拆解,封装成内核态kvm_io_bus和_ioeventfd结构,保存到kvm结构体的对应成员。
3 触发流程
当kvm检查VM-Exit退出原因,如果是缺页引起的退出并且原因是EPT misconfiguration,首先检查缺页的物理地址是否落在已注册ioeventfd的物理区间,如果是,调用对应区间的write函数,触发eventfd。虚机触发缺页的流程如下:
vmx_handle_exit
kvm_vmx_exit_handlers
handle_ept_misconfig
kvm_io_bus_write
__kvm_io_bus_write
kvm_iodevice_write
dev->ops->write
ioeventfd_write
eventfd_signal
wake_up_locked_poll //polling
四 ioeventfd注册的时序图
qemu
-- virtio磁盘启动data plane -->
-- virtio磁盘的每个队列关联一个ioeventfd -->
-- 注册ioeventfd,最终会通过ioctl命令字KVM_IOEVENTFD注册到KVM -->
v
kvm
-- 注册ioeventfd,将用户态的信息拆解,封装成内核态的kvm_io_bus和 ioeventfd结构,保存到kvm结构体的对应成员 -->
五 一次完整的io流程
guest
-- 往ioeventfd地址写数据,产生vmexit -->
v
kvm
-- ioeventfd_write检查访问的地址和长度是否符合,如果符合则调用eventfd_signal触发一次POLLIN事件 -->
v
qemu
-- 检测到POLLIN事件,调用回调函数处理io请求。