Stage 3 follow-on: message-arrival eligibility hook + trampoline-blind switch-storm fix (FABRIC-3.md §XXVIII.2)
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
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
862d7d9c48
commit
66beae7fd4
@@ -29,7 +29,7 @@
|
||||
|
||||
#include "starkernel/capsule_vm_switch_signal.h"
|
||||
#include "vm.h"
|
||||
#include "starkernel/log_attrib.h" /* vm_log_attributed_vm() */
|
||||
#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
|
||||
@@ -49,6 +49,11 @@
|
||||
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];
|
||||
@@ -73,6 +78,7 @@ 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++;
|
||||
}
|
||||
|
||||
@@ -80,8 +86,21 @@ void sk_vm_switch_signal_tick(void) {
|
||||
VM *current;
|
||||
int current_slot, i;
|
||||
|
||||
current = vm_log_attributed_vm();
|
||||
if (!current) return; /* nothing interpreting right now (boot/driver/HAL context) */
|
||||
/* 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 */
|
||||
@@ -92,6 +111,7 @@ void sk_vm_switch_signal_tick(void) {
|
||||
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++;
|
||||
}
|
||||
@@ -99,7 +119,9 @@ void sk_vm_switch_signal_tick(void) {
|
||||
|
||||
if (!g_pending) {
|
||||
for (i = 0; i < g_slot_count; i++) {
|
||||
if (i != current_slot && g_slots[i].readiness >= SK_SWITCH_READINESS_THRESHOLD) {
|
||||
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;
|
||||
@@ -114,11 +136,38 @@ VMUuid sk_vm_switch_signal_take_pending(void) {
|
||||
return g_pending_target;
|
||||
}
|
||||
|
||||
void sk_vm_switch_signal_note_switch_performed(void) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
#include "starkernel/capsule_vm_physics.h"
|
||||
#include "starkernel/vm/vm_internal.h"
|
||||
#include "starkernel/vm/stadium.h" /* item 4.2 -- STADIUM-* words */
|
||||
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.md §XXVIII Stage 3 follow-on -- SWITCH-MARK-WORK */
|
||||
#include "starkernel/vm/stadium_words.h" /* item 4.2 -- stadium_words_resident_heat() */
|
||||
#include "starkernel/repl.h"
|
||||
#include "starkernel/capsule_generated.h"
|
||||
@@ -785,6 +786,43 @@ static void mama_word_vm_exec(VM *vm)
|
||||
/* Stack clean on exit */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief SWITCH-MARK-WORK ( name-caddr name-u -- )
|
||||
* FABRIC-3.md §XXVIII Stage 3 follow-on (2026-09-14): message-arrival
|
||||
* eligibility hook. Called from MSG-SEND (capsules/common/messaging.4th)
|
||||
* every time a message is sent to a VM, so the Stage 3 switch signal can
|
||||
* tell "has recently been sent something" apart from "has just been idle
|
||||
* long enough" before ever selecting it as a switch target. Silently
|
||||
* ignores a malformed or unknown name -- this fires on the hot path of
|
||||
* every message send, not a user-facing command, so it must never error
|
||||
* or spam the console.
|
||||
*/
|
||||
static void mama_word_switch_mark_work(VM *vm)
|
||||
{
|
||||
char name[VM_NAME_MAX];
|
||||
cell_t u, caddr;
|
||||
uint32_t i;
|
||||
VMRegistryEntry entry;
|
||||
|
||||
if (vm->dsp < 2) { vm->error = 1; return; }
|
||||
|
||||
u = vm_pop(vm);
|
||||
caddr = vm_pop(vm);
|
||||
|
||||
if (u <= 0 || (uint32_t)u >= VM_NAME_MAX) return;
|
||||
|
||||
{
|
||||
const uint8_t *p = vm_ptr(vm, (vaddr_t)caddr);
|
||||
if (!p) return;
|
||||
for (i = 0; i < (uint32_t)u; i++) name[i] = (char)p[i];
|
||||
name[u] = '\0';
|
||||
}
|
||||
|
||||
if (capsule_vm_find_by_name_nocase(name, &entry) == 0) {
|
||||
sk_vm_switch_signal_mark_work(entry.vm_id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief VM-CALL ( cmd-caddr cmd-u vm-name-caddr vm-name-u -- n )
|
||||
* Like VM-EXEC but pops one cell from the target VM's TOS and pushes
|
||||
@@ -1653,6 +1691,7 @@ void register_mama_forth_words(VM *vm)
|
||||
register_word(vm, "VM-PHYSICS-STATUS", mama_word_vm_physics_status);
|
||||
register_word(vm, "VM-STEP", mama_word_vm_step);
|
||||
register_word(vm, "VM-EXEC", mama_word_vm_exec);
|
||||
register_word(vm, "SWITCH-MARK-WORK", mama_word_switch_mark_work);
|
||||
register_word(vm, "VM-CALL", mama_word_vm_call);
|
||||
register_word(vm, "VM-HEAT", mama_word_vm_heat);
|
||||
/* FABRIC-3.md SXX: the 8 STADIUM-* primitives register_child_vm_words()
|
||||
@@ -1711,6 +1750,7 @@ void register_mama_forth_words(VM *vm)
|
||||
register_word(vm, "VM-PHYSICS-STATUS", mama_word_vm_physics_status);
|
||||
register_word(vm, "VM-STEP", mama_word_vm_step);
|
||||
register_word(vm, "VM-EXEC", mama_word_vm_exec);
|
||||
register_word(vm, "SWITCH-MARK-WORK", mama_word_switch_mark_work);
|
||||
register_word(vm, "VM-CALL", mama_word_vm_call);
|
||||
register_word(vm, "VM-HEAT", mama_word_vm_heat);
|
||||
/* FABRIC-3.md SXX: the 8 STADIUM-* primitives register_child_vm_words()
|
||||
@@ -1946,6 +1986,7 @@ void register_child_vm_words(VM *vm)
|
||||
register_word(vm, "STOP", mama_word_stop);
|
||||
register_word(vm, "EXEC", mama_word_exec);
|
||||
register_word(vm, "VM-EXEC", mama_word_vm_exec);
|
||||
register_word(vm, "SWITCH-MARK-WORK", mama_word_switch_mark_work);
|
||||
register_word(vm, "VM-CALL", mama_word_vm_call);
|
||||
register_word(vm, "VM-HEAT", mama_word_vm_heat);
|
||||
/* USE (FABRIC-2.md §F.24): not console-specific -- any VM can
|
||||
|
||||
@@ -64,6 +64,7 @@ EFI_RUNTIME_SERVICES *g_sk_runtime_services = NULL;
|
||||
#include "starkernel/capsule_birth.h" /* capsule_birth_mama, capsule_find_mama_init */
|
||||
#include "starkernel/capsule_zuse_boot.h" /* capsule_zuse_boot_load_root_pubkey */
|
||||
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.md §XXVIII Stage 3 */
|
||||
#include "starkernel/vm/switch.h" /* sk_vm_switch_set_current() -- Stage 3 follow-on */
|
||||
#include "starkernel/artemis_sig.h" /* artemis_sig_check/genesis_stamp */
|
||||
#include "starkernel/kmalloc.h"
|
||||
#include "starkernel/repl.h"
|
||||
@@ -997,6 +998,11 @@ static void kernel_main_deep(BootInfo *boot_info) {
|
||||
VMRegistryEntry hera_entry, hermes_entry, artemis_entry;
|
||||
if (capsule_vm_registry_get(vm_uuid_hera(), &hera_entry) == 0) {
|
||||
sk_vm_switch_signal_register(hera_entry.vm_id);
|
||||
/* Seed the switch mechanism's own "who is running" tracker
|
||||
* (FABRIC-3.md §XXVIII Stage 3 follow-on, 2026-09-14) -- Hera
|
||||
* is genuinely the one running here, before any switch has
|
||||
* ever happened. */
|
||||
sk_vm_switch_set_current(mama);
|
||||
}
|
||||
if (capsule_vm_find_by_name_nocase("Hermes", &hermes_entry) == 0 &&
|
||||
hermes_entry.state == VM_STATE_LIVE) {
|
||||
|
||||
@@ -51,6 +51,26 @@ extern uint64_t sk_vm_switch_prime(uint64_t stack_top, void *entry);
|
||||
* who most recently switched into it does. */
|
||||
static VM *g_switch_entry_vm;
|
||||
|
||||
/* 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). */
|
||||
static VM *g_switch_current_vm;
|
||||
|
||||
VM *sk_vm_switch_current_vm(void) { return g_switch_current_vm; }
|
||||
|
||||
void sk_vm_switch_set_current(VM *vm) { g_switch_current_vm = vm; }
|
||||
|
||||
/**
|
||||
* sk_vm_switch_entry - trampoline for a VM's first-ever entry.
|
||||
*
|
||||
@@ -64,6 +84,18 @@ static VM *g_switch_entry_vm;
|
||||
* 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.
|
||||
*/
|
||||
void sk_vm_switch_entry(void) {
|
||||
VM *self = g_switch_entry_vm;
|
||||
@@ -95,12 +127,14 @@ int sk_vm_context_switch(VM *from, VM *to) {
|
||||
|
||||
capsule_vm_set_state(to->stadium_vm_id, VM_STATE_LIVE);
|
||||
capsule_vm_set_state(from->stadium_vm_id, VM_STATE_SWITCHED_OUT);
|
||||
g_switch_current_vm = to;
|
||||
|
||||
sk_vm_switch_to(&from->native_stack_saved_sp, new_sp);
|
||||
|
||||
/* Resumes here only once something later switches back to `from` --
|
||||
* from `from`'s own point of view, this call simply took a while. */
|
||||
capsule_vm_set_state(from->stadium_vm_id, VM_STATE_LIVE);
|
||||
g_switch_current_vm = from;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -920,14 +920,16 @@ void execute_colon_word(VM* vm)
|
||||
* `vm`, which could be an arbitrary number of ticks later --
|
||||
* calling it after would attribute switch_count_cumulative/
|
||||
* switch_ticks_since to the wrong (resume) event instead of the
|
||||
* actual switch-out this counts. */
|
||||
* actual switch-out this counts. It also resets target_id's own
|
||||
* eligibility state directly (2026-09-14 correction) -- see its
|
||||
* own doc comment for why that can't be left to ISR polling. */
|
||||
if (sk_vm_at_outermost_interpret()) {
|
||||
VMUuid target_id = sk_vm_switch_signal_take_pending();
|
||||
if (!vm_uuid_equal(target_id, vm_uuid_none())) {
|
||||
VMRegistryEntry target_entry;
|
||||
if (capsule_vm_registry_get(target_id, &target_entry) == 0 &&
|
||||
target_entry.vm_ptr && target_entry.vm_ptr != (void *)vm) {
|
||||
sk_vm_switch_signal_note_switch_performed();
|
||||
sk_vm_switch_signal_note_switch_performed(target_id);
|
||||
sk_vm_context_switch(vm, (VM *)target_entry.vm_ptr);
|
||||
/* Resumes here once something later switches back. */
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user