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