Files
LithosAnanake/src/starkernel/vm/switch.c
T
Robert Allan JamesandClaude Opus 5.5 c8b754fd80 FABRIC-3.10.8 MC-2: observers stay on core 0
The decision's observer pass runs only on core 0; a switch into an
observer on any other core is refused and counted (a guard, 0 in every
run). Probe posing as core 1: no observer chosen, switch refused 0 -> 1;
as core 0 the same decision returns Hestia. 3-ISA acceptance clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 14:24:47 -04:00

323 lines
16 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* switch.c - Cooperative VM context switch glue (FABRIC-3.md §XXVIII,
* Stage 2, 2026-09-13). See switch.h for the contract.
*/
#ifdef __STARKERNEL__
#include "starkernel/vm/switch.h"
#include "vm.h"
#include "starkernel/capsule_birth.h" /* capsule_vm_set_state */
#include "starkernel/vm/kernel_hermes.h" /* FABRIC-3.8.md §3.2 -- sk_hermes_drain_checkpoint() */
#include "starkernel/hal/hal.h" /* sk_hal_panic() */
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.8.md §3.13.3 -- the decision */
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.h" /* sk_get_mama_vm() -- the idle root */
#include "starkernel/percpu.h" /* FABRIC-3.10.8 MC-0a -- this core's current VM */
#include "starkernel/spinlock.h" /* FABRIC-3.10.8 MC-0b -- no lock crosses a switch */
#include "starkernel/smp.h" /* FABRIC-3.10.8 MC-1 -- sk_smp_report() */
#include "log.h" /* FABRIC-3.10.8 MC-2 -- the refusal report */
#include <stdint.h>
/* Arch-specific (switch.S in each arch dir). sk_vm_switch_prime() builds
* the exact synthetic first-entry frame sk_vm_switch_to() expects -- kept
* in assembly, not duplicated here, so the layout can never drift out of
* sync between the two. */
extern void sk_vm_switch_to(uint64_t *save_sp_out, uint64_t new_sp);
extern uint64_t sk_vm_switch_prime(uint64_t stack_top, void *entry);
/* Single-writer-mainline global, safe under this stage's cooperative-only,
* one-VM-runs-at-a-time-on-one-hart discipline (same class of bookkeeping
* as vm_core.c's g_log_attrib_vm) -- identifies which VM a freshly-primed
* native stack belongs to. Read exactly once, synchronously, at the very
* top of sk_vm_switch_entry() before any other first-entry could occur.
* Safe to keep as a plain global (unlike the old g_switch_back_to this
* file used to also carry, which moved to per-VM native_switch_back_to and
* was then removed at FABRIC-3.8.md Stage I once the trampoline stopped
* yielding to "whoever switched in"): a VM's identity
* never changes across the lifetime of its own trampoline instance, only
* who most recently switched into it does. */
static VM *g_switch_entry_vm;
/* FABRIC-3.10.8 MC-2: switches refused because they would have entered an
* observer on a core other than core 0. A guard -- expected to stay 0. */
static uint64_t g_observer_refusals, g_observer_refusals_reported;
/* FABRIC-3.md §XXVIII Stage 3 follow-on (2026-09-14): which VM is
* currently running, from the switch mechanism's own point of view --
* independent of vm_log_attributed_vm() (vm_core.c), which is scoped to
* nested vm_interpret() calls for log-source attribution and has no way
* to know a VM is "running" while parked inside this file's own raw C
* trampoline (sk_vm_switch_entry()), never itself calling vm_interpret().
* sk_vm_switch_signal_tick() (capsule_vm_switch_signal.c) consults this
* instead of vm_log_attributed_vm() for exactly that reason -- see its
* own updated comment. Single-writer-mainline, same discipline as
* g_switch_entry_vm above: set unconditionally on every switch, by the
* only code path that can ever change who is physically running.
* Seeded once at boot via sk_vm_switch_set_current() for whichever VM is
* running before any switch has ever happened (Hera, at Stage 3
* registration time -- kernel_main.c). */
/* FABRIC-3.10.8 Stage MC-0a: "who is running" is per core now, held in this
* core's per-core block (starkernel/percpu.h) instead of the file-scope
* global g_switch_current_vm this comment used to describe. Same semantics
* on one core; with more, each core answers for itself. */
VM *sk_vm_switch_current_vm(void) { return sk_percpu()->current_vm; }
void sk_vm_switch_set_current(VM *vm) { sk_percpu()->current_vm = vm; }
/**
* sk_vm_switch_entry - trampoline for a VM's first-ever entry.
*
* A freshly-entered VM has no production behavior defined yet -- what a
* switched-to VM should actually DO once running is later-stage work
* (message dispatch, an idle loop, whatever Stage 3+ needs). For now this
* is a minimal, permanent placeholder: immediately yield back to whoever
* most recently switched to it, forever. Reads self->native_switch_back_to
* fresh every iteration (not a value captured once) so a later switch-in
* from a different VM is honored, not silently bounced to whoever entered
* first. Must never fall off the end -- there is no legitimate return
* address below a synthetic first-entry frame, unlike an ordinary
* function.
*
* FABRIC-3.md §XXVIII Stage 3 follow-on (2026-09-14): an attempt to make
* this attributable by calling vm_interpret(self, "MSG-TICK") here was
* tried and reverted -- it introduced a second, distinct hang (a stall in
* sk_repl_headless_wait()'s WIREBIND-attachment wait, not root-caused) in
* place of the switch-storm it was meant to fix. The switch-storm's real
* cause (sk_vm_switch_signal_tick()'s "who is current" check via
* vm_log_attributed_vm() never seeing a VM that only ever runs this raw
* C trampoline) is instead fixed at the source in switch.c/
* capsule_vm_switch_signal.c: sk_vm_context_switch() now tracks "current"
* itself, independent of vm_interpret()'s own attribution bookkeeping.
* This placeholder's behavior is therefore unchanged from Stage 2.
*/
/*
* FABRIC-3.8.md §3.2 (Stage B, 2026-09-23): the placeholder above is
* superseded -- this trampoline is now every switch participant's own
* kernel-side idle loop, the base of its native stack. Entirely OS-driven:
* nothing a VM executes decides to give up control (Captain Bob: "no
* special words saying I'm giving up control... strictly all operating
* system and strictly all compudynamically driven"). Each pass:
*
* - Drain one of this VM's own kernel-Hermes messages, in its own
* context, on its own stack (sk_hermes_drain_checkpoint() at depth 0
* here, so the payload runs at this VM's depth 1 and its own word
* checkpoints pass the Stage A ownership gate).
* - If nothing was drained, this VM has no work: mark it parked-idle
* (§3.5 -- nothing borrowed on its stack, nothing in progress) and
* yield back to whoever switched in.
*
* Driven off the drain's return value, never a separate pending-count
* probe: any path where a drain returns without consuming a message would
* otherwise spin here forever with no word checkpoint to break it --
* indistinguishable from a switch storm. A failed yield is a kernel
* invariant violation, not something to retry, so it panics loudly.
*
* Stage C-b (below) adds the compudynamic decision at this point between
* messages, so a VM with a queue of checkpoint-free payloads cannot hold
* the CPU indefinitely.
*/
/*
* FABRIC-3.8.md §3.13.3 (Stage C-b, 2026-09-23): the trampoline now asks
* the compudynamic decision at its between-messages point, instead of
* always draining to empty and then yielding to whoever switched in:
*
* - still has mail: keep draining unless some VM with work is strictly
* cooler (then switch to it; this VM stays eligible through its mail);
* - no mail: switch to the coolest VM with work, or -- nobody has any --
* to Hera, the console root, whose idle point polls for input.
*
* The no-work fallback is Hera rather than native_switch_back_to because
* back_to is "whoever switched in last": two idle trampolines that switched
* into each other would each yield to the other forever -- a switch storm.
* Hera is never waiting on a trampoline, so the chain always ends at her.
*/
void sk_vm_switch_entry(void) {
VM *self = g_switch_entry_vm;
for (;;) {
VM *next;
int drained, has_mail;
drained = sk_hermes_drain_checkpoint(self); /* at most one message */
has_mail = sk_hermes_pending_count(self->stadium_vm_id) > 0;
/* Stage B's rule, kept: never spin on mail the drain won't take --
* with no word checkpoint in this loop, that would be a silent storm. */
if (drained == 0 && has_mail) {
sk_hal_panic("switch trampoline: pending mail the drain did not consume");
}
/* FABRIC-3.9.8 Stage M-B (§7.3): a participant that did real work
* and now has none tells whoever joined EV-DONE -- the OS emits its
* completion; its own code never decides to. Funded by the VM it's
* about (M-5); observers never announce their own idleness (M-4). */
if (!has_mail && self->ev_real_work) {
self->ev_real_work = 0;
if (!sk_vm_switch_signal_is_observer(self->stadium_vm_id)) {
VMRegistryEntry e;
if (capsule_vm_registry_get(self->stadium_vm_id, &e) == 0) {
(void)sk_hermes_event(SK_HERMES_CHANNEL_EV_DONE, self->stadium_vm_id,
self->stadium_vm_id, e.name);
}
}
}
sk_vm_switch_reap_pending();
next = sk_vm_switch_decide(self, has_mail);
if (!next) {
if (has_mail) continue; /* still coolest with work: keep going */
next = (VM *)sk_get_mama_vm(); /* nobody has work: back to the root */
if (!next || next == self) sk_hal_panic("switch trampoline: no root to idle to");
}
/* Stage FR (§3.12): parked is a VM_FRAME_IDLE frame on this VM's own
* stack, pushed only around the switch and popped (by ID) on resume,
* BEFORE any further drain -- so the drain's own frame is still the
* only frame and the ownership gate's exactly-one-frame rule holds.
* It records WHERE the VM is parked: at its trampoline, nothing in
* progress (any remaining mail keeps it eligible by itself). */
{
VMFrameID idle_id = vm_frame_push(&self->frames, self->stadium_vm_id, VM_FRAME_IDLE);
if (idle_id == 0) {
sk_hal_panic("switch trampoline: idle frame push failed");
}
sk_vm_switch_signal_note_switch_performed(next->stadium_vm_id);
if (sk_vm_context_switch(self, next) != 0) {
sk_hal_panic("switch trampoline: switch failed");
}
vm_frame_pop(&self->frames, idle_id); /* resumed: someone switched back in */
}
}
}
/* See switch.h. The caller has already established that `vm` owns its
* outermost frame (sk_vm_owns_outermost_frame(), vm_core.c). */
void sk_vm_switch_checkpoint(VM *vm) {
VM *next;
if (!sk_vm_switch_signal_take_decision_due()) return;
sk_vm_switch_reap_pending();
next = sk_vm_switch_decide(vm, 1); /* vm is mid-task: it still has work */
if (!next) return;
sk_vm_switch_signal_note_switch_performed(next->stadium_vm_id);
(void)sk_vm_context_switch(vm, next);
/* Resumes here once the decision, somewhere, switches back to vm. */
}
/* See switch.h. FABRIC-3.8.md §3.13.5 (ruling C-5): Hera between lines,
* polling for input, is the idle point. She has no work of her own there,
* so any VM with work goes -- the coolest one. She is parked with an IDLE
* frame (nothing in progress), so she becomes eligible again only through
* mail or pending console input, or is returned to by a trampoline with
* nothing left to do. */
void sk_vm_idle_point(int may_drain_self) {
VM *self = sk_percpu()->current_vm;
VM *next;
/* FABRIC-3.10.8 MC-0b: nothing is held on return to the idle point,
* which makes this the place to log any lock failure counted since. */
sk_lock_check_none_held("idle point");
sk_lock_report();
sk_smp_report(); /* FABRIC-3.10.8 MC-1: a parked core that woke */
if (g_observer_refusals != g_observer_refusals_reported) { /* MC-2 */
g_observer_refusals_reported = g_observer_refusals;
log_message(LOG_ERROR, "SWITCH: observer refused off core 0 x%llu",
(unsigned long long)g_observer_refusals);
}
if (!self || self != (VM *)sk_get_mama_vm()) return;
if (self->frames.count != 0) return; /* inside a word (KEY/EXPECT): not idle */
/* FABRIC-3.9.8 Stage O-B (FABRIC-3.9.0.md §2.5): Hera acts on her own
* mail the moment she is idle -- one message, then back to the caller's
* poll loop, which calls again. Was sk_repl_idle()'s drain, paced by the
* ~1 s SK_IDLE_BEAT_INTERVAL. Safe here by the check just above: no
* frame means no line, word or drain of hers is in progress, so the
* drain can't re-enter her interpreter. */
if (may_drain_self && sk_hermes_pending_count(self->stadium_vm_id) > 0) {
(void)sk_hermes_drain_checkpoint(self);
return;
}
sk_vm_switch_reap_pending();
next = sk_vm_switch_decide(self, 0);
if (!next) return;
{
VMFrameID idle_id = vm_frame_push(&self->frames, self->stadium_vm_id, VM_FRAME_IDLE);
if (idle_id == 0) return; /* fail closed: stay put rather than park unmarked */
sk_vm_switch_signal_note_switch_performed(next->stadium_vm_id);
(void)sk_vm_context_switch(self, next);
vm_frame_pop(&self->frames, idle_id);
}
}
int sk_vm_parked_idle(const VM *vm) {
return vm && vm->native_stack_saved_sp != 0 &&
vm->frames.count == 1 &&
vm->frames.entries[0].kind == VM_FRAME_IDLE &&
vm_uuid_equal(vm->frames.entries[0].vm_id, vm->stadium_vm_id);
}
uint64_t sk_vm_switch_observer_refusals(void) { return g_observer_refusals; }
int sk_vm_context_switch(VM *from, VM *to) {
uint64_t new_sp;
if (!from || !to || to->native_stack_top == 0) {
return -1;
}
/* MC-2 (MC-R1): the observers run on core 0 only. */
if (sk_percpu()->core_id != 0 && sk_vm_switch_signal_is_observer(to->stadium_vm_id)) {
__atomic_add_fetch(&g_observer_refusals, 1, __ATOMIC_RELAXED);
return -1;
}
if (to->native_stack_saved_sp == 0) {
/* First-ever entry: synthesize the initial frame. */
g_switch_entry_vm = to;
new_sp = sk_vm_switch_prime(to->native_stack_top, (void *)&sk_vm_switch_entry);
} else {
new_sp = to->native_stack_saved_sp;
to->native_stack_saved_sp = 0; /* about to be running, not parked */
}
capsule_vm_set_state(to->stadium_vm_id, VM_STATE_LIVE);
capsule_vm_set_state(from->stadium_vm_id, VM_STATE_SWITCHED_OUT);
sk_vm_switch_signal_note_parked(from->stadium_vm_id); /* §3.13.3 wait stamp */
sk_percpu()->current_vm = to;
/* FABRIC-3.10.8 MC-0b: no lock crosses a switch (spinlock.h). */
sk_lock_check_none_held("context switch, leaving");
sk_vm_switch_to(&from->native_stack_saved_sp, new_sp);
sk_lock_check_none_held("context switch, resumed");
/* Resumes here only once something later switches back to `from` --
* from `from`'s own point of view, this call simply took a while.
* sk_percpu() is read fresh here, not cached from before the switch:
* once workers migrate (MC-3), `from` may resume on a different core. */
capsule_vm_set_state(from->stadium_vm_id, VM_STATE_LIVE);
sk_percpu()->current_vm = from;
return 0;
}
#endif /* __STARKERNEL__ */