操作系统 朴实无华逐行剖析 epoll 内核实现
本文剖析了Linux内核中epoll的实现细节,重点关注数据结构、并发控制和关键操作路径。核心结构包括epitem(每个被监视的fd)和eventpoll(epoll实例),使用红黑树组织epitem,就绪链表rdllist传递事件。epoll采用三层锁机制(epnested_mutex、ep->mtx、ep->lock)确保并发安全,严格遵循获取顺序避免死锁。epoll_ctl操作通
下面给出 基于内核源码 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),建议在本机用
git
checkout 对照具体 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)
更多推荐
所有评论(0)