epoll 维护感兴趣的描述符及就绪事件,让程序集中等待多个 I/O 来源。它返回的是“现在可以尝试读写”的提示,不是已经替应用完成读写,也不保证一次事件对应一条业务消息。
为什么适合许多空闲连接
select/poll 每轮提交描述符集合或数组并进行相关扫描;epoll 保留兴趣集合,通过底层就绪通知维护候选列表,通常能减少大量空闲描述符的重复处理。
这不等于任何 epoll_wait 都是 O(1):返回 k 个事件至少需要相应处理与复制成本,注册修改、回调、锁竞争和大量活跃连接也有代价。poll、epoll 的描述符数受资源限制,不能写成无限制;glibc select 的 fd_set 还有固定 FD_SETSIZE 限制。
三类操作
int epfd = epoll_create1(EPOLL_CLOEXEC);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = client_fd };
int rc = epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
这是一段需要 <sys/epoll.h> 的 API 片段,须检查 epfd 和 rc。ADD 注册,MOD 修改兴趣和用户数据,DEL 删除。epoll_wait 的正返回值是本批事件数,0 是超时,-1 是错误;EINTR 不应被误认为没有连接或所有连接已关闭。
.data 是由应用保存、再原样返回的关联信息,可以用 fd、指针或带代次的 ID。内核不会替应用维护该指针对象的生命周期。
事件位不是互斥枚举
| 事件或标志 | 含义与处理 |
| EPOLLIN | 尝试读取;监听 socket 则尝试 accept |
| EPOLLOUT | 尝试写入,不表示任意长度都能一次发送 |
| EPOLLERR | 错误,可结合 SO_ERROR 等查询;无需显式订阅才收到 |
| EPOLLHUP | 挂断;仍可能有缓冲数据需要读取 |
| EPOLLRDHUP | 流 socket 对端关闭写方向,Linux 2.6.17 起提供 |
| EPOLLET | 改用边沿触发模式 |
| EPOLLONESHOT | 交付事件后禁用该注册的后续通知,需 MOD 重置 |
| EPOLLEXCLUSIVE | 特定注册场景减少多个 epoll 实例的唤醒,不是通用锁 |
IN、HUP、ERR 等可以同时出现。ONESHOT 不是自动执行 DEL,也不会关闭 fd。epoll_ctl(2)
LT 与 ET:何时可以再次等待
LT 是默认模式,条件仍就绪时可继续报告。应用可以处理一部分后回到等待,但若一直不消费数据或一直订阅可写,循环会持续被唤醒。
ET 不保证未消费的就绪状态会被反复提醒。通用做法是使用非阻塞 I/O,处理到 EAGAIN 后才认为可以依赖新的通知。不要把 ET 简化成对所有 fd 都严格只报告某一种二值状态翻转;具体唤醒来自底层事件,应用不应依赖额外通知修复漏处理。epoll(7)
下面是流 socket 的读取片段,consume_bytes 必须快速处理或入应用队列,不能在事件线程里阻塞。返回 0 表示读到 EAGAIN,1 表示输入方向 EOF,-1 表示其他错误:
#include <errno.h>
#include <stddef.h>
#include <sys/socket.h>
void consume_bytes(const unsigned char *data, size_t size);
int drain_socket(int fd) {
unsigned char buf[4096];
for (;;) {
ssize_t n = recv(fd, buf, sizeof buf, 0);
if (n > 0) {
consume_bytes(buf, (size_t)n);
continue;
}
if (n == 0) return 1;
if (errno == EINTR) continue;
if (errno == EAGAIN || errno == EWOULDBLOCK) return 0;
return -1;
}
}
EOF 只结束对端到本端的输入方向,可能仍需发送响应或排空输出。是否关闭整个连接应由协议状态决定,而不是任何 HUP 都立即丢弃对象。
还有数据,为什么 epoll_wait 没有新事件?
在 ET 下执行“到达→wait→read→wait”,第二次 wait 可以返回0,而缓冲仍有两字节。应用保留的待处理标记让剩余工作能够继续调度;它不能因本轮没有新事件就被删除。读到恰好两字节也不自动证明排空,再尝试读得到 EAGAIN 才能确认这个边界。
正在呈现知识画面
还有数据,为什么 epoll_wait 没有新事件?ET 不保证重复提醒未读数据。预算用尽时应保留应用待处理状态;read 返回 EAGAIN 才表明此时已排空。
{"id":"epoll-unread-work","title":"还有数据,为什么 epoll_wait 没有新事件?","summary":"ET 不保证重复提醒未读数据。预算用尽时应保留应用待处理状态;read 返回 EAGAIN 才表明此时已排空。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"mode\"><button data-mode=\"lt\" data-i18n=\"lt\"></button><button data-mode=\"et\" data-i18n=\"et\"></button></div><div class=\"actions\"><button id=\"arrive\" data-i18n=\"arrive\"></button><button id=\"poll\" data-i18n=\"poll\"></button><button id=\"read\" data-i18n=\"read\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><div class=\"track-row\"><span data-i18n=\"buffer\"></span><div id=\"buffer\" class=\"track\"></div></div><div class=\"metrics\"><div><span data-i18n=\"event\"></span><strong id=\"event\"></strong></div><div><span data-i18n=\"result\"></span><strong id=\"result\"></strong></div><div><span data-i18n=\"queue\"></span><strong id=\"queue\"></strong></div></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n#buffer{display:grid;grid-template-columns:repeat(8,minmax(0,1fr))}.metrics strong{font-size:17px}#queue{min-height:6em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}let mode='et',bytes=0,notify=false,event=null,result=null,work='unknown';function draw(){pressed(mode);cells('buffer',Array.from({length:8},(_,i)=>({text:i<bytes?'B':'—',cls:i<bytes?'on':'empty'})));set('event',event??'—');set('result',result??'—');set('queue',t(work));$('#arrive').disabled=bytes>4;$('#buffer').dataset.bytes=bytes;}function reset(){bytes=0;notify=false;event=result=null;work='unknown';draw()}$('#arrive').onclick=()=>{bytes+=4;notify=true;draw()};$('#poll').onclick=()=>{event=mode==='lt'?Number(bytes>0):Number(notify&&bytes>0);notify=false;if(event)work='retain';draw()};$('#read').onclick=()=>{const n=Math.min(2,bytes);bytes-=n;result=n||'EAGAIN';work=n?'retain':'drained';draw()};document.querySelectorAll('[data-mode]').forEach(e=>e.onclick=()=>{mode=e.dataset.mode;reset()});$('#reset').onclick=reset;draw();","audio":false,"strings":{"arrive":"到达4字节","buffer":"未读字节","drained":"已观察到 EAGAIN","et":"ET","event":"最近 wait 事件数","heading":"还有数据,为什么 epoll_wait 没有新事件?","intro":"注入4字节,取一次事件,再只读2字节,对照 LT 与 ET。","lt":"LT","mode":"通知方式","poll":"epoll_wait(timeout=0)","queue":"仍有工作的应用记录","read":"read 最多2字节","reset":"重置","result":"最近 read 结果","retain":"应继续安排处理","scope":"单个非阻塞流,容量8,无 EOF、错误和 ONESHOT。每次注入产生一个可合并通知,体现一种合法 ET 情形,不将 ET 定义为所有 fd 的严格二值翻转。","scopeLabel":"模型范围与约定","unknown":"尚未检查"}}
写入背压与公平性
应用有待发数据时先尝试非阻塞发送;若只写了一部分或 EAGAIN,保存剩余区间并关注 EPOLLOUT。待发送队列清空后通常取消可写关注,否则 LT 模式可能形成空转。socket 写入还要有明确的 SIGPIPE 或 MSG_NOSIGNAL 策略。
一直服务一个高吞吐连接直到 EAGAIN,可能让其他连接饥饿。可以为每个连接设字节或时间预算;ET 下若预算用尽却未排空,需把连接留在应用就绪队列并继续安排,不能直接回 epoll_wait 指望再次通知。
监听 socket 也应循环 accept 到 EAGAIN;accept4 可原子指定新连接的 NONBLOCK/CLOEXEC。还需处理 fd 耗尽等错误,避免监听就绪导致持续失败的忙循环。
公平调度需要显式保存“还有工作”
假设 A 当前可读 10 个单位、B 可读 2 个单位,每轮预算为 4。A 用完第一轮预算后仍有工作,将它排到队尾,B 就能先完成。若 ET 下直接遗忘 A,剩余字节可能一直等待一个并不保证会再来的通知。
from collections import deque
remaining = {"A": 10, "B": 2}
ready = deque(["A", "B"])
queued = set(ready)
turns = []
while ready:
connection = ready.popleft()
queued.remove(connection)
count = min(4, remaining[connection])
remaining[connection] -= count
turns.append((connection, count))
if remaining[connection] and connection not in queued:
ready.append(connection)
queued.add(connection)
assert turns == [("A", 4), ("B", 2), ("A", 4), ("A", 2)]
assert all(n == 0 for n in remaining.values())
这是已知工作量的离线模型。真实 socket 不能提前知道 remaining;用非阻塞 read 的 EAGAIN、EOF 和错误来更新状态,并把预算用尽但尚未排空的连接保留为应用可运行状态。若解析一条消息本身很耗时,限制读取字节仍不足以保证公平,还需限制解析/业务执行时间。
多工作线程与 ONESHOT
ONESHOT 常用于让一个处理者临时接管连接。处理完后,通过 EPOLL_CTL_MOD 重新启用关注;这不自动让所有连接字段变成线程安全,也不替代任务转交时的内存同步。
重置时要先发布好连接状态和兴趣集合,并考虑重置后其他线程马上收到事件。若未读尽就绪数据,重新启用后可能立即再次交付。锁、所有权或串行执行器应明确规定谁能修改输出队列和关闭连接。
用 eventfd 看见 ONESHOT 与重新启用
保存为 oneshot-demo.c。eventfd 初始值为 3,第一次 epoll_wait 收到可读事件后故意不读;计数还在,但 ONESHOT 已禁用后续通知。重新 MOD 后,现存的可读条件使事件再次返回。
#include <stdint.h>
#include <stdio.h>
#include <sys/epoll.h>
#include <sys/eventfd.h>
#include <unistd.h>
int main(void) {
int fd = eventfd(3, EFD_NONBLOCK | EFD_CLOEXEC);
if (fd == -1) return 1;
int ep = epoll_create1(EPOLL_CLOEXEC);
if (ep == -1) { close(fd); return 1; }
int failed = 1;
struct epoll_event interest = { .events = EPOLLIN | EPOLLONESHOT };
interest.data.u64 = 42;
struct epoll_event event;
if (epoll_ctl(ep, EPOLL_CTL_ADD, fd, &interest) == -1) goto done;
if (epoll_wait(ep, &event, 1, 0) != 1 || event.data.u64 != 42) goto done;
if (!(event.events & EPOLLIN)) goto done;
if (epoll_wait(ep, &event, 1, 0) != 0) goto done;
if (epoll_ctl(ep, EPOLL_CTL_MOD, fd, &interest) == -1) goto done;
if (epoll_wait(ep, &event, 1, 0) != 1 || event.data.u64 != 42) goto done;
uint64_t count;
if (read(fd, &count, sizeof count) != sizeof count || count != 3) goto done;
if (epoll_ctl(ep, EPOLL_CTL_MOD, fd, &interest) == -1) goto done;
if (epoll_wait(ep, &event, 1, 0) != 0) goto done;
puts("notified, disabled, rearmed; count=3; drained");
failed = 0;
done:
if (close(ep) == -1) failed = 1;
if (close(fd) == -1) failed = 1;
return failed;
}
运行 cc -Wall -Wextra oneshot-demo.c -o oneshot-demo 后执行 ./oneshot-demo。所有等待都用零超时,检查已经存在的就绪条件;本例没有其他信号源,等待失败直接终止,通用循环仍需处理 EINTR。ONESHOT
这里 read 返回 8 字节,读取的计数为 3,两者含义不同。未启用 EFD_SEMAPHORE 时,成功读取把当前累计值取出并清零;eventfd 作为通知计数器,并不保存三份独立业务消息。eventfd(2)
关闭与 fd 数字复用
fd 是可复用的整数。一个批次中可能已有旧连接的事件,随后某回调关闭它,新连接又取得相同 fd。直接按这个数字查当前对象,可能误把旧事件应用到新连接;对象代次、稳定 ID、延迟回收或校验可解决此类问题。
dup 后的描述符还可能共享打开文件描述,关闭一个 fd 不一定让所有相关注册立即消失。需要确定性撤销时,在对象生命周期仍有效时执行 DEL,并按应用队列中已有事件的规则清理。内核移除注册不能撤销已经复制到用户空间的事件。
一个旧事件怎样误投给新连接
假设本批事件保存了 (slot=7, generation=4)。前一个回调关闭该连接并释放槽位,新连接复用 slot=7,但代次变为 5。分发剩余事件时必须核对代次,不能只按 slot 或 fd 找到一个非空对象就继续。
current = {7: {"generation": 5, "name": "new connection"}}
old_event = (7, 4)
new_event = (7, 5)
def resolve(event):
slot, generation = event
item = current.get(slot)
if item is None or item["generation"] != generation:
return None
return item
assert resolve(old_event) is None
assert resolve(new_event)["name"] == "new connection"
代次需要足够的范围,并定义回绕与复用策略;它也不替代并发内存回收。若 data.ptr 已经是释放后的指针,先解引用去读代次仍然不安全。可以用整数 ID 查询仍有效的注册表,或让事件批次持有对象引用直到分发结束。
timerfd、signalfd 与 eventfd
timerfd 创建后还需 timerfd_settime 才启动;读取通常取得累计到期次数,不等于每次可读只有一次到期。eventfd 读取的是通知计数。两者都应按接口规定的记录大小读,并处理非阻塞空读。
signalfd 需要先在相关线程阻塞目标信号。下面是用于程序初始化阶段的片段,需 <signal.h>、<pthread.h>、<sys/signalfd.h>:
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
int err = pthread_sigmask(SIG_BLOCK, &mask, NULL);
if (err != 0) return err;
int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
if (sfd == -1) return errno;
应在创建工作线程前建立希望它们继承的 mask,或逐线程协调;否则信号可能被未阻塞它的其他线程接收。忘记 sigemptyset 会让 mask 内容未初始化。SIGKILL、SIGSTOP 不能经 signalfd 接管。signalfd(2)
事件驱动的真实边界
就绪回调里执行阻塞 DNS、磁盘读取、长计算或等待普通锁,仍会阻塞整个事件线程。把耗时任务交给合适的执行机制,并通过队列、eventfd 或其他通知回传结果。
epoll 适合 readiness 模型;io_uring 以请求完成为主要接口。两者都需要应用状态机、背压、取消和资源管理,接口名称不会自动解决这些问题。
关键词:epoll, readiness, LT, ET, EPOLLONESHOT, half close, backpressure, fairness, fd reuse, timerfd, signalfd, eventfd