Skip to content

方法Method

就绪驱动的有界事件循环

Readiness-driven event loop · Edge-triggered ready list · 就绪队列续执行

把内核就绪通知变成去重的应用待处理队列,以双重预算保留未耗尽的续执行,并证明有界轮转及分帧容量条件。

形式陈述 ​

一次通知留下什么责任 ​

固定单线程分派器,每个连接的读方向只有它消费,读取均为非阻塞、正长度;没有回调重入。连接身份是 (slot,generation),不是可复用的整数 fd。代号在可能残留的通知消失以前不回绕。读方向结束不表示整个双向连接都已关闭。

沿用短返回与有界缓冲合同:正数 n 表示本次取得 n 字节;EAGAIN 表示此刻不能继续;读返回零表示本模型的有序 EOF;其他不可恢复错误结束读方向。就绪通知只是值得尝试非阻塞操作的提示,不是完成一条消息的凭证。可写提示同样不能证明整块短写已完成,更不证明远端业务完成。

每个读方向保存状态 WAITING、QUEUED、RUNNING、PAUSED、FINISHED 或 FAILED,另存 hint 和解析续点。hint 为真表示“尚未证实这条续执行已经耗尽”,并不声称下一次读取一定成功。应用维护 FIFO 队列 Q;在每个分派边界,集合 R 定义为所有 QUEUED 身份,要求

set(Q)=R,|Q|=|R|.

因此同一代身份恰入队一次。运行中从 Q 移除的唯一身份另记为 current,不偷偷同时留在 Q。PAUSED 仍可保存 hint,但因容量不足暂不属于 R。

状态转移与双重预算 ​

一次量子最多取得 q>0 字节、尝试 k>0 次 I/O,并且本地解析/回调工作有界。字节预算控制搬运量,尝试预算连 EINTR 等无进展重试也计入;二者不能互相替代。这里把 EINTR 作为可重试、零进展事件,不把它当 EAGAIN。

当前事件 必须完成的更新
收到有效代的通知 FINISHED/FAILED忽略;其他有效状态置 hint。WAITING 有容量则入队、无容量则→PAUSED;已 QUEUED 不重复入队;PAUSED 保留 hint
取出 Q 首项 QUEUED→RUNNING,保存为 current,重置本次两项预算
read 返回 n>0 接受实际 n 字节,更新游标与预算;不得超过预留容量
read 返回 EAGAIN 清 hint,RUNNING→WAITING,保留半帧与未完成业务状态
字节或尝试预算耗尽 不清 hint;仍有容量则 RUNNING→QUEUED 并排队尾
容量不足 RUNNING→PAUSED,保留 hint 与续点;不得反复空排队
容量恢复 若 hint 为真,PAUSED→QUEUED;否则→WAITING
正长度 read 返回 0 或致命错误 清 hint,分别进入 FINISHED 或 FAILED;不再排读任务

关闭连接或更换代号时,先使旧身份不可分派,再删除它的应用队列项;已取回的通知批次仍须逐个核对代号。不能用一条旧 fd=3 通知去调用后来复用为3的新对象。实现若用墓碑延迟删除,也必须把物理队列与逻辑有效成员区分,不能继续声称物理 Q 没有陈旧项。

循环的顺序是:每轮先以零等待收取至多 h 条 OS 通知,并处理有界数量的到期定时器、容量恢复和完成事件;随后运行 Q 首项的一个量子。Q 非空时绝不进入阻塞等待。只有 Q 空、没有已知本地工作时,才等待下一批通知或最近定时器;返回后仍按同一入队规则合并。即使 A 永远有字节,每轮的收取步骤也继续给新到达的 B 入场机会。不同本地事件队列采用轮转或另一种明确公平政策,使每份已接纳的完成和容量恢复事件最终被处理;只说“每轮处理有界数量”不足以排除某一队列被无限优先任务饿死。

直觉

内核通知像“这扇窗口现在可以办理”,应用队列则记录“我还有手续没办完”。为了照顾其他连接,A只读两字节便让出位置,剩下的手续必须留在自己的账本里。不能因为用过一次通知,就把这项责任删掉。

反过来,已经读到 EAGAIN 还不停把自己排回去,会在没有输入时空转。两个分支看起来都叫“本轮停止”,实际原因完全不同:预算耗尽是主动让路;EAGAIN是缺少外部输入。容量暂停又是第三种原因,它等待下游释放,不能仅等新的网络边沿。

通知、应用续执行与容量恢复的分工
例子与边界

两个连接,一次边沿 ​

A 已有 abcdef,B 已有 XY,发送端暂不关闭。初始各收到一次通知,Q=[A,B]。每个量子 q=2、k=1,没有容量暂停;每次成功读取尽量填满请求,输入随后不再变化。

量子 首项与实际返回 完成后的 Q 未读输入
1 A:ab [B,A] A=cdef,B=XY
2 B:XY [A,B] A=cdef,B=空
3 A:cd [B,A] A=ef,B=空
4 B:EAGAIN [A] A=ef,B=空
5 A:ef [A] 均为空
6 A:EAGAIN [] 均为空

读满两字节并不证明后面还有数据,所以 B 有一次无进展探测;它也不证明后面没有数据,所以 A 必须续排。这里故意采用“见 EAGAIN 才撤销 hint”的保守规则,不利用具体流接口的短返回优化。

Linux epoll 的 LT(水平触发)会继续报告仍就绪的对象,ET(边沿触发)不提供每个量子一份新通知的承诺。删掉预算耗尽后的续排:ET 的 A 在 ab 后再也未被运行,留下 cdef;LT 在再次取得 A 的水平通知后仍可能读完,却把应用续执行依赖到重复通知上。正确队列在两种方式下都得到同样字节;LT 可以多送提示,去重只改变通知账本,不改变输出。这里的 ET 反例是一个符合其通知合同的执行,不要求所有实际运行都恰好只通知一次。[1]

没有字节进展也能耗掉整个线程 ​

若代码写成“读到 q 字节才让路”,连续 EINTR 可以使它永不退出;即使流一直可读,一个无界业务回调也可以占住线程。尝试数 k 使前者每轮返回,后者必须拆成有界续执行或交给另有调度合同的工作器。仅把函数标成 async 不产生抢占。

EPOLLHUP/ERR这类挂起或错误通知也不能直接当EOF:尚有待读数据时应按目标接口继续检查实际read结果;本页只让正长度read返回0进入FINISHED,不让一个通知位丢掉尾部数据。

EOF 与 EAGAIN不能合并。半个长度头后 EAGAIN 要等待下一批;同样位置的 EOF 要报告截断。读方向 FINISHED 后还可能有待发送响应;把 EOF 直接解释成整个请求成功也会丢掉协议义务。

迁移:一个量子最多交付一帧 ​

采用两字节大端长度头,这次限定载荷至多3字节,最大编码帧 M=5。给连接9字节预算,计“已完成但未消费帧的编码大小 + 当前帧预留”,不另存无界读取批次。开始读新头以前预留5;读取限制在当前帧需要的头或载荷内;得知长度后可缩小预留。完整帧转为实际占用,消费者释放后才归还额度。

输入依次到达 00、03 43 41、54 00 02 4F 4B。前两批产生半头、部分 CAT,EAGAIN 后仍保留当前帧预算;第三批先用54补成CAT。帧量子 f=1用尽,必须保存续执行。若CAT尚未消费,占用5,剩余4不足以预留下一条最大5字节帧,于是 PAUSED 且 hint=true。消费CAT释放5后,即使没有新 ET 边沿,也重新排队,解析OK;它占编码4字节,最后探测 EAGAIN。

证明分两笔:预留新帧前检查 used+M≤9,读入只花已有预留,完成转账不增加总量,因此容量不越界;让路或容量暂停均不丢 hint,只有 EAGAIN 撤销,因此剩余OK不丢调度。帧预算之外仍保留尝试上限 k 及每帧有限长度;否则一个永不完整的大帧或连续中断仍可破坏量子界。超长头进入失败,半帧 EOF 报截断;不会为未知长度先无条件分配。

推论与应用

不变量与服务轮次界 ​

初态 Q 与 R 均空。通知仅在原本不在 Q 的有效身份上追加;分派同时从二者移除;让路同时加回;EAGAIN、暂停、终结都不留下队列成员。逐事件归纳得到成员一致和无重复。续点记录读取偏移、半帧与剩余发送前缀,预算让路不更改这些未完成事实。

若某身份已入队,含它在内至多 N 个有效身份,没有优先级插队,新入队与续排都去队尾,则它之前至多 N−1 个量子。令H界定入队后尚余的本轮收取、清理与首次派发工作,T界定每个前驱量子及下一轮收取、清理与派发的总时间,它在至多 H+(N−1)T 后开始自己的量子;N=1时也可能要等待H。这个界从已入队边界计起;刚错过一次收取的新通知,还要加其被 OS 交付的时间。h 有限只限制每轮成本,不证明 OS 在无限重复提示下必交付所有新身份;新到达的活性另假设通知最终被收取。长期阻塞回调、无限优先任务或调度线程暂停,均撤回墙钟界。

把费用分开记 ​

主例两次注册、两个初始通知、六次实际 read,其中四次有数据、两次 EAGAIN,总读取8字节;B的首次服务在一个 A 量子之后。具体 LT 执行可以有更多重复通知,ET也可能合并事件;不能把通知数当字节数。应用队列以固定容量循环结构维护,入队、出队和成员位检查为 O(1),N 条连接需要 O(N) 状态;每个字节解析常数次时另计 O(n)。OS 注册、等待和通知产生的成本必须按所用接口和负载测量,本页不宣称 epoll 所有操作 O(1)。减少系统调用也不直接给出墙钟加速倍数。

同一个事件循环还要消费异步请求的目标完成与取消完成。它们是带身份的完成记录,不能混进“可读”的单一布尔位;停止逻辑等待以后仍须安排清理工作。共同终点与复算器把这两份队列责任合起来。

参考资料

[1] Linux man-pages 6.19,epoll(7),“Level-triggered and edge-triggered”、“Questions and answers”及“Possible pitfalls and ways to avoid them”,2026-10-08核查:LT/ET、非阻塞耗尽、ready list去重与旧事件缓存。本文双预算、有界收取、六量子轨迹及分帧预留是独立规定的教学协议。

[2] The Open Group,POSIX.1-2024,recv:返回值与无数据、关闭的接口区别。本文不覆盖信号屏蔽与pselect的原子等待协议,也不处理多个读者抢同一流。

关系图谱7 个相邻概念 · 2 类关系

拖动节点调整位置。

显示关系

显示:依赖

  1. 前置三跳
  2. 前置二跳
  3. 前置一跳
  4. 当前条目
  5. 后续一跳
  6. 后续二跳
  7. 后续三跳
文字版关系按与当前条目的最短距离分组
类型化关系