把虚拟机的通知写直接转成 eventfd 信号
KVM 预先匹配设备通知地址,命中后 signal eventfd,使通知无需让 vCPU 逐次退回 QEMU 分派。
工程问题#
virtio 驱动已经把请求描述符放入共享 virtqueue,通知寄存器写只是在告诉后端有新工作;若每次都让 KVM_RUN 返回 QEMU,再分派给后端,就在通知路径上多走一段。
实际实现#
QEMU 先把通知地址、宽度、可选队列号与 eventfd 建立关联。KVM 的 ioeventfd_write 命中规则后发出 eventfd 信号;后端从自己的队列读取真正的请求内容。
处理流程#
- QEMU 注册设备通知地址与 EventNotifier 的关系
- KVM 按地址、宽度及可选数据匹配一次 PIO/MMIO 写
- 命中时发出 eventfd 信号,后端处理共享队列中的请求
源码片段#
固定版本 · 原文摘录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。
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 决定是否还需检查数据。数据比较位于完整函数的后半段,片段不包含它。
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 中完成。
内核写处理、匹配规则和 QEMU 注册位置分别摘录,未拼接为同一个函数。
收益与取舍#
工程分析带来的收益
- 已注册且命中的通知写无需逐次让 KVM_RUN 返回用户态
- eventfd 作为通知接口,使 KVM 与 vhost 后端不必直接依赖对方的设备实现
付出的代价
- 只适合能用预登记条件识别的通知;eventfd 信号不携带任意 MMIO 写入负载
- 仍可能发生硬件 VM exit、后端唤醒和队列同步,不能称为零退出或零开销
可比较的选择
用户态处理 KVM I/O exit通用性强,能读取每次访问的完整参数并执行设备模拟;纯门铃通知也需要往返用户态。
后端轮询 virtqueue可以减少等待唤醒,但空闲时也占用 CPU;与 eventfd 通知的成本和适用负载不同。
版本与适用范围#
代码固定为 Linux v6.12 与 QEMU v9.2.0。这里分析来宾到后端的 ioeventfd 通知,不包含反向中断注入 irqfd;不把 userspace exit 与硬件 VM exit 混为一谈。
参考来源#
源码、官方文档与原始论文相关实现与比较#
2 个相关案例Go 的 HTTP 正文传输复用 io.CopyBuffer;复制器以 Reader/Writer 为基础,并探测可选的专用传输能力。
libvirt 把 FD 和定时器接到 GLib 主循环,将停止监听与释放对象分开,并提高连接清理任务的优先级,避免资源用尽时清理一直排不上。