SPEC-008 - Thread Lifecycle¶
Implementation and release scope: Project status records the implemented subset; roadmap defines release gates. Acceptance of this design does not establish implementation, verification, or support. Reconciled 2026-10-14.
Status: Accepted 2026-10-07 by the project owner (“do all”), with the defaults of §16. Specification work item 7 of the roadmap (07 §3). Reference model: tools/model/ (§15).
Requirements: docs/requirements/KRN-THR.md (KRN-THR-001 to 008 from v0.1 §3.3 and 009 to 014 from 03 §1.1 restated; new from 015).
Builds on: 03 §1 (states and wait reasons), §2.1 (priorities), §9.3 (stacks); SPEC-004 (START, SUSPEND, JOIN, SLEEP, NOTIFY are wait reasons over one protocol; suspend overlay §6.5; cancellation §6.6); SPEC-005 §2, §10 (base and effective priority; owner death); SPEC-001 §6, §12 (attributes, storage, entry points); SPEC-002 §7 (interrupt stacks); ADR-029 (CRITICAL attribute, budgets), ADR-032 (handle generations), ADR-033 (partition-local storage), ADR-036 (table scheduler).
Research: R-001 §1 and §7: Hubris’s fixed task set with generation-tagged ids and restart, FreeRTOS’s deferred reclamation of self-deleted tasks by the idle task, Zephyr’s abort that must wait for in-flight timeouts before storage reuse, ThreadX’s completion and terminate hooks. The design below keeps lifetime of execution and lifetime of the object separate (KRN-THR-006) and makes storage reuse a defined point, not an idle-task side effect.
1. Scope and terms¶
Term |
Meaning |
|---|---|
Thread object |
The thread control block in caller-provided storage, from |
Execution |
From |
Joinable |
The default: termination keeps the object until one |
Detached |
Termination makes the object’s storage reusable at once; no exit code is kept |
Cancellation point |
A blocking call, |
Exit code |
An |
Priorities: larger numeric values mean higher priority; 0 is the idle level and is not assignable to application threads; application priorities are 1 .. CONFIG_EMB_PRIORITY_COUNT - 1 (v0.1 §3.5, KRN-SCH-036).
2. Object and attributes¶
typedef void (*emb_thread_entry_t)(void *arg); /* SPEC-001 §12; returning terminates the thread */
typedef struct emb_thread_attr {
const char *name; /* kept by pointer; NULL allowed */
uint8_t priority; /* 1 .. CONFIG_EMB_PRIORITY_COUNT - 1 */
uint8_t flags; /* EMB_THREAD_DETACHED | EMB_THREAD_CRITICAL (ADR-029) | EMB_THREAD_CANCEL_DISABLED */
void *stack; /* required; aligned to EMB_STACK_ALIGN */
size_t stack_size; /* >= EMB_THREAD_STACK_MIN (per architecture) */
emb_partition_t partition; /* isolated profiles; EMB_HANDLE_NULL = the caller's partition */
const emb_budget_t *budget; /* optional thread budget (ADR-029); NULL = none */
uint8_t cpu_affinity; /* SMP (FUTURE): bit mask; 0 = any */
} emb_thread_attr_t;
#define EMB_THREAD_STACK(name, size) /* declares an aligned stack array of at least `size` bytes */
#define EMB_THREAD_DEFINE(name, attr..., entry, arg) /* storage + handle + stack + init-table registration (SPEC-001 §6.4) */
3. Interface¶
Function |
|
|
|
Notes |
|---|---|---|---|---|
|
prekernel thread |
no |
O(1) + stack fill |
the object exists, execution has not begun: state |
|
prekernel thread isr |
no |
O(1) |
makes it READY; before |
|
thread |
no |
O(1) |
|
|
thread |
never returns |
O(owned) |
self only; §5 |
|
thread |
timeout |
O(1) |
one joiner; §6 |
|
thread isr |
no |
O(1) |
§6 |
|
thread |
no |
O(1) |
overlay, SPEC-004 §6.5; idempotent |
|
thread isr |
no |
O(1) |
idempotent |
|
thread isr |
no |
O(1) |
§7 |
|
thread |
may return at once |
O(1) |
returns |
|
thread |
no |
O(1) |
counting; §7 |
|
thread |
no |
O(owned + depth) |
SPEC-005 §10 |
|
thread isr |
no |
O(1) |
base, effective |
|
thread |
no |
O(1) |
SPEC-003 §6 |
|
thread |
timeout |
SPEC-003 §6 |
|
|
thread isr |
no |
O(1) |
|
|
thread isr |
no |
O(1) |
the index is the small stable integer of the debug descriptor |
|
thread |
no |
O(stack) scan |
fill-pattern scan when |
|
thread |
no |
O(1) |
|
|
thread |
no |
O(threads) |
|
|
thread |
no |
O(1) |
only when |
Every function that takes t validates the handle per SPEC-001 §5.3 and, in isolated profiles, the right the operation needs (SPEC-009): START, SUSPEND, CANCEL, PRIORITY, JOIN, NOTIFY, DESTROY.
4. Creation and start¶
initfills in the control block, builds the initial context frame through the architecture contract (emb_arch_context_init, SPEC-011) so that the first dispatch entersentry(arg)with a return address that leads toemb_thread_exit(0)(KRN-THR-005), fills the stack withEMB_STACK_FILLwhenCONFIG_EMB_STACK_STATSor a checked build is configured, assigns the stable index, setsbase_prio = eff_prio = priority, and leaves the thread BLOCKED with reasonSTART(KRN-THR-009). Nothing is allocated (KRN-THR-007).startis a wake with reasonSTART(SPEC-004 §5.2): the thread becomes READY behind its peers; beforeemb_kernel_start()the wake is recorded and performed at P3.startof a thread that is notINACTIVEisEMB_ESTATE.EMB_THREAD_DEFINEobjects are initialized from the generated init table duringemb_kernel_init(), before scheduler start and started atemb_kernel_start()unless defined withEMB_THREAD_NO_AUTOSTART.Isolated profiles: a thread belongs to
attr.partitionor the caller’s partition; an unprivileged partition may create threads only in itself, only from storage inside its own region (ADR-033), and only beforeemb_system_freeze()(KRN-OBJ-004).
5. Termination¶
Only the thread itself ends its execution: by returning from entry, by emb_thread_exit(code), or by observing EMB_ECANCELED and exiting. There is no terminate(t): threads are never killed asynchronously (KRN-THR-011). The one forced path is partition restart (SPEC-010), which stops the partition’s threads at the kernel boundary and re-initializes them.
The exit path, in thread context with interrupts enabled except where noted:
cancel_disableis implied: a cancel request can no longer be delivered.Owned mutexes are released per
CONFIG_EMB_MUTEX_OWNER_DEATH(SPEC-005 §3.6); in checked builds, owned pool blocks are a lifecycle fault (SPEC-007 §4.3).Statistics are finalized (CPU time, switches, stack high-water) and the
thread_exit(code)trace event is emitted.In one critical section: the exit code is stored in
wake_data(no waker writes it after termination, A16); the joiner, if blocked, is woken with the code handed over (SPEC-004 §6.1); the thread’s own pending timeout, notification bits, TLS slots, and binding references are discarded; the state becomesTERMINATED; the thread is removed from the ready structure;embk_sched_reschedule_if_needed()performs the final switch (P2).The object becomes reusable when the architecture confirms the switch-out has completed (
emb_arch_switch_out_done, SPEC-011): on uniprocessor this is the moment the next thread runs; on SMP the reusability flag is set by the switching code after the old stack is no longer in use, andjoin/destroyon another CPU wait for it. A joinable object is reusable afterjoinreturns; a detached one at step 5. Reusable means the owner maydestroyit or re-initthe storage; the kernel frees nothing (KRN-OBJ-003).
The exiting thread’s stack is in use until step 5. Interrupts taken between steps 4 and 5 run on the interrupt stack where one exists (SPEC-002 §7) and on the dying stack otherwise, which is why reusability is published only after the switch.
6. Join and detach¶
join(t, timeout, out_code): iftisTERMINATED, returns its code at once and marks the object reusable. Otherwise the caller blocks with reasonJOINont’s join slot (one slot: a second joiner getsEMB_EBUSY). The hand-off delivers the exit code. Joining a detached thread isEMB_EINVAL; joining oneself isEMB_EDEADLK; joining anINACTIVEthread blocks until it has run and exited (it may never start, which the timeout covers). A thread whose generation changed because its partition restarted answersEMB_ESTALE(KRN-OBJ-005).detach(t): a joinable thread becomes detached; if it is alreadyTERMINATEDthe object becomes reusable now. Detaching with a joiner blocked isEMB_EBUSY.The join slot is a one-entry wait queue (the same shape as the notification pseudo-queue), so join costs nothing per object beyond one pointer.
7. Suspend, resume, cancel¶
suspendandresumeare the overlay of SPEC-004 §6.5 and KRN-THR-010: a suspended thread keeps its wait-queue position and its deadline; a wake that arrives while suspended takes effect at resume. Both are idempotent (EMB_OKwhen already in the requested state). A thread may suspend itself; the kernel’s own threads (work queue threads, idle) refusesuspendwithEMB_EPERM.cancel(t)sets the request. Iftis blocked with a cancelable reason (SLEEP,OBJECT,JOIN,NOTIFY) and cancellation is enabled for it, it is woken withCANCELEDat once (SPEC-004 §6.6); otherwise the request stays pending and is delivered at the next cancellation point. Delivery clears the request: a thread that keeps running afterEMB_ECANCELEDis not canceled again unless asked again.cancelof aTERMINATEDorINACTIVEthread isEMB_ESTATE.cancel_disable()increments a per-thread count; while nonzero, no cancellation point delivers and no wake withCANCELEDhappens (the request waits).cancel_enable()decrements; reaching zero with a request pending does not deliver by itself: the next cancellation point does.EMB_THREAD_CANCEL_DISABLEDstarts the thread with count 1. The count is bounded (255); overflow is misuse.The recommended shape of a cancelable thread: a loop whose blocking calls check for
EMB_ECANCELED, release resources, and return fromentry.
8. Destroy¶
destroy(t) requires INACTIVE (never started), or TERMINATED and reusable (joined or detached, switch-out complete). Anything else is misuse (EMB_EBUSY). Destroy removes the thread from the all-threads list, bumps the handle generation (KRN-OBJ-002), and returns the storage to the owner. A thread that is still a mutex owner cannot be TERMINATED (step 2 of §5 released them), so no ownership survives destroy. After destroy or re-init, stale handles fail with EMB_ESTALE in profiles that check generations and are undefined in the tiny profile, as for every object (SPEC-009).
9. Thread-local storage slots¶
CONFIG_EMB_TLS_SLOTS (default 0) adds an array of that many void * to each control block; emb_tls_set and emb_tls_get access the calling thread’s slots in O(1) with no locking. No destructors exist in 1.0: a thread that owns slot contents cleans them up before exiting. Kernel code never uses the slots (KRN-THR-013, KRN-THR-014: this is the only per-thread storage mechanism, since compiler TLS is banned).
10. Stacks¶
Caller-provided, aligned to
EMB_STACK_ALIGN(8 on Cortex-M and RISC-V, 1 on AVR), at leastEMB_THREAD_STACK_MINfor the architecture (SPEC-011 states each value);EMB_THREAD_STACK(name, size)declares a correctly aligned array. The size includes the context frame the port saves at switch and the worst-case kernel call depth from a thread, both documented per port; interrupt handling uses the interrupt stack where one exists (KRN-MEM-011).Fill pattern
EMB_STACK_FILL(0xEB) written atinitin checked and statistics builds;emb_thread_stack_infoscans for the high-water mark (KRN-MEM-009).Overflow detection (KRN-MEM-010): at every switch-out the port checks the guard words at the stack limit (all profiles with
CONFIG_EMB_STACK_CHECK, default on in checked builds); hardware stack limits (PSPLIMon Armv8-M, PMP on RISC-V) where available; MPU guard regions in isolated profiles. A detected overflow is a kernel fault for privileged threads and a partition fault for unprivileged ones (03 §10.1).The pre-kernel stack (the one
main()runs on) is reused afteremb_kernel_start(): as the interrupt stack on architectures with one (Cortex-MMSP), or as the idle thread’s stack where there is none (AVR), per SPEC-011.main()never returns.
11. Idle and kernel threads¶
The idle thread exists per CPU unless the tiny profile removes it (KRN-SCH-043, ADR-036); its priority is
0, it is not visible as an application thread, cannot be suspended, canceled, joined, or re-prioritized (EMB_EPERM), and it runs the power core’s idle hook (04 §4).Work queue threads (SPEC-006) are kernel threads: created by
emb_workq_init, destroyed with the queue, same restrictions as idle except priority, which is the queue’s.The supervisor partition’s thread (03 §8) is an ordinary thread of a privileged partition.
12. Misuse and checked-build diagnostics¶
Condition |
Checked build |
Release build |
|---|---|---|
|
fault |
|
|
runtime condition |
|
|
fault |
|
|
fault |
|
|
fault |
|
|
per SPEC-005 §3.6 / fault |
mutexes released per configuration |
|
fault |
|
stack overflow detected at switch |
kernel or partition fault per 03 §10.1 |
same |
storage re-initialized before reusable (SMP: before switch-out completed) |
fault |
undefined |
13. Observability¶
Trace events: thread_init(t, prio, partition), thread_start, thread_exit(t, code), thread_join(joiner, t, result), thread_suspend, thread_resume, thread_cancel(t, delivered_now), thread_priority(t, base, eff), thread_destroy, plus the switch events of the scheduler. Statistics (04 §7.5, optional): per thread CPU time, switch count, blocked time, stack high-water, deadline misses, budget overruns. The debug descriptor exports the control block layout, the all-threads list, and the index so a debugger enumerates threads without symbols (ADR-011).
14. Tiny profile¶
With the table scheduler (ADR-036), init binds the thread to the table slot equal to its priority (unique priorities, up to 16 including idle); start, suspend, resume, join are bit operations; no all-threads list is needed (the table is the list); CONFIG_EMB_TLS_SLOTS=0, CONFIG_EMB_STACK_STATS off by default, destroy is allowed only before freeze. Everything else is identical.
15. Reference model¶
tools/model/ gains Start(tid) with threads created INACTIVE when a scenario says so, Join(tid, timeout) with the exit-code hand-off and the one-joiner rule, and ExitCode(code) as the explicit exit op. New invariants: a TERMINATED thread is in no queue and owns nothing; at most one joiner per thread; a joiner of a terminated thread is never left blocked. Scenarios: join before and after exit, join with timeout, cancel of a joiner, start of an inactive thread by another, exit of a thread that owns a mutex (owner-death policy already modeled).
16. Decisions taken at acceptance (2026-10-07)¶
Threads are joinable by default;
EMB_THREAD_DETACHEDopts out. One joiner per thread; a second joiner isEMB_EBUSY.No
terminate(t)API; the only forced stop is partition restart (SPEC-010). Cancellation is cooperative with a per-thread disable count.The exit code is an
int; entry return means code 0.Object reusability is published only after the switch-out completes; on SMP
joinanddestroywait for that point. No idle-task reclamation.startis ISR-safe and legal before kernel start;EMB_THREAD_DEFINEthreads autostart unless marked otherwise.suspendandresumeare idempotent; kernel threads (idle, work queues) refuse suspend, cancel, join, and application priority changes.Priority numbering: larger is higher;
0is idle and not assignable (restates v0.1 §3.5).TLS slots have no destructors in 1.0.
The pre-kernel stack becomes the interrupt stack or the idle stack per architecture;
main()never returns.The model gains start, join, and explicit exit codes.