Implements the message-arrival eligibility signal FABRIC-3.md §XXVIII.1 left open (has_work per-slot flag, set via new SWITCH-MARK-WORK primitive from MSG-SEND) so an idle VM never becomes a switch target purely by waiting out the readiness threshold. Also root-causes and fixes a second, independent switch-storm: the tick's "who is current" check used vm_log_attributed_vm(), which can't see a VM parked in switch.c's own raw trampoline. Replaced with a dedicated g_switch_current_vm tracked by the switch mechanism itself, and moved target-slot eligibility reset to the switch decision point instead of relying on ISR polling to observe a window that can be only a few instructions wide. Verified live on all 3 architectures: clean boot to zuse@Hera] ok>, live cross-VM message dispatch, and (since a quiet log looks identical to a livelocked storm once the DoE probe is gone) confirmed genuine REPL liveness via QMP send-key + screendump on aarch64/riscv64, not log inspection alone. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BWpNjdwPtFLuVLaAq44L9K
199 lines
7.2 KiB
C
199 lines
7.2 KiB
C
/*
|
||
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.
|
||
*/
|
||
|
||
/**
|
||
* capsule_vm_switch_signal.c - see the header for the full contract
|
||
* (FABRIC-3.md §XXVIII, Stage 3, 2026-09-13).
|
||
*/
|
||
|
||
#ifdef __STARKERNEL__
|
||
|
||
#include "starkernel/capsule_vm_switch_signal.h"
|
||
#include "vm.h"
|
||
#include "starkernel/vm/switch.h" /* sk_vm_switch_current_vm() -- see below */
|
||
#include <stddef.h>
|
||
|
||
/* Headroom beyond today's 3 participants (Hera/Hermes/Artemis) -- see
|
||
* the header's own doc comment on why this isn't hardcoded to 3. */
|
||
#define SK_SWITCH_MAX_SLOTS 8
|
||
|
||
/* Ticks a non-running participant must accumulate readiness before a
|
||
* switch to it is requested. Simple linear accumulate-then-threshold,
|
||
* not the fancier relaxation law a later pass might want -- correctness
|
||
* and a clean single-writer/single-reader story mattered more than the
|
||
* exact law for this first cut (this stage's own design-review latitude,
|
||
* per the plan). Reset to 0 the instant a slot becomes current, so a
|
||
* VM that just ran needs a full threshold's worth of ticks idle before
|
||
* it can be selected again -- a crude but real fairness property. */
|
||
#define SK_SWITCH_READINESS_THRESHOLD 50u
|
||
|
||
typedef struct {
|
||
VMUuid vm_id;
|
||
uint32_t readiness;
|
||
int has_work; /* Stage 3 follow-on (2026-09-14): set by
|
||
* sk_vm_switch_signal_mark_work() (mainline,
|
||
* MSG-SEND hook), cleared here in tick() the
|
||
* instant this slot becomes current -- see that
|
||
* function's own doc comment in the header. */
|
||
} sk_switch_slot_entry_t;
|
||
|
||
static sk_switch_slot_entry_t g_slots[SK_SWITCH_MAX_SLOTS];
|
||
static int g_slot_count;
|
||
static int g_pending;
|
||
static VMUuid g_pending_target;
|
||
|
||
/* DoE CSV exposure (FABRIC-3.md §XXVIII Stage 3 follow-on, 2026-09-13). */
|
||
static int g_current_slot_cached = -1;
|
||
static uint64_t g_switch_count;
|
||
static uint32_t g_ticks_since_switch;
|
||
|
||
static int slot_for_vm_id(VMUuid vm_id) {
|
||
int i;
|
||
for (i = 0; i < g_slot_count; i++) {
|
||
if (vm_uuid_equal(g_slots[i].vm_id, vm_id)) return i;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
int sk_vm_switch_signal_register(VMUuid vm_id) {
|
||
if (g_slot_count >= SK_SWITCH_MAX_SLOTS) return -1;
|
||
g_slots[g_slot_count].vm_id = vm_id;
|
||
g_slots[g_slot_count].readiness = 0;
|
||
g_slots[g_slot_count].has_work = 0;
|
||
return g_slot_count++;
|
||
}
|
||
|
||
void sk_vm_switch_signal_tick(void) {
|
||
VM *current;
|
||
int current_slot, i;
|
||
|
||
/* FABRIC-3.md §XXVIII Stage 3 follow-on correction (2026-09-14): was
|
||
* vm_log_attributed_vm() -- wrong source. That tracks nested
|
||
* vm_interpret() calls for log attribution and has no way to know a
|
||
* VM is running while parked inside switch.c's own raw-C trampoline
|
||
* (sk_vm_switch_entry()), which never itself calls vm_interpret(). A
|
||
* VM that only ever ran the trampoline was therefore never seen as
|
||
* "current," so its own readiness/has_work below never reset --
|
||
* tick() re-armed the very next timer tick, forever, once nothing
|
||
* else happened to be independently flipping attribution around it
|
||
* (found live: a switch storm, 101% CPU, log frozen). sk_vm_switch_
|
||
* current_vm() is authoritative instead: the switch mechanism itself
|
||
* updates it on every switch, so it reflects who is physically
|
||
* running regardless of whether that VM ever calls vm_interpret(). */
|
||
current = sk_vm_switch_current_vm();
|
||
if (!current) return; /* boot: no switch has happened yet, not seeded */
|
||
|
||
current_slot = slot_for_vm_id(current->stadium_vm_id);
|
||
if (current_slot < 0) return; /* current VM isn't a registered participant */
|
||
|
||
g_current_slot_cached = current_slot;
|
||
g_ticks_since_switch++;
|
||
|
||
for (i = 0; i < g_slot_count; i++) {
|
||
if (i == current_slot) {
|
||
g_slots[i].readiness = 0;
|
||
g_slots[i].has_work = 0;
|
||
} else {
|
||
g_slots[i].readiness++;
|
||
}
|
||
}
|
||
|
||
if (!g_pending) {
|
||
for (i = 0; i < g_slot_count; i++) {
|
||
if (i != current_slot &&
|
||
g_slots[i].readiness >= SK_SWITCH_READINESS_THRESHOLD &&
|
||
g_slots[i].has_work) {
|
||
g_pending = 1;
|
||
g_pending_target = g_slots[i].vm_id;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
VMUuid sk_vm_switch_signal_take_pending(void) {
|
||
if (!g_pending) return vm_uuid_none();
|
||
g_pending = 0;
|
||
return g_pending_target;
|
||
}
|
||
|
||
void sk_vm_switch_signal_note_switch_performed(VMUuid target_id) {
|
||
/* FABRIC-3.md §XXVIII Stage 3 follow-on correction (2026-09-14):
|
||
* reset the target's own readiness/has_work HERE, deterministically,
|
||
* at the switch decision point -- not by waiting for tick()'s
|
||
* current-slot polling to observe it running. That polling depends on
|
||
* sk_vm_switch_current_vm() catching a real ISR tick during whatever
|
||
* window the target is genuinely "current" for, and for a VM whose
|
||
* only production behavior is switch.c's placeholder trampoline (an
|
||
* immediate yield-back), that window can be as small as a few
|
||
* instructions -- too narrow for a timer tick to reliably land in.
|
||
* Without this, the target's eligibility never clears, tick() re-arms
|
||
* on the very next timer tick, and the result is a continuous switch
|
||
* storm (found live: 101% CPU, zero forward progress). This makes
|
||
* correctness independent of ISR timing; current_slot (via
|
||
* sk_vm_switch_current_vm()/tick()) remains useful as a DoE-observable
|
||
* value and for excluding the running slot from readiness accrual,
|
||
* but is no longer load-bearing for storm prevention. */
|
||
int slot = slot_for_vm_id(target_id);
|
||
if (slot >= 0) {
|
||
g_slots[slot].readiness = 0;
|
||
g_slots[slot].has_work = 0;
|
||
}
|
||
g_switch_count++;
|
||
g_ticks_since_switch = 0;
|
||
}
|
||
|
||
void sk_vm_switch_signal_mark_work(VMUuid vm_id) {
|
||
int slot = slot_for_vm_id(vm_id);
|
||
if (slot < 0) return;
|
||
g_slots[slot].has_work = 1;
|
||
}
|
||
|
||
uint64_t sk_vm_switch_signal_switch_count(void) {
|
||
return g_switch_count;
|
||
}
|
||
|
||
uint32_t sk_vm_switch_signal_ticks_since_switch(void) {
|
||
return g_ticks_since_switch;
|
||
}
|
||
|
||
int sk_vm_switch_signal_current_slot(void) {
|
||
return g_current_slot_cached;
|
||
}
|
||
|
||
int sk_vm_switch_signal_slot_count(void) {
|
||
return g_slot_count;
|
||
}
|
||
|
||
uint32_t sk_vm_switch_signal_readiness(int slot) {
|
||
if (slot < 0 || slot >= g_slot_count) return 0;
|
||
return g_slots[slot].readiness;
|
||
}
|
||
|
||
uint32_t sk_vm_switch_signal_readiness_of(VMUuid vm_id) {
|
||
int slot = slot_for_vm_id(vm_id);
|
||
if (slot < 0) return 0;
|
||
return g_slots[slot].readiness;
|
||
}
|
||
|
||
#endif /* __STARKERNEL__ */
|