Serene Runtime 1.0.0
C runtime for the Serene programming language
Loading...
Searching...
No Matches
fiber.h File Reference

AI Generated (🤦) Fiber subsystem overview. More...

#include <stddef.h>
#include <stdint.h>
Include dependency graph for fiber.h:
This graph shows which files directly or indirectly include this file:

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.
 

Typedefs

typedef struct srn_fiber_t srn_fiber_t
 
typedef void * srn_fiber_result_t
 
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_tsrn_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, int 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_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_tsrn_fiber_make (srn_context_t *ctx, srn_scheduler_t *sched, srn_fiber_entry_t entry, void *arg, size_t stack_size)
 Create a fiber that will run entry(ctx, arg).
 
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.
 
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_tsrn_fiber_current (void)
 The fiber currently running on this os thread (the bootstrap fiber if none).
 
srn_fiber_tsrn_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)
 

Detailed Description

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:

  • yield still runnable, so it moves to the back of the ready queue.
  • suspend off every queue until some party calls srn_fiber_ready. The wake seam used by peer fibers, locks, and a future IO reactor.
  • finish its entry returns and the fiber is reaped.

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.

Macro Definition Documentation

◆ SRN_ASAN

#define SRN_ASAN   0

Definition at line 94 of file fiber.h.

◆ SRN_FIBER_DEFAULT_STACK_SIZE

#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.

Definition at line 138 of file fiber.h.

◆ srn_fiber_get_scheduler_m

#define srn_fiber_get_scheduler_m ( fiber)
Value:
fiber->ctx->engine->scheduler

Definition at line 119 of file fiber.h.

◆ SRN_TSAN

#define SRN_TSAN   0

Definition at line 105 of file fiber.h.

Typedef Documentation

◆ srn_fiber_ctx_t

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.

◆ srn_fiber_entry_t

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?

Definition at line 199 of file fiber.h.

◆ srn_fiber_park_fn

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 registers self with whatever will wake it and re-checks the awaited condition. It returns true to stay suspended, or false to resume self at once because the condition already holds. It must not switch fibers.

Definition at line 206 of file fiber.h.

◆ srn_fiber_result_t

typedef void* srn_fiber_result_t

Definition at line 117 of file fiber.h.

◆ srn_fiber_stack_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.

the layout is like:

|... frames ...|... guard page ...| ^ start ^ limit ^ guard

It is a bit unintuitive to see the guard pointing at the end of the the region, but conventionally, the guard is an OS page and since the stack grows downward and allocations normally grow upward, guard is actually the address that OS (POSIX in this example) will return to us as the starting point of the memory region. Or to put it in different terms, we store the stack frames, starting from the end of the allocated memory for the region and move toward the start of the allocated memory.

◆ srn_fiber_state_t

◆ srn_fiber_t

typedef struct srn_fiber_t srn_fiber_t

Definition at line 110 of file fiber.h.

Enumeration Type Documentation

◆ srn_fiber_state_e

enum srn_fiber_state_e : uint8_t
Enumerator
SRN_FIBER_NEW 

Created, stack mapped, never resumed.

SRN_FIBER_READY 

On the run queue, eligible to run.

SRN_FIBER_RUNNING 

Currently executing.

SRN_FIBER_SUSPENDED 

Parked off the run queue, awaits srn_fiber_ready.

SRN_FIBER_DONE 

Entry returned. The result is final.

Definition at line 180 of file fiber.h.

180 : uint8_t {
181 /// Created, stack mapped, never resumed
182 SRN_FIBER_NEW = 0,
183 /// On the run queue, eligible to run
185 /// Currently executing
187 /// Parked off the run queue, awaits srn_fiber_ready
189 /// Entry returned. The result is final
@ SRN_FIBER_NEW
Created, stack mapped, never resumed.
Definition fiber.h:182
@ SRN_FIBER_RUNNING
Currently executing.
Definition fiber.h:186
@ SRN_FIBER_READY
On the run queue, eligible to run.
Definition fiber.h:184
@ SRN_FIBER_DONE
Entry returned. The result is final.
Definition fiber.h:190
@ SRN_FIBER_SUSPENDED
Parked off the run queue, awaits srn_fiber_ready.
Definition fiber.h:188
enum srn_fiber_state_e srn_fiber_state_t

Function Documentation

◆ srn_fiber_ctx_make()

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 }

Definition at line 29 of file ctx_x86_64.c.

30 {
31 // Lay srn_fiber_swap's frame (see rt/fiber/stack.h and switch_x86_64.S) at
32 // the top of `stack`, rounded down to SRN_FIBER_STACK_ALIGN so the
33 // return-address slot -- and thus the trampoline's entry rsp -- is correctly
34 // aligned. The compound literal zero-fills the slots not named here. Register
35 // slots are uintptr_t, so storing fn/trampoline is a
36 // function-pointer-to-integer conversion (allowed), not the
37 // function-to-object-pointer conversion that -Wpedantic forbids.
38 uintptr_t top = (uintptr_t)stack.start;
39 uintptr_t sp = (top - sizeof(srn_fiber_frame_t)) & ~(uintptr_t)(SRN_FIBER_STACK_ALIGN - 1);
40
44 .r13 = (uintptr_t)fn,
45 .r12 = (uintptr_t)arg,
46 .ret_addr = (uintptr_t)srn_fiber_trampoline,
47 };
48
49 fiber_ctx->sp = (void *)sp;
50}
void srn_fiber_trampoline(void)
Defined in switch_x86_64.S.
#define SRN_FIBER_FCW_DEFAULT
x87 control word
#define SRN_FIBER_MXCSR_DEFAULT
Control-register values a fresh fiber starts with: all FP exceptions masked, round-to-nearest,...
#define SRN_FIBER_STACK_ALIGN
SysV AMD64 requires 16-byte stack alignment at a call; srn_fiber_ctx_make rounds the frame's base dow...
struct srn_fiber_frame srn_fiber_frame_t
srn_fiber_frame is the exact frame srn_fiber_swap (switch_x86_64.S) pushes and pops,...
void * sp
Definition fiber.h:148
void * start
High end, stack pointer initialises to this address.
Definition fiber.h:170
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_fiber_current()

srn_fiber_t * srn_fiber_current ( void )

The fiber currently running on this os thread (the bootstrap fiber if none).

Definition at line 930 of file scheduler.c.

930 {
931 return current_worker != nullptr ? current_worker->current : nullptr;
932}
static _Thread_local srn_worker_t * current_worker
The worker the calling os thread is running, or null when this os thread is not running the worker ro...
Definition scheduler.c:240
Here is the caller graph for this function:

◆ srn_fiber_init_thread()

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 137 of file fiber.c.

137 {
138 // Represent the calling thread as the running fiber. Its stack bounds are not
139 // queried here (there is no portable way). Under the sanitizer the first
140 // fiber's srn_fiber_on_entry records them. The saved context (fiber_ctx)
141 // stays empty, and the first switch away from the thread fills it.
142 memset(f, 0, sizeof(*f));
144 f->name = "thread";
145#if SRN_TSAN
146 // The loop fiber stands in for this OS thread, so it takes the thread's own
147 // current TSan fiber rather than a freshly created one.
148 f->tsan_fiber = __tsan_get_current_fiber();
149#endif
150}
_Atomic srn_fiber_state_t state
The lifecycle state.
Definition fiber.h:217
const char * name
For debugging purposes.
Definition fiber.h:270
Here is the caller graph for this function:

◆ srn_fiber_make()

srn_fiber_t * srn_fiber_make ( srn_context_t * ctx,
srn_scheduler_t * sched,
srn_fiber_entry_t entry,
void * arg,
size_t stack_size )
nodiscard

Create a fiber that will run entry(ctx, arg).

The fiber allocates into its block and never releases it. A stack_size of 0 selects SRN_FIBER_DEFAULT_STACK_SIZE.

Definition at line 169 of file fiber.c.

170 {
171
172 srn_fiber_t *f = ALLOC(ctx, srn_fiber_t);
173 memset((void *)f, 0, sizeof(srn_fiber_t));
174
175 // TODO(lxsameer): Make the fiber stack configurable via cli arg or something.
176 // This is the acquire side of the stack-ring TODO in fiber.h: a pooled stack
177 // would be pulled from the per-thread ring here, falling back to a fresh
178 // mapping only on a miss.
179 f->stack = srn_fiber_stack_alloc(stack_size);
180 f->ctx = ctx;
181 f->state = SRN_FIBER_NEW;
182 f->entry = entry;
183 f->arg = arg;
184#if SRN_TSAN
185 f->tsan_fiber = __tsan_create_fiber(0);
186#endif
187 srn_fiber_ctx_make(&f->fiber_ctx, f->stack, &srn_fiber_launcher, (void *)f);
188
189 // Register before enqueuing: the scheduler must know about the fiber for its
190 // whole life, independent of which queue (if any) it currently sits on. The
191 // registry is how a SUSPENDED fiber, off every run queue, stays reachable for
192 // cleanup and cancellation.
193 srn_sched_register(sched, f);
194
195 // TODO(lxsameer): revisit whether make should auto-enqueue (spawn semantics)
196 // or stay NEW and require an explicit srn_fiber_ready to start. For now
197 // make == spawn: the fiber is runnable the moment it is created.
198 srn_sched_enqueue(sched, f);
199 return f;
200}
#define ALLOC(ctx, T)
Definition context.h:82
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 stac...
Definition ctx_x86_64.c:29
static void srn_fiber_launcher(void *fiber_ptr)
Definition fiber.c:152
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 scheduler.c:305
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 scheduler.c:593
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 whe...
Definition stack_posix.c:34
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_fiber_on_entry()

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 107 of file fiber.c.

107 {
108#if SRN_ASAN
109 // A nullptr fake-stack: a fresh fiber has no previously saved bookkeeping.
110 // The out-params report the stack this fiber was started from -- this is the
111 // one chance to learn `from`'s bounds (there is no portable way to query a
112 // thread's own stack), and a later switch back to `from` needs them, so
113 // record them. They flow through the sanitizer rather than libc.
114 const void *bottom = nullptr;
115 size_t size = 0;
116 __sanitizer_finish_switch_fiber(nullptr, &bottom, &size);
117
118 if (from != nullptr) {
119 // ASan reports the came-from stack as (bottom, size). The struct keeps the
120 // low and high boundaries, so limit = bottom and start = bottom + size.
121 from->stack.limit = (void *)bottom;
122 from->stack.start = (char *)bottom + size;
123 }
124#else
125 UNUSED(from);
126#endif
127}
void * limit
Low end of usable region.
Definition fiber.h:172
srn_fiber_stack_t stack
Definition fiber.h:211
#define UNUSED(x)
Definition utils.h:43
Here is the caller graph for this function:

◆ srn_fiber_on_reap()

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 129 of file fiber.c.

129 {
130#if SRN_TSAN
131 __tsan_destroy_fiber(fiber->tsan_fiber);
132#else
133 UNUSED(fiber);
134#endif
135}
Here is the caller graph for this function:

◆ srn_fiber_ready()

void srn_fiber_ready ( srn_fiber_t * fiber)

Mark a suspended fiber runnable again.

This is the seam an IO reactor, timer, or peer fiber uses to wake a fiber when the event it awaited occurs.

Definition at line 918 of file scheduler.c.

918 {
919 PANIC_IF_NULL(fiber);
920
921 // Wake a suspended fiber. The flip in `ready_fiber` lets exactly one of
922 // several racing wakers enqueue it (an IO completion and a timeout firing on
923 // it, say), while the rest find it no longer `SUSPENDED` and do nothing. The
924 // scheduler is resolved from the fiber, not the calling os thread, so a waker
925 // that is not itself running a worker (the IO reactor, a peer os thread) can
926 // wake it.
928}
#define srn_fiber_get_scheduler_m(fiber)
Definition fiber.h:119
static void ready_fiber(srn_scheduler_t *sched, srn_fiber_t *fiber)
Wake a parked fiber by flipping SUSPENDED to READY and enqueuing it.
Definition scheduler.c:605
#define PANIC_IF_NULL(ptr)
Definition utils.h:64
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_fiber_stack_alloc()

srn_fiber_stack_t srn_fiber_stack_alloc ( size_t size)
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.

Definition at line 34 of file stack_posix.c.

34 {
35 const size_t os_page_size = srn_mm_get_os_page_size();
36 const size_t desired_size = size ? size : (size_t)SRN_FIBER_DEFAULT_STACK_SIZE;
37
38 // Round upto a multiple of PAGE size
39 const size_t usable = (desired_size + os_page_size - 1) & ~(os_page_size - 1);
40 // Add a page to act as a guard page
41 const size_t total = usable + os_page_size;
42
43 // Since stack grows downward, the starting point where mmap returns will
44 // be the end of the stack for us. We could of used MAP_GROWSDOWN to
45 // change the behaviour, but that causes the stack to grow, but we have
46 // a fix stack.
47 void *end = mmap(nullptr, total, PROT_READ | PROT_WRITE, STACK_FLAGS,
48 // according to the man page, for anonymous mode fd should be -1
49 -1,
50 // according to the man page, for anonymous mode offset should be 0
51 0);
52
53 PANIC_IF(end == MAP_FAILED, "Failed to allocate the stack for the fiber subsystem");
54
55 if (mprotect(end, os_page_size, PROT_NONE) == -1) {
56 munmap(end, total);
57 PANIC("Failed to allocate the guard page for the fiber subsystem");
58 }
59
60 const uintptr_t start = (uintptr_t)end + (uintptr_t)total;
61 const uintptr_t limit = (uintptr_t)end + (uintptr_t)os_page_size;
62
63 srn_fiber_stack_t stack = {.start = (void *)start, .limit = (void *)limit, .guard = end};
64 return stack;
65}
static atomic_int total
Definition 05_parallel.c:42
size_t srn_mm_get_os_page_size(void)
Retutrns the OS page size.
Definition default.c:270
#define SRN_FIBER_DEFAULT_STACK_SIZE
Default size of every fiber stack.
Definition fiber.h:138
#define STACK_FLAGS
Definition stack_posix.c:30
One stack per fiber, mapped with a guard page at the low end so an overflow faults deterministically ...
Definition fiber.h:168
#define PANIC_IF(cond, msg)
Definition utils.h:57
#define PANIC(msg)
Definition utils.h:51
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_fiber_stack_free()

void srn_fiber_stack_free ( srn_fiber_stack_t stack)

Definition at line 67 of file stack_posix.c.

67 {
68 munmap(stack.guard, (uintptr_t)stack.start - (uintptr_t)stack.guard);
69}
void * guard
The protected page to detect stack overflows.
Definition fiber.h:174
Here is the caller graph for this function:

◆ srn_fiber_stack_size()

static size_t srn_fiber_stack_size ( srn_fiber_stack_t s)
inlinestatic

Definition at line 437 of file fiber.h.

437 {
438 return (size_t)((char *)s.start - (char *)s.limit);
439}
Here is the caller graph for this function:

◆ srn_fiber_suspend()

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 898 of file scheduler.c.

898 {
899 PANIC_IF_NULL(commit);
900
903
904 srn_fiber_t *self = worker->current;
905 PANIC_IF_NULL(self);
906
907 // The fiber carries its own commit. The worker routine runs it after we
908 // switch out -- the one safe point to publish a fully suspended fiber to its
909 // waker. The routine also stamps the `SUSPENDED` state once the switch
910 // completes, so the `state` never marks a fiber that is still suspending.
911 // This call leaves the `state` as `RUNNING` and lets the switch carry the
912 // fiber off the os thread.
913 self->park_commit = commit;
914 self->park_arg = arg;
915 srn_fiber_switch(self, &worker->loop);
916}
static srn_fiber_result_t worker(srn_context_t *ctx, void *arg)
Definition 03_wait_for.c:41
void srn_fiber_switch(srn_fiber_t *from, srn_fiber_t *to)
Compiled without AddressSanitizer instrumentation: in stack-use-after-return mode ASan would place fr...
Definition fiber.c:62
void * park_arg
Definition fiber.h:229
srn_fiber_park_fn park_commit
While this fiber is suspending, the commit the worker routine runs once the fiber is off the stack,...
Definition fiber.h:228
The state one os thread uses to run fibers.
Definition scheduler.c:218
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_fiber_swap()

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.

Here is the caller graph for this function:

◆ srn_fiber_switch()

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 62 of file fiber.c.

63 {
64
65#if SRN_ASAN
66 const size_t size = srn_fiber_stack_size(to->stack);
67 __sanitizer_start_switch_fiber(&from->fake_stack, to->stack.limit, size);
68#endif
69#if SRN_TSAN
70 // Move TSan's notion of the running fiber to `to` before the stack swaps. A
71 // fiber built outside srn_fiber_make / srn_fiber_init_thread has no handle
72 // and is simply not tracked. Only the low-level switch tests build such a
73 // fiber. Every fiber the scheduler runs has one.
74 if (to->tsan_fiber != nullptr) {
75 __tsan_switch_to_fiber(to->tsan_fiber, 0);
76 }
77#endif
79#if SRN_ASAN
80 __sanitizer_finish_switch_fiber(from->fake_stack, nullptr, nullptr);
81#endif
82}
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.
static size_t srn_fiber_stack_size(srn_fiber_stack_t s)
Definition fiber.h:437
srn_fiber_ctx_t fiber_ctx
Saved stack pointer (see srn_fiber_ctx_t)
Definition fiber.h:210
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_fiber_switch_final()

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 85 of file fiber.c.

85 {
86#if SRN_ASAN
87 // A nullptr fake-stack tells ASan the current fiber is finished and will not
88 // resume, so it can discard the bookkeeping rather than leak it.
89 const size_t size = srn_fiber_stack_size(to->stack);
90 __sanitizer_start_switch_fiber(nullptr, to->stack.limit, size);
91#endif
92#if SRN_TSAN
93 // The finished fiber's handle is released later, at reap. Here just move
94 // TSan to `to` before the swap, when `to` has a handle (see
95 // srn_fiber_switch).
96 if (to->tsan_fiber != nullptr) {
97 __tsan_switch_to_fiber(to->tsan_fiber, 0);
98 }
99#endif
100 // srn_fiber_swap always writes the outgoing sp somewhere. This fiber is done,
101 // so discard it. Control loads `to` and never comes back.
102 srn_fiber_ctx_t discard;
103 srn_fiber_swap(&discard, &to->fiber_ctx);
105}
The saved context of a suspended fiber is a single word: its stack pointer at the moment it was switc...
Definition fiber.h:147
#define SHOULD_NOT_HAPPEN
Definition utils.h:77
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_fiber_wait_for()

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 963 of file scheduler.c.

963 {
964 PANIC_IF_NULL(target);
965 PANIC_IF(target == srn_fiber_current(), "srn_fiber_wait_for: a fiber cannot wait for itself");
966
967 // Suspend until the target finishes (wait_for_park registers us on its waiter
968 // list). The target's DONE handling in the worker routine wakes us. The
969 // result is read from the struct, which survives the target's reap.
971 return target->result;
972}
srn_fiber_t * srn_fiber_current(void)
The fiber currently running on this os thread (the bootstrap fiber if none).
Definition scheduler.c:930
static bool wait_for_park(srn_fiber_t *self, void *arg)
Add the calling fiber to the target's waiter list and stay parked, unless the target has already fini...
Definition scheduler.c:945
void srn_fiber_suspend(srn_fiber_park_fn commit, void *arg)
A suspended fiber is on no scheduler queue, and the scheduler does not track what it waits on – whoev...
Definition scheduler.c:898
srn_fiber_result_t result
Set when state reaches SRN_FIBER_DONE.
Definition fiber.h:223
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_fiber_worker_loop()

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 934 of file scheduler.c.

934 {
936 return &current_worker->loop;
937}
Here is the caller graph for this function:

◆ srn_fiber_yield()

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 876 of file scheduler.c.

876 {
879
880 // Switch to the worker's loop without enqueuing first. The worker routine
881 // puts this fiber back on the ready queue once the switch has saved its
882 // context. Enqueuing here, before the switch, would let another os thread
883 // dequeue and resume the fiber while this os thread is still saving its
884 // context -- two os threads on one fiber stack, which corrupts the switch.
885 srn_fiber_t *self = worker->current;
886 srn_fiber_switch(self, &worker->loop);
887}
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_sched_enqueue()

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 593 of file scheduler.c.

593 {
594 PANIC_IF_NULL(sched);
595 PANIC_IF_NULL(fiber);
596
597 fiber->state = SRN_FIBER_READY;
598 push_ready(sched, fiber);
599}
static void push_ready(srn_scheduler_t *sched, srn_fiber_t *fiber)
Put a runnable fiber on a queue, with its state already set to READY.
Definition scheduler.c:576
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_sched_init()

srn_scheduler_t * srn_sched_init ( srn_engine_t * engine)
nodiscard

Definition at line 246 of file scheduler.c.

246 {
247 PANIC_IF_NULL(engine);
248 // The scheduler outlives every context and fiber, so it is allocated from the
249 // immortal region rather than a releasable block.
251 PANIC_IF_NULL(sched);
252
253 sched->engine = engine;
254 sched->ready_head = nullptr;
255 sched->ready_tail = nullptr;
256 sched->registry = nullptr;
257 sched->idle = 0;
258 sched->runnable = 0;
259 sched->nworkers = 0;
260 sched->state = SRN_SCHED_IDLE;
261 sched->workers = nullptr;
262 sched->os_threads = nullptr;
263 sched->destroyed = false;
264 atomic_init(&sched->run_active, false);
265
267 "failed to initialise the scheduler lock");
268
270 "failed to initialise the scheduler condition");
271
272 // srn_engine_make will store this scheduler in the engine
273 return sched;
274}
#define srn_mm_immortal_allocate(mm, T)
Definition interface.h:169
@ SRN_SCHED_IDLE
Definition scheduler.c:126
srn_mm_t * mm
Memory manager.
Definition engine.h:58
bool destroyed
Set once srn_sched_shutdown has torn the scheduler down.
Definition scheduler.c:201
srn_engine_t * engine
Definition scheduler.c:132
_Atomic bool run_active
True for the duration of an srn_sched_run call.
Definition scheduler.c:197
srn_fiber_t * registry
Registry: head of the doubly-linked list (through reg_prev/reg_next) of every live fiber,...
Definition scheduler.c:149
atomic_int idle
Definition scheduler.c:175
srn_mutex_t lock
Global lock.
Definition scheduler.c:138
srn_cond_t work
Worker coordination.
Definition scheduler.c:174
srn_fiber_t * ready_head
Global / overflow queue.
Definition scheduler.c:142
srn_thread_t * os_threads
Definition scheduler.c:192
atomic_int runnable
Definition scheduler.c:176
srn_fiber_t * ready_tail
Definition scheduler.c:143
_Atomic srn_sched_state_t state
Definition scheduler.c:178
srn_worker_t * workers
srn_sched_run allocates these two arrays and srn_sched_shutdown frees them.
Definition scheduler.c:191
@ SRN_THREAD_OK
Definition thread.h:62
srn_thread_status_t srn_mutex_init(srn_mutex_t *m)
srn_thread_status_t srn_cond_init(srn_cond_t *c)
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_sched_register()

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 305 of file scheduler.c.

305 {
306 PANIC_IF_NULL(sched);
307 PANIC_IF_NULL(fiber);
308
309 srn_mutex_lock(&sched->lock);
310 registry_add(sched, fiber);
311 srn_mutex_unlock(&sched->lock);
312}
static void registry_add(srn_scheduler_t *sched, srn_fiber_t *fiber)
Insert at the head of the registry. Caller must hold sched->lock.
Definition scheduler.c:277
srn_thread_status_t srn_mutex_unlock(srn_mutex_t *m)
srn_thread_status_t srn_mutex_lock(srn_mutex_t *m)
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_sched_run()

void srn_sched_run ( srn_scheduler_t * sched,
int 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 784 of file scheduler.c.

784 {
785 PANIC_IF_NULL(sched);
786 PANIC_IF(sched->destroyed, "srn_sched_run called on a scheduler that was already shut down");
787
788 if (nworkers < 1) {
789 nworkers = 1;
790 }
791
792 if (nworkers > SRN_MAX_WORKERS) {
793 nworkers = SRN_MAX_WORKERS;
794 }
795
796 // Allocate `workers` and `os_threads` on the scheduler so
797 // `srn_sched_shutdown` can join the threads and free them later. `workers`
798 // has one entry per worker. `os_threads` has one per spawned thread, with
799 // slot 0 left empty because the caller runs worker 0 inline (see the struct
800 // comment). `runnable` is left alone, it already counts the fibers queued
801 // before the run.
802 sched->workers = srn_mm_allocate(sched->engine->mm, (size_t)nworkers * sizeof(srn_worker_t));
803 PANIC_IF_NULL(sched->workers);
804
805 sched->os_threads = srn_mm_allocate(sched->engine->mm, (size_t)nworkers * sizeof(srn_thread_t));
807
808 for (int i = 0; i < nworkers; i++) {
809 srn_worker_t *w = &sched->workers[i];
810 w->sched = sched;
811 w->id = i;
812 w->current = nullptr;
813 // The deque indices start empty. Its ring slots are written before they are
814 // read, and the worker's loop is set up by worker_main on its own os
815 // thread.
816 atomic_init(&w->top, 0);
817 atomic_init(&w->bottom, 0);
818 }
819
820 // Publish the coordination state before any os thread starts. `nworkers` must
821 // be set first so the quiescence check counts the right total, and the state
822 // must be RUNNING before an os thread can observe it. `run_active` lets
823 // shutdown see that a run is in flight.
824 sched->idle = 0;
825 sched->nworkers = nworkers;
826 atomic_store(&sched->state, SRN_SCHED_RUNNING);
827 atomic_store(&sched->run_active, true);
828
829 // Spawn nworkers - 1 os threads. The calling os thread runs worker 0 inline.
830 // A spawn failure at startup is fatal, a partial pool would never reach `idle
831 // == nworkers` and so never quiesce.
832 for (int i = 1; i < nworkers; i++) {
833 if (srn_thread_spawn(&sched->os_threads[i], worker_main, &sched->workers[i]) != SRN_THREAD_OK) {
834 PANIC("failed to spawn an os thread");
835 }
836 }
837
838 worker_main(&sched->workers[0]);
839
840 // Worker 0 has stopped, so the run is over from the caller's point of view.
841 // The spawned os threads may still be winding down, so they are NOT joined
842 // here. `srn_sched_shutdown` joins them (the `os_threads` live on the
843 // scheduler) as part of tearing the subsystem down. Clearing `run_active`
844 // lets shutdown proceed. The state stays STOPPING, which keeps any os thread
845 // still looping on its way out.
846 atomic_store(&sched->run_active, false);
847}
void * srn_mm_allocate(srn_mm_t *mm, size_t size)
Generic allocations that do not participate in the block based pools.
Definition default.c:141
#define SRN_MAX_WORKERS
Upper bound on workers per run.
Definition scheduler.c:44
static void worker_main(void *arg)
The entry an os thread starts in.
Definition scheduler.c:778
@ SRN_SCHED_RUNNING
Definition scheduler.c:127
atomic_intptr_t top
Chase-Lev deque.
Definition scheduler.c:228
srn_fiber_t * current
Definition scheduler.c:221
srn_scheduler_t * sched
Definition scheduler.c:219
atomic_intptr_t bottom
Definition scheduler.c:229
srn_thread_status_t srn_thread_spawn(srn_thread_t *t, void(*fn)(void *), void *arg)
Run fn(arg) on a new OS thread.
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_sched_shutdown()

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 314 of file scheduler.c.

314 {
315 PANIC_IF_NULL(sched);
316
317 if (sched->destroyed) {
318 return;
319 }
320
321 // It must run on a thread outside the pool: a worker, or a fiber (which runs
322 // on a worker), would be tearing down the scheduler it is itself running on.
323 // `current_worker` is null only off a worker, so it is the test for that.
324 PANIC_IF(current_worker != nullptr,
325 "srn_sched_shutdown must be called from outside the worker pool");
326
327 // And it must run after `srn_sched_run` has returned, not while a run is in
328 // flight. Stop a running pool with `srn_sched_stop` and let `srn_sched_run`
329 // return first.
330 PANIC_IF(atomic_load(&sched->run_active),
331 "srn_sched_shutdown called while srn_sched_run is active; call "
332 "srn_sched_stop and let the run return first");
333
334 // The run has returned, so worker 0 has stopped. The spawned os threads may
335 // still be winding down (`srn_sched_run` does not join them), so join them
336 // now. `os_threads` slot 0 is the inline worker 0, never spawned, so the
337 // spawned os threads to join are 1..nworkers-1.
338 for (int i = 1; i < sched->nworkers; i++) {
339 (void)srn_thread_join(&sched->os_threads[i]);
340 }
341
342 // Every worker is gone, so this runs single threaded now.
343 //
344 // Any fiber still in the registry never finished: it was left parked in
345 // SRN_FIBER_SUSPENDED with no party able to wake it (a deadlock), or was left
346 // queued when the run stopped early. Release its stack so it does not leak.
347 // The fiber structs themselves live in context blocks and are reclaimed with
348 // those blocks, not here. The scheduler is immortal-allocated, so it is not
349 // freed either.
350 //
351 // Unlike the reap path, this unmaps for good: shutdown runs after the workers
352 // are gone, so the per-thread stack ring from the fiber.h TODO no longer
353 // exists and there is nothing to recycle into. (Draining that ring, when it
354 // exists, also belongs here.)
355 srn_fiber_t *fiber = sched->registry;
356 while (fiber != nullptr) {
357 srn_fiber_t *next = fiber->reg_next;
358 SCHED_LOG("shutdown reaping unfinished fiber '%s' (suspended with no waker?)",
359 fiber->name != nullptr ? fiber->name : "<unnamed>");
360 // TODO(lxsameer): Free up the ring here as well
362 srn_fiber_on_reap(fiber);
363 fiber->reg_prev = nullptr;
364 fiber->reg_next = nullptr;
365 fiber = next;
366 }
367 sched->registry = nullptr;
368
369 // Release the run-scoped storage (srn_mm_free tolerates null, so a scheduler
370 // that never ran is fine).
371 srn_mm_free(sched->engine->mm, sched->os_threads);
372 srn_mm_free(sched->engine->mm, sched->workers);
373 sched->os_threads = nullptr;
374 sched->workers = nullptr;
375 sched->nworkers = 0;
376
377 // Destroy the synchronisation primitives. The scheduler is not usable after
378 // this, so they are not re-initialised. No worker holds or waits on them now,
379 // the join above made sure of that.
380 (void)srn_cond_destroy(&sched->work);
381 (void)srn_mutex_destroy(&sched->lock);
382
383 sched->destroyed = true;
384}
void srn_mm_free(srn_mm_t *mm, void *ptr)
Release a pointer previously returned by srn_mm_allocate or srn_mm_reallocate.
Definition default.c:151
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.
Definition fiber.c:129
#define SCHED_LOG(FMT,...)
Definition scheduler.c:28
void srn_fiber_stack_free(srn_fiber_stack_t stack)
Definition stack_posix.c:67
srn_fiber_t * reg_prev
Registry links.
Definition fiber.h:266
srn_fiber_t * reg_next
Definition fiber.h:267
srn_thread_status_t srn_mutex_destroy(srn_mutex_t *m)
Release a mutex's resources.
srn_thread_status_t srn_cond_destroy(srn_cond_t *c)
Release a condition's resources.
srn_thread_status_t srn_thread_join(srn_thread_t *t)
Block until the thread started for t returns.
Here is the call graph for this function:
Here is the caller graph for this function:

◆ srn_sched_stop()

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, including a signal-driven context or a fiber. A no-op if the scheduler is not running.

Definition at line 849 of file scheduler.c.

849 {
850 PANIC_IF_NULL(sched);
851 // Flip RUNNING to STOPPING once. If the scheduler is not running, or is
852 // already stopping, there is nothing to do.
854
855 if (!atomic_compare_exchange_strong(&sched->state, &expected, SRN_SCHED_STOPPING)) {
856 return;
857 }
858
859 // Running os threads see STOPPING at the top of their next turn. Parked os
860 // threads are roused to observe it. The notify is under the lock, paired with
861 // the park path, so no wakeup is lost.
862 srn_mutex_lock(&sched->lock);
863 srn_cond_notify_all(&sched->work);
864 srn_mutex_unlock(&sched->lock);
865 SCHED_LOG("stop requested");
866}
srn_sched_state_t
The scheduler's lifecycle as one atomic value.
Definition scheduler.c:125
@ SRN_SCHED_STOPPING
Definition scheduler.c:128
srn_thread_status_t srn_cond_notify_all(srn_cond_t *c)
Wake every waiter.
Here is the call graph for this function:
Here is the caller graph for this function: