Kernel header tiers — the public/internal API boundary¶
Status: Design record. Implemented in the root tree: the four-tier header split
below matches kernel/tk_kernel.h / tk_kernel_port.h / tk_kernel_sync.h /
tk_kernel_internal.h almost verbatim (each header’s banner states its own tier).
Two items remain open, flagged inline: the per-symbol [rt]/[setup] privilege tags
are documented but not yet annotated on individual declarations, and the introspection
snapshot still leaks a uint8_t field. Scope: which kernel symbol lives in which
header, and why. Companions: composability.md (the layering law
these tiers serve), board-contract.md (who calls the Tier-B port
contract), ADR 0001 (the
tk_stack_cell_t type Tier A exposes).
1. The problem this closes¶
tk_kernel.h is the application header (every example includes it) — yet it also once
declared arch-port hooks and sync-construction primitives an app must never call.
The tell: tk_kernel_wait() documents “MUST be called holding the kernel lock
(tk_sched_enter)”, but tk_sched_enter isn’t in the header, so a reader cannot honor
the contract. The opaque handle redesign hid the TCB layout; these leaked entry points
were the remnant. The tier split gives them their own headers.
2. The consumers (who includes what)¶
Consumer |
Needs |
Must NOT see |
|---|---|---|
Application |
thread lifecycle, sched config, sync objects, time |
port hooks, wait-queue builders, TCB layout |
Arch port ( |
the kernel’s tick/switch entry points + |
app niceties, TCB layout |
Sync/IPC builder (kernel mutex/sem/cond and an app adding a new primitive) |
wait queue + block/wake + current-thread |
TCB layout, scheduler guts |
Kernel internals ( |
everything (TCB struct, kernel singleton, |
— |
Sovereign / HAL-only build |
nothing from the kernel |
the whole kernel |
3. The four-tier boundary (the decision)¶
Tier |
Header(s) at root |
Prefix |
Who includes it |
|---|---|---|---|
A — Application API |
|
|
the app (and examples) |
B — Port contract |
|
|
|
C — IPC-extension contract |
|
|
the kernel’s sync + an app/lib building a new primitive |
D — Kernel-internal |
|
|
only |
Naming: arch_* = what the port PROVIDES (kernel_arch.h, the arch contract);
tk_kernel_* = what the kernel provides (to app or port — disambiguated by which
header declares it); tk_* = app. The tier is defined by the header that declares
a symbol, not by the prefix.
Decided against — a distinct
tk_port_*prefix for Tier B. The analysis floated renaming the ~7 Tier-B port hooks totk_port_*so the tier is legible from the call site. The root kept thetk_kernel_*names (tk_kernel_tick_preempt,tk_kernel_preempt_select, …): the header that declares a symbol already names the tier, and a second prefix convention would churn the arch ports for legibility the#includeline already provides. Rejected as cost without a guarantee.
4. The manifest — every symbol, placed¶
→ Tier A (tk_kernel.h, app-facing): tk_thread_create/tk_thread_spawn/
tk_thread_attr_*, tk_thread_exit/_join/_detach/_yield/_sleep_ms,
tk_thread_set_quantum/_affinity/_pin/_privileged/_deadline, tk_kernel_init/
_start/_set_idle_hook/_set_sched_policy/_set_preempt_mode/_now_ms, the opaque
tk_thread_t + tk_thread_fn + tk_thread_attr_t + enums + TK_THREAD_STACK,
tk_thread_get_stats (gated). The tk_sleep_ms inline forwarder (ADR 0002 §8) lives
here over tk_thread_sleep_ms. Sync objects live in their own Tier-A headers
(tk_mutex.h/tk_sem.h/tk_cond.h); time in tk_time.h.
→ Tier A-introspect (read-only monitor, TK_CONFIG_ENABLE_INTROSPECT): the thread
snapshot + struct tk_thread_info, tk_thread_set_name. App/trace-facing, so they are
introspection surface, not the port contract.
→ Tier B (tk_kernel_port.h): tk_kernel_tick_preempt, tk_kernel_preempt_select,
tk_kernel_switch_select, tk_kernel_start_this_core, tk_kernel_trace_switched_in,
tk_svc_dispatch, tk_kernel_fault_contain. These pair with the arch_* contract in
kernel_arch.h: the port PROVIDES arch_*, the kernel PROVIDES these; both belong at
the port seam, never in the app header. All take/return raw stack pointers or exception
frames and assume the kernel-lock discipline — calling them from app code corrupts the
scheduler.
→ Tier C (tk_kernel_sync.h): struct tk_wait_queue + tk_wait_queue_init,
tk_kernel_wait, tk_kernel_wake_one, tk_kernel_current_thread. Consumed by the
kernel’s own mutex/sem/cond (tk_sync.c) AND by an app building a new primitive (a
message queue, a barrier). Discipline: tk_kernel_wait/_wake_one MUST be called
holding the kernel lock (tk_sched_enter, Tier D) so the object’s check-then-block stays
atomic — now the discipline and the lock primitive live in the same visible tier pair,
which is what closes the original leak.
→ Tier D (tk_kernel_internal.h): struct tk_tcb layout, the kernel singleton,
tk_sched_enter/tk_sched_leave, the static tk_kernel_tick/tk_kernel_schedule. Only
kernel/*.c and the tests include it; it pulls Tier A (enums/handle) + Tier C (the
wait-queue the TCB embeds).
5. The orthogonal dimension: privilege (TK_CONFIG_UNPRIV_THREADS)¶
Within Tier A, an unprivileged thread cannot call everything at runtime. Two sub-classes:
Runtime-safe
[rt](syscall path):tk_thread_yield/_sleep_ms/_exit/_joinand the sync pend/post ops — these trap throughtk_svc_dispatchand run privileged briefly.Setup-only
[setup](privileged):tk_thread_create/_spawn/allset_*,*_set_name— valid only beforetk_kernel_start(or from a privileged system thread). Passing a rawtk_thread_t*to a kernel call from unprivileged code makes the kernel a confused deputy; that path needs a kernel-validated id.
Open — the tags are documented, not yet annotated.
tk_kernel.h’s banner defines the[rt]/[setup]convention, but individual declarations do not yet carry the one-word tag. Applying the per-symbol annotation is the remaining ergonomics to-do.
6. Config-gated public surface (the one gate idiom)¶
The app API itself flexes with configuration. k2 uses one gate mechanism, stated
here as the authority: a TK_FEAT_* macro is defined iff the app opted in
(TK_CONFIG_*) and the arch can do it (ARCH_HAS_*). From kernel/tk_config.h:
TK_FEAT_MPU_GUARD = (ENABLE_MPU_GUARD || UNPRIV_THREADS) && ARCH_HAS_MPU
TK_FEAT_UNPRIV = UNPRIV_THREADS && ARCH_HAS_UNPRIV
TK_FEAT_ISOLATION = ENABLE_MPU_ISOLATION && TK_FEAT_UNPRIV && ARCH_HAS_MPU
TK_FEAT_AFFINITY = ENABLE_AFFINITY && (ARCH_CORE_COUNT > 1)
A feature compiled off makes its API vanish (not stub). The ARCH_HAS_* capability
half is declared by each port (defaults in arch/arch_defaults.h); which board satisfies
which capability is the coverage matrix in board-contract.md §8 —
this doc owns the idiom, that doc owns the coverage.
7. Ergonomics — kept, they are proven¶
Singleton kernel (no kernel pointer in any call) —
init → create → start. ADR 0002 generalizes this to N instances while keeping the single-instance calls source-compatible ([rt]ops resolvethis_kernel()).Opaque kernel-owned handles (
tk_thread_t,tk_mutex_t,tk_sem_t,tk_cond_t) — the freedom-from-interference design; the app holds a handle, never the layout.pthread shape (
spawn/attr/join/detach,void*(*)(void*)entry).
8. Residual leak (open)¶
tk_kernel.h’s introspection snapshot struct tk_thread_info declares
uint8_t base_priority / uint8_t state — the kind-1-as-uint8_t anti-pattern ADR
0001 (Addendum 2) forbids for a word-addressed arch. Public-header leak, predates this
record; fix to unsigned char when the introspection surface is next touched. (Tracked
identically in ADR 0001.)