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请求。