select 在高连接数下 CPU 飙高、或 epoll_wait 空转/漏事件:需要看清内核 就绪链表与回调 ,而不是只背 API。本文沿 epoll_create1 → epoll_ctl(ADD) → 文件就绪回调 → ep_poll/epoll_wait 走通 fs/eventpoll.c 主路径,便于用 strace/perf 验证。
源码锚点
| 路径 | 作用 |
|---|---|
fs/eventpoll.c |
do_epoll_create、do_epoll_ctl、ep_poll、ep_send_events |
include/uapi/linux/eventpoll.h |
epoll_event、EPOLLIN/EPOLLET 等用户 UAPI |
include/linux/eventpoll.h |
内核侧声明 |
fs/select.c |
经典 select/poll 实现(对比用) |
net/ipv4/tcp.c 等 |
socket 通过 ->poll / wait queue 上报就绪 |
核心用户结构:
c
/* include/uapi/linux/eventpoll.h */
struct epoll_event {
__poll_t events;
__u64 data;
};
内核侧(概念):每个 epoll 实例有红黑树存监控项(epitem),就绪队列串起「已就绪」项;目标文件通过 epoll 回调挂到其 wait queue。
c
/* 用户态典型调用 */
int epfd = epoll_create1(EPOLL_CLOEXEC);
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
n = epoll_wait(epfd, events, maxevents, timeout);
调用链
text
epoll_create1(flags)
→ do_epoll_create
→ 分配 eventpoll 对象,匿名 inode + file
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event)
→ do_epoll_ctl
→ 红黑树插入 epitem(key=fd 对应的 file)
→ ep_insert
→ 对目标 file 调用 f_op->poll,挂 epoll 回调
→ 若已就绪 → 链入 rdllist(就绪链表)
目标 fd 数据到达 / 可写
→ sock 等子系统 wake_up
→ epoll 回调 ep_poll_callback
→ epitem 进入就绪链表,唤醒正在 ep_poll 等待的进程
epoll_wait(epfd, ...)
→ do_epoll_wait → ep_poll
→ 若就绪链表空则睡眠至超时/唤醒
→ ep_send_events 拷贝到用户 events[]
水平触发(默认)vs 边沿触发(EPOLLET):
text
LT:只要条件仍满足,每次 wait 都可再次报到
ET:仅状态变化时报到;必须一次读到 EAGAIN,否则可能饿死
重点知识
1. 为何 epoll 能撑高并发
select/poll 每次把整张 fd 集合从用户拷进内核并线性扫描;epoll 用红黑树索引监控集,就绪用链表,热路径只处理就绪子集 。连接数上万时差异明显。代价是:每 fd 需先 epoll_ctl,且语义(尤其 ET)要正确。
2. data 字段是用户私有,内核不解释
epoll_event.data 常存 fd 或对象指针。关闭 fd 前必须 EPOLL_CTL_DEL(或依赖 DUP/CLOEXEC 生命周期约定),否则可能出现 野事件(fd 号复用后事件张冠李戴)。
3. ET + 非阻塞是标配
边沿触发下,若 read 未读到 EAGAIN 就去 epoll_wait,剩余数据可能不再通知。生产网络库普遍:SOCK_NONBLOCK + EPOLLET + 循环读到 EAGAIN。LT 更易用,但高活跃 fd 会重复报到。
4. 配置与观测
bash
# 系统调用轨迹
strace -e epoll_create1,epoll_ctl,epoll_wait ./your_server
# 句柄与上限
ulimit -n
cat /proc/sys/fs/epoll/max_user_watches # 监控项上限(若存在)
# 性能
perf record -e syscalls:sys_enter_epoll_wait -a -- sleep 5
ss -s # 连接规模旁证
对比实验:同连接数下分别用 poll 与 epoll 看 CPU;或故意 ET 不读干,观察是否「假死」。
5. 设计意图与踩坑
- 意图:把「谁就绪」从 O(n) 扫描变成就绪队列消费。
- 坑 :
epoll_wait的maxevents过小,一轮取不完,逻辑上要循环到取尽或配合 LT。 - 坑 :把阻塞 fd 塞进 epoll 却假设一定非阻塞 →
read卡死工作线程。 - 坑 :
EPOLLONESHOT用后忘记EPOLL_CTL_MOD重新武装。 - 坑:在多线程间乱共享同一 epfd 无同步(内核允许,但用户状态机要自己保证)。
Checklist
- 能指出
do_epoll_ctl/ep_poll位于fs/eventpoll.c,并口述就绪唤醒链 - 说明 LT 与 ET 差异,并在 ET 下写到「读至 EAGAIN」的循环
-
strace看到 create/ctl/wait;关闭连接路径有DEL或等价生命周期管理 - 对比理解
select/poll全量扫描与 epoll 就绪链表的复杂度差异 - 检查
ulimit -n与max_user_watches,避免 EMFILE/监控项耗尽 - 使用
EPOLLONESHOT时有明确的重新MOD路径