Stage 3: timer-driven preemptive switching, live on all 3 arches (FABRIC-3.md §XXVIII)
Fourth stage of the preemptive context-switching plan, and the biggest. LithosAnanke now genuinely, continuously preempts between Hera, Hermes, and Artemis -- timer-driven, running live for the entire remainder of every boot once the Tripod fleet registers, not a bounded probe. A real design fork was resolved before writing code: the naive approach (the timer ISR calling Stage 2's sk_vm_context_switch() directly) is broken -- Stage 0's trap frame lives on whatever stack was active at interrupt time, and jumping to a different stack via Stage 2's own independent swap mid-handler would abandon that trap frame unresumed, guaranteed corruption on the first tick. Chose the safer of two named options: the ISR only ever sets a flag and returns completely normally through its own full epilogue; the actual switch happens moments later, via Stage 2's already-proven mechanism, at a safe cooperative checkpoint on the mainline (execute_colon_word()'s per-word dispatch loop, checked on literally every word, not throttled to the existing 256-word heartbeat-tuning cadence) -- confirmed with the user that word-level granularity is fine-grained enough given the eventual Zynq FPGA target where a word is a mnemonic. New capsule_vm_switch_signal.c/.h: a purpose-built run-readiness signal, deliberately separate from capsule_vm_physics.c's execution-heat engine (that one's own header documents itself as never touched from interrupt context, by design). Slot table sized with headroom (8) rather than hardcoded to today's 3 participants, so extending participation later is another register() call, not a redesign -- per direct request to leave room for swapping the participant set. Simple linear accumulate-then- threshold for this first cut; a fancier law can replace it later without touching the mechanism around it. heartbeat_tick() gains its one deliberate, documented amendment to this file's own top-half/bottom-half discipline -- the first time this codebase reaches into VM-scheduling state from real ISR context. Registration happens only after all three VMs are fully born, right before the REPL starts -- no critical-section protection yet against being switched away mid-birth-setup. Known, flagged rough edge (not reconciled this pass): MSG-TICK's own idle-pump and this new mechanism can still independently move control between the same VMs; not observed to interact badly in verification, but not fully unified either. Verified interactively at the console on all 3 architectures with continuous background preemption running throughout -- amd64 computed `1 1 + .` -> 2, aarch64 computed `1 1 + dup DUP * . CR` -> 4, both correct, REPL fully responsive, zero fault indicators over sustained runtime. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016UNhH1mhi52i6Qihh7ZV5S
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
f790d0995e
commit
986d042aa7
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
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/log_attrib.h" /* vm_log_attributed_vm() */
|
||||
#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;
|
||||
} 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;
|
||||
|
||||
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;
|
||||
return g_slot_count++;
|
||||
}
|
||||
|
||||
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) */
|
||||
|
||||
current_slot = slot_for_vm_id(current->stadium_vm_id);
|
||||
if (current_slot < 0) return; /* current VM isn't a registered participant */
|
||||
|
||||
for (i = 0; i < g_slot_count; i++) {
|
||||
if (i == current_slot) {
|
||||
g_slots[i].readiness = 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_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;
|
||||
}
|
||||
|
||||
#endif /* __STARKERNEL__ */
|
||||
@@ -36,6 +36,7 @@
|
||||
*/
|
||||
|
||||
#include "starkernel/timer.h"
|
||||
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.md §XXVIII Stage 3 */
|
||||
|
||||
#if defined(ARCH_AMD64)
|
||||
#define HEARTBEAT_HAS_VARIANCE 1
|
||||
@@ -162,6 +163,20 @@ void heartbeat_tick(void)
|
||||
g_pending_counter = heartbeat_read_counter();
|
||||
g_heartbeat.ticks++;
|
||||
g_pending_valid = 1;
|
||||
|
||||
/* FABRIC-3.md §XXVIII, Stage 3 (2026-09-13): the one deliberate,
|
||||
* documented amendment to this file's own top-half/bottom-half
|
||||
* discipline above. sk_vm_switch_signal_tick() reaches into VM
|
||||
* scheduling state from real ISR context -- something this file's
|
||||
* header comment previously said nothing here did. It stays cheap
|
||||
* (bounded iteration over a small slot table, no locking needed for
|
||||
* the same reason nothing else here needs it: single hart, nothing
|
||||
* concurrent with this ISR while it runs) and it never itself
|
||||
* switches anything -- it only ever sets a flag, consumed later at a
|
||||
* safe cooperative checkpoint on the mainline
|
||||
* (execute_colon_word()). The GAP-A1 split itself is not overturned:
|
||||
* everything else in this function is exactly as minimal as before. */
|
||||
sk_vm_switch_signal_tick();
|
||||
}
|
||||
|
||||
void heartbeat_service(void)
|
||||
|
||||
@@ -63,6 +63,7 @@ EFI_RUNTIME_SERVICES *g_sk_runtime_services = NULL;
|
||||
#include "starkernel/capsule_loader.h"
|
||||
#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/artemis_sig.h" /* artemis_sig_check/genesis_stamp */
|
||||
#include "starkernel/kmalloc.h"
|
||||
#include "starkernel/repl.h"
|
||||
@@ -986,6 +987,27 @@ static void kernel_main_deep(BootInfo *boot_info) {
|
||||
}
|
||||
console_println("");
|
||||
|
||||
/* FABRIC-3.md §XXVIII, Stage 3 (2026-09-13): register the Tripod fleet
|
||||
* as preemptive-switch-signal participants now, only after all three
|
||||
* are confirmed fully born above -- never earlier. This stage has no
|
||||
* critical-section protection against being switched away mid-setup,
|
||||
* so registering any earlier would risk the signal firing during
|
||||
* Hermes/Artemis's own birth sequencing. */
|
||||
{
|
||||
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);
|
||||
}
|
||||
if (capsule_vm_find_by_name_nocase("Hermes", &hermes_entry) == 0 &&
|
||||
hermes_entry.state == VM_STATE_LIVE) {
|
||||
sk_vm_switch_signal_register(hermes_entry.vm_id);
|
||||
}
|
||||
if (capsule_vm_find_by_name_nocase("Artemis", &artemis_entry) == 0 &&
|
||||
artemis_entry.state == VM_STATE_LIVE) {
|
||||
sk_vm_switch_signal_register(artemis_entry.vm_id);
|
||||
}
|
||||
}
|
||||
|
||||
/* Decided 2026-09-05: no console for the running system unless a
|
||||
* thumbdrive is present -- headless by default (EMERGENCY_CONSOLE_
|
||||
* ENABLED off), reusing that flag's own existing "does this build
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
#include "starkernel/console.h" /* g_sk_fault_word */
|
||||
#include "platform_alloc.h" /* sf_free for call_stack */
|
||||
#include "starkernel/vm/stadium_words.h" /* item 4.1: word patrons on the Stadium */
|
||||
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.md §XXVIII Stage 3 */
|
||||
#include "starkernel/vm/switch.h" /* FABRIC-3.md §XXVIII Stage 2 -- sk_vm_context_switch() */
|
||||
#include "starkernel/capsule_birth.h" /* capsule_vm_registry_get() */
|
||||
#endif
|
||||
#include "word_source/include/vocabulary_words.h"
|
||||
#include "vm_internal.h"
|
||||
@@ -880,6 +883,30 @@ void execute_colon_word(VM* vm)
|
||||
vm->heartbeat.check_counter = 0;
|
||||
}
|
||||
|
||||
#ifdef __STARKERNEL__
|
||||
/* FABRIC-3.md §XXVIII, Stage 3 (2026-09-13): the cooperative
|
||||
* preemption checkpoint. Deliberately unconditional (every word,
|
||||
* not throttled like the heartbeat cycle above) -- the check
|
||||
* itself is a single flag read, cheap regardless of frequency,
|
||||
* and "a word is a mnemonic" (this project's own eventual FPGA
|
||||
* framing) is the granularity preemption should approximate here.
|
||||
* Only ever acts when this word's own execution left the VM in a
|
||||
* clean, resumable state (past the error/abort/exit_colon checks
|
||||
* below would be too late -- this runs before them, deliberately,
|
||||
* so a switch never happens mid-unwind). */
|
||||
{
|
||||
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_context_switch(vm, (VM *)target_entry.vm_ptr);
|
||||
/* Resumes here once something later switches back. */
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (vm->error) { vm->ecw_nesting--; return; }
|
||||
|
||||
/* ABORT clears stacks and unwinds to QUIT: leave the flag set so
|
||||
|
||||
Reference in New Issue
Block a user