epoll maintains a set of interested file descriptors and their ready events, allowing a program to wait centrally for multiple I/O sources. It returns hints that "read/write can be attempted now," not that I/O has been completed on behalf of the application, nor does it guarantee that one event corresponds to one business message.
Why It Suits Many Idle Connections
select/poll submit a set or array of file descriptors and scan them in each iteration; epoll retains the interest set and maintains a candidate list through underlying ready notifications, typically reducing the repetitive processing of a large number of idle descriptors.
This does not mean that any epoll_wait is O(1): returning k events incurs at least the cost of processing and copying them. Registration modifications, callbacks, lock contention, and a large number of active connections also have costs. The number of descriptors for poll and epoll is limited by resources and cannot be written as unlimited; glibc's select also has a fixed FD_SETSIZE limit.
Three Types of Operations
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);
This is an API snippet requiring <sys/epoll.h>; you must check epfd and rc. ADD registers, MOD modifies interest and user data, and DEL deletes. A positive return value from epoll_wait is the number of events in this batch, 0 indicates a timeout, and -1 indicates an error; EINTR should not be mistaken for "no connections" or "all connections closed."
.data is association information saved by the application and returned verbatim; it can use an fd, a pointer, or an ID with a generation counter. The kernel does not manage the lifecycle of the pointed-to object on behalf of the application.
Event Bits Are Not Mutually Exclusive Enums
Event or Flag
Meaning and Handling
EPOLLIN
Attempt to read; for listening sockets, attempt accept
EPOLLOUT
Attempt to write; does not imply that any length can be sent in one go
EPOLLERR
Error; can be queried using SO_ERROR, etc.; received even without explicit subscription
EPOLLHUP
Hang up; there may still be buffered data to read
EPOLLRDHUP
The peer has closed the write direction of a stream socket; available since Linux 2.6.17
EPOLLET
Switch to edge-triggered mode
EPOLLONESHOT
Disables subsequent notifications for this registration after delivering the event; requires MOD to reset
EPOLLEXCLUSIVE
Reduces wake-ups for multiple epoll instances in specific registration scenarios; not a general-purpose lock
IN, HUP, ERR, etc., can appear simultaneously. ONESHOT does not automatically execute DEL, nor does it close the fd. epoll_ctl(2)
LT vs. ET: When to Wait Again
LT (Level-Triggered) is the default mode; it continues to report if the condition remains ready. The application can process some data and return to waiting, but if it never consumes data or always subscribes to writable events, the loop will continue to be woken up.
ET (Edge-Triggered) does not guarantee that unconsumed ready states will be repeatedly notified. The common practice is to use non-blocking I/O and only consider the notification reliable after processing until EAGAIN is returned. Do not simplify ET to strictly reporting a binary state flip for all fds; specific wake-ups come from underlying events, and the application should not rely on extra notifications to fix missed processing. epoll(7)
Below is a reading snippet for a stream socket. consume_bytes must process quickly or be enqueued to the application queue; it must not block in the event thread. Returning 0 means EAGAIN was reached, 1 means EOF on the input direction, and -1 means other errors:
EOF only ends the input direction from the peer to this end; you may still need to send responses or drain the output. Whether to close the entire connection should be determined by protocol state, not by immediately discarding the object upon any HUP.
There is still data, why didn't epoll_wait return new events?
Under ET, executing "arrive → wait → read → wait" can result in the second wait returning 0, while two bytes remain in the buffer. The pending processing marker retained by the application allows the remaining work to continue scheduling; it cannot be deleted just because there are no new events in this round. Reading exactly two bytes does not automatically prove the buffer is drained; you must attempt to read again and get EAGAIN to confirm this boundary.
Preparing the visual
There is still data, why didn't epoll_wait return new events?
ET does not guarantee repeated reminders for unread data. When the budget is exhausted, the application's pending state should be retained; read returning EAGAIN indicates that the buffer is empty at this moment.
{"id":"epoll-unread-work","title":"There is still data, why didn't epoll_wait return new events?","summary":"ET does not guarantee repeated reminders for unread data. When the budget is exhausted, the application's pending state should be retained; read returning EAGAIN indicates that the buffer is empty at this moment.","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":"Arrive 4 bytes","buffer":"Unread bytes","drained":"EAGAIN observed","et":"ET","event":"Number of recent wait events","heading":"There is still data, why didn't epoll_wait return new events?","intro":"Inject 4 bytes, get one event, then read only 2 bytes, comparing LT and ET.","lt":"LT","mode":"Notification Mode","poll":"epoll_wait(timeout=0)","queue":"Application record of pending work","read":"read up to 2 bytes","reset":"Reset","result":"Recent read result","retain":"Should continue scheduling processing","scope":"A single non-blocking stream, capacity 8, no EOF, errors, or ONESHOT. Each injection produces a coalesced notification, illustrating a valid ET scenario, not defining ET as a strict binary flip for all fds.","scopeLabel":"Model Scope and Conventions","unknown":"Not yet checked"}}
Write Backpressure and Fairness
When the application has data to send, it attempts non-blocking send first; if only part is written or EAGAIN is returned, save the remaining range and watch for EPOLLOUT. Once the send queue is empty, it is usually best to cancel writable interest; otherwise, LT mode may spin idly. Socket writing also requires clear policies for SIGPIPE or MSG_NOSIGNAL.
Serving a single high-throughput connection until EAGAIN may starve other connections. You can set byte or time budgets per connection; under ET, if the budget is exhausted but the buffer is not drained, the connection must remain in the application's ready queue and continue scheduling, rather than returning directly to epoll_wait hoping for another notification.
Listening sockets should also loop accept until EAGAIN; accept4 can atomically specify NONBLOCK/CLOEXEC for new connections. You must also handle errors such as fd exhaustion to avoid busy loops caused by persistent failures when the listener is ready.
Fair scheduling requires explicitly saving "still has work"
Assume A currently has 10 units readable and B has 2 units readable, with a per-round budget of 4. After A uses its first round of budget but still has work, placing it at the end of the queue allows B to finish first. If A is forgotten directly under ET, the remaining bytes may wait indefinitely for a notification that is not guaranteed to come again.
This is an offline model with known workload. Real sockets cannot know remaining in advance; use EAGAIN, EOF, and errors from non-blocking read to update state, and keep connections that have exhausted their budget but not yet drained as application-runnable. If parsing a single message is itself time-consuming, limiting read bytes is still insufficient to guarantee fairness; you must also limit parsing/business execution time.
Multiple Worker Threads and ONESHOT
ONESHOT is often used to let one handler temporarily take over a connection. After processing, re-enable interest via EPOLL_CTL_MOD; this does not automatically make all connection fields thread-safe, nor does it replace memory synchronization during task handover.
When resetting, publish the connection state and interest set first, and consider that other threads may receive events immediately after reset. If unread ready data remains, re-enabling may deliver events immediately again. Locks, ownership, or serial executors should clearly specify who can modify the output queue and close connections.
Using eventfd to See ONESHOT and Rearming
Save as oneshot-demo.c. eventfd is initialized with a value of 3; after the first epoll_wait receives a readable event, intentionally do not read; the count remains, but ONESHOT has disabled subsequent notifications. After re-MODding, the existing readable condition causes the event to return again.
Run cc -Wall -Wextra oneshot-demo.c -o oneshot-demo then execute ./oneshot-demo. All waits use zero timeout, checking already existing ready conditions; in this example, there are no other signal sources, so wait failures terminate directly; general loops still need to handle EINTR. ONESHOT
Here read returns 8 bytes, and the read count is 3; the two have different meanings. Without EFD_SEMAPHORE, a successful read retrieves the current accumulated value and clears it; eventfd acts as a notification counter and does not store three separate business messages. eventfd(2)
Closing and fd Number Reuse
fds are reusable integers. A batch may already contain events from an old connection, which is then closed by a callback, and a new connection obtains the same fd. Directly looking up the current object by this number may incorrectly apply old events to the new connection; object generations, stable IDs, deferred reclamation, or checksums can solve such issues.
Duplicated descriptors may still share open file descriptions; closing one fd does not necessarily make all related registrations disappear immediately. For deterministic revocation, execute DEL while the object lifecycle is still valid, and clean up according to the rules for events already in the application queue. The kernel removing a registration cannot revoke events already copied to user space.
How an old event can be misdelivered to a new connection
Assume the current batch saved (slot=7, generation=4). The previous callback closed that connection and released the slot; the new connection reuses slot=7, but the generation becomes 5. When dispatching remaining events, you must check the generation; you cannot just find a non-empty object by slot or fd and continue.
Generations need sufficient range and must define wrap-around and reuse strategies; they do not replace concurrent memory reclamation. If data.ptr is already a freed pointer, dereferencing it to read the generation is still unsafe. You can use integer IDs to query a still-valid registry, or have the event batch hold object references until dispatch ends.
timerfd, signalfd, and eventfd
timerfd needs timerfd_settime to start after creation; reading usually gets the accumulated expiration count, not meaning only one expiration per readable event. eventfd reads the notification count. Both should be read according to the record size specified by the interface, and handle non-blocking empty reads.
signalfd requires blocking the target signal in the relevant thread first. Below is a snippet for program initialization, requiring <signal.h>, <pthread.h>, <sys/signalfd.h>:
You should establish the mask you want worker threads to inherit before creating them, or coordinate per-thread; otherwise, signals may be received by other threads that did not block them. Forgetting sigemptyset leaves the mask contents uninitialized. SIGKILL and SIGSTOP cannot be taken over by signalfd. signalfd(2)
Real Boundaries of Event-Driven Programming
Executing blocking DNS, disk reads, long computations, or waiting for regular locks inside a ready callback will still block the entire event thread. Offload time-consuming tasks to appropriate execution mechanisms and pass results back via queues, eventfd, or other notifications.
epoll suits the readiness model; io_uring uses request completion as its primary interface. Both require application state machines, backpressure, cancellation, and resource management; interface names do not automatically solve these problems.