本质 本质上是内核维护的 64 位无符号整型计数器,用作一种轻量级的事件通知机制,可用于线程间、父子进程间以及内核态与用户态之间高效通信。基本操作方式如下:
创建返回一个文件描述符 fd。
写操作:向 fd 写入一个 8 字节整数,内核将这个值累加到内部计数器上。
读操作:从 fd 读取时,返回当前计数器值,并将内核计数器清零(信号量模式 EFD_SEMAPHORE 为减 1)。
相较于信号,eventfd 可以深度融入 I/O 多路复用体系,计数器值支持累加,且处理上下文高度安全。
示例 父子进程通信 以下是一个 eventfd 结合 epoll 实现的父子进程通信的代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <stdint.h> #include <sys/eventfd.h> #include <sys/wait.h> #include <sys/epoll.h> int main () { int efd; pid_t pid; uint64_t u; efd = eventfd(0 , 0 ); pid = fork(); if (pid == 0 ) { printf ("[Child] Starting task, estimated time 2 seconds...\n" ); sleep(2 ); uint64_t msg = 1 ; printf ("[Child] Task completed, preparing to write signal to eventfd...\n" ); write(efd, &msg, sizeof (uint64_t )); printf ("[Child] Signal sent, preparing to exit.\n" ); exit (EXIT_SUCCESS); } else { printf ("[Parent] Using epoll to block and wait for the child process to complete...\n" ); int epfd = epoll_create1(0 ); struct epoll_event ev ; ev.events = EPOLLIN; ev.data.fd = efd; epoll_ctl(epfd, EPOLL_CTL_ADD, efd, &ev); struct epoll_event events [1]; epoll_wait(epfd, events, 1 , -1 ); printf ("[Parent] epoll awakened, eventfd is ready! Preparing to read...\n" ); read(efd, &u, sizeof (uint64_t )); printf ("[Parent] Notification received! Value read from eventfd: %llu\n" , (unsigned long long )u); wait(NULL ); close(epfd); close(efd); printf ("[Parent] Resource cleanup completed, exiting.\n" ); } return 0 ; }
执行结果如下:
1 2 3 4 5 6 7 [Parent] Using epoll to block and wait for the child process to complete... [Child] Starting task, estimated time 2 seconds... [Child] Task completed, preparing to write signal to eventfd... [Child] Signal sent, preparing to exit. [Parent] epoll awakened, eventfd is ready! Preparing to read... [Parent] Notification received! Value read from eventfd: 1 [Parent] Resource cleanup completed, exiting.
用户态与内核态通信 除了用于父子进程之前的通信外,eventfd 还能用做用户态和内核态之间的通信。由于 eventfd 底层数据结构存储在内核空间中,用户态实际上是借助文件相关系统调用来间接对其进行操作(如 read 或 write),而内核中的子系统则可以绕过用户态读写需要走的 VFS 路径,直接读写 eventfd 的底层数据结构,并结合任务调度等模块实现用户态与内核态之间的同步。其基本的工作流程如下(以下为了简单,以直接 read/write 的方式进行说明,实际工作中通常像上一小节的代码示例一样结合 epoll 使用):
当内核态想要通知用户态时,调用函数 eventfd_signal()(如下所示)。该函数获取内部的自旋锁,直接修改底层的计数器 count。随后,内核直接调用 wake_up_locked_poll(&ctx->wqh, ...),该操作会去检查结构体里的 wqh(等待队列),如果先前用户态调用了 read() 但 count == 0,内核会把该用户进程的状态设为 TASK_INTERRUPTIBLE(睡眠),并挂载到这个 wqh 队列上,交出 CPU。此时,内核态修改了 count 并触发了 wake_up,系统的调度器就会将挂在这个 wqh 上的用户进程重新标记为可运行(TASK_RUNNING)。用户进程醒来后,从 count 中读出数值,并将 count 清零,最后系统调用返回到用户态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 void eventfd_signal_mask (struct eventfd_ctx *ctx, __poll_t mask) { unsigned long flags; if (WARN_ON_ONCE(current->in_eventfd)) return ; spin_lock_irqsave(&ctx->wqh.lock, flags); current->in_eventfd = 1 ; if (ctx->count < ULLONG_MAX) ctx->count++; if (waitqueue_active(&ctx->wqh)) wake_up_locked_poll(&ctx->wqh, EPOLLIN | mask); current->in_eventfd = 0 ; spin_unlock_irqrestore(&ctx->wqh.lock, flags); } static ssize_t eventfd_read (struct kiocb *iocb, struct iov_iter *to) { struct file *file = iocb->ki_filp; struct eventfd_ctx *ctx = file->private_data; __u64 ucnt = 0 ; if (iov_iter_count(to) < sizeof (ucnt)) return -EINVAL; spin_lock_irq(&ctx->wqh.lock); if (!ctx->count) { if ((file->f_flags & O_NONBLOCK) || (iocb->ki_flags & IOCB_NOWAIT)) { spin_unlock_irq(&ctx->wqh.lock); return -EAGAIN; } if (wait_event_interruptible_locked_irq(ctx->wqh, ctx->count)) { spin_unlock_irq(&ctx->wqh.lock); return -ERESTARTSYS; } } eventfd_ctx_do_read(ctx, &ucnt); current->in_eventfd = 1 ; if (waitqueue_active(&ctx->wqh)) wake_up_locked_poll(&ctx->wqh, EPOLLOUT); current->in_eventfd = 0 ; spin_unlock_irq(&ctx->wqh.lock); if (unlikely(copy_to_iter(&ucnt, sizeof (ucnt), to) != sizeof (ucnt))) return -EFAULT; return sizeof (ucnt); }
反过来,如果是用户态通知内核态,则基本流程如下:
1 2 3 4 用户态调用 write() 写入数据 └─ 陷入内核修改 count 的值 └─ 调用 wake_up 唤醒挂在 wqh 队列上的内核线程(如 KVM 的 vCPU 线程) └─ 完成特定操作(如将中断注入虚拟机)
在虚拟化环境下,为了 Guest 与 Host 通信的高效,KVM 引入了基于 eventfd 的两个衍生机制:ioeventfd 和 irqfd。
ioeventfd 对于 ioeventfd,其基本流程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 用户态 内核态 | | | fd = eventfd() | do_eventfd() | | - craete eventfd_ctx | | - return fd | | | ioctl(KVM_IOEVENTFD) | kvm_assign_ioeventfd() |----------> fd,addr | - register to I/O bus | | | poll/epoll wait | |<-------------------------| kernel event happens | | (such as MMIO write) | | | wakeup and read | ioeventfd_write() |<-------------------------| - eventfd_signal() | | - count++ | | - wake_up() | read() return event |
irqfd 对于 irqfd,其基本流程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 用户态 内核态 | | | fd = eventfd() | do_eventfd() | | - craete eventfd_ctx | | - return fd | | | ioctl(KVM_IRQFD) | kvm_irqfd_assign() |----------> fd,gsi | - init irqfd | | - init_waitqueue_func_entry() | | - add_wait_queue() | | - register kvm->irqfds | poll/epoll wait | |<------------------------------------| (irqfd has been registered) | | | write(fd, 1) | |----------> | | | EPOLLIN event triggers | | irqfd_wakeup() | | - eventfd_ctx_do_read() | | - kvm_set_irq() | | - inject interrupt into guest | poll return, writable again | |<------------------------------------| | | (guest received an interrupt) | | vCPU handle interrupt
在虚拟化I/O中的应用 接下来将介绍 eventfd 在虚拟化 I/O 中的应用,主要分为半虚拟化的 Virtio 和基于 VFIO 的设备直通。
Virtio 在 Virtio 中,Guest 与 Host(通常是 QEMU 或 vhost 内核线程,以下均以 QEMU 为例)需要通过 Virtqueue 共享内存区域交换数据,并相互发送通知。
对于 Guest 通知 Host 的情况,通过 ioeventfd 来完成。在传统方式中,基本流程如下:
1 2 3 4 5 Guest 写 MMIO/PIO 寄存器通知 Host └─ VM-Exit 到 KVM 中 └─ KVM 发现是 I/O 操作,退出到 QEMU └─ QEMU 处理 └─ 回到 KVM 恢复虚拟机运行
而经过了 eventfd 优化后的流程为:QEMU 向 KVM 注册 ioeventfd,将 MMIO 地址与一个 eventfd 绑定,当 Guest 写入该地址时,同样触发 VM-Exit 到 KVM 中,KVM 通过写入这个 eventfd 向用户态的 QEMU 发出通知,然后 立即让 Guest 恢复运行,无需等待 QEMU 完成 I/O 的过程 。QEMU 通过 epoll 监听到该 eventfd 可读,立刻被唤醒并开始 I/O 的处理。
对于 Host 通知 Guest 的情况,通过 irqfd 来完成。在传统方式中,基本流程为:QEMU 处理完 I/O,通过 ioctl 进入 KVM,要求 KVM 给 Guest 注入一个虚拟中断。而经过 eventfd 优化后的流程为:QEMU 向 KVM 注册 irqfd,将一个 eventfd 与虚拟机的某个中断号绑定。当 QEMU 处理完网络包后,只需向这个 eventfd 写入值。KVM 通过 epoll 监听到这个事件后,直接向 Guest 注入虚拟中断。 绕过了 QEMU 用户态 ioctl 的上下文切换开销。
VFIO(设备直通) 设备直通将宿主机的物理硬件直接给虚拟机使用,实现几乎零损耗的性能。其中一个挑战是: 物理硬件产生的中断,如何高效地变成虚拟机的虚拟中断?
这通常基于 eventfd 机制,基本流程为:
QEMU 利用 VFIO 接口,将直通设备的硬件中断与一个 eventfd 绑定。
QEMU 又将同一个 eventfd 作为 irqfd 注册给 KVM,与 Guest 的某个虚拟中断线绑定。
(以网卡设备为例)当直通的物理网卡收到数据包后,触发宿主机物理 CPU 的硬件中断。宿主机的 VFIO 驱动充当中断处理程序,向之前绑定的 eventfd 写入一个信号。由于该 eventfd 也被 KVM 的 irqfd 监听,KVM 内核模块被立刻唤醒。KVM 直接将对应的虚拟中断注入到 Guest 内部。
可以看到,在上述的过程中,QEMU 全称只作为 控制面 而存在,只在配置虚拟机的直通设备时建立起设备硬件中断和 KVM 虚拟中断线之间的关系,而后续 数据面 中实际的“中断转发”的工作完全由内核中的不同线程(VFIO 驱动和 KVM)来完成。