接上文poll机制:Linux 系统 poll 与 epoll 机制1。
3. epoll 机制:高并发 I/O 的优化实现
epoll(Efficient event polling implementation)机制诞生于 Linux 2.5.44 版本,是内核为解决高并发 I/O 场景(如万级以上 FD 监听)而设计的新一代 I/O 多路复用技术。epoll全称event poll,但其中的e有double e的味道:efficient & event。它通过红黑树管理注册 FD、就绪链表存储就绪 FD、内存映射(mmap)减少拷贝三大优化,彻底解决了poll的性能瓶颈,实现了 “O (1) 就绪事件查询”,成为高性能服务器的标配。
3.1 epoll 的核心接口与数据结构
3.1.1 三大核心系统调用
epoll 通过三个独立的系统调用来实现 “FD 注册 - 事件监听 - 就绪查询” 的完整流程,避免了poll每次调用都需重新传递 FD 集合的问题。epool是anon_inode技术的一个应用案例。
-
epoll_create():创建一个 epoll 实例(内核的核心数据结构);
-
epoll_ctl():向 epoll 实例中添加、修改或删除需监听的 FD 及其事件;
-
epoll_wait():等待 epoll 实例中注册的 FD 就绪,并返回就绪事件。
3.1.2 关键数据结构
epoll 的内核实现依赖三个核心数据结构:struct eventpoll、struct epitem、struct epoll_event。
(1)用户空间的struct epoll_event
用户进程通过该结构体与内核交互,描述 FD 的监听事件和就绪状态,定义如下:
#include <sys/epoll.h>
struct epoll_event {uint32_t events; // 监听事件/就绪事件(与poll的events含义类似)epoll_data_t data; // 关联的用户数据(如FD、自定义指针)
};
// 联合体,存储用户数据
typedef union epoll_data {void *ptr; // 自定义指针(可指向用户空间数据)int fd; // 关联的文件描述符uint32_t u32; // 32位无符号整数uint64_t u64; // 64位无符号整数
} epoll_data_t;
与pollfd相比,epoll_event的优势是通过epoll_data_t联合体支持自定义数据,方便用户进程关联 FD 的上下文信息(如客户端连接的会话数据)。
(2)内核中的struct epitem
epitem是内核中描述 “epoll 实例与 FD 关联关系” 的结构,每个注册到 epoll 的 FD 对应一个epitem,定义简化如下:
struct epitem {struct rb_node rbn; // 红黑树节点(用于加入epoll实例的红黑树)struct list_head rdllink; // 就绪链表节点(FD就绪时加入就绪链表)struct file *file; // 关联的FD对应的struct filestruct epoll_event event; // 存储用户设置的监听事件与用户数据struct epoll_table_struct *epoll_table; // 关联的poll_table
};
-
rbn:用于将epitem插入 epoll 实例的红黑树,实现 FD 的快速查找、插入、删除;
-
rdllink:用于 FD 就绪时,将epitem插入 epoll 实例的就绪链表,避免遍历所有 FD;
-
epoll_table:关联poll_table,用于将进程注册到 FD 的等待队列。
(3)内核中的struct eventpoll
eventpoll是 epoll 实例的核心结构,每个epoll_create()调用会创建一个eventpoll,该对象是内核对象通过anon_inode技术供用户态访问。定义简化如下:
struct eventpoll {spinlock_t lock; // 保护红黑树和就绪链表的自旋锁struct rb_root rbr; // 红黑树的根节点(管理所有注册的epitem)struct list_head rdllist; // 就绪链表的头节点(存储所有就绪的epitem)wait_queue_head_t wq; // epoll的等待队列(存储调用epoll_wait的进程)struct page *tmp_page; // 用于mmap的内存页(减少用户态与内核态拷贝)
};
-
rbr:红黑树的根,用于管理所有通过epoll_ctl注册的epitem,红黑树的 key 是 FD,确保插入、删除、查找的时间复杂度为 O (logn);
-
rdllist:就绪链表,存储所有就绪的epitem,epoll_wait只需遍历该链表即可获取就绪 FD,无需遍历所有注册 FD;
-
wq:epoll 的等待队列,当无 FD 就绪时,调用epoll_wait的进程会睡眠在该队列中,FD 就绪时被唤醒;
-
tmp_page:通过mmap将内核内存页映射到用户空间,避免epoll_event数组的拷贝。
3.2 epoll 的核心流程(基于 ET 模式)
epoll 支持两种触发模式:水平触发(Level Trigger,LT) 和边缘触发(Edge Trigger,ET)。LT 模式与poll类似,只要 FD 就绪,每次调用epoll_wait都会返回该 FD;ET 模式仅在 FD 状态从 “未就绪” 变为 “就绪” 时返回一次,需配合非阻塞 I/O 使用,避免漏读数据。以下以 ET 模式为例,拆解 epoll 的核心流程。
3.2.1 步骤 1:创建 epoll 实例(epoll_create)
用户进程调用epoll_create(size_t size)(注:size参数在 Linux 2.6.8 后被忽略,仅用于兼容旧版本),内核会:
-
分配一个struct eventpoll结构体;
-
初始化结构体中的红黑树(rbr)、就绪链表(rdllist)、等待队列(wq);
-
分配一个内存页(tmp_page),用于后续mmap;
-
创建一个匿名文件(内核内部使用),并返回一个epoll 文件描述符(epfd),用户进程通过epfd操作该 epoll 实例。
3.2.2 步骤 2:注册 FD 到 epoll 实例(epoll_ctl)
用户进程调用epoll_ctl(int epfd, int op, int fd, struct epoll_event *event),其中op为操作类型(EPOLL_CTL_ADD:添加、EPOLL_CTL_MOD:修改、EPOLL_CTL_DEL:删除),内核会:
-
通过epfd找到对应的eventpoll结构体;
-
根据op类型执行对应操作。
OP | 操作 |
EPOLL_CTL_ADD(添加 FD) | a. 检查fd是否已注册(通过红黑树查找epitem),若已注册则返回错误; b. 分配一个struct epitem结构体,初始化rbn(红黑树节点)、rdllink (就绪链表节点),并关联fd对应的struct file和用户传入的event; c. 将epitem插入eventpoll的红黑树(rbr); |
EPOLL_CTL_MOD(修改 FD) | a. 通过红黑树查找fd对应的epitem; b. 更新epitem中的event字段(如修改监听事件); c. 重新调用设备poll方法,更新等待队列注册。 d. 调用fd对应的设备poll方法,传递epoll_table(由epitem关联), 将当前进程注册到fd的等待队列中(若后续fd就绪,会触发唤醒逻辑)。 |
EPOLL_CTL_DEL(删除 FD) | a. 通过红黑树查找fd对应的epitem; b. 将epitem从红黑树和就绪链表中移除; c. 调用设备poll方法,将进程从fd的等待队列中删除; d. 释放epitem结构体。 |
3.2.3 步骤 3:等待 FD 就绪(epoll_wait)
用户进程调用epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout),其中events是用户空间存储就绪事件的数组,maxevents是数组长度,内核会:
-
通过epfd找到eventpoll结构体,并加锁(spinlock);
-
检查就绪链表(rdllist)是否为空:
-
若非空:遍历就绪链表,将每个epitem的event(含就绪事件和用户数据)拷贝到events数组中,最多拷贝maxevents个元素;
-
若为空:判断timeout值,若timeout > 0,则将当前进程睡眠在eventpoll的等待队列(wq)中,直到 FD 就绪、超时或被信号中断;若timeout = 0,直接返回 0;若timeout = -1,无限等待。
3. 解锁并返回就绪事件的数量(若超时返回 0,失败返回 - 1)。
3.2.4 步骤 4:FD 就绪与进程唤醒
当某个注册的 FD 就绪时(如 TCP socket 收到数据),内核会:
-
触发 FD 所属设备的中断处理函数(如网络中断);
-
中断处理函数调用 FD 的poll方法,判断 FD 状态并标记为就绪;
-
找到该 FD 对应的epitem(通过struct file关联),将epitem加入eventpoll的就绪链表(rdllist);
-
遍历eventpoll的等待队列(wq),唤醒所有睡眠在该队列中的进程(即调用epoll_wait的进程);
-
进程被唤醒后,重新执行epoll_wait的逻辑,遍历就绪链表并返回就绪事件。
3.2.5 步骤 5:用户进程处理 I/O 事件
用户进程通过events数组获取就绪 FD 后,需:
-
检查每个就绪 FD 的events字段(如POLLIN),确定事件类型;
-
由于 ET 模式仅通知一次,需通过非阻塞 I/O(如read时设置O_NONBLOCK)循环读取 / 写入数据,直到返回EAGAIN(无更多数据可读 / 写);
-
处理完数据后,可继续调用epoll_wait等待下一次事件。
3.3 epoll 的效率优化关键点
epoll 相比poll的性能优势,源于四个核心优化:
-
红黑树管理注册 FD:O (logn) 的操作复杂度
poll每次调用都需遍历所有 FD,时间复杂度为 O (n);而 epoll 通过红黑树管理注册的 FD,插入、删除、查找的时间复杂度均为 O (logn),即使 FD 数量达到 10 万级,操作耗时也可忽略。
-
就绪链表存储就绪 FD:O (1) 的就绪查询
poll需遍历所有 FD 才能判断是否就绪,而 epoll 在 FD 就绪时主动将其加入就绪链表,epoll_wait只需遍历就绪链表即可获取就绪 FD,时间复杂度为 O (k)(k 为就绪 FD 数量),当大多数 FD 未就绪时(高并发场景常见),效率提升极为显著。
-
mmap 减少用户态与内核态拷贝
poll每次调用都需拷贝struct pollfd数组(用户态→内核态→用户态),而 epoll 通过mmap将内核中的tmp_page内存页映射到用户空间,epoll_event数组直接在映射内存中传递,无需拷贝,大幅减少了数据传输开销。
-
边缘触发(ET)减少无效通知
LT 模式下,若用户进程未完全处理 FD 数据,内核会反复通知该 FD,导致无效系统调用;ET 模式仅在 FD 状态变化时通知一次,配合非阻塞 I/O 可避免无效通知,减少系统调用次数,进一步提升效率。