libxtc 0.4.0
Async concurrency for C: Tokio + Seastar + BEAM, in one library
Loading...
Searching...
No Matches
xtc_slab.h
1/*-
2 * Copyright (c) 2026, The XTC Project
3 * @stability stable
4 * Use of this source code is governed by the ISC License.
5 *
6 * src/inc/xtc_slab.h
7 * libumem-inspired slab allocator with per-loop magazines.
8 * Foundation for fixed-size hot-path allocations across xtc.
9 * Replaces ad-hoc per-module pools (lockmgr entries, proc
10 * mailbox envelopes, channel slots, RCU retired nodes, timer
11 * nodes).
12 *
13 * Design:
14 * - Each cache owns one or more "chunks" (large mmap regions),
15 * carved into per-object slots. No per-object malloc.
16 * - Chunks are sub-divided into "slabs" (default 64 KiB each)
17 * tracked on free/partial/full lists.
18 * - Each loop has a magazine -- a small array of recently-freed
19 * object pointers -- for lock-free fast-path alloc/free.
20 * On magazine miss, the cache mutex is taken and the magazine
21 * is refilled from a partial slab.
22 * - On magazine overflow (free), the magazine spills back to
23 * the cache's free list under the mutex.
24 * - Constructor runs once per object lifetime (when first
25 * pulled out of the slab); destructor runs once at cache
26 * destroy or at slab reap.
27 * - Magazines flush on memory pressure; full slabs return to
28 * the OS via munmap.
29 *
30 * Two storage modes:
31 * PROCESS_LOCAL -- chunks via mmap MAP_ANONYMOUS; objects are
32 * process-local pointers.
33 * SHARED_MEMORY -- chunks carved from a single user-supplied
34 * region (mmap MAP_SHARED of a tmpfs/posix-shm fd). Object
35 * handles are offsets (xtc_slab_off_t, a base-relative
36 * offset) so a peer process mapping the same fd at a
37 * different VA can resolve them. Use xtc_slab_resolve() and
38 * xtc_slab_offset() to convert.
39 *
40 * Cross-process support: verified working. Multiple processes
41 * can attach to the same shm region (via shm_open + mmap) and
42 * allocate AND free concurrently without collision. The region
43 * contains a 64-byte header at offset 0 holding an atomic cursor
44 * (carves fresh slots) and a cross-process free list threaded
45 * through region-relative offsets (reclaims freed slots), so a
46 * slot freed by any attacher is reusable by any other -- memory
47 * is not bump-only and does not leak until destroy. First
48 * attacher initializes the header; subsequent attachers verify
49 * magic and the slot stride, then share the cursor and free list.
50 * NOTE: the shm header (the first 64 bytes) is private to the
51 * allocator; do not stash inter-process scratch there.
52 *
53 * Typical usage:
54 * 1. Process A: shm_open + ftruncate + mmap(MAP_SHARED)
55 * 2. Process A: xtc_slab_create with shm_base/shm_size
56 * 3. Process B: shm_open + mmap(MAP_SHARED) same path
57 * 4. Process B: xtc_slab_create with same shm_base/shm_size
58 * 5. Either process can now alloc; the other can resolve
59 * the offset to access the object.
60 *
61 * Debug flags (M11.5a):
62 * REDZONE -- 16-byte guard before+after each object; checked
63 * on free
64 * AUDIT -- small ring of recent (alloc, free, who, when)
65 * events per cache for postmortem
66 * BACKTRACE -- capture backtrace on alloc; combine with AUDIT
67 * to identify leakers (via the __os_backtrace seam;
68 * a no-op where no backend is available)
69 *
70 * OOM policies:
71 * FAIL -- return NULL, set XTC_E_RESOURCE in caller
72 * BACKOFF -- short usleep + retry once, then FAIL
73 * ABORT -- call abort() (debug builds only; never in prod)
74 *
75 * Memory-pressure: callers can register `xtc_slab_pressure_listen`
76 * on Linux (/proc/pressure/memory PSI). When pressure is high,
77 * all caches drop their magazines and reap empty slabs.
78 */
79
80#ifndef XTC_SLAB_H
81#define XTC_SLAB_H
82
83#include "xtc_export.h"
84
85#include <stddef.h>
86#include <stdint.h>
87
88#include "xtc.h"
89#include "xtc_res.h"
90#include "xtc_proc.h"
91#include "xtc_loop.h"
92
93typedef struct xtc_slab xtc_slab_t;
94
95/* Offset handle for shared-memory caches (a base-relative offset). */
96typedef int64_t xtc_slab_off_t;
97#define XTC_SLAB_OFF_NONE ((xtc_slab_off_t)-1)
98
99/* Per-object lifecycle hooks. */
100typedef int (*xtc_slab_ctor_fn)(void *obj, void *user);
101typedef void (*xtc_slab_dtor_fn)(void *obj, void *user);
102
103/* Memory-pressure callback signature. level: 0=normal 1=warning
104 * 2=critical. Caller may reap, drop caches, etc. */
105typedef void (*xtc_slab_pressure_fn)(int level, void *user);
106
107/* Opaque handle for a stoppable pressure listener (see
108 * xtc_slab_pressure_listen_ex / xtc_slab_pressure_stop). */
109typedef struct psi_listener xtc_slab_pressure_t;
110
111/* Storage mode. */
112typedef enum xtc_slab_mode {
113 XTC_SLAB_PROCESS_LOCAL = 0,
114 XTC_SLAB_SHARED_MEMORY = 1
115} xtc_slab_mode_t;
116
117/* OOM policy. */
118typedef enum xtc_slab_oom_policy {
119 XTC_SLAB_OOM_FAIL = 0, /* return NULL, increment rejects */
120 XTC_SLAB_OOM_BACKOFF = 1, /* brief usleep + retry once */
121 XTC_SLAB_OOM_ABORT = 2 /* call abort() (debug builds) */
122} xtc_slab_oom_policy_t;
123
124/* Debug flags (bit OR'd into opts.flags). */
125#define XTC_SLAB_REDZONE (1u << 0)
126#define XTC_SLAB_AUDIT (1u << 1)
127#define XTC_SLAB_BACKTRACE (1u << 2)
128#define XTC_SLAB_NO_MAGAZINE (1u << 3) /* always go through cache lock */
129
130typedef struct xtc_slab_opts {
131 size_t size; /* sizeof(xtc_slab_opts_t) */
132 const char *name; /* required, for stats/diag */
133 size_t obj_size; /* required */
134 size_t align; /* default cache-line, 64 */
135 size_t chunk_size; /* mmap unit; default 64 KiB */
136 int magazine_size; /* per-loop cache; default 16 */
137 xtc_slab_ctor_fn ctor; /* optional */
138 xtc_slab_dtor_fn dtor; /* optional */
139 void *cb_user;
140 unsigned flags;
141 xtc_slab_oom_policy_t oom_policy;
142 xtc_res_t *res; /* optional accounting */
143
144 /* Shared-memory mode only: caller-supplied base + size of an
145 * already-mapped region. When set, the cache carves chunks
146 * from this region instead of mmap'ing fresh ones. Released
147 * via shm_unlink when the cache is destroyed. */
148 xtc_slab_mode_t mode;
149 void *shm_base;
150 size_t shm_size;
152
153#define XTC_SLAB_OPTS_DEFAULT { \
154 .size = sizeof(xtc_slab_opts_t), \
155 .name = "anon", \
156 .obj_size = 0, \
157 .align = 64, \
158 .chunk_size = 64 * 1024, \
159 .magazine_size = 16, \
160 .ctor = NULL, .dtor = NULL, .cb_user = NULL, \
161 .flags = 0, \
162 .oom_policy = XTC_SLAB_OOM_FAIL, \
163 .res = NULL, \
164 .mode = XTC_SLAB_PROCESS_LOCAL, \
165 .shm_base = NULL, \
166 .shm_size = 0 \
167}
168
169typedef struct xtc_slab_stats {
170 size_t size; /* sizeof(xtc_slab_stats_t): XTC_SLAB_STATS_INIT */
171 uint64_t alloc_fast; /* magazine hit */
172 uint64_t alloc_slow; /* magazine miss -> cache lock */
173 uint64_t free_fast;
174 uint64_t free_slow;
175 uint64_t n_inuse;
176 uint64_t n_free; /* available in slabs */
177 uint64_t n_chunks;
178 uint64_t bytes_inuse;
179 uint64_t bytes_total;
180 uint64_t reaps;
181 uint64_t oom_fails;
182 uint64_t redzone_violations;
184
185#define XTC_SLAB_STATS_INIT { .size = sizeof(xtc_slab_stats_t) }
186
187/*
188 * PUBLIC: int xtc_slab_create __P((const xtc_slab_opts_t *, xtc_slab_t **));
189 * PUBLIC: void xtc_slab_destroy __P((xtc_slab_t *));
190 *
191 * PUBLIC: void *xtc_slab_alloc __P((xtc_slab_t *));
192 * PUBLIC: void xtc_slab_free __P((xtc_slab_t *, void *));
193 *
194 * PUBLIC: int xtc_slab_reap __P((xtc_slab_t *));
195 * PUBLIC: int xtc_slab_stat __P((const xtc_slab_t *, xtc_slab_stats_t *));
196 *
197 * PUBLIC: xtc_slab_off_t xtc_slab_offset __P((const xtc_slab_t *, const void *));
198 * PUBLIC: void *xtc_slab_resolve __P((const xtc_slab_t *, xtc_slab_off_t));
199 *
200 * PUBLIC: int xtc_slab_pressure_listen __P((const char *, xtc_slab_pressure_fn, void *));
201 * PUBLIC: int xtc_slab_pressure_listen_ex __P((const char *, xtc_slab_pressure_fn, void *, xtc_slab_pressure_t **));
202 * PUBLIC: int xtc_slab_pressure_stop __P((xtc_slab_pressure_t *));
203 * PUBLIC: int xtc_slab_reap_all __P((void));
204 */
205
206XTC_API int xtc_slab_create(const xtc_slab_opts_t *opts, xtc_slab_t **out);
207XTC_API void xtc_slab_destroy(xtc_slab_t *slab);
208
209XTC_API void *xtc_slab_alloc(xtc_slab_t *slab);
210XTC_API void xtc_slab_free(xtc_slab_t *slab, void *obj);
211
212/* Drop all magazines + return empty slabs to the OS. */
213XTC_API int xtc_slab_reap(xtc_slab_t *slab);
214
215XTC_API int xtc_slab_stat(const xtc_slab_t *slab, xtc_slab_stats_t *out);
216
217/* Shared-memory helpers -- convert between process-local pointer
218 * and a stable offset. In PROCESS_LOCAL mode these are still
219 * meaningful but only valid within this process. */
220XTC_API xtc_slab_off_t xtc_slab_offset(const xtc_slab_t *slab, const void *p);
221XTC_API void *xtc_slab_resolve(const xtc_slab_t *slab, xtc_slab_off_t off);
222
223/* Install a process-wide memory-pressure listener. On Linux, opens
224 * /proc/pressure/memory in poll mode at the "some" trigger (>10 ms
225 * stall in 1s); on platforms without PSI, returns XTC_E_NOSYS. When
226 * pressure fires, all registered slab caches receive a reap call.
227 * `psi_path` is "/proc/pressure/memory" by default (NULL = use it).
228 */
229XTC_API int xtc_slab_pressure_listen(const char *psi_path,
230 xtc_slab_pressure_fn fn,
231 void *user);
232
233/* Like xtc_slab_pressure_listen, but returns an opaque handle so the
234 * listener can be stopped (its background thread joined and its fds
235 * closed) with xtc_slab_pressure_stop. On platforms without PSI,
236 * returns XTC_E_NOSYS and leaves *out NULL. *out must be non-NULL. */
237XTC_API int xtc_slab_pressure_listen_ex(const char *psi_path,
238 xtc_slab_pressure_fn fn,
239 void *user,
240 xtc_slab_pressure_t **out);
241
242/* Stop a listener created by xtc_slab_pressure_listen_ex: signal its
243 * thread, join it, close its fds, and free the handle. Idempotent on
244 * NULL (returns XTC_E_INVAL). After this returns the handle is
245 * invalid. */
246XTC_API int xtc_slab_pressure_stop(xtc_slab_pressure_t *handle);
247
248/* Fan a reap across every cache currently registered. Returns the
249 * total number of objects reaped. */
250XTC_API int xtc_slab_reap_all(void);
251
252/* Spawn an xtc_proc that calls xtc_slab_reap_all() every
253 * `interval_ns` and logs the count via xtc_log. The proc runs
254 * forever; supervise it or rely on loop teardown to kill it.
255 * Returns the proc's pid (XTC_PID_NONE on failure). */
256int xtc_slab_reaper_spawn(xtc_loop_t *loop, int64_t interval_ns,
257 xtc_pid_t *out_pid);
258
259#endif /* XTC_SLAB_H */