下面给出 基于内核源码 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,初始化 mtxrbr、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)的大致流程:

  1. 参数检查:验证 epfdfd 合法性,op(ADD/MOD/DEL)是否合法,event 合法(如果不是 DEL)。(典型 syscall 输入检验)(codebrowser.dev)
  2. 获取 struct file *:分别拿到 ep 的 file(epfd)和被监视对象的 filefd)。
  3. 检查 fd 是否可被 poll(有 file->f_op->poll)或者是 epoll 类型(嵌套 epoll 时要特殊处理)。
  4. EPOLLEXCLUSIVE / EPOLLWAKEUP 等私有标志进行合法性检查(例如 EPOLLEXCLUSIVE 只能在 EPOLL_CTL_ADD 使用,不能在 MOD)。(man page 与实现中都有相关约束)(man7.org)
  5. 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 cache epi_cache),初始化 ffdevent,并将 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)
  • 为什么要有 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() 的主要步骤(按源码流程梳理):

  1. 处理 timeout -> 转换为 ktime_t expires(并估算 slack 用于 select 精度)。如果 timeout == 0,直接非阻塞检查一次 ready-list。(codebrowser.dev)

  2. 初始化等待结构 wait_queue_entry_t wait; 并把自己放到 ep->wq 上(以便唤醒)。

  3. 进入主循环:调用 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)
  4. 如果没有事件且没有超时,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_eventcopy_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 放回 rdllistlist_add_tail(&epi->rdllink, &ep->rdllist)),以便下次 epoll_wait 再检查。
    • 完成后解锁/释放并返回事件数或错误码。(codebrowser.dev)

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) 源码逐段(逐行)阅读建议(如何“逐行剖析”)

我已经把关键函数及逻辑块的顺序和语义解释清楚了。若你要“逐行”跟着读源码(推荐做法):

  1. 从文件头部的 LOCKING 注释开始读(理解三层锁与获取顺序)。(codebrowser.dev)
  2. struct epitem / struct eventpoll 定义(明白字段角色)。(codebrowser.dev)
  3. ep_alloc / do_epoll_create / ep_free(创建销毁)。(codebrowser.dev)
  4. do_epoll_ctl -> ep_insert -> attach_epitem(添加逻辑,注意锁与 RCU)。(codebrowser.dev)
  5. poll hook / attach 回调 的实现(回调如何把 epi 加入 ovflist/rdllist)。(在 ep_item_poll / 回调相关函数处阅读)(codebrowser.dev)
  6. ep_poll / ep_send_events / ep_send_events_proc(等待与事件传递,注意 ONESHOT/ET 的分支)。(codebrowser.dev)
  7. 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)
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐