IO多路复用与linux epoll

定义:I/O多路复用是指在单线程中同时监视多个文件描述符的状态变化(如可读、可写、异常等),当其中一个或多个文件描述符发生状态变化时,内核会通知应用程序进行相应的处理。
1. epoll 与 select/poll 区别
epoll的实现代码fs/eventpoll.c,其分为三个接口函数:
-
epoll_create: 创建一个epoll实例,返回一个文件描述符。 -
epoll_ctl: 向epoll实例中添加、修改或删除需要监视的文件描述符。 -
epoll_wait: 等待epoll实例中监视的文件描述符发生状态变化,并返回就绪的文件描述符列表。
具体流程分为两个阶段:

注册阶段(epoll_ctl EPOLL_CTL_ADD):
- 为目标
fd创建epitem,插入ep->rbr红黑树。 - 调用
ep_item_poll(epi, &epq.pt, 1),内部通过ep_ptable_queue_proc分配一个eppoll_entry,将ep_poll_callback注册为回调函数,并通过add_wait_queue将其挂入目标 fd(如 socket)自身的等待队列,完成事件监听挂钩。
等待/触发阶段(epoll_wait):
-
epoll_wait调用ep_poll,先检查ep->rdllist是否有就绪事件:若有则直接调用ep_try_send_events传递事件并返回;若无则将当前进程加入ep->wq(epoll 实例自身的等待队列),挂起进程等待唤醒。 - 当目标 fd(如 socket)收到数据后,内核驱动层(网络栈/设备驱动)调用
wake_up()唤醒该 fd 自身的等待队列,从而触发挂在其上的回调ep_poll_callback。 -
ep_poll_callback将对应epitem加入ep->rdllist(若此时正在向用户空间传输事件,则暂存于ep->ovflist溢出链表),然后调用wake_up(&ep->wq)唤醒epoll_wait中挂起的进程。 - 进程被唤醒,
ep_poll调用ep_send_events,后者先通过ep_start_scan调用list_splice_init(&ep->rdllist, txlist)将rdllist中的epitem原子地转移到临时链表txlist,同时将ep->ovflist从EP_UNACTIVE_PTR置为NULL,以接收此传输期间新到的事件。 -
ep_send_events遍历txlist中每个epitem,调用ep_item_poll(内部调用vfs_poll)获取 fd 上最新的 events(防止使用过时状态),再将 events 与epoll_event.data通过epoll_put_uevent拷贝到用户空间。LT 模式下若事件仍未处理完,epitem会被重新加回ep->rdllist,保证下次epoll_wait仍能返回;ET 模式则不重新加入。 -
ep_done_scan将ovflist中传输期间到达的新事件合并回ep->rdllist,并在rdllist非空时再次调用wake_up(&ep->wq)通知等待者。
epoll_wait流程如下:

图中涉及两个不同的等待队列:目标 fd(如 socket)自身的等待队列(ep_poll_callback 注册于此,收到数据时被触发);以及 ep->wq(epoll 实例的等待队列,epoll_wait 调用者挂于此处休眠)。两者概念不同,需加以区分。
1.1. epoll 与 poll/select 特点总结
select/poll给定一个fd数组,每次都是从用户态传递到内核态,内核态遍历该数组,填充有事件发生的fd。然后内核态将该数组传回用户态,用户态再次遍历该数组,处理有事件发生的fd。因此,select/poll的时间复杂度为O(n)。
epoll只在有事件发生时才通知用户程序,只将就绪的fd返回给用户程序。epoll有如下特点:
-
红黑树: 内核中epoll_ctl将需要监听的文件描述符(针对网络通信就是套接字)时,保存在红黑树中。添加/删除/索引的时间复杂度为O(log n)。 -
rdllist:ep->rdllist是内核存储的就绪事件列表,当有事件发生时比如套接字数据可读,驱动将fd对应的epitem加入到rdllist中(通过fd等待队列上的回调函数ep_poll_callback)。epoll_wait通过ep_start_scan将rdllist转移到临时txlist后扫描处理,更新对应的epoll_event数据,并返回给用户程序。
2. ET模式与LT模式区别
LT模式:当fd就绪时,epoll_wait 会一直返回该fd,直到事件被处理。例如,如果一个socket连接有数据可读,epoll_wait 会每次都返回该socket fd,直到数据被完全读取。
ET模式: 当fd从未就绪变为就绪时,epoll_wait 只会返回一次该fd。例如,如果一个socket有数据可读(kernel中缓冲区由空变为非空),epoll_wait 只会在数据第一次到达时返回该套接字,之后即使有更多数据到达,也不会再次返回。
ET的这个特性使得其要求应用程序必须一次性读取所有数据,否则可能会错过后续的数据到达事件,不能实时处理数据。因此,使用ET模式时,必须将套接字设置为非阻塞模式,并在事件处理函数中循环读取数据,直到返回EWOULDBLOCK错误。
以下是ET模式下的事件处理示例:
while (1) {
ssize_t n = read(fd, buffer, sizeof(buffer));
if (n == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break; // 数据读完
}
// 处理其他异常情形
}
// 处理读取的数据
}
明显的,
ET模式相较于LT模式,减少了系统调用次数,提高了性能。
3. 设置socket为非阻塞
| Blocking read | Non-blocking read |
|---|---|
![]() | ![]() |
ET模式下,使用阻塞模式socket,如果数据量较大,需要多次read,最后一次可能没有数据可读,此时read将一直阻塞。使用非阻塞模式socket,read返回EWOULDBLOCK即代表数据读完。
4. epoll 调用流程图
graph TD
A[启动服务器] --> B[创建 epoll 实例]
B --> C[创建监听套接字]
C --> D[设置监听套接字为非阻塞]
D --> E[绑定监听套接字到指定端口]
E --> F[监听连接请求]
F --> G[将监听套接字添加到 epoll 实例中]
G --> H[进入事件循环]
H --> I[调用 epoll_wait 等待事件发生]
I --> J{有事件发生吗?}
J -->|是| K[处理就绪事件]
J -->|否| I
K --> L{事件类型}
L -->|新连接| M[接受新连接]
M --> N[设置新连接套接字为非阻塞]
N --> O[将新连接套接字添加到 epoll 实例中]
L -->|可读事件| P[读取数据]
P --> Q{读取成功吗?}
Q -->|是| R[处理读取的数据]
Q -->|否| S[关闭套接字]
R --> T[将套接字修改为可写事件]
L -->|可写事件| U[写入数据]
U --> V[将套接字修改为可读事件]
S --> H
O --> H
T --> H
V --> H
5. libEvent
libEvent是一个跨平台的事件通知库,提供了统一的接口来处理不同操作系统上的事件驱动机制(如select、poll、epoll等)。它封装了底层的事件处理细节,使得开发者可以更方便地编写高性能的网络应用程序。
libEvent可以处理的事件类型包括(还有很多高级功能没有列出来,比如处理复杂的事件组合、优先级事件等):
- I/O事件:如套接字可读、可写等。
- 定时器事件:在指定时间后触发的事件。
- 信号事件:当特定信号发生时触发的事件,使用
evsigsel。
libEvent基本使用流程为,首先创建一个事件基础设施(event_base),然后为每个需要监视的事件创建一个事件对象(event),并将其添加到基础设施中。最后,调用event_base_dispatch进入事件循环,等待事件发生并处理。
资源:
- PcapNG Playback:基于
libEvent实现的时间戳回放 - libevent:博客文章
- 23.libevent:博客文章
参考
Enjoy Reading This Article?
Here are some more articles you might like to read next:

