本质

本质上是内核维护的 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;

/* 创建 eventfd */
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");

/* 向 eventfd 写数据,使得其对于父进程可读(EPOLLIN) */
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");

/* 创建 epoll 实例 */
int epfd = epoll_create1(0);

/* 向 epoll 实例添加 eventfd 并监控可读事件 */
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = efd;
epoll_ctl(epfd, EPOLL_CTL_ADD, efd, &ev);

/* 用于存储准备好的事件 */
struct epoll_event events[1];

/* -1 表示一直阻塞到事件发生 */
epoll_wait(epfd, events, 1, -1);

/* epoll_wait 返回,表示 eventfd 已经可读 */
printf("[Parent] epoll awakened, eventfd is ready! Preparing to read...\n");

/* 读取 eventfd(本次读取保证不会阻塞) */
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 底层数据结构存储在内核空间中,用户态实际上是借助文件相关系统调用来间接对其进行操作(如 readwrite),而内核中的子系统则可以绕过用户态读写需要走的 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
/* fs/eventfd.c:56 */
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);
}

/* fs/eventfd.c:214 */
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);

/* 如果 count 为 0,处理阻塞逻辑 */
if (!ctx->count) {
/* 非阻塞模式:立即返回 -EAGAIN */
if ((file->f_flags & O_NONBLOCK) ||
(iocb->ki_flags & IOCB_NOWAIT)) {
spin_unlock_irq(&ctx->wqh.lock);
return -EAGAIN;
}

/* 阻塞模式:睡眠等待 count 变为非零 */
if (wait_event_interruptible_locked_irq(ctx->wqh, ctx->count)) {
spin_unlock_irq(&ctx->wqh.lock);
return -ERESTARTSYS;
}
}

/* 读取并清零 count */
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 的两个衍生机制:ioeventfdirqfd

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 机制,基本流程为:

  1. QEMU 利用 VFIO 接口,将直通设备的硬件中断与一个 eventfd 绑定。
  2. QEMU 又将同一个 eventfd 作为 irqfd 注册给 KVM,与 Guest 的某个虚拟中断线绑定。
  3. (以网卡设备为例)当直通的物理网卡收到数据包后,触发宿主机物理 CPU 的硬件中断。宿主机的 VFIO 驱动充当中断处理程序,向之前绑定的 eventfd 写入一个信号。由于该 eventfd 也被 KVM 的 irqfd 监听,KVM 内核模块被立刻唤醒。KVM 直接将对应的虚拟中断注入到 Guest 内部。

可以看到,在上述的过程中,QEMU 全称只作为 控制面 而存在,只在配置虚拟机的直通设备时建立起设备硬件中断和 KVM 虚拟中断线之间的关系,而后续 数据面 中实际的“中断转发”的工作完全由内核中的不同线程(VFIO 驱动和 KVM)来完成。