qemu, kvm 和 ioeventfd
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请求。
评论