|
Serene Runtime 1.0.0-dev
C runtime for the Serene programming language
|
AI Generated (🤦) Fiber subsystem overview. More...
#include <stddef.h>#include <stdint.h>Go to the source code of this file.
Data Structures | |
| struct | srn_fiber_ctx_t |
| The saved context of a suspended fiber is a single word, its stack pointer at the moment it was switched away from. More... | |
| struct | srn_fiber_stack_t |
| One stack per fiber, mapped with a guard page at the low end so an overflow faults deterministically instead of corrupting a neighbour. More... | |
| struct | srn_fiber_t |
Macros | |
| #define | SRN_ASAN 0 |
| #define | SRN_TSAN 0 |
| #define | srn_fiber_get_scheduler_m(fiber) |
| #define | SRN_FIBER_DEFAULT_STACK_SIZE (128U * 1024U) |
| Default size of every fiber stack. | |
| #define | SRN_FIBER_NAME_MAX 32U |
| Size of the embedded debug name buffer on every fiber, terminator included. | |
| #define | SRN_FIBER_SPAWN_COPY(ctx, entry, value) |
| srn_fiber_spawn_copy for an lvalue, the address and size are taken for the caller. | |
Typedefs | |
| typedef struct srn_fiber_t | srn_fiber_t |
| typedef size_t | srn_worker_id_t |
| typedef void * | srn_fiber_result_t |
| What a fiber's entry produces, type-erased. | |
| typedef struct srn_fiber_ctx_t | srn_fiber_ctx_t |
| The saved context of a suspended fiber is a single word, its stack pointer at the moment it was switched away from. | |
| typedef struct srn_fiber_stack_t | srn_fiber_stack_t |
| One stack per fiber, mapped with a guard page at the low end so an overflow faults deterministically instead of corrupting a neighbour. | |
| typedef enum srn_fiber_state_e | srn_fiber_state_t |
| typedef srn_fiber_result_t(* | srn_fiber_entry_t) (srn_context_t *ctx, void *arg) |
| The function a fiber runs. | |
| typedef bool(* | srn_fiber_park_fn) (srn_fiber_t *self, void *arg) |
| Suspend commit callback. | |
Enumerations | |
| enum | srn_fiber_state_e : uint8_t { SRN_FIBER_NEW = 0 , SRN_FIBER_READY , SRN_FIBER_RUNNING , SRN_FIBER_SUSPENDED , SRN_FIBER_DONE } |
Functions | |
| srn_scheduler_t * | srn_sched_init (srn_engine_t *engine) |
| void | srn_sched_shutdown (srn_scheduler_t *sched) |
| The one stop tear down of the fiber subsystem, should be called once srn_sched_run has returned. | |
| void | srn_sched_run (srn_scheduler_t *sched, size_t nworkers) |
| Run the scheduler with nworkers os threads draining it, returning once the pool goes quiescent (every os thread parked on an empty queue) or a stop is requested with srn_sched_stop. | |
| void | srn_sched_stop (srn_scheduler_t *sched) |
| Ask a running scheduler to stop. | |
| void | srn_sched_drain (srn_scheduler_t *sched) |
| Ask a running scheduler to wind down gracefully. | |
| void | srn_sched_register (srn_scheduler_t *sched, srn_fiber_t *fiber) |
| Record a fiber in the scheduler's registry of live fibers, where it stays until it is reaped. | |
| void | srn_sched_enqueue (srn_scheduler_t *sched, srn_fiber_t *fiber) |
| Place a fiber on a scheduler's ready queue, making it eligible to run. | |
| srn_fiber_t * | srn_fiber_make (srn_context_t *ctx, srn_scheduler_t *sched, const char *name, srn_fiber_entry_t entry, void *arg, size_t stack_size) |
| Create a fiber that will run entry(ctx, arg), registered with sched but NOT scheduled. | |
| void | srn_fiber_schedule (srn_fiber_t *fiber) |
| Schedule a NEW fiber, making it eligible to run. | |
| srn_fiber_t * | srn_fiber_spawn (srn_context_t *ctx, srn_fiber_entry_t entry, void *arg) |
| Make and schedule a fiber with every default, the engine's scheduler, the configured stack size, and an autogenerated name. | |
| srn_fiber_t * | srn_fiber_spawn_copy (srn_context_t *ctx, srn_fiber_entry_t entry, const void *arg, size_t size) |
| srn_fiber_spawn with size bytes of *arg copied into ctx first, so the fiber owns its argument and the caller's stack frame can die freely. | |
| void | srn_fiber_yield (void) |
| Yield cooperatively, re-enqueue the running fiber and run the next ready one. | |
| void | srn_fiber_suspend (srn_fiber_park_fn commit, void *arg) |
| Park the running fiber until a party calls srn_fiber_ready. | |
| void | srn_fiber_ready (srn_fiber_t *fiber) |
| Mark a suspended fiber runnable again, waking it when the event it awaited occurs. | |
| srn_fiber_result_t | srn_fiber_wait_for (srn_fiber_t *target) |
| Block the calling fiber until target finishes, then return its result. | |
| srn_fiber_t * | srn_fiber_current (void) |
| The fiber currently running on this os thread, or null when the calling thread is not a worker or the worker is running its own loop rather than a fiber. | |
| srn_fiber_t * | srn_fiber_worker_loop (void) |
| The worker's loop of the worker running on the calling os thread. | |
| void | srn_fiber_swap (srn_fiber_ctx_t *from, srn_fiber_ctx_t *to) |
| Save the current execution context into from, restore to, and resume on to's stack. | |
| void | srn_fiber_ctx_make (srn_fiber_ctx_t *fiber_ctx, srn_fiber_stack_t stack, void(*fn)(void *), void *arg) |
| Initialise a fresh fiber context so the first srn_fiber_swap into it begins executing fn(arg) on stack. | |
| void | srn_fiber_switch (srn_fiber_t *from, srn_fiber_t *to) |
| Switch from from to to, both live fibers, telling AddressSanitizer about the stack change. | |
| void | srn_fiber_switch_final (srn_fiber_t *to) |
| Like srn_fiber_switch, but for a fiber that has finished and must not be resumed, control transfers to to and never comes back. | |
| void | srn_fiber_on_entry (srn_fiber_t *from) |
| Call as the first action inside a fresh fiber's entry. | |
| void | srn_fiber_on_reap (srn_fiber_t *fiber) |
| Call when a finished fiber is reaped, after it has switched away for the last time. | |
| void | srn_fiber_init_thread (srn_fiber_t *f) |
| Represent the calling OS thread as the running fiber ("#0"), so the scheduler or a test can switch away from it and back. | |
| srn_fiber_stack_t | srn_fiber_stack_alloc (size_t size) |
| Allocate a stack of at least size usable bytes plus a guard page, or SRN_FIBER_DEFAULT_STACK_SIZE when size is 0. | |
| void | srn_fiber_stack_free (srn_fiber_stack_t stack) |
| static size_t | srn_fiber_stack_size (srn_fiber_stack_t s) |
| srn_worker_id_t | srn_sched_current_worker_id (void) |
| Return the id of the worker that the calling os thread is running, or SIZE_MAX when the calling thread is not a worker. | |
| bool | srn_sched_accepting_submissions (srn_scheduler_t *sched) |
| Whether the scheduler still accepts new IO submissions. | |
| void | srn_sched_wake_worker (srn_scheduler_t *sched, size_t channel) |
| Rouse parked workers so the owner of channel consumes its completions. | |
AI Generated (🤦) Fiber subsystem overview.
Stackful, cooperative fibers for the runtime. A fiber is a function plus its own stack, so it can pause at any call depth and resume later. Nothing is preempted. A fiber runs until it explicitly yields, suspends, or finishes.
Terminology (used throughout the fiber subsystem) fiber a unit of work with its own stack. It can pause and resume. os thread a thread the operating system schedules. worker the state one os thread uses to run fibers. It holds a local queue of ready fibers and a worker's loop. There is one worker per os thread. worker's loop a special fiber that stands in for the os thread. A running fiber switches back to it to hand control over, and it picks the next fiber to run. worker routine the steps an os thread repeats. It takes a fiber, switches into it, deals with how the fiber gave up control, and parks when no fiber is runnable. scheduler there is exactly one. It owns the shared ready queue and the list of live fibers and decides what runs next. It does not run fibers itself, workers do. suspend a fiber steps off every queue and waits to be woken. Its os thread stays free to run other fibers. park an os thread blocks because no fiber is runnable anywhere. A notify wakes it.
Every fiber stack is the same fixed size (SRN_FIBER_DEFAULT_STACK_SIZE), one mapping with a guard page that never grows. Uniform size is a deliberate constraint, not an accident. It lets a finished fiber's stack be handed to any later fiber (the stack ring TODO below) without tracking size classes, and it keeps switch and mapping costs predictable.
Control flow is a hub. A fiber never jumps to another fiber. It switches back to its worker's loop, which picks the next one.
scheduler ready queue: [F2] -> [F5] -> ...
| the os thread takes the head
v
+-----------> worker's loop --- switch ---> running fiber
| |
| yield (re-enqueue, run next) ----------------+
| suspend (off-queue until srn_fiber_ready) -----+
| finish (reap) --------------------------------+
+--------------------------------------------------+
A running fiber gives up control three ways:
Signal handling is not part of this subsystem. No scheduler entry point is async signal safe, and by design: a runtime that owns an event loop folds signals into it rather than promising handler safety call by call. srn_sched_stop and srn_sched_drain take the scheduler mutex and signal its condition variable, so calling them from a signal handler can self-deadlock. A signal-initiated shutdown routes the signal into ordinary thread context first (a dedicated thread in sigwait, or a signalfd registered with the reactor) and calls stop or drain from there.
The spec book's fibers chapter (docs/spec/fibers.typ) is the long-form reference. Keep this overview in step with it.
Definition in file fiber.h.
| #define SRN_FIBER_DEFAULT_STACK_SIZE (128U * 1024U) |
Default size of every fiber stack.
All fiber stacks share one size (see the subsystem overview), so this is a process-wide value, not a per-fiber one: the default here, eventually overridable through a CLI argument.
A fiber stack is fixed and cannot grow (see the fibers chapter), so the size must hold realistic call chains yet stay cheap to reserve in bulk. 128 KiB strikes that balance, deep enough for the runtime's C and JIT frames, while lazy page commit means a shallow fiber faults in only the one or two pages it touches and leaves the rest a virtual reservation. For scale, an OS thread stack is far too large to spawn fibers by the thousand, and a few KiB would overflow on modest nesting.
| #define srn_fiber_get_scheduler_m | ( | fiber | ) |
| #define SRN_FIBER_NAME_MAX 32U |
| #define SRN_FIBER_SPAWN_COPY | ( | ctx, | |
| entry, | |||
| value ) |
srn_fiber_spawn_copy for an lvalue, the address and size are taken for the caller.
Definition at line 461 of file fiber.h.
| typedef struct srn_fiber_ctx_t srn_fiber_ctx_t |
The saved context of a suspended fiber is a single word, its stack pointer at the moment it was switched away from.
The callee-saved registers live on the fiber's own stack, pushed by srn_fiber_swap and popped when it is resumed.
| typedef srn_fiber_result_t(* srn_fiber_entry_t) (srn_context_t *ctx, void *arg) |
The function a fiber runs.
Allocations made through ctx land in the context's block. TODO(lxsameer): The shape is generic until code generation produces fibers, at which point it adopts the Serene ABI. TODO(lxsameer): Should we support two types of entry functions? one for C calling convention and one for FastC?
| typedef bool(* srn_fiber_park_fn) (srn_fiber_t *self, void *arg) |
Suspend commit callback.
The worker routine runs it on its own side once the fiber has switched out. It re-checks the awaited condition and only then registers self with whatever will wake it, returning true to stay suspended, or false to resume self at once because the condition already holds. The order matters, registering before the re-check can leave a live registration behind a false return, and that stale waker would wake the fiber's NEXT suspend. It must not switch fibers.
| typedef void* srn_fiber_result_t |
| typedef struct srn_fiber_stack_t srn_fiber_stack_t |
One stack per fiber, mapped with a guard page at the low end so an overflow faults deterministically instead of corrupting a neighbour.
With addresses increasing to the right, the layout is:
|... guard page ...|.......... frames ..........| ^ guard ^ limit ^ start
guard is the mmap base, the lowest page of the region. The stack grows downward, so frames start at the high end (start) and grow toward limit; one step past limit lands on the protected guard page and faults.
| typedef enum srn_fiber_state_e srn_fiber_state_t |
| typedef size_t srn_worker_id_t |
| enum srn_fiber_state_e : uint8_t |
Definition at line 230 of file fiber.h.
| void srn_fiber_ctx_make | ( | srn_fiber_ctx_t * | fiber_ctx, |
| srn_fiber_stack_t | stack, | ||
| void(* | fn )(void *), | ||
| void * | arg ) |
Initialise a fresh fiber context so the first srn_fiber_swap into it begins executing fn(arg) on stack.
fn is the fiber's entry, a void (*)(void *) that runs on the new stack and receives arg. It must NEVER return – there is no frame beneath it, so a return traps in the trampoline. Instead it transfers control away with srn_fiber_switch (to yield) or srn_fiber_switch_final (when finished), and as its first action it calls srn_fiber_on_entry(). Typical shape:
static void worker(void *arg) { srn_fiber_on_entry(&scheduler); // hand the switch to the sanitizer ... do the fiber's work using arg ... srn_fiber_switch_final(&scheduler); // finished, never returns }
| srn_fiber_t * srn_fiber_current | ( | void | ) |
The fiber currently running on this os thread, or null when the calling thread is not a worker or the worker is running its own loop rather than a fiber.
Definition at line 1058 of file scheduler.c.
| void srn_fiber_init_thread | ( | srn_fiber_t * | f | ) |
Represent the calling OS thread as the running fiber ("#0"), so the scheduler or a test can switch away from it and back.
The thread's stack bounds are not queried here (there is no portable way). Under the sanitizer they are recorded by the first fiber's srn_fiber_on_entry. The saved context is written by the first switch away.
Definition at line 154 of file fiber.c.
|
nodiscard |
Create a fiber that will run entry(ctx, arg), registered with sched but NOT scheduled.
The fiber stays NEW until srn_fiber_schedule starts it, so a caller can build several fibers and wire them up before any of them runs. Registration at creation means the scheduler reaps every fiber at shutdown, scheduled or not.
name is a debug label copied into the fiber, truncated to SRN_FIBER_NAME_MAX - 1 bytes, or nullptr for an autogenerated unique one. A stack_size of 0 selects the configured per fiber default.
Definition at line 201 of file fiber.c.
| void srn_fiber_on_entry | ( | srn_fiber_t * | from | ) |
Call as the first action inside a fresh fiber's entry.
from is the fiber that started this one (the scheduler, or the bootstrap thread fiber). It completes the switch for AddressSanitizer and, since from's stack bounds only become discoverable here, records them into from so a later switch back is tracked. from may be null. A no-op when the sanitizer is not in use.
Definition at line 111 of file fiber.c.
| void srn_fiber_on_reap | ( | srn_fiber_t * | fiber | ) |
Call when a finished fiber is reaped, after it has switched away for the last time.
Releases the fiber's ThreadSanitizer handle. A no-op when the sanitizer is not in use.
Definition at line 133 of file fiber.c.
| void srn_fiber_ready | ( | srn_fiber_t * | fiber | ) |
Mark a suspended fiber runnable again, waking it when the event it awaited occurs.
Callable from a fiber, a worker, or the reactor. The reactor is the only legitimate waker outside the worker pool.
Definition at line 1044 of file scheduler.c.
| void srn_fiber_schedule | ( | srn_fiber_t * | fiber | ) |
Schedule a NEW fiber, making it eligible to run.
A fiber is scheduled exactly once, scheduling one that is not NEW panics, and waking a suspended fiber is srn_fiber_ready's job instead.
Definition at line 630 of file scheduler.c.
| srn_fiber_t * srn_fiber_spawn | ( | srn_context_t * | ctx, |
| srn_fiber_entry_t | entry, | ||
| void * | arg ) |
Make and schedule a fiber with every default, the engine's scheduler, the configured stack size, and an autogenerated name.
The caller keeps ownership of arg and must keep it alive until the fiber is done with it, srn_fiber_spawn_copy lifts that burden.
Definition at line 247 of file fiber.c.
| srn_fiber_t * srn_fiber_spawn_copy | ( | srn_context_t * | ctx, |
| srn_fiber_entry_t | entry, | ||
| const void * | arg, | ||
| size_t | size ) |
srn_fiber_spawn with size bytes of *arg copied into ctx first, so the fiber owns its argument and the caller's stack frame can die freely.
The copy lives until the context is released, like the fiber itself.
Definition at line 255 of file fiber.c.
|
nodiscard |
Allocate a stack of at least size usable bytes plus a guard page, or SRN_FIBER_DEFAULT_STACK_SIZE when size is 0.
Platform specific.
| void srn_fiber_stack_free | ( | srn_fiber_stack_t | stack | ) |
|
inlinestatic |
| void srn_fiber_suspend | ( | srn_fiber_park_fn | commit, |
| void * | arg ) |
Park the running fiber until a party calls srn_fiber_ready.
The fiber switches out first. Only then does the scheduler run commit (see srn_fiber_park_fn) to register it and confirm it should stay parked. Registering only after the park completes is what makes it race free – a waker can never observe a half parked fiber. Acts on the fiber currently running on this thread.
Park the running fiber until a party calls srn_fiber_ready.
The commit callback runs on the worker's loop side once the fiber has switched out. It hands the fiber's pointer to the event source it blocks on (a peer fiber, a lock's waiter list, the IO reactor's fd table), so that party can call srn_fiber_ready when the awaited event occurs. Running commit only after the suspend completes is what makes the hand-off race free, a waker can never observe a half-suspended fiber. If commit registers the fiber nowhere, it is genuinely lost – a deadlock, like an os thread blocking on a condition nobody signals.
Definition at line 1024 of file scheduler.c.
| void srn_fiber_swap | ( | srn_fiber_ctx_t * | from, |
| srn_fiber_ctx_t * | to ) |
Save the current execution context into from, restore to, and resume on to's stack.
Returns, on from's stack, when a fiber later switches back into from.
| void srn_fiber_switch | ( | srn_fiber_t * | from, |
| srn_fiber_t * | to ) |
Switch from from to to, both live fibers, telling AddressSanitizer about the stack change.
from resumes where it left off when something later switches back into it.
Switch from from to to, both live fibers, telling AddressSanitizer about the stack change.
Not instrumented by either sanitizer, the stack swaps mid-function, which confuses ASan's fake stack and TSan's shadow stack. The explicit annotations keep each sanitizer's fiber tracking correct across the swap instead.
Definition at line 66 of file fiber.c.
| void srn_fiber_switch_final | ( | srn_fiber_t * | to | ) |
Like srn_fiber_switch, but for a fiber that has finished and must not be resumed, control transfers to to and never comes back.
The fiber's entry function therefore ends at this call – any code after it is unreachable, which is why this is [[noreturn]]. Nothing is freed here. The spent fiber's stack is left frozen and reclaimed later by its owner (the scheduler reaps a finished fiber and frees its stack via srn_fiber_stack_free).
Definition at line 88 of file fiber.c.
| srn_fiber_result_t srn_fiber_wait_for | ( | srn_fiber_t * | target | ) |
Block the calling fiber until target finishes, then return its result.
Definition at line 1091 of file scheduler.c.
| srn_fiber_t * srn_fiber_worker_loop | ( | void | ) |
The worker's loop of the worker running on the calling os thread.
The fiber that resumes when the current fiber yields, suspends, or finishes, the resumer a fiber's launcher hands control back to. Each worker has its own, so this is valid only on an os thread that is currently running the worker routine.
Definition at line 1062 of file scheduler.c.
| void srn_fiber_yield | ( | void | ) |
Yield cooperatively, re-enqueue the running fiber and run the next ready one.
Acts on the fiber currently running on this thread.
Definition at line 1000 of file scheduler.c.
| bool srn_sched_accepting_submissions | ( | srn_scheduler_t * | sched | ) |
Whether the scheduler still accepts new IO submissions.
True only while RUNNING; false once srn_sched_drain or srn_sched_stop has begun winding the pool down. The IO bridge reads this to fence submissions during a wind-down, so a fiber's IO call comes back cancelled instead of parking on an op the wind-down would have to wait out.
Definition at line 1108 of file scheduler.c.
| srn_worker_id_t srn_sched_current_worker_id | ( | void | ) |
Return the id of the worker that the calling os thread is running, or SIZE_MAX when the calling thread is not a worker.
Definition at line 1102 of file scheduler.c.
| void srn_sched_drain | ( | srn_scheduler_t * | sched | ) |
Ask a running scheduler to wind down gracefully.
Unlike srn_sched_stop, the workers keep running every runnable fiber and let in-flight IO complete; only new IO submissions are fenced, so a fiber's next IO call returns -ECANCELED and the fiber unwinds rather than parking on a fresh op. The pool then reaches the same quiescence as a natural finish and srn_sched_run returns. An in-flight op that never completes (an idle recv, a long sleep) stalls the wind-down until it finishes; bounding that needs op cancellation and is not provided yet. Does not wait. Safe to call from any os thread or a fiber. A no-op if the scheduler is not running.
Definition at line 963 of file scheduler.c.
| void srn_sched_enqueue | ( | srn_scheduler_t * | sched, |
| srn_fiber_t * | fiber ) |
Place a fiber on a scheduler's ready queue, making it eligible to run.
Definition at line 622 of file scheduler.c.
|
nodiscard |
Definition at line 257 of file scheduler.c.
| void srn_sched_register | ( | srn_scheduler_t * | sched, |
| srn_fiber_t * | fiber ) |
Record a fiber in the scheduler's registry of live fibers, where it stays until it is reaped.
Definition at line 318 of file scheduler.c.
| void srn_sched_run | ( | srn_scheduler_t * | sched, |
| size_t | nworkers ) |
Run the scheduler with nworkers os threads draining it, returning once the pool goes quiescent (every os thread parked on an empty queue) or a stop is requested with srn_sched_stop.
The calling os thread becomes worker 0, so it does not return until then. nworkers is clamped to at least 1; with 1 it is the calling os thread alone, which keeps execution single-threaded and cooperatively ordered. The spawned os threads are not joined here – srn_sched_shutdown joins them as part of tearing the subsystem down.
Definition at line 843 of file scheduler.c.
| void srn_sched_shutdown | ( | srn_scheduler_t * | sched | ) |
The one stop tear down of the fiber subsystem, should be called once srn_sched_run has returned.
It joins the os threads, then releases the stack of every fiber still registered (any left unreaped, such as one suspended with no party to wake it, or one left queued when the run stopped early), and frees the scheduler's own resources. The fiber structs themselves live in context blocks and are reclaimed with those blocks, not here.
The scheduler is NOT usable after this. A later srn_sched_run panics, and a repeated srn_sched_shutdown is a no-op (so the engine can call it unconditionally even after a caller already did).
Must be called from outside the pool, never from an os thread that is running a worker nor from a fiber, and only after srn_sched_run has returned. Calling it on a live pool panics – stop the pool with srn_sched_stop, let srn_sched_run return, then shut down.
Definition at line 327 of file scheduler.c.
| void srn_sched_stop | ( | srn_scheduler_t * | sched | ) |
Ask a running scheduler to stop.
Each worker routine checks the request at the top of each turn and stops once the fiber it is running yields or finishes, so a slice in flight is never cut mid-execution. srn_sched_run then returns. Fibers left queued stay unrun and are reclaimed by srn_sched_shutdown. Does not wait. Safe to call from any os thread or a fiber, but NOT from a signal handler. It takes the scheduler mutex and signals the condition variable, neither of which is async signal safe.
Definition at line 939 of file scheduler.c.
| void srn_sched_wake_worker | ( | srn_scheduler_t * | sched, |
| size_t | channel ) |
Rouse parked workers so the owner of channel consumes its completions.
The reactor's notify hook. The wake is currently a broadcast, channel names the worker that must look, but every parked worker is notified and the ones with nothing to do re-park.
Definition at line 1116 of file scheduler.c.