操作系统 朴实无华逐行剖析 epoll 内核实现
下面给出 基于内核源码 fs/eventpoll.c 的逐步(逐块/逐行级别)剖析,重点讲清楚数据结构、关键路径、并发/锁策略、以及 epoll_ctl / epoll_wait 的内核实现细节和交互。分析以 upstream 源码为准(参考:fs/eventpoll.c),本文会把最关键的事实点都用源码引用标注,便于跟着源码逐行对照阅读。
参考源码(示例镜像/浏览器视图):codebrowser 的
fs/eventpoll.c(下文引用为主要来源)。(codebrowser.dev)
1) 总体结构与要点先览
epoll的内核实现核心在fs/eventpoll.c,其实现把被监视的每个 fd用一个轻量结构(struct epitem)表示,并把这些 epitem 组织在一个 红黑树(rbr) 中以支持快速查找;同时维护一个就绪链表rdllist(ready list)用于把“刚发生的事件”推给epoll_wait。(codebrowser.dev)- 为了在回调(poll callback)上下文与用户态事件传递(可能阻塞 copy_to_user)之间兼顾并发与可睡眠,epoll 使用了三层锁(全局 epnested_mutex -> ep->mtx -> ep->lock),并且对不同场景用不同类型的锁(mutex 与 rwlock / spin)做权衡。源码文件开头就有详细的锁顺序注释(这是理解并发行为的关键)。(codebrowser.dev)
2) 重要数据结构(逐字段解读)
(下面我把源码里的字段含义用自然语言逐项说明,便于逐行理解)
-
struct epitem(每个被监视的条目)- 内含
rbn(红黑树节点,用于插入到 ep->rbr),以及rdllink(链表头,用来把 epitem 链到ep->rdllist,即“就绪列表”)。还有next字段用于实现溢出链(ovflist)。ffd保存被监视的struct file *+ fd 编号;pwqlist保存与该 epitem 相关的一组 poll wait hooks(以便把 epitem 注册到目标 file 的 waitqueue)。event保存struct epoll_event(用户传入的 mask & data)。(codebrowser.dev)
- 内含
-
struct eventpoll(epoll 实例)mtx:互斥量,保护需要可睡眠的操作(例如从内核拷贝事件到用户空间时要 sleep)。lock(rwlock):保护rdllist/ovflist的并发访问;其可在回调(软中断/irq)或 wake 上下文短时间持有。rbr:红黑树根,存 epitem(用于查找、修改、删除)。rdllist:双向就绪链表 — epoll_wait 从这里取事件;回调在不可睡眠情形下会把事件加入ovflist,等待ep_send_events时合并。ovflist:单链表,保存回调在没有拿到mtx时产生的临时就绪项,后续再合并回rdllist。该设计避免在回调中阻塞而丢失事件。(codebrowser.dev)
3) 锁定模型(必须先读懂)
源码注释明确指出三层锁与获取顺序(极重要,避免死锁):
- 全局:
epnested_mutex(用于处理把一个 epoll fd 插入到另一个 epoll fd 时的环检测/互斥) - per-epoll:
ep->mtx(mutex,可睡眠,保护事件传递、epoll_ctl(DEL)、销毁等) - per-epoll:
ep->lock(rwlock / 用作短期保护,适用于 poll 回调上下文)
获取顺序:epnested_mutex -> ep->mtx -> ep->lock。在可能需要同时拿多个 ep->mtx(将 epoll 嵌套加入另一个 epoll)时,用 epnested_mutex 保证顺序与避免环。源码注释对这个顺序有详尽说明(强烈建议把这段注释连同代码一起读)。(codebrowser.dev)
4) epoll_create / 实例创建与释放(要点)
do_epoll_create()/ep_alloc():分配struct eventpoll,初始化mtx、rbr、waitqueues、slab caches 等,同时给file->private_data指回eventpoll。创建过程相对直接,但要注意user计数(每个 epoll 实例记录创建它的用户)。(codebrowser.dev)ep_free()释放资源时,会注销 wakeup source、释放用户计数、销毁 mutex 等,确保在释放时rbr是空的(否则会 WARN)。(codebrowser.dev)
5) epoll_ctl 路径(控制接口的逐步剖析)
系统调用入口(sys_epoll_ctl / 内核包装 do_epoll_ctl)的大致流程:
- 参数检查:验证
epfd、fd合法性,op(ADD/MOD/DEL)是否合法,event合法(如果不是 DEL)。(典型 syscall 输入检验)(codebrowser.dev) - 获取
struct file *:分别拿到ep的 file(epfd)和被监视对象的file(fd)。 - 检查
fd是否可被poll(有file->f_op->poll)或者是 epoll 类型(嵌套 epoll 时要特殊处理)。 - 对
EPOLLEXCLUSIVE/EPOLLWAKEUP等私有标志进行合法性检查(例如EPOLLEXCLUSIVE只能在EPOLL_CTL_ADD使用,不能在 MOD)。(man page 与实现中都有相关约束)(man7.org) - 对
EPOLL_CTL_ADD:调用ep_insert()(必须持ep->mtx)把一个epitem分配并插入红黑树、在目标 file 的 f_ep 列表注册 poll hook(通过attach_epitem());如果是监视的 target 本身是 epoll fd,那么要做环检测(ep_loop_check),此时会用epnested_mutex保护。(codebrowser.dev)
6) ep_insert():添加一个监控项(逐步)
ep_insert()(源码注释:Must be called with “mtx” held.)的主要步骤(按代码顺序):
- 限额检查:检查当前用户(
ep->user)下的epoll_watches是否已经超过/proc/sys/fs/epoll/max_user_watches(防止 DoS / 资源耗尽)。超出返回-ENOSPC。(codebrowser.dev) - 分配
struct epitem(从 slab cacheepi_cache),初始化ffd、event,并将epi->ep = ep。 - 若事件带
EPOLLWAKEUP,为该条目创建wakeup_source(用于阻止系统进入 suspend,使得 wakeup 能够触发)。(codebrowser.dev) - 把 epitem 插入 ep 的 rbtree(通过
rb_link_node/rb_insert_color)以便后续查找;同时通过attach_epitem()把 epitem 注册到file->f_ep链表中(在file->f_lock下进行)。attach_epitem()的作用是:确保file->f_ep的头存在(lazy 分配),并使用 RCU/hlist_add_head_rcu等安全地把 epitem 链到该 file 的 ep-list 上。(codebrowser.dev)
关键并发点:在
ep_insert()中,一方面要保持ep->mtx(因为这是修改 epoll 集合的“重”操作,且会发生 copy_to_user 的场景),另一方面attach_epitem()操作需要拿file->f_lock,因此锁顺序和死锁避免非常重要(如前述锁顺序注释)。(codebrowser.dev)
7) 注册 poll hook(回调)与 wake 路径(发生事件时的 fast path)
-
注册时为
epi构造eppoll_entry列表(pwqlist),并把每个eppoll_entry->wait注册到目标文件的 waitqueue head(poll_wait机制的底层)。这就保证当目标 file 的wake_up()触发时,内核会调用我们注册的 wait 回调函数(epoll 的 poll hook)。(codebrowser.dev) -
回调(poll hook)在什么情形下执行? 当目标文件的驱动/子系统触发
poll_wake/wake_up,内核会在合适的上下文(可能是软中断或 process context)调用我们注册的 poll 回调;回调会:- 检查该 fd 的实际可用事件(通过
file->f_op->poll的结果与我们保存的 event mask 做交叉), - 如果满足用户感兴趣的事件,把对应的
epitem放到 epoll 实例的 ready list:通常是list_add_tail(&epi->rdllink, &ep->rdllist)(或在无法拿mtx时,先放到ovflist,等待 later merging)。并做__pm_stay_awake(如果需要 EPOLLWAKEUP)。最后唤醒等待epoll_wait的进程(wake_up(&ep->wq))。源码在将事件插入 ready 列表并唤醒 epoll waiters 的部分有明确实现。(codebrowser.dev)
- 检查该 fd 的实际可用事件(通过
-
为什么要有
ovflist? 因为回调不能 sleep,也可能拿不到ep->mtx(该锁是可睡眠的)。所以回调在短时间不可睡眠上下文会把 epitem 放进ovflist(单链),等到ep_send_events(即 epoll_wait 的用户态 copy 路径拿到mtx)时,会把ovflist合并回rdllist再统一处理,保证事件不会丢失。ep_start_scan/ep_done_scan就是 steal/merge rdllist/ovflist 的实现点。(codebrowser.dev)
8) epoll_wait 路径(ep_poll() / do_epoll_wait())逐步

do_epoll_wait()(syscall 层)内部调用 ep_poll()(实现实际等待与事件转移):
ep_poll() 的主要步骤(按源码流程梳理):
-
处理 timeout -> 转换为
ktime_t expires(并估算 slack 用于 select 精度)。如果 timeout == 0,直接非阻塞检查一次 ready-list。(codebrowser.dev) -
初始化等待结构
wait_queue_entry_t wait;并把自己放到ep->wq上(以便唤醒)。 -
进入主循环:调用
ep_try_send_events()(其内部会调用ep_send_events()),ep_send_events()会把rdllist(和ovflist合并)中的条目逐个写入用户空间events缓冲,过程中要注意:- 对
EPOLLONESHOT:在成功返回该事件后会把 epi->event.events 清掉私有位(只保留 private bits),防止重复返回(由 epoll 语义决定); - 对
EPOLLET(边沿触发):如果设置了 ET,则在传递事件后不会把其重新插回rdllist(边沿语义);对于 LT(level),如果事件仍然存在且不是 ONESHOT/ET,就会把 epi 重新插回rdllist以便下次检查。实现细节见ep_send_events_proc(事件发送过程)中的条件判断与 list 操作。(codebrowser.dev)
- 对
-
如果没有事件且没有超时,
ep_poll会把当前线程挂到ep->wq上 sleep,直到被 wake(回调里会wake_up(&ep->wq)) 或超时。然后循环再试。最终把拷贝到用户空间的事件数作为返回值返回给用户态。(codebrowser.dev)
9) 事件传递的具体细节(ep_send_events + ep_send_events_proc)
-
ep_send_events():构建ep_send_events_data,并调用ep_scan_ready_list()/ep_send_events_proc()来把rdllist的内容逐个转成struct epoll_event并copy_to_user。返回成功写入的事件数或错误。实现中注意对信号/致命错误的检测(如果在等待过程中有致命信号要中断返回),也会在发送成功后做ep_pm_stay_awake/relax等 power-management 调整。(Android Git Repositories) -
ep_send_events_proc()(处理函数)要点:- 它会遍历 txlist(临时 steal 出来的 ready 列表),对每个
epi计算实际的revents(把epi->event.events和文件实际的 poll 状态结合起来),把 uevent(用户可见事件)构造并copy_to_user。 - 处理
EPOLLONESHOT:在传递后清掉非私人位(保持 PRIVATE bits),确保下一次不会重复传递。 - 对 LT 模式(非 ET):如果事件仍然存在,要重新把 epi 放回
rdllist(list_add_tail(&epi->rdllink, &ep->rdllist)),以便下次 epoll_wait 再检查。 - 完成后解锁/释放并返回事件数或错误码。(codebrowser.dev)
- 它会遍历 txlist(临时 steal 出来的 ready 列表),对每个
10) 嵌套 epoll(epoll 上监视 epoll)的循环检测
- 如果用户把 epoll fd A 加入到 epoll fd B(即监视一个 epoll 实例本身),内核必须检测是否会引入循环(A 包含 B,B 包含 A),否则可能导致死锁。实现用
ep_loop_check/reverse_path_check等逻辑遍历ep->refs链并限制嵌套深度(EP_MAX_NESTS)。在这类操作中会用到epnested_mutex以避免并发导致的 TOCTOU。(codebrowser.dev)
11) EPOLLEXCLUSIVE / EPOLLWAKEUP 等私有位行为(实现注意事项)
-
EPOLLEXCLUSIVE:用于避免多 waiter 间的 thundering herd。内核在EPOLL_CTL_ADD时把 epoll 的 wake registration 标记为 exclusive,只在ADD时允许(不能在 MOD 使用)。manpage 明确了约束,源码也在ep_insert/attach_epitem处对此做检查和限定。(man7.org) -
EPOLLWAKEUP:会为该 epitem/epoll 创建wakeup_source(pm keepalive),在传递事件或回调时做__pm_stay_awake/__pm_relax来避免在低功耗状态错过 wakeup。实现中有ep_create_wakeup_source()、ep_pm_stay_awake()等辅助函数。(codebrowser.dev)
12) 清理 / 删除路径(EPOLL_CTL_DEL 或 close)
ep_remove()(和__ep_remove)负责把 epitem 从 rbtree 中删除、注销 pollwait(ep_unregister_pollwait)、从file->f_ep链表 unlink,并放到 RCU 回收路径或直接释放;必须在ep->mtx下运行(同样注意与 file->f_lock 的交互)。当一个 fd 被 close 而没有被删除,eventpoll_release_file()会遍历 file 的 ep 链把所有 epitem 从各个 epoll 实例中移除,避免悬挂条目。(codebrowser.dev)
13) 性能与设计考量(总结式说明)
-
使用 红黑树(rbr) 而不是 simple array 保证了查找/删除的良好复杂度(插入/删除 O(log n)),适合高并发大量 fd 场景(同时避免每次系统调用带宽复制大量 fd 描述)。源码大量使用 list 的 splice、ovflist 技巧目的是最大化回调路径的轻量性(回调尽量短小,不 sleep),把复杂工作延后到可以安全 sleep 的地方处理(即 epoll_wait 的
ep_send_events)。(codebrowser.dev) -
ovflist+rdllist的双列表策略是 epoll 能做到高并发与低延迟的关键之一:回调可以 lockless(或短锁)把事件放入 ovflist;用户态等待方拿到mtx后再统一合并与 copy_to_user。(codebrowser.dev)
14) 源码逐段(逐行)阅读建议(如何“逐行剖析”)
我已经把关键函数及逻辑块的顺序和语义解释清楚了。若你要“逐行”跟着读源码(推荐做法):
- 从文件头部的 LOCKING 注释开始读(理解三层锁与获取顺序)。(codebrowser.dev)
struct epitem/struct eventpoll定义(明白字段角色)。(codebrowser.dev)ep_alloc/do_epoll_create/ep_free(创建销毁)。(codebrowser.dev)do_epoll_ctl->ep_insert->attach_epitem(添加逻辑,注意锁与 RCU)。(codebrowser.dev)- poll hook / attach 回调 的实现(回调如何把 epi 加入 ovflist/rdllist)。(在
ep_item_poll/ 回调相关函数处阅读)(codebrowser.dev) ep_poll/ep_send_events/ep_send_events_proc(等待与事件传递,注意 ONESHOT/ET 的分支)。(codebrowser.dev)ep_remove/eventpoll_release_file(清理)。(codebrowser.dev)
15) 进一步阅读 & 补充资料(源码注释 + 补丁历史)
- upstream 源码(fs/eventpoll.c),建议在本机用
gitcheckout 对照具体 kernel 版本逐行阅读(我上文引用的是 codebrowser 镜像视图)。(codebrowser.dev) - manpage(epoll_ctl(2)、epoll_wait(2))概述用户接口语义(例如
EPOLLEXCLUSIVE的约束)。(man7.org) - 若想看实现演进、bugfix 与竞态修复,搜索 LKML 上关于 epoll 的 patch(例如 priority-inversion / busy-poll 优化补丁等),这些补丁能帮助理解为什么实现中要有
ovflist、wakeup-source 等设计。(ZX2C4 Git)
结语(要点回顾)
- epoll 的内核实现把高并发回调路径与可睡眠用户态拷贝路径通过
ovflist/rdllist、rwlock + mutex 的组合巧妙地解耦开,保证回调尽量短小、事件不丢失,同时在 epoll_wait 时能够安全地将事件拷贝到用户空间。核心数据结构epitem/eventpoll、以及锁顺序是理解整个实现的关键。(codebrowser.dev)
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120297

浙公网安备 33010602011771号