qemu-kvm的ioeventfd机制

qemu-kvm的ioeventfd机制

Guest一个完整的IO流程包括从虚拟机内部到KVM,再到QEMU,并由QEMU最终进行分发,IO完成之后的原路返回。这样的一次路径称为同步IO,即指Guest需要等待IO操作的结果才能继续运行,但是存在这样一种情况,即某次IO操作只是作为一个通知事件,用于通知QEMU/KVM完成另一个具体的IO,这种情况下没有必要像普通IO一样等待数据完全写完,只需要触发通知并等待具体IO完成即可。

ioeventfd正是为IO通知提供机制的东西,QEMU可以将虚拟机特定地址关联一个eventfd,对该eventfd进行POLL,并利用ioctl(KVM_IOEVENTFD)向KVM注册这段特定地址,当Guest进行IO操作exit到kvm后,kvm可以判断本次exit是否发生在这段特定地址中,如果是,则直接调用eventfd_signal发送信号到对应的eventfd,导致QEMU的监听循环返回,触发具体的操作函数,进行普通IO操作。这样的一次IO操作相比于不使用ioeventfd的IO操作,能节省一次在QEMU中分发IO请求和处理IO请求的时间。

QEMU注册ioeventfd

注册EventNotifier

struct EventNotifier {
#ifdef _WIN32
    HANDLE event;
#else
    int rfd;
    int wfd;
#endif
};

QEMU进行ioeventfd注册的时候需要一个EventNotifier,该EventNotifier由event_notifier_init()初始化,event_notifier_init中判断系统是否支持EVENTFD机制,如果支持,那么EventNotifier中的rfd和wfd相等,均为eventfd()系统调用返回的新建的fd,并根据event_notifier_init收到的参数active决定是否唤醒POLLIN事件,即直接触发eventfd/EventNotifer对应的handler。

如果系统不支持EVENTFD机制,则QEMU会利用pipe模拟eventfd,略过不看。

关联IO地址&注册进KVM

在注册了EventNotifier之后,需要将EventNotifier中含有的fd(ioeventfd)与对应的Guest IO地址关联起来。

核心函数为memory_region_add_eventfd。

void memory_region_add_eventfd(MemoryRegion *mr,
                               hwaddr addr,
                               unsigned size,
                               bool match_data,
                               uint64_t data,
                               EventNotifier *e);

参数中:

  • mr指IO地址所在的MemoryRegion
  • addr表示IO地址(GPA)
  • size表示IO地址的大小
  • match_data是一个bool值,表示的是Guest向addr写入的值是否要与参数data完全一致才让KVM走ioeventfd路径,如果match_data为true,那么需要完全一致才让KVM走ioeventfd路径,如果为false。则不需要完全一致。
  • data与match_data共同作用,用于限制Guest向addr写入的值
  • e指前面注册的EventNotifier
memory_region_add_eventfd
=> memory_region_ioeventfd_before // 寻找本次要处理的ioeventfd应该在ioeventfd数组中的什么位置
=> g_realloc // 分配原ioeventfd数组大小+1的空间,用于将新的ioeventfd插入到ioeventfd数组中
=> memmove // 将第一步找到的位置之后的ioeventfd从ioeventfd数组中后移一位
=> mr->ioeventfds[i] = mrfd // 将新的ioeventfd插入到MemoryRegion的ioeventfd数组中
=> // 设置ioeventfd_update_pending
=> memory_region_transaction_commit // 该函数中会调用address_space_update_ioeventfds对KVM的ioeventfd布局进行更新

MemoryRegion中有很多ioeventfd,他们以地址从小到大的顺序排列,ioeventfd_nb是MemoryRegion中ioeventfd的数量,通过for循环找到本次要添加的ioeventfd应该放在ioeventfd数组中的什么位置,为ioeventfd数组分配原大小+sizeof(ioeventfd)的空间,然后将之前找到的ioeventfd数组中位置之后的ioeventfd向后移动一个位置,然后将新的ioeventfd插入到ioeventfd数组中。最后设置ioevetfd_update_pending标志,调用memory_region_transaction_commit更新KVM中的ioeventfd布局。

memory_region_transaction_commit
=> address_space_update_ioeventfds
   => address_space_add_del_ioeventfds
      => MEMORY_LISTENER_CALL(as, eventfd_add, Reverse, &section,
                                 fd->match_data, fd->data, fd->e)

即memory_region_add_eventfd最终会调用memory_region_transaction_commit,而后者会调用eventfd_add函数,该eventfd_add函数在qemu中的定义如下:

// PIO
static MemoryListener kvm_io_listener = {
    .eventfd_add = kvm_io_ioeventfd_add,
    .eventfd_del = kvm_io_ioeventfd_del,
    .priority = 10,
};

// MMIO
if (kvm_eventfds_allowed) {
    s->memory_listener.listener.eventfd_add = kvm_mem_ioeventfd_add;

对于MMIO和PIO,最终调用的eventfd_add函数不同,MMIO对应的是kvm_mem_ioeventfd_add,而PIO调用的是kvm_io_ioeventfd_add。KVM对MMIO和PIO注册的ioeventfd进行分辨,靠的是在调用kvm_vm_ioctl(kvm_state, KVM_IOEVENTFD, &iofd)中的iofd->flags进行辨认,如果flag为0,则为MMIO,如果flag为2,则为PIO。

接下来分别看这两个不同的eventfd_add函数。

kvm_io_ioeventfd_add

kvm_io_ioeventfd_add
=> fd = event_notifier_get_fd(e) // 获取之前注册的EventNotifier中的eventfd的fd
=> kvm_set_ioeventfd_pio(fd, section->offset_within_address_space,
              data, true, int128_get64(section->size),match_data); 
   => // 定义一个kvm_ioeventfd结构类型变量kick,将要注册的ioeventfd的data_match,io地址,flags(MMIO/PIO),io地址范围,fd填充进去
   => // 确定flags中是否要设置KVM_IOEVENTFD_FLAG_DATAMATCH,表明需要全匹配才让kvm走irqfd路径
   => // 确定flags中是否要设置KVM_IOEVENTFD_FLAG_DEASSIGN,该flag在ioctl后告知kvm,需要将某地址和该ioeventfd解除关联
   => kvm_vm_ioctl(kvm_state, KVM_IOEVENTFD, &kick) // 将ioeventfd注册进kvm

kvm_io_ioeventfd_add的逻辑很简单,就是先获取本次要注册到kvm的ioeventfd的相关信息,然后调用ioctl注册进kvm。

kvm_mem_ioeventfd_add

kvm_mem_ioeventfd_add
=> fd = event_notifier_get_fd(e) // 获取之前注册的EventNotifier中的eventfd的fd
=> kvm_set_ioeventfd_mmio(fd, section->offset_within_address_space,
               data, true, int128_get64(section->size),match_data);
   => // 定义一个kvm_ioeventfd结构类型变量iofd,将要注册的ioeventfd的data_match,io地址,flags(MMIO/PIO),io地址范围,fd填充进去
   => // 确定flags中是否要设置KVM_IOEVENTFD_FLAG_DATAMATCH,表明需要全匹配才让kvm走irqfd路径
   => // 确定flags中是否要设置KVM_IOEVENTFD_FLAG_DEASSIGN,该flag在ioctl后告知kvm,需要将某地址和该ioeventfd解除关联
   => kvm_vm_ioctl(kvm_state, KVM_IOEVENTFD, &iofd);// 将ioeventfd注册进kvm

可以看到,kvm_mem_ioeventfd_add与kvm_io_ioeventfd_add的处理步骤几乎完全一样,只是在kvm_ioeventfd结构中将flags置为0,标志这是一个MMIO ioeventfd注册。

KVM注册ioeventfd

在QEMU调用了kvm_vm_ioctl(KVM_IOEVENTFD)之后,kvm会对该ioctl做出反应。

kvm_vm_ioctl
=> case KVM_IOEVENTFD:{
    copy_from_user(&data, argp, sizeof(data))
    kvm_ioeventfd(kvm, &data)
}

kvm在获得了QEMU传入的参数,也就是kvm_ioeventfd结构的值之后,会调用kvm_ioeventfd。

kvm_ioeventfd
=> // 判断flags中是否含有KVM_IOEVENTFD_FLAG_DEASSIGN,如果有则调用解除io地址和ioeventfd关联的函数kvm_deassign_ioeventfd
   // 如果没有,则调用将io地址和ioeventfd关联起来的函数---kvm_assign_ioeventfd

kvm_assign_ioeventfd中首先从kvm_ioeventfd->flags中提取出该eventfd是MMIO还是PIO,并获得相应的总线号,也就是代码中的bus_idx,然后对kvm_ioeventfd结构中的flags进行一些检查,最终调用kvm_assign_ioeventfd_idx进行实际关联。

kvm_assign_ioeventfd_idx(kvm, bus_idx, kvm_ioeventfd)
=> eventfd = eventfd_ctx_fdget(args->fd) // 获取内核态的eventfd
=> kzalloc // 分配一个_ioeventfd结构p,用于表示eventfd和io地址之间的关联
=> p->addr    = args->addr;
		p->bus_idx = bus_idx;
		p->length  = args->len;
		p->eventfd = eventfd;
=> // 判断kvm_ioeventfd结构中的flags是否含有datamatch,如果有,则置p->datamatch为true
=> ioeventfd_check_collision // 判断本次与地址关联的ioeventfd是否之前存在
=> kvm_iodevice_init(&p->dev, &ioeventfd_ops); // 初始化_ioevetfd结构中的kvm_io_device成员,将该设备的IO操作设置为ioevetfd_ops
=> kvm_io_bus_register_dev(kvm, bus_idx, p->addr, p->length,
				      &p->dev) // 将_ioevetfd结构中的kvm_io_device 设备注册到Guest上
=> // 增加该bus上的ioeventfd_count数量
=> // 将该ioeventfd添加进ioeventfd链表中

以上代码段为kvm_assign_ioeventfd_idx,即,将ioeventfd和具体IO地址进行关联的主要过程。其中的核心数据为_ioeventfd,具体结构如下:

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;
};

list用于将当前ioeventfd链接到kvm的ioeventfd链表中去.

addr是ioeventfd对应的IO地址.

Length指的是eventfd关联的长度.

eventfd即指的是该ioeventfd对应的eventfd.

datamatch用于确认Guest访问该io地址是否需要全匹配才走ioeventfd路径.

dev用于将该ioeventfd与Guest关联起来(通过注册该dev到Guest实现).

bus_idx指的是该ioeventfd要注册到kvm的MMIO总线还是PIO总线.

wildcard与datamatch互斥,如果kvm_ioeventfd中datamatch为false,则_ioeventfd->wildcard设备true.

所以_ioeventfd描述符了一个ioeventfd要注册到kvm中的所有信息,其中包含了ioeventfd信息和需要注册到Guest的总线和设备信息。

所以整个KVM注册ioeventfd的逻辑是:

  1. 将一个ioeventfd与一个虚拟设备dev联系起来
  2. 该虚拟设备dev拥有写函数
  3. 当Guest访问ioeventfd对应的io地址时,则调用虚拟设备的write方法。
static const struct kvm_io_device_ops ioeventfd_ops = {
	.write      = ioeventfd_write,
	.destructor = ioeventfd_destructor,
};

需要注意的是,ioeventfd对应的文件操作只有write操作,而没有read操作。

write操作对应Guest中写入ioeventfd对应的IO地址时触发的操作,也就是Guest执行OUT类汇编指令时触发的操作,相反read操作就是Guest执行IN类汇编指令时触发的操作,OUT类指令只是简单向外部输出数据,无需等待QEMU处理完成即可继续运行Guest,但IN指令需要从外部获取数据,必须要等待QEMU处理完成IO请求再继续运行Guest。

ioeventfd设计的初衷就是节省Guest运行OUT类指令时的时间,IN类指令执行时间无法节省,因此这里的ioeventfd 文件操作中只有write而没有read。

ioeventfd对应的虚拟dev的操作(write)

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);

    if (!ioeventfd_in_range(p, addr, len, val))
        return -EOPNOTSUPP;

    eventfd_signal(p->eventfd, 1);
    return 0;
}

可以看到ioeventfd_write函数首先从kvm_io_device得到了_ioeventfd,然后检查访问的地址和长度是否符合ioeventfd设置的条件,如果符合,则触发eventfd_signal,后者增加了eventfd_ctx->count的值,并唤醒等待队列中的EPOLLIN进程。

虚拟机进行IO操作时QEMU-kvm的处理

当虚拟机向注册了ioeventfd的地址写数据时,与所有IO操作一样,会产生vmexit,接下来的函数处理流程为:

handle_io
=> kvm_fast_pio
   => kvm_fast_pio_out
      => emulator_pio_out_emulated
         => emulator_pio_in_out
            => kernel_pio
               => kvm_io_bus_write
int kvm_io_bus_write(struct kvm_vcpu *vcpu, enum kvm_bus bus_idx, gpa_t addr,
		     int len, const void *val)
{
	struct kvm_io_bus *bus;
	struct kvm_io_range range;
	int r;

	range = (struct kvm_io_range) {
		.addr = addr,
		.len = len,
	};

	bus = srcu_dereference(vcpu->kvm->buses[bus_idx], &vcpu->kvm->srcu);
	if (!bus)
		return -ENOMEM;
	r = __kvm_io_bus_write(vcpu, bus, &range, val);
	return r < 0 ? r : 0;
}

kvm_io_bus_write首先构造了一个kvm_io_range结构,其中记录了本次Guest操作的IO地址和长度,然后调用__kvm_io_bus_write.

static int __kvm_io_bus_write(struct kvm_vcpu *vcpu, struct kvm_io_bus *bus,
			      struct kvm_io_range *range, const void *val)
{
	int idx;

	idx = kvm_io_bus_get_first_dev(bus, range->addr, range->len);
	if (idx < 0)
		return -EOPNOTSUPP;

	while (idx < bus->dev_count &&
		kvm_io_bus_cmp(range, &bus->range[idx]) == 0) {
		if (!kvm_iodevice_write(vcpu, bus->range[idx].dev, range->addr,
					range->len, val))
			return idx;
		idx++;
	}

	return -EOPNOTSUPP;
}

在__kvm_io_bus_write中,kvm_io_bus_get_first_dev用于获得bus上由kvm_io_range指定的具体地址和长度范围内的第一个设备的id,然后在bus的这个地址范围内,针对每一个设备调用kvm_iodevice_write,该函数会调用每个设备之前注册好的kvm_io_device_ops操作函数,对于ioeventfd”设备”来说,就是我们上面提到的ioeventfd_write,该函数检查访问的地址和长度是否符合ioeventfd设置的要求,如果符合则调用eventfd_signal触发一次POLLIN事件,如果QEMU有对该eventfd的检测,便会在QEMU中进行本次IO的处理,与此同时,kvm中的kernel_pio会返回0,表示成功完成了IO请求。

总结

整个ioeventfd的逻辑流程如下:

  1. QEMU分配一个eventfd,并将该eventfd加入KVM维护的eventfd数组中
  2. QEMU向KVM发送更新eventfd数组内容的请求
  3. QEMU构造一个包含IO地址,IO地址范围等元素的ioeventfd结构,并向KVM发送注册ioeventfd请求
  4. KVM根据传入的ioeventfd参数内容确定该段IO地址所属的总线,并在该总线上注册一个ioeventfd虚拟设备,该虚拟设备的write方法也被注册
  5. Guest执行OUT类指令(包括MMIO Write操作)
  6. VMEXIT到KVM
  7. 调用虚拟设备的write方法
  8. write方法中检查本次OUT类指令访问的IO地址和范围是否符合ioeventfd设置的要求
  9. 如果符合则调用eventfd_signal触发一次POLLIN事件并返回Guest
  10. QEMU监测到ioeventfd上出现了POLLIN,则调用相应的处理函数处理IO
posted @ 2021-02-24 12:52  EwanHai  阅读(2470)  评论(0编辑  收藏  举报