Serene Runtime 1.0.0-dev
C runtime for the Serene programming language
Loading...
Searching...
No Matches
epoll.c
Go to the documentation of this file.
1/* -*- C -*-
2 * Serene programming language
3 * Copyright (C) 2019-2026 Sameer Rahmani <[email protected]>
4 *
5 * This library is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Lesser General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Lesser General Public License for more details.
14 *
15 * You should have received a copy of the GNU Lesser General Public License
16 * along with this library. If not, see <https://www.gnu.org/licenses/>.
17 */
18
19/** @file
20## The epoll backend. Linux only.
21
22It arms a descriptor, and when epoll reports it ready it performs the transfer
23itself and posts the finished result. A self-wake `eventfd`, registered in the
24epoll set, lets a submission on any thread break the reactor thread out of
25`epoll_wait`.
26
27## Notes:
28- Operations (`epoll_op_t`) live in a fixed pool sized from the reactor ring
29magnitude, with an intrusive free list. No need for growth as we're limited by
30the magnitude of the reactor.
31
32- Several operations can share one descriptor (two fibers reading and writing
33the same socket, many fibers accepting on one listener). They are grouped in a
34per-fd list (`epoll_op_list_t`) and the fd is registered once with the union of
35their interests. `epoll_event.data.ptr` carries the list, so a readiness event
36resolves to the fd's ops with no search. The wait loop then walks the list and
37completes those whose direction is ready. The wake fd is distinguished by a NULL
38data pointer (a real list pointer is never NULL).
39
40- The fd -> list mapping is a hashmap (check the serene/rt/impl/hashmap.h). The
41fd integer is stuffed straight into the key's `data` field (it fits in a
42pointer), so the map needs no separate key storage; a custom control hashes and
43compares `data` by value instead of dereferencing it.
44*/
45
46#include <assert.h>
47
48#include "serene/rt/engine.h"
49#include "serene/rt/errors.h"
50
51#ifdef __linux__
52
53# include <errno.h>
54# include <stdint.h>
55# include <sys/epoll.h>
56# include <sys/eventfd.h>
57# include <sys/socket.h>
58# include <unistd.h>
59
60# include "../backend.h"
61# include "../internal.h"
63# include "serene/utils.h"
64
65// Since use fd value as (void *) for the map keys. We don't use them as
66// pointers so we're safe really.
67// NOLINTBEGIN(performance-no-int-to-ptr)
68typedef struct epoll_op_t {
69 /// Whether this slot holds a live operation. The discriminator between the
70 /// slot's two lives, read op fields only when true, `next_free` only when
71 /// false.
72 bool active;
73 /// While this slot is free, the index of the next free slot (SIZE_MAX ends
74 /// the chain). Meaningless while active.
75 size_t next_free;
76
77 int fd;
78 /// Next op on the same fd, or NULL. The per-fd list is threaded through here.
79 struct epoll_op_t *fd_next;
80
81 /// The kind of transfer. The epoll interest (EPOLLIN/EPOLLOUT) is derived
82 /// from it by `op_events`, so it is not stored twice.
84 /// Where the completion is posted back.
85 size_t channel;
86
87 void *fiber_data;
88 void *buf;
89 size_t len;
90 int64_t offset;
91
92 /// Socket address buffer, the peer to use for CONNECT/SENDTO, the peer to
93 /// fill in for ACCEPT/RECVFROM, or NULL. A `struct sockaddr *` held as `void
94 /// *`.
95 void *addr;
96 /// Length of `addr`, the exact length for CONNECT/SENDTO, the buffer capacity
97 /// for ACCEPT/RECVFROM.
98 uint32_t addrlen;
99 /// Operation flags, the `MSG_*` flags for the datagram and message ops, the
100 /// `accept4` flags for ACCEPT, and the requested readiness for POLL.
101 int flags;
102} epoll_op_t;
103
104typedef struct epoll_op_list_t {
105 /// The map key for this fd. `key.data` holds the fd value itself (see the
106 /// custom control), so it doubles as the fd's stable storage. The map keeps a
107 /// pointer to it, so it must live as long as the entry, it does, inside this
108 /// arena-allocated list.
109 hmap_key_t key;
110 /// Tombstone. The map is never deleted from (that keeps allocation bounded by
111 /// distinct fd integers). Instead, when the last op on an fd finishes, the fd
112 /// is removed from epoll and the list is marked dormant. A later op on the
113 /// same fd integer revives this same list. A dormant list (`deleted == true`)
114 /// is not registered in epoll and has no ops. And since POSIX specifies that
115 /// when allocating a FD, the smallest available FD should be chosen. So
116 /// We will end up recycling the deleted slots quite often.
117 bool deleted;
118 /// Head of the ops waiting on this fd
119 epoll_op_t *ops;
120} epoll_op_list_t;
121
122typedef struct epoll_state_t {
123 /// The main epoll descriptor.
124 int epfd;
125 /// Self-wake eventfd, registered in `epfd` with EPOLLIN. The reactor thread
126 /// sleeps in `epoll_wait`, which the kernel only breaks for IO readiness --
127 /// but two non-IO events must also break it, a worker submitting a new
128 /// request (so the reactor drains the SQ and arms the new fd) and shutdown
129 /// (so the thread observes `running == false` and exits).
130 /// `epoll_backend_wake` writes this fd from any thread, making it readable so
131 /// `epoll_wait` returns at once. The wait loop recognises it, drains the
132 /// counter (`drain_wake`) to silence the level-triggered fd, and treats it as
133 /// a no-op event. This is the submit side nudge; without it the reactor would
134 /// block until some already armed descriptor fired and never notice new work.
135 int wake_fd;
136 /// The pool of armed, but not yet completed operations. epoll only reports
137 /// readiness, not the request, so this is the backend's memory of "what to do
138 /// when each fd fires". Its size is fixed at init from
139 /// `reactor.ring_magnitude` (the per-channel ring capacity) times the channel
140 /// count, which is the most operations that can be in flight, so the pool
141 /// never needs to grow.
142 epoll_op_t *ops;
143 /// Number of slots in `ops`.
144 size_t total_slots;
145 /// Head of the intrusive free list, the first free slot, or SIZE_MAX if the
146 /// pool is full.
147 size_t free_head;
148 /// fd -> `epoll_op_list_t`. Grouped ops live here so a readiness event finds
149 /// every op on its fd. Persistent and arena-backed; reused via tombstones, so
150 /// it grows only with distinct fd integers.
151 hmap_t fdmap;
152} epoll_state_t;
153
154// -----------------------------------------------------------------------------
155// fd -> op-list map
156// -----------------------------------------------------------------------------
157// The key is the fd integer stored in `hmap_key_t.data` by value, not by
158// pointer. The default control would dereference `data` to hash and compare it;
159// this control treats `data` as the value, so no separate key storage is
160// needed.
161
162/**
163 * Compare two fd keys. The fd lives in `data` itself, so identity of the value
164 * is identity of the key.
165 */
166static bool fdmap_cmp(const hmap_t *hmap, const hmap_key_t *a, const hmap_key_t *b) {
167 UNUSED(hmap);
168 assert(a != nullptr);
169 assert(b != nullptr);
170 // fd 0 is a valid descriptor and stores as a null `data`, so there is no
171 // non-null invariant to assert on the key payload.
172 return a->data == b->data;
173}
174
175/**
176 * Hash an fd key, the fd is its own hash. Distinct fds give distinct hashes, so
177 * there are no collision nodes, and small sequential fds fan out evenly across
178 * the trie's low-order 5-bit chunks. Running them through a scrambling hash
179 * would only add cost and could manufacture collisions the raw value avoids.
180 */
181static hmap_hash_t fdmap_hash(const hmap_t *hmap, const hmap_key_t *k) {
182 UNUSED(hmap);
183 assert(k != nullptr);
184 // fd 0 is a valid descriptor and stores as a null `data`; its hash is 0.
185 return (hmap_hash_t)(intptr_t)k->data;
186}
187
188/**
189 * Copy an fd key into the map's context. The fd lives in `data` by value, so
190 * cloning the struct is enough; the default byte copier would dereference it
191 * as a pointer.
192 */
193static hmap_key_t *fdmap_copy_key(const hmap_t *hmap, const hmap_key_t *k) {
194 hmap_key_t *copy = ALLOC(hmap->map_ctx, hmap_key_t);
195 *copy = *k;
196 return copy;
197}
198
199static hmap_control_t fdmap_control = {
200 .cmp = fdmap_cmp,
201 .hash = fdmap_hash,
202 .insert = hmap_insert_ctl,
203 .lookup = hmap_lookup_ctl,
204 .copy_key = fdmap_copy_key,
205};
206
207/**
208 * Find the op list for `fd`, creating a dormant one on first sight. A freshly
209 * created list is `deleted` (not yet registered in epoll); the first op to arm
210 * it revives it. The fd is stored inside the list's key, which is what the map
211 * points at.
212 */
213static epoll_op_list_t *fd_list(srn_context_t *ctx, epoll_state_t *st, int fd) {
214 assert(ctx != nullptr);
215 assert(st != nullptr);
216
217 hmap_key_t probe = {.data = (void *)(intptr_t)fd, .len = sizeof(int)};
218
219 epoll_op_list_t *l = hmap_lookup_ctl(&fdmap_control, &st->fdmap, &probe, nullptr);
220
221 if (l != nullptr) {
222 return l;
223 }
224
225 // Create the entry as it is not there
226 l = ALLOC(ctx, epoll_op_list_t);
227 l->key = (hmap_key_t){.data = (void *)(intptr_t)fd, .len = sizeof(int)};
228 l->deleted = true; // dormant until the first op arms it
229 l->ops = nullptr;
230 st->fdmap = hmap_insert_ctl(&fdmap_control, &st->fdmap, &l->key, l);
231 return l;
232}
233
234/**
235 * The epoll interest an op waits on. Read-side ops (READ, RECVFROM, RECVMSG,
236 * ACCEPT) wait for EPOLLIN; write-side ops (WRITE, SENDTO, SENDMSG, CONNECT)
237 * wait for EPOLLOUT. POLL carries its own requested events in `flags`.
238 */
239static uint32_t op_events(const epoll_op_t *op) {
240 switch (op->op) {
245 return EPOLLIN;
250 return EPOLLOUT;
252 return (uint32_t)op->flags;
253 default:
254 // NOP/SLEEP/CANCEL never reach a backend; anything else is a routing bug.
255 PANIC("epoll backend: operation has no readiness direction");
256 }
257}
258
259/**
260 * The union of every armed op's interest on this fd, what the fd is registered
261 * for in epoll.
262 */
263static uint32_t list_mask(const epoll_op_list_t *l) {
264 uint32_t m = 0;
265 for (epoll_op_t *o = l->ops; o != nullptr; o = o->fd_next) {
266 m |= op_events(o);
267 }
268 return m;
269}
270
271// -----------------------------------------------------------------------------
272// Operation pool (intrusive free list)
273// -----------------------------------------------------------------------------
274
275/**
276 * Take a free slot from the pool, or null when the pool is full. The caller
277 * fails the operation back to its fiber (a posted completion), so there is no
278 * need to allocate or carry an error here.
279 */
280static epoll_op_t *alloc_op(epoll_state_t *st) {
281 if (st->free_head == SIZE_MAX) {
282 return nullptr; // pool exhausted
283 }
284
285 // Pop the head, take the first free slot, advance the head to its successor.
286 size_t idx = st->free_head;
287 st->free_head = st->ops[idx].next_free;
288 return &st->ops[idx];
289}
290
291/**
292 * Return a slot to the pool, pushing it onto the front of the free list. Takes
293 * the op pointer (the wait loop holds the pointer, not the index) and recovers
294 * the index by its offset in the fixed `ops` array.
295 */
296static void free_op(epoll_state_t *st, epoll_op_t *op) {
297 size_t idx = (size_t)(op - st->ops); // pointer back to its slot index
298 op->active = false;
299 op->next_free = st->free_head; // point at the old head
300 st->free_head = idx; // become the new head
301}
302
303// -----------------------------------------------------------------------------
304// IO helpers
305// -----------------------------------------------------------------------------
306
307/**
308 * Empty the self-wake eventfd's counter. It is level-triggered, so epoll keeps
309 * reporting it readable until it is drained back to zero.
310 */
311static void drain_wake(epoll_state_t *st) {
312 assert(st != nullptr);
313
314 // A read of an eventfd returns its accumulated counter and resets it to zero.
315 // The fd is EFD_NONBLOCK, so once the counter is zero the read fails with
316 // EAGAIN instead of blocking, which ends the loop. So this absorbs however
317 // many wakes were coalesced into the counter and leaves it drained. And since
318 // we are draining the `wake_fd`, this loop is really trivial.
319 uint64_t v;
320 while (read(st->wake_fd, &v, sizeof(v)) == (ssize_t)sizeof(v)) {
321 }
322}
323
324/**
325 * Perform the transfer for a ready operation and fill `*out`. Returns false on
326 * EAGAIN/EWOULDBLOCK (the descriptor was not actually ready, so the op stays
327 * armed for the next readiness event); otherwise true with the result set.
328 *
329 * This function deliberately only does the transfer and records the result. It
330 * does NOT free the slot, deregister the fd, or post the completion. The wait
331 * loop owns that ordering (it must deregister before the completion is visible,
332 * so a worker that reaps it can safely close the fd).
333 *
334 * `ready` is the readiness mask epoll reported for the fd; only POLL uses it
335 * (to report which events fired), the transfers ignore it.
336 */
337static bool perform_op(epoll_op_t *op, uint32_t ready, srn_reactor_io_completion_t *out) {
338 assert(op != nullptr);
339
340 // Success by default; the error paths set `maybe_error`. `result` and
341 // `addrlen` are meaningful only on success, but default them so a failed op
342 // never leaves them indeterminate. Only ACCEPT and RECVFROM fill `addrlen`.
343 out->maybe_error = nullptr;
344 out->result = 0;
345 out->addrlen = 0;
346
347 ssize_t n;
348
349 switch (op->op) {
351 n = (op->offset < 0) ? read(op->fd, op->buf, op->len)
352 : pread(op->fd, op->buf, op->len, (off_t)op->offset);
353 break;
355 n = (op->offset < 0) ? write(op->fd, op->buf, op->len)
356 : pwrite(op->fd, op->buf, op->len, (off_t)op->offset);
357 break;
359 // The result is the accepted descriptor, carried back in `result` like a
360 // byte count. `addrlen` is value-result for accept4: pass the buffer
361 // capacity in, report the actual peer length back (valid when n >= 0).
362 socklen_t al = (socklen_t)op->addrlen;
363 n = accept4(
364 op->fd, (struct sockaddr *)op->addr, (op->addr != nullptr) ? &al : nullptr, op->flags
365 );
366 out->addrlen = (uint32_t)al;
367 break;
368 }
370 // Readiness means the non-blocking connect finished; SO_ERROR carries the
371 // verdict (0, or an errno). There is no transfer and no EAGAIN here.
372 int soerr = 0;
373 socklen_t errlen = sizeof(soerr);
374 if (getsockopt(op->fd, SOL_SOCKET, SO_ERROR, &soerr, &errlen) < 0) {
375 soerr = errno;
376 }
377 out->fiber_data = op->fiber_data;
378 if (soerr != 0) {
380 } else {
381 out->result = 0;
382 }
383 return true;
384 }
386 // `addrlen` is value-result here too, report the sender address length
387 // back.
388 socklen_t al = (socklen_t)op->addrlen;
389 n = recvfrom(
390 op->fd, op->buf, op->len, op->flags, (struct sockaddr *)op->addr,
391 (op->addr != nullptr) ? &al : nullptr
392 );
393 out->addrlen = (uint32_t)al;
394 break;
395 }
397 n = sendto(
398 op->fd, op->buf, op->len, op->flags, (struct sockaddr *)op->addr, (socklen_t)op->addrlen
399 );
400 break;
402 n = recvmsg(op->fd, (struct msghdr *)op->buf, op->flags);
403 break;
405 n = sendmsg(op->fd, (struct msghdr *)op->buf, op->flags);
406 break;
408 // No transfer, report which events are ready.
409 out->fiber_data = op->fiber_data;
410 out->result = (size_t)ready;
411 return true;
412 default:
413 // NOP/SLEEP/CANCEL are handled by the reactor core and never reach a
414 // backend. Anything else is a routing bug, not a runtime condition.
415 PANIC("epoll backend: unexpected operation kind");
416 }
417
418 if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
419 return false; // spurious readiness; leave it armed
420 }
421
422 out->fiber_data = op->fiber_data;
423 if (n < 0) {
425 } else {
426 out->result = (size_t)n; // bytes or accepted fd
427 }
428 return true;
429}
430
431// -----------------------------------------------------------------------------
432// Lifecycle
433// -----------------------------------------------------------------------------
434
435/**
436 * Bring the epoll backend up, build the operation pool and the fd map, open the
437 * epoll instance, and register the self-wake eventfd.
438 */
439static srn_error_t *epoll_backend_init(srn_reactor_t *reactor) {
440 PANIC_IF_NULL(reactor);
441
442 srn_context_t *ctx = reactor->ctx;
443
444 epoll_state_t *st = ALLOC(ctx, epoll_state_t);
445 size_t cap = (size_t)1 << ctx->engine->config.reactor.ring_magnitude;
446 st->total_slots = reactor->nchannels * cap;
447 st->ops = ALLOCN(ctx, epoll_op_t, st->total_slots);
448 st->fdmap = hmap_empty(ctx);
449
450 // Chain every slot into the free list: 0 -> 1 -> ... -> last -> end.
451 for (size_t i = 0; i < st->total_slots; i++) {
452 st->ops[i].active = false;
453 st->ops[i].next_free = i + 1;
454 }
455 st->ops[st->total_slots - 1].next_free = SIZE_MAX;
456 st->free_head = 0;
457
458 st->epfd = epoll_create1(EPOLL_CLOEXEC);
459 st->wake_fd = -1;
460
461 if (st->epfd == -1) {
462 return ERR(ctx, EPOLL_INIT_FAILED, "Couldn't create the epoll file descriptor.");
463 }
464
465 st->wake_fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
466 if (st->wake_fd == -1) {
467 close(st->epfd);
468 return ERR(
469 ctx, EPOLL_EVENT_FD_FAILED, "Couldn't create the event file descriptor of the reactor."
470 );
471 }
472
473 // Register the wake fd with a NULL data pointer. Armed fds carry a non-NULL
474 // pointer to their op list (below), so NULL uniquely marks "this is the wake
475 // fd".
476 struct epoll_event ee = {.events = EPOLLIN, .data.ptr = nullptr};
477 if (epoll_ctl(st->epfd, EPOLL_CTL_ADD, st->wake_fd, &ee) == -1) {
478 close(st->wake_fd);
479 close(st->epfd);
480 return ERR(ctx, EPOLL_FAILED_TO_ADD_FD, "Failed to add the wake_fd to the epfd");
481 }
482
483 reactor->backend_state = st;
484 return nullptr;
485}
486
487/**
488 * Tear the backend down, close the descriptors and detach the state. The pool,
489 * the lists and the map are arena-allocated (from the reactor context), so they
490 * are reclaimed in bulk with the context; there is nothing to free here.
491 */
492static void epoll_backend_shutdown(srn_reactor_t *reactor) {
493 PANIC_IF_NULL(reactor);
494
495 epoll_state_t *st = (epoll_state_t *)reactor->backend_state;
496 if (st == nullptr) {
497 return;
498 }
499
500 close(st->wake_fd);
501 close(st->epfd);
502 reactor->backend_state = nullptr;
503}
504
505// -----------------------------------------------------------------------------
506// Submit / wait / wake
507// -----------------------------------------------------------------------------
508
509/**
510 * Arm a descriptor operation. The op joins its fd's list, and the fd is
511 * registered (or re-registered) for the union of its ops' interests. On a full
512 * pool, or if epoll refuses the fd, the operation is completed with its
513 * `maybe_error` set rather than aborting, so the waiting fiber wakes and can
514 * react. Called by the reactor core on the reactor thread while draining
515 * submissions.
516 */
517static void
518epoll_backend_submit(srn_reactor_t *reactor, size_t channel, const srn_reactor_io_request_t *req) {
519 PANIC_IF_NULL(reactor);
521
522 // A POLL that asks for no pollable event would register an empty interest
523 // mask, nothing could ever match it, and the fiber would park forever.
524 // Fail it back as malformed rather than arming it.
525 if (req->op == SRN_REACTOR_IO_POLL && (req->flags & (EPOLLIN | EPOLLOUT)) == 0) {
527 reactor, channel,
529 .fiber_data = req->fiber_data, .maybe_error = srn_reactor_errors_static(INVALID_ARGUMENT)
530 }
531 );
532 return;
533 }
534
535 srn_context_t *ctx = reactor->ctx;
536 epoll_state_t *st = reactor->backend_state;
537 epoll_op_t *op = alloc_op(st);
538
539 if (op == nullptr) {
540 // Pool full, fail this op back to its fiber. -ENOBUFS ("no buffer space
541 // available") is the closest errno for an exhausted op pool; it is
542 // transient, so the fiber can retry once in-flight operations drain.
545 reactor, channel,
546 (srn_reactor_io_completion_t){.fiber_data = req->fiber_data, .maybe_error = err}
547 );
548 return;
549 }
550
551 op->active = true;
552 op->fd = req->fd;
553 op->op = req->op;
554 op->channel = channel;
555 op->fiber_data = req->fiber_data;
556 op->buf = req->buf;
557 op->len = req->len;
558 op->offset = req->offset;
559 op->addr = req->addr;
560 op->addrlen = req->addrlen;
561 op->flags = req->flags;
562
563 // CONNECT issues the (non-blocking) connect now; the readiness later reports
564 // its verdict via SO_ERROR. An immediate success, or a hard failure that is
565 // not EINPROGRESS, completes here without arming anything.
566 if (op->op == SRN_REACTOR_IO_CONNECT) {
567 int rc = connect(op->fd, (struct sockaddr *)op->addr, (socklen_t)op->addrlen);
568
569 if (rc == 0 || errno != EINPROGRESS) {
570 srn_error_t *err =
571 (rc == 0) ? nullptr : srn_reactor_errors_static(srn_errno_to_tag(errno, UNKNOWN_IO_ERROR));
572 free_op(st, op);
574 reactor, channel,
575 (srn_reactor_io_completion_t){.fiber_data = req->fiber_data, .maybe_error = err}
576 );
577
578 return;
579 }
580 // EINPROGRESS: fall through and arm EPOLLOUT like any other op.
581 }
582
583 // Join the fd's list. A dormant (deleted) list is not yet in epoll, so this
584 // is the first op on the fd and needs an ADD; otherwise the fd is already
585 // registered and its interest mask only needs widening with a MOD.
586 epoll_op_list_t *l = fd_list(ctx, st, req->fd);
587 bool need_add = l->deleted;
588 l->deleted = false;
589 op->fd_next = l->ops;
590 l->ops = op;
591
592 // The list pointer rides in epoll's data, so a readiness event lands on every
593 // op for the fd with no search of the pool.
594 struct epoll_event ee = {.events = list_mask(l), .data.ptr = l};
595 int rc = epoll_ctl(st->epfd, (int)need_add ? EPOLL_CTL_ADD : EPOLL_CTL_MOD, req->fd, &ee);
596
597 if (rc == -1) {
598 // Could not arm it (bad fd, ...). Pop the op back off the list, return the
599 // slot and fail the op back; otherwise it would sit armed-in-software but
600 // never fire and the fiber would hang. If that emptied the list, mark it
601 // dormant again so the next op on this fd re-adds rather than mods.
602 int err = errno;
603 l->ops = op->fd_next;
604 free_op(st, op);
605 if (l->ops == nullptr) {
606 l->deleted = true;
607 }
608
611 reactor, channel,
612 (srn_reactor_io_completion_t){.fiber_data = req->fiber_data, .maybe_error = err1}
613 );
614 }
615}
616
617/**
618 * Wait for readiness up to `timeout_ms` (epoll's own unit for timout: -1 blocks
619 * until an event, 0 polls), then perform and complete every ready operation.
620 * Each event carries its fd's op list, so it resolves with no scan of the pool;
621 * the loop walks the list and completes the ops whose direction is ready.
622 * Called by the reactor thread's loop each pass.
623 */
624static void epoll_backend_wait(srn_reactor_t *reactor, int timeout_ms) {
625 PANIC_IF_NULL(reactor);
627
628 epoll_state_t *st = reactor->backend_state;
629 size_t batch = reactor->ctx->engine->config.reactor.reap_batch;
630
631 struct epoll_event events[batch];
632 int n = epoll_wait(st->epfd, events, (int)batch, timeout_ms);
633
634 if (n < 0) {
635 // The only error expected here is EINTR: a signal interrupted the wait, and
636 // the reactor loop simply calls wait again. Any other errno means epfd or
637 // the arguments are invalid -- a setup bug, not a runtime condition -- and
638 // silently returning would turn the reactor loop into a hot spin, so it
639 // panics in every build.
640 PANIC_IF(errno != EINTR, "reactor: epoll_wait failed");
641 return;
642 }
643
644 if (n == 0) {
645 return; // timed out; nothing was ready
646 }
647
648 for (int i = 0; i < n; i++) {
649 // The wake fd is the one event registered with a NULL data pointer. It
650 // carries no operation, someone (a worker that just submitted, or shutdown)
651 // wrote the eventfd purely to force this epoll_wait to return so the
652 // reactor thread re-checks its queues. Drain it and move on.
653 if (events[i].data.ptr == nullptr) {
654 drain_wake(st);
655 continue;
656 }
657
658 epoll_op_list_t *l = (epoll_op_list_t *)events[i].data.ptr;
659
660 // An error or hangup on the fd wakes both directions, a reader and a writer
661 // on the same fd should both observe it.
662 uint32_t ready = events[i].events;
663 if (ready & (EPOLLERR | EPOLLHUP)) {
664 ready |= (EPOLLIN | EPOLLOUT);
665 }
666
667 uint32_t before = list_mask(l);
668
669 // Walk the fd's ops, completing those the readiness covers. `link` is the
670 // address of the pointer that reaches the current op, so unlinking is a
671 // single store and the walk continues from the same spot.
672 epoll_op_t **link = &l->ops;
673 while (*link != nullptr) {
674 epoll_op_t *op = *link;
675
676 if ((op_events(op) & ready) == 0) {
677 link = &op->fd_next; // not this op's direction
678 continue;
679 }
680
681 // === PERFORM THE ACTUAL OPERATION ===
683 if (!perform_op(op, ready, &c)) {
684 link = &op->fd_next; // EAGAIN: still armed, will fire again
685 continue;
686 }
687
688 // Take this op off the list. If it was the last op on the fd, drop the fd
689 // from epoll and mark the list dormant -- and do both before posting the
690 // completion. Once the completion is posted, a worker may reap it and
691 // close the fd, so the fd has to be out of epoll by that point.
692 //
693 // The unlink already set `*link` to the next op, so the loop carries on
694 // without moving the cursor forward.
695 size_t ch = op->channel;
696 *link = op->fd_next;
697 if (l->ops == nullptr) {
698 epoll_ctl(st->epfd, EPOLL_CTL_DEL, op->fd, nullptr);
699 l->deleted = true;
700 }
701 free_op(st, op);
702 srn_reactor_post_completion(reactor, ch, c);
703 }
704
705 // If the fd is still armed but some ops finished, narrow its interest to
706 // what remains. Level-triggered epoll would otherwise report a stale
707 // EPOLLOUT every wait and spin.
708 if (!l->deleted) {
709 uint32_t now = list_mask(l);
710 if (now != before) {
711 int fd = (int)(intptr_t)l->key.data;
712 struct epoll_event ee = {.events = now, .data.ptr = l};
713 epoll_ctl(st->epfd, EPOLL_CTL_MOD, fd, &ee);
714 }
715 }
716 }
717}
718
719/**
720 * Break a concurrent epoll_wait out of its sleep from any thread, by making the
721 * self-wake eventfd readable. The reactor uses this as the submit-side nudge
722 * and during shutdown to re-check `running`. Called from srn_reactor_submit
723 * (after queuing a request) and srn_reactor_shutdown (to break the wait).
724 */
725static void epoll_backend_wake(srn_reactor_t *reactor) {
726 PANIC_IF_NULL(reactor);
727
728 epoll_state_t *st = (epoll_state_t *)reactor->backend_state;
729
730 uint64_t one = 1;
731 ssize_t rc = write(st->wake_fd, &one, sizeof(one));
732 // A full counter (EAGAIN) is fine, it is already readable, so the reactor
733 // will still wake. Any other error is ignored for the same reason.
734 UNUSED(rc);
735}
736
737// -----------------------------------------------------------------------------
738// Vtable
739// -----------------------------------------------------------------------------
741 .name = "epoll",
742 .init = epoll_backend_init,
743 .shutdown = epoll_backend_shutdown,
744 .submit = epoll_backend_submit,
745 .wait = epoll_backend_wait,
746 .wake = epoll_backend_wake,
748};
749
750#endif
751// NOLINTEND(performance-no-int-to-ptr)
int n
Definition acutest.h:525
Internal reactor backend interface.
const srn_reactor_backend_t srn_reactor_backend_epoll
#define SRN_REACTOR_BACKEND_FEAT_FILE_OFFLOAD
The backend cannot wait on regular file readiness, so the core serves regular file READ and WRITE off...
Definition backend.h:49
#define ALLOCN(ctx, T, N)
Definition context.h:85
#define ALLOC(ctx, T)
Definition context.h:84
struct hmap_t hmap_t
Definition engine.h:33
Error handling for the runtime.
@ EPOLL_EVENT_FD_FAILED
Definition errors.h:86
@ INVALID_ARGUMENT
The request itself is malformed (an empty POLL interest mask, for example), so retrying it unchanged ...
Definition errors.h:113
@ UNKNOWN_IO_ERROR
Definition errors.h:118
@ EPOLL_FAILED_TO_ADD_FD
Definition errors.h:87
@ EPOLL_INIT_FAILED
Definition errors.h:85
srn_error_tag_t srn_errno_to_tag(int errno_value, srn_error_tag_t default_tag)
Map a POSIX errno to error tag.
#define ERR(ctx, err, msg)
Definition errors.h:148
hmap_t hmap_empty(const srn_context_t *ctx)
Create, initialize and return a new hashmap pinned to ctx.
Definition hashmap.c:655
void * hmap_lookup_ctl(const hmap_control_t *ctl, const hmap_t *hmap, const hmap_key_t *k, void *default_value)
Just like the hmap_lookup function but, it receives a hmap_control_t to customize the comparison and ...
Definition hashmap.c:638
hmap_t hmap_insert_ctl(const hmap_control_t *ctl, const hmap_t *hmap, hmap_key_t *k, void *v)
Just like the hmap_insert function but, it receives a hmap_control_t to customize the comparison and ...
Definition hashmap.c:594
This is an implementation of Compressed Hash-Array Mapped Prefix-tree, which is a bit-partitioned,...
struct hmap_key_t hmap_key_t
Note: For key equality we use the memcpy function.
HMAP_HASH_TYPE hmap_hash_t
Definition hashmap.h:61
srn_error_t * srn_reactor_errors_static(srn_error_tag_t tag)
Return the shared, immutable error for an IO failure tag.
Definition reactor.c:125
void srn_reactor_post_completion(srn_reactor_t *reactor, size_t channel, srn_reactor_io_completion_t completion)
Post a finished operation to channel's completion queue and rouse the channel's worker through the no...
Definition reactor.c:141
srn_reactor_io_op_t
The operation a submission asks the reactor to perform.
Definition reactor.h:60
@ SRN_REACTOR_IO_ACCEPT
Accept a pending connection on the listening socket fd, with flags (the accept4 flags).
Definition reactor.h:79
@ SRN_REACTOR_IO_READ
Read from a descriptor into buf. Uses fd, buf, len, offset.
Definition reactor.h:68
@ SRN_REACTOR_IO_SENDTO
Send buf/len on fd to the address in addr/addrlen with flags.
Definition reactor.h:92
@ SRN_REACTOR_IO_CONNECT
Connect fd to the address in addr/addrlen.
Definition reactor.h:83
@ SRN_REACTOR_IO_RECVFROM
Receive a datagram from fd into buf/len with flags.
Definition reactor.h:88
@ SRN_REACTOR_IO_RECVMSG
Receive into the struct msghdr that buf points at, on fd, with flags (scatter/gather and ancillary da...
Definition reactor.h:96
@ SRN_REACTOR_IO_WRITE
Write buf to a descriptor. Uses fd, buf, len, offset.
Definition reactor.h:70
@ SRN_REACTOR_IO_POLL
Wait until fd is ready for the events requested in flags (EPOLLIN/EPOLLOUT) without performing any tr...
Definition reactor.h:105
@ SRN_REACTOR_IO_SENDMSG
Send the struct msghdr that buf points at, on fd, with flags.
Definition reactor.h:100
If we ever want to modify some of these behaviours for a new instance of hashmap, we should use this ...
Definition hashmap.h:109
Note: For key equality we use the memcpy function.
Definition hashmap.h:66
void * data
len 0 -> data == nullptr
Definition hashmap.h:68
const srn_context_t * map_ctx
The context that owns every allocation the map retains.
Definition hashmap.h:104
srn_reactor_config_t reactor
srn_engine_t * engine
Long term state of the compiler.
Definition context.h:49
srn_configuration_t config
The runtime's tunable knobs, the single source for every configurable value (see configuration....
Definition engine.h:62
A runtime error, a tag classifying the failure and a human-readable message.
Definition errors.h:125
One async system behind the reactor contract.
Definition backend.h:55
size_t reap_batch
Most completions a worker drains from its channel in a single pass.
size_t ring_magnitude
Magnitude of each channel's SQ/CQ rings, capacity is 1 << magnitude.
A completion, the result of one submission, echoed back on the channel.
Definition reactor.h:156
void * fiber_data
The fiber_data of the submission this completes.
Definition reactor.h:159
uint32_t addrlen
For SRN_REACTOR_IO_ACCEPT and SRN_REACTOR_IO_RECVFROM, the actual length of the address written into ...
Definition reactor.h:169
size_t result
The result of the operation on success, bytes transferred for the transfers, the accepted descriptor ...
Definition reactor.h:163
A submission, one request a fiber places on its channel.
Definition reactor.h:113
int fd
The descriptor the operation targets.
Definition reactor.h:117
int flags
Operation flags, the MSG_* flags for the datagram and message ops, the accept4 flags for SRN_REACTOR_...
Definition reactor.h:147
void * addr
Socket address.
Definition reactor.h:139
srn_reactor_io_op_t op
Definition reactor.h:114
void * buf
The transfer buffer for READ, WRITE, RECVFROM, and SENDTO, and the struct msghdr * for RECVMSG and SE...
Definition reactor.h:120
size_t len
The transfer length, in bytes, for READ, WRITE, RECVFROM, and SENDTO.
Definition reactor.h:122
int64_t offset
The file offset for SRN_REACTOR_IO_READ and SRN_REACTOR_IO_WRITE; -1 reads at the current position.
Definition reactor.h:125
void * fiber_data
Opaque token, round tripped to the matching completion.
Definition reactor.h:133
uint32_t addrlen
Length of addr, the exact address length for SRN_REACTOR_IO_CONNECT and SRN_REACTOR_IO_SENDTO,...
Definition reactor.h:143
srn_context_t * ctx
Definition internal.h:44
void * backend_state
Definition internal.h:48
#define PANIC_IF_NULL(ptr)
Definition utils.h:66
#define PANIC_IF(cond, msg)
Definition utils.h:59
#define UNUSED(x)
Definition utils.h:45
#define PANIC(msg)
Definition utils.h:53