libxtc 0.4.0
Async concurrency for C: Tokio + Seastar + BEAM, in one library
Loading...
Searching...
No Matches
io_int.h
1/*-
2 * Copyright (c) 2026, The XTC Project
3 * Use of this source code is governed by the ISC License.
4 *
5 * src/inc/io_int.h
6 * Internal struct definition for the L1 backend implementations.
7 */
8
9#ifndef XTC_IO_INT_H
10#define XTC_IO_INT_H
11
12#include "xtc_io.h"
13#include <stdatomic.h>
14
15#if defined(XTC_IO_BACKEND_EPOLL)
16/* nothing to predefine; epoll keeps tags via epoll_data_t */
17#elif defined(XTC_IO_BACKEND_KQUEUE)
18/* kqueue's EV_ADD is idempotent and EV_DELETE silently ignores absent
19 * filters, so we keep an authoritative side-table of currently-
20 * registered fds to enforce M2's semantic contract. */
21#elif defined(XTC_IO_BACKEND_SOLARIS)
22/* illumos event ports are one-shot per association, so we maintain
23 * the same side-table as kqueue and re-arm on each delivery. */
24struct __xtc_solaris_reg {
25 int fd;
26 uint32_t interest;
27 void *tag;
28};
29#elif defined(XTC_IO_BACKEND_IOCP)
30#include <pthread.h> /* MSVC: the compat shim; MinGW: winpthreads */
31/*
32 * Windows IOCP backend (round 2): native completion port plus the
33 * AFD poll fast path that turns a connected/listening socket's
34 * readiness into an IOCP completion.
35 *
36 * Each socket registration owns one AFD poll OVERLAPPED that is
37 * armed (submitted via NtDeviceIoControlFile(IOCTL_AFD_POLL)) into
38 * the kernel and re-armed after every completion -- level-triggered
39 * emulation matching the epoll/kqueue contract.
40 *
41 * OVERLAPPED OWNERSHIP RULE (the classic IOCP correctness invariant,
42 * enforced throughout io_iocp.c):
43 * The OVERLAPPED (ov) and the AFD_POLL_INFO (poll_info) embedded in
44 * a registration belong to the KERNEL from the moment the AFD poll
45 * is armed (NtDeviceIoControlFile returns STATUS_PENDING) until the
46 * matching completion is dequeued from the port by
47 * GetQueuedCompletionStatusEx. While armed (pending == 1) neither
48 * buffer may be freed or reused. Deregistering an armed socket
49 * issues NtCancelIoFileEx and marks the registration dead; the
50 * storage is only released once the (possibly canceled) completion
51 * is reaped. This is why the struct embeds ov/poll_info by value
52 * and the registration node is freed lazily, never inline with
53 * xtc_io_del_fd while a poll is in flight.
54 *
55 * The OVERLAPPED is the FIRST member so a completion's
56 * OVERLAPPED_ENTRY.lpOverlapped pointer can be cast straight back to
57 * the owning registration. Each registration is a separately
58 * heap-allocated node with a STABLE address (held in a pointer array,
59 * never an inline array), because the kernel-owned OVERLAPPED carries
60 * a back-pointer to its node for the whole time the poll is armed --
61 * a realloc or swap-remove of an inline array would dangle it.
62 */
63struct __xtc_iocp_reg {
64 struct __xtc_iocp_overlapped *ovp; /* OVERLAPPED + AFD_POLL_INFO (heap) */
65 int fd; /* the SOCKET as an int (Winsock handle) */
66 void *base; /* base socket HANDLE (SIO_BASE_HANDLE) */
67 uint32_t interest;
68 void *tag;
69 int pending; /* 1 while an AFD poll is armed in the kernel */
70 int dead; /* deregistered; awaiting terminal completion */
71 int64_t armed_at_ns; /* __os_clock_mono() when this arm was issued;
72 * drives the bounded re-poll workaround for
73 * the AFD async-completion bug (see io_iocp.c
74 * __xtc_iocp_repoll_sweep) */
75};
76/* A file AIO (pread/pwrite) in flight on the IOCP backend. The
77 * file HANDLE is associated with the completion port, so the
78 * overlapped ReadFile/WriteFile completion is dequeued by
79 * GetQueuedCompletionStatusEx like any socket event -- no hEvent and
80 * no WaitForMultipleObjects. fsync has no async form on Windows
81 * (FlushFileBuffers is synchronous) and is offloaded. The OVERLAPPED
82 * is the FIRST member so the completion's lpOverlapped recovers the
83 * node; ownership follows the same kernel-owns-while-pending rule as
84 * the socket poll OVERLAPPED above. */
85struct __xtc_iocp_aio {
86 void *ov; /* OVERLAPPED * (heap; first field; owned here) */
87 void *aio; /* xtc_aio_t * awaiting completion */
88 void *fh; /* HANDLE: the file, for GetOverlappedResult */
89};
90#elif defined(XTC_IO_BACKEND_AIX)
91/* AIX pollset_* backend. Like solaris/kqueue, we maintain a
92 * side-table for duplicate detection and to map fd -> user tag
93 * (pollset itself doesn't carry udata). */
94struct __xtc_aix_reg {
95 int fd;
96 uint32_t interest;
97 void *tag;
98};
99#elif defined(XTC_IO_BACKEND_URING)
100#include <liburing.h>
101#include <pthread.h>
102/*
103 * Per-fd state for the io_uring backend. The user_data passed to
104 * each POLL_ADD points at one of these. io->tab maps fd -> the LIVE
105 * node; a retired node whose poll is still in the kernel waits on
106 * io->zombies for its terminal CQE.
107 *
108 * armed a poll for this node is in the kernel (CQEs may arrive).
109 * Cleared by the terminal CQE (no IORING_CQE_F_MORE).
110 * dead retired by del/mod; no longer in io->tab.
111 * pending on io->pending: needs an SQE the SQ had no room for (a
112 * re-arm for a live node, a cancel for a dead armed one).
113 * linger retired by del while its one-shot poll was still armed, and
114 * NOT cancelled: it waits on io->lingers (lnext/lprev) for a
115 * re-registration of the same socket to adopt it, or for the
116 * end of the next poll to cancel it. ino/dev identify the
117 * socket (a closed fd's number can be reused).
118 *
119 * A node is on io->zombies exactly while dead && armed && !linger, and
120 * is freed once dead && !armed && !pending.
121 */
122struct __xtc_uring_fd {
123 int fd;
124 uint32_t interest;
125 void *tag;
126 int is_wakeup; /* 1 for the internal wakeup pipe */
127 int dead;
128 int armed;
129 int pending;
130 int linger;
131 uint64_t ino; /* socket identity (0 = not a socket) */
132 uint64_t dev;
133 struct __xtc_uring_fd *next; /* io->zombies linkage */
134 struct __xtc_uring_fd *prev; /* io->zombies linkage */
135 struct __xtc_uring_fd *pnext; /* io->pending linkage */
136 struct __xtc_uring_fd *lnext; /* io->lingers linkage */
137 struct __xtc_uring_fd *lprev;
138};
139#elif defined(XTC_IO_BACKEND_POLL)
140#include <poll.h>
141#include <pthread.h>
142#elif defined(XTC_IO_BACKEND_SELECT)
143#include <sys/select.h>
144#include <pthread.h>
145#elif defined(XTC_IO_BACKEND_SIM)
146/* Deterministic-simulation backend (DST): no kernel poller. Readiness
147 * and file-AIO completions come from a scripted in-process event store
148 * driven against the virtual clock; the wakeup is an in-process flag.
149 * See src/io/io_sim.c. */
150#else
151# error "M2 build expects XTC_IO_BACKEND_{POLL,EPOLL,URING,KQUEUE,IOCP,SOLARIS,AIX,SELECT,SIM} to be defined"
152#endif
153
154struct xtc_io {
155 int wakeup_rfd;
156 int wakeup_wfd;
157
158#if defined(XTC_IO_BACKEND_EPOLL)
159 int epfd;
160#elif defined(XTC_IO_BACKEND_KQUEUE)
161 int epfd; /* kqueue fd */
162 int *reg_fds; /* registered fd list */
163 int n_reg;
164 int cap_reg;
165#elif defined(XTC_IO_BACKEND_SOLARIS)
166 int epfd; /* event-port fd */
167 struct __xtc_solaris_reg *reg_fds;
168 int n_reg;
169 int cap_reg;
170#elif defined(XTC_IO_BACKEND_IOCP)
171 void *iocp; /* HANDLE: the completion port */
172 void *afd; /* HANDLE: \Device\Afd, port-associated */
173 _Atomic int wakeup_pending; /* 1 = a wakeup completion is queued */
174 struct __xtc_iocp_reg **reg_iocp; /* live registration nodes (stable) */
175 int n_reg;
176 int cap_reg;
177 struct __xtc_iocp_reg **dead_iocp; /* deregistered, awaiting completion */
178 int n_dead;
179 int cap_dead;
180 struct __xtc_iocp_aio *aio_pend; /* file AIOs in flight */
181 int n_aio;
182 int cap_aio;
183 /*
184 * Cross-loop deferred unregister, as io_uring/poll/select have:
185 * reg_iocp/dead_iocp are a USERSPACE registry owned by this io's
186 * poll thread. A migrated fiber's xtc_proc_wait_fd cleanup used to
187 * call xtc_io_del_fd on this io from ANOTHER thread while the owner
188 * was mid-poll -- racing the swap-remove and the dead-list free:
189 * heap corruption (/m5/exec/Blk3_cross_loop_del_fd under MSVC and
190 * MinGW, 1.55.0 and 1.56.0). A foreign thread now queues the fd
191 * under del_lock and nudges the owner, which drains the queue at the
192 * top of xtc_io_poll and before a re-registration.
193 */
194 int *pending_del;
195 int n_pending_del;
196 int cap_pending_del;
197 _Atomic int has_pending_del;
198 pthread_mutex_t del_lock;
199 pthread_t owner_tid;
200 _Atomic int owner_set;
201#elif defined(XTC_IO_BACKEND_AIX)
202 int ps; /* pollset_t */
203 void *reg_aix; /* struct __xtc_aix_reg * */
204 int n_reg;
205 int cap_reg;
206#elif defined(XTC_IO_BACKEND_URING)
207 struct io_uring ring;
208 struct __xtc_uring_fd **tab; /* fd -> live node (NULL = none) */
209 int tab_cap; /* entries in tab */
210 struct __xtc_uring_fd *zombies; /* deleted fds awaiting terminal CQE */
211 struct __xtc_uring_fd *pending; /* nodes awaiting an SQE (SQ was full) */
212 int n_zombies; /* length of zombies (diagnostics) */
213 struct __xtc_uring_fd *lingers; /* deleted, still armed, adoptable */
214 struct __xtc_uring_fd **ltab; /* fd -> lingering node; sized as tab */
215 unsigned char *idle; /* fd -> 1: last del found it armed */
216 int n_lingers;
217 unsigned long n_adopted; /* re-registrations served by a linger */
218 unsigned long n_cancels; /* POLL_REMOVEs submitted (tests) */
219 /*
220 * Cross-loop deferred unregister queue. io->tab / io->zombies and
221 * the SQ ring are single-producer, owned by this io's loop thread.
222 * A migratable fiber that parked on this loop but was woken via a
223 * non-fd path (timeout / xtc_proc_wake / mailbox) and then work-
224 * stolen resumes on ANOTHER thread with its park_fd still live, and
225 * its xtc_proc_wait_fd cleanup would otherwise call xtc_io_del_fd on
226 * THIS io from the wrong thread -- racing the fd list AND the SQ ring
227 * (the native-path concurrent-commit collapse, TSan-caught
228 * 2026-08-30). Instead the foreign thread posts the fd here under
229 * del_lock and nudges this loop; the owning thread drains it at the
230 * top of xtc_io_poll and performs the real unregister on its own
231 * ring. del_lock guards ONLY this small queue, never the hot fds
232 * list or the ring. */
233 int *pending_del; /* fds awaiting owner-thread unregister */
234 int n_pending_del;
235 int cap_pending_del;
236 _Atomic int has_pending_del; /* fast, lock-free "is queue non-empty?" */
237 pthread_mutex_t del_lock; /* guards ONLY pending_del (not fds/ring) */
238 /*
239 * The exec-relative id of the loop that owns this io, purely as a
240 * LABEL for XTC_TAIL_REAP (which is emitted inside xtc_io_poll, where
241 * the reap outcome is visible but the loop is not). -1 until the
242 * owning loop publishes it; never used for control flow.
243 */
244 int tail_loop_id;
245 /*
246 * The owner: the thread that polls this io and the only one that
247 * may submit to its ring. A per-thread token, never reused (not a
248 * pthread_t: glibc reuses a dead thread's descriptor, so a second
249 * xtc_exec_run's worker can compare EQUAL to the first run's). 0 =
250 * no owner yet. owner_active: the owner declared itself
251 * (__xtc_io_set_owner, at the start of a loop run) and has not left
252 * (__xtc_io_owner_leave, at its end). A thread touching an io that
253 * has no active owner takes it over (__own); on a SINGLE_ISSUER ring
254 * that rebuilds the ring for it (__ring_rebind), and a loop run
255 * upgrades a COOP ring to SINGLE_ISSUER the same way.
256 * owner_tid/owner_set mirror the owner for the debugger (xtc-rings).
257 */
258 _Atomic uint64_t owner_tok;
259 _Atomic int owner_active; /* owner declared (set_owner), not left */
260 pthread_t owner_tid; /* the thread that polls this io */
261 _Atomic int owner_set; /* 1 once owner_tid is recorded */
262 /*
263 * Ring mode. ring_flags: the IORING_SETUP_* the ring was built with
264 * (rebuilds reuse them). ring_single: SINGLE_ISSUER (the DEFER
265 * tier), so only the creating thread may submit or reap and an owner
266 * change rebuilds the ring. n_aio: file-AIO SQEs in the kernel; a
267 * rebuild needs 0 (see __xtc_io_owner_leave).
268 * carry: events a leaving owner reaped while it waited for its AIO,
269 * returned by the next poll before anything else. ring_dead: a
270 * rebuild found no memory for the new ring; the io is unusable.
271 */
272 unsigned ring_flags;
273 int ring_single;
274 int ring_try_defer; /* a loop run may upgrade to DEFER */
275 int n_aio;
276 int n_aio_queued; /* of n_aio: prepared, not yet submitted */
277 int ring_dead;
278 xtc_io_event_t *carry;
279 int n_carry;
280 int cap_carry;
281 unsigned n_rebinds; /* ring rebuilds (tests) */
282 unsigned long n_enter_submit; /* enters that carried SQEs (tests) */
283 unsigned long n_enter_wait; /* enters that only waited (tests) */
284 /*
285 * L2 ring-pointer preempt (INSPIRED BY Glommio's need_preempt():
286 * reactor.rs / sys/uring.rs preempt_pointers). A dedicated tiny
287 * ring carrying ONLY a rearmed TIMEOUT SQE, so
288 * io_uring_cq_ready(&preempt_ring) -- two relaxed/acquire loads of
289 * that ring's own head/tail, no syscall, no signal -- means "the
290 * preempt interval elapsed". A separate ring (not io->ring) is
291 * what makes the pointer test isolate the timeout: I/O CQEs on the
292 * main ring advance its head/tail unpredictably, but this ring sees
293 * only the timeout. preempt_armed gates it (0 = off, the ring is
294 * not initialised). */
295 struct io_uring preempt_ring;
296 int preempt_armed;
297 struct __kernel_timespec preempt_ts; /* the rearm interval */
298#elif defined(XTC_IO_BACKEND_POLL)
299 struct pollfd *pfds;
300 void **tags;
301 int n;
302 int cap;
303 int *slot_of; /* fd -> slot + 1 (0 = not registered) */
304 int slot_cap; /* entries in slot_of */
305 /*
306 * Cross-loop deferred unregister, as io_uring has: the fd arrays are
307 * a USERSPACE registry owned by this io's poll thread. A migrated
308 * fiber's xtc_proc_wait_fd cleanup used to xtc_io_del_fd this io from
309 * ANOTHER thread while the owner was mid-poll -- racing the array
310 * compaction, so a registration could survive its fiber and its fd's
311 * close (select then failed the whole poll with EBADF and the worker
312 * thread exited, stranding every fiber on its loop). A foreign
313 * thread now queues the fd under del_lock and nudges the owner, which
314 * drains the queue at the top of xtc_io_poll.
315 */
316 int *pending_del;
317 int n_pending_del;
318 int cap_pending_del;
319 _Atomic int has_pending_del;
320 pthread_mutex_t del_lock;
321 pthread_t owner_tid;
322 _Atomic int owner_set;
323#elif defined(XTC_IO_BACKEND_SELECT)
324 /* Parallel fd[], interest[], tag[] arrays. fd_set is built
325 * each poll() call from these. Capped at FD_SETSIZE. */
326 int *fds;
327 uint32_t *interests;
328 void **tags;
329 int n;
330 int cap;
331 int *slot_of; /* fd -> slot + 1 (0 = not registered) */
332 int slot_cap; /* entries in slot_of */
333 /*
334 * Cross-loop deferred unregister, as io_uring has: the fd arrays are
335 * a USERSPACE registry owned by this io's poll thread. A migrated
336 * fiber's xtc_proc_wait_fd cleanup used to xtc_io_del_fd this io from
337 * ANOTHER thread while the owner was mid-poll -- racing the array
338 * compaction, so a registration could survive its fiber and its fd's
339 * close (select then failed the whole poll with EBADF and the worker
340 * thread exited, stranding every fiber on its loop). A foreign
341 * thread now queues the fd under del_lock and nudges the owner, which
342 * drains the queue at the top of xtc_io_poll.
343 */
344 int *pending_del;
345 int n_pending_del;
346 int cap_pending_del;
347 _Atomic int has_pending_del;
348 pthread_mutex_t del_lock;
349 pthread_t owner_tid;
350 _Atomic int owner_set;
351#elif defined(XTC_IO_BACKEND_SIM)
352 /* Registered fds (tag map) for readiness simulation, a scripted
353 * event queue ordered by virtual-time due, and an in-process
354 * wakeup flag. Defined in io_sim.c; opaque here. */
355 struct __xtc_sim_io *sim;
356#endif
357};
358
359/*
360 * Cross-loop deferred fd-unregister. Each backend .c provides one:
361 * io_uring queues the fd and drains it on the owning thread (its fds
362 * list + SQ ring are single-owner); the other backends passthrough to
363 * xtc_io_del_fd (kernel-synchronized or their own registry). Internal;
364 * the sole caller is xtc_proc_wait_fd's post-migration cleanup.
365 */
366int __xtc_io_defer_del_fd(xtc_io_t *io, int fd);
367
368/*
369 * Label an io with the exec-relative id of the loop that owns it, for
370 * XTC_TAIL_REAP. No-op on backends that do not reap CQEs. Label only --
371 * never used for control flow.
372 */
373void __xtc_io_set_tail_loop_id(xtc_io_t *io, int id);
374
375/*
376 * Registration accounting (io_uring; XTC_E_NOSYS elsewhere): live
377 * registrations, retired nodes awaiting a terminal CQE, nodes queued for
378 * an SQE. Tests assert no node leaks. Any out pointer may be NULL.
379 */
380int __xtc_io_reg_stats(xtc_io_t *io, int *live, int *zombies, int *pending);
381
382/*
383 * io_uring (XTC_E_NOSYS elsewhere): the IORING_SETUP_* flags the main ring
384 * has now (after any fallback for an older kernel), how many SQEs the
385 * kernel has consumed from the current ring, and how many times the ring
386 * was rebuilt (a loop run upgrading it to DEFER, a new owner thread).
387 * Tests. Out pointers may be NULL.
388 */
389int __xtc_io_uring_info(xtc_io_t *io, unsigned *flags, unsigned *sq_consumed,
390 unsigned *rebinds);
391
392/*
393 * The calling thread stops running this io's loop (an executor worker
394 * exiting, xtc_loop_run returning). On a SINGLE_ISSUER io_uring ring it
395 * reaps every file-AIO completion still in the kernel, because only this
396 * thread can, and keeps them for the next owner's first poll. No-op on
397 * other backends and when the caller is not the owner.
398 */
399void __xtc_io_owner_leave(xtc_io_t *io);
400
401/*
402 * io_uring (XTC_E_NOSYS elsewhere): POLL_REMOVEs submitted, and
403 * re-registrations served by adopting a lingering poll, since xtc_io_init.
404 * Tests. Out pointers may be NULL.
405 */
406int __xtc_io_uring_poll_stats(xtc_io_t *io, unsigned long *cancels,
407 unsigned long *adopted);
408
409/*
410 * io_uring (XTC_E_NOSYS elsewhere): io_uring_enter calls since xtc_io_init
411 * that handed SQEs to the kernel, those that only waited for a completion
412 * (a poll's wait; not the reap loop's task-work flush), and the SQEs
413 * prepared but not yet submitted (see __submit_wait). Tests. Out
414 * pointers may be NULL.
415 */
416int __xtc_io_uring_enter_stats(xtc_io_t *io, unsigned long *submits,
417 unsigned long *waits, unsigned *queued);
418
419/*
420 * An xtc_aio_t is read and written in place for its whole flight, so it
421 * must cover the 2.0 layout (through `res`). A constant, not sizeof: a
422 * field a later minor appends is read under XTC_FIELD_OK, and a 2.0
423 * caller's struct stays valid.
424 */
425#define __XTC_AIO_SIZE_2_0 (offsetof(xtc_aio_t, res) + sizeof(int32_t))
426#define __XTC_AIO_OK(a) ((a) != NULL && (a)->size >= __XTC_AIO_SIZE_2_0)
427
428#endif /* XTC_IO_INT_H */