KVM ioeventfd:省去门铃通知的用户态分派
请求内容由 virtqueue 传递,匹配的队列通知通过 eventfd 直接交给后端。
1问题背景#
virtio 驱动将请求描述符放入共享 virtqueue 后,通过写通知寄存器告知后端处理请求。若每次通知都使 KVM_RUN 返回 QEMU,再由 QEMU 分派给后端,就会增加一次用户态分派。
2原有方案#
通用设备模拟让 KVM_RUN 返回 QEMU,再根据 I/O exit 分派寄存器写。这一路径支持完整的设备行为;对于仅表示队列有新请求的门铃通知,也需要经过同样的用户态分派。
3设计方案#
预先注册通知的地址、宽度和可选数据匹配条件。命中的写操作由 eventfd_signal 转为事件通知,请求数据仍由后端从 virtqueue 读取,KVM 无须参与后端设备的具体实现。
4改进分析#
匹配的通知写不再逐次使 KVM_RUN 返回用户态,省去了 QEMU 的寄存器写分派。KVM 与 vhost 通过 eventfd 交换通知,双方无须共享设备处理的内部接口。
5源码分析#
5.1通知地址与数据匹配#
ioeventfd_in_range 检查注册地址、访问宽度和可选数据条件。不匹配的访问由其余 I/O 设备继续处理,普通 MMIO 写的载荷不能按门铃通知忽略。
721static bool722ioeventfd_in_range(struct _ioeventfd *p, gpa_t addr, int len, const void *val)723{724 u64 _val;725726 if (addr != p->addr)727 /* address must be precise for a hit */728 return false;729730 if (!p->length)731 /* length = 0 means only look at the address, so always a hit */732 return true;733734 if (len != p->length)735 /* address-range must be precise for a hit */736 return false;737738 if (p->wildcard)739 /* all else equal, wildcard is always a hit */740 return true;地址必须精确匹配。长度为 0 时忽略写入宽度;非零长度必须匹配,wildcard 决定是否还需检查数据。数据比较位于完整函数的后半段,片段不包含它。
5.2eventfd 通知传递#
匹配成功后,ioeventfd_write 调用 eventfd_signal 通知后端。后端从 virtqueue 读取并处理请求,请求内容无须经过该函数。
766/* MMIO/PIO writes trigger an event if the addr/val match */767static int768ioeventfd_write(struct kvm_vcpu *vcpu, struct kvm_io_device *this, gpa_t addr,769 int len, const void *val)770{771 struct _ioeventfd *p = to_ioeventfd(this);772773 if (!ioeventfd_in_range(p, addr, len, val))774 return -EOPNOTSUPP;775776 eventfd_signal(p->eventfd);777 return 0;778}命中预先登记的地址、宽度和可选写入值后,只向 eventfd 发信号并返回已处理;不匹配时返回 -EOPNOTSUPP,交由其余 I/O 处理路径继续处理。这里避免的是 KVM_RUN 返回用户态,并不意味着没有硬件 VM exit。
5.3virtio 队列通知注册#
legacy 通知寄存器共享一个地址,以写入值 n 识别队列;现代 MMIO 通知按队列地址注册。
335 bool modern_pio = proxy->flags & VIRTIO_PCI_FLAG_MODERN_PIO_NOTIFY;336 MemoryRegion *modern_mr = &proxy->notify.mr;337 MemoryRegion *modern_notify_mr = &proxy->notify_pio.mr;338 MemoryRegion *legacy_mr = &proxy->bar;339 hwaddr modern_addr = virtio_pci_queue_mem_mult(proxy) *340 virtio_get_queue_index(vq);341 hwaddr legacy_addr = VIRTIO_PCI_QUEUE_NOTIFY;342343 if (assign) {344 if (modern) {345 memory_region_add_eventfd(modern_mr, modern_addr, 0,346 false, n, notifier);347 if (modern_pio) {348 memory_region_add_eventfd(modern_notify_mr, 0, 2,349 true, n, notifier);350 }351 }352 if (legacy) {353 memory_region_add_eventfd(legacy_mr, legacy_addr, 2,354 true, n, notifier);355 }QEMU 把 virtqueue 的通知地址与 EventNotifier 关联:现代 MMIO 分支通过每队列地址区分通知,legacy 分支在共享通知寄存器上匹配队列号 n。这一段只展示注册,具体 KVM_IOEVENTFD ioctl 在 KVM MemoryListener 中完成。
6适用范围与限制#
该路径只适用于通知语义,eventfd 不承载任意写入数据。硬件 VM exit、后端唤醒和队列同步的成本依然存在。
代码固定为 Linux v6.12 与 QEMU v9.2.0。这里分析来宾到后端的 ioeventfd 通知,不包含反向中断注入 irqfd;不把 userspace exit 与硬件 VM exit 混为一谈。
7参考资料#
QEMU Internals: vhost architecture
路径对比基于 KVM API、作者说明和源码;没有给出本站性能实测结论。