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, §ion,
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的逻辑是:
- 将一个ioeventfd与一个虚拟设备dev联系起来
- 该虚拟设备dev拥有写函数
- 当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的逻辑流程如下:
- QEMU分配一个eventfd,并将该eventfd加入KVM维护的eventfd数组中
- QEMU向KVM发送更新eventfd数组内容的请求
- QEMU构造一个包含IO地址,IO地址范围等元素的ioeventfd结构,并向KVM发送注册ioeventfd请求
- KVM根据传入的ioeventfd参数内容确定该段IO地址所属的总线,并在该总线上注册一个ioeventfd虚拟设备,该虚拟设备的write方法也被注册
- Guest执行OUT类指令(包括MMIO Write操作)
- VMEXIT到KVM
- 调用虚拟设备的write方法
- write方法中检查本次OUT类指令访问的IO地址和范围是否符合ioeventfd设置的要求
- 如果符合则调用eventfd_signal触发一次POLLIN事件并返回Guest
- QEMU监测到ioeventfd上出现了POLLIN,则调用相应的处理函数处理IO