Stage C-b: compudynamic switch decision, idle point, timer path removed (FABRIC-3.8 §3.13.3-§3.13.5)
Who runs next is now sk_vm_switch_decide(): among participants with work (pending kernel-Hermes mail, parked mid-task, or Hera with console input), the coolest by VM-physics heat; ties with the incumbent keep the incumbent, ties among candidates go to the longest waiting (unique wait stamps). No scheduler state, no timer: heartbeat_tick() no longer touches switching, readiness/threshold/has_work/take_pending removed. Decided at owned word checkpoints (colon words and, new, between top-level words) after a beat or new mail, at the trampoline between messages, and at Hera's idle point in the console poll loops. The idle pump skips participants (G3 closed for them): the WIREBIND relay is delivered by a real switch to rajames~user. Probe (restored away): relay via switch Hera->rajames~user->Hera on all three ISAs, parked saved_sp inside own stack, walk-order disagreements 0 over tens of millions of decisions, 0 bytes idle log growth. Acceptance: dict_hash unchanged, relay 3, no faults. Rulings: C-3, C-5 as proposed; C-4 taken as (alpha), consequence recorded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7c3736841d
commit
06bcab504d
@@ -879,3 +879,102 @@ explicit instruction.
|
|||||||
her Stage 3 registration onward and is charged for her own beats. She holds nearly all the
|
her Stage 3 registration onward and is charged for her own beats. She holds nearly all the
|
||||||
heat, so her charges pull almost nothing; that is the C-4 arithmetic playing out as
|
heat, so her charges pull almost nothing; that is the C-4 arithmetic playing out as
|
||||||
expected. The per-beat DoE heat columns will read differently from pre-C-a runs.
|
expected. The per-beat DoE heat columns will read differently from pre-C-a runs.
|
||||||
|
- [x] **Stage C-b — the compudynamic switch decision, the idle point, the timer path removed
|
||||||
|
(§3.13.3–§3.13.5).** 2026-09-23. Captain Bob: *"go ahead with C-b as proposed."*
|
||||||
|
- Rulings: **C-3** as proposed (the incumbent stays on a tie; among candidates, the one
|
||||||
|
waiting longest, by unique wait stamp). **C-5** as proposed (the idle point folded in;
|
||||||
|
Hera's pending console input is her mail).
|
||||||
|
- **C-4** had no proposal, so I took **(α) accept**. No special case for Hera; the
|
||||||
|
consequence is recorded below.
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
- `capsule_vm_switch_signal.c/.h`:
|
||||||
|
- Removed: readiness, `has_work`, the 50-tick threshold, `tick()`, `take_pending()` and
|
||||||
|
the readiness getters.
|
||||||
|
- New: `sk_vm_switch_decide(self, self_competes)`. It considers participants other
|
||||||
|
than `self` that are alive, not `pending_reap`, and have a native stack. A candidate
|
||||||
|
is eligible with pending kernel-Hermes mail, or when parked mid-task (frame stack
|
||||||
|
non-empty, top frame not IDLE), or for Hera, pending console input. The lowest
|
||||||
|
VM-physics heat wins; on equal heat, the lowest wait stamp. Stamps are unique:
|
||||||
|
either a message's admission or the last time the VM was parked. With
|
||||||
|
`self_competes` set, it returns a candidate only if that candidate is **strictly**
|
||||||
|
cooler than `self`. It keeps no state between calls.
|
||||||
|
- Also new: wait stamps (`next_stamp`, `note_parked`), the "decision due" flag (set by
|
||||||
|
`note_beat` and `note_mail`, consumed at the next owned checkpoint), `is_participant`,
|
||||||
|
and `sk_vm_switch_reap_pending()`. The latter is the Stage 4 `pending_reap` handling,
|
||||||
|
now run at every safe point, one reap per call.
|
||||||
|
- `ticks_since_switch` now counts beats.
|
||||||
|
- `heartbeat.c`: the ISR's `sk_vm_switch_signal_tick()` call is gone. The timer decides
|
||||||
|
nothing.
|
||||||
|
- `kernel_hermes.c/.h`: each admitted message carries `wait_stamp`, and a send sets
|
||||||
|
decision-due.
|
||||||
|
- `vm_runtime.c`: the running VM's beat also calls `note_beat()`, right after its C-a
|
||||||
|
charge.
|
||||||
|
- `switch.c/.h`:
|
||||||
|
- `sk_vm_context_switch()` stamps `from` as parked.
|
||||||
|
- `sk_vm_switch_checkpoint(vm)` re-decides only when a decision is due.
|
||||||
|
- The trampoline decides after every drain: with mail left, it switches only if
|
||||||
|
someone is strictly cooler; with no mail, it goes to the coolest VM with work, or to
|
||||||
|
Hera when nobody has any. That fallback isn't `native_switch_back_to`, because two
|
||||||
|
idle trampolines would bounce into each other forever. A drain that leaves mail
|
||||||
|
unconsumed panics rather than spins.
|
||||||
|
- `sk_vm_idle_point()`: Hera, running with no frame live (between lines), parks with an
|
||||||
|
IDLE frame and switches to the coolest VM with work.
|
||||||
|
- `vm_core.c`:
|
||||||
|
- The `execute_colon_word` checkpoint calls `sk_vm_switch_checkpoint()`.
|
||||||
|
- **New checkpoint between top-level words** in `vm_interpret_kind`, with the same
|
||||||
|
ownership gate, so a primitives-only payload can be displaced.
|
||||||
|
- `repl.c`:
|
||||||
|
- The idle pump skips participants. Their mail goes on their own stacks now; this
|
||||||
|
closes G3 for participants.
|
||||||
|
- `sk_vm_idle_point()` is called on every no-input pass of all three console poll
|
||||||
|
loops (`sk_console_getkey`, `sk_repl_headless_wait`, `sk_console_readline`), **not**
|
||||||
|
at the `SK_IDLE_BEAT_INTERVAL` cadence. That cadence comes from the timer.
|
||||||
|
- `doe_log.c`: the three readiness columns are kept and always 0, so the CSV shape
|
||||||
|
doesn't change. Stage I decides whether to drop them.
|
||||||
|
|
||||||
|
**Evidence, as named in §4.** A temporary probe, restored afterwards from a pre-probe
|
||||||
|
snapshot and verified free of probe code:
|
||||||
|
- a walk-order check that recomputes every decision walking the slot table in reverse;
|
||||||
|
- a line per switch (names, heats, decision and disagreement counts);
|
||||||
|
- a check that each parked VM's `native_stack_saved_sp` lies inside its own native stack
|
||||||
|
(guard, top).
|
||||||
|
|
||||||
|
Probe runs with the full relay: `logs/20260923-171359/amd64/`, `-172010/aarch64/`,
|
||||||
|
`-172635/riscv64/` (plus an earlier amd64 run, `-170740`, before the stack check could
|
||||||
|
print). On every ISA:
|
||||||
|
- **`switch 1 Hera(heat 65536) → rajames~user(heat 0)`** happens at Hera's idle point
|
||||||
|
after she forwards the console line. `rajames~user` drains `CONSOLE-CMD-EVENT` **on its
|
||||||
|
own stack** (`stadium_conserved(self)=true`) and prints `3`.
|
||||||
|
- **`switch 2 rajames~user → Hera`** is the trampoline's no-work fallback.
|
||||||
|
- `switch_count` reached 2.
|
||||||
|
- **`parked rajames~user saved_sp inside own stack (checked=1 bad=0)`**.
|
||||||
|
- **`disagree=0` over 40,246,330 (amd64), 4,182,971 (aarch64) and 25,030,081 (riscv64)
|
||||||
|
decisions.** The decision doesn't depend on walk order.
|
||||||
|
- **Idle log growth over 20 s after the relay: 0 bytes.** No switch storm.
|
||||||
|
- Nothing switched during boot. No participant has mail then, and Hera is hottest.
|
||||||
|
|
||||||
|
**Formal acceptance with the probe gone:** `logs/20260923-173341/amd64/`,
|
||||||
|
`-173947/aarch64/`, `-174610/riscv64/`.
|
||||||
|
- All reach `[zuse@Hera] ok>` with zero `UNKNOWN WORD`, failures, faults, frame corruption
|
||||||
|
and refused interprets.
|
||||||
|
- The `dict_hash` set is identical and unchanged.
|
||||||
|
- Mint → WIREBIND → `USE` → `1 2 + .` → **`3`**, delivered by a real switch.
|
||||||
|
- Idle growth: 0 bytes.
|
||||||
|
- Clean build on all three plus hosted, no new warnings.
|
||||||
|
|
||||||
|
**Observed and recorded, not changed:**
|
||||||
|
- The relayed line's output now follows Hera's `ok>` on the same line
|
||||||
|
(`[rajames@rajames] ok> Kernel-Hermes CONSOLE-CMD-EVENT …`), because delivery happens
|
||||||
|
at her idle point after she prints the prompt. This is cosmetic.
|
||||||
|
- **Idle cost:** the idle point runs the decision on every poll pass (tens of millions of
|
||||||
|
decisions per boot while idle). Each walks the participant table, but the CPU was
|
||||||
|
spinning there anyway.
|
||||||
|
- **C-4 (α) consequence, now concrete:** Hera's other idle work, including xHCI event
|
||||||
|
servicing, USB attach and detach, and block sync, runs from her idle beat. Under
|
||||||
|
sustained worker load Hera is the hottest VM with work and gets no turn, so those
|
||||||
|
services wait until the workers go idle. This doesn't affect the relay. It will matter
|
||||||
|
for Stage G/H, and for any USB keyboard input under load (serial input still makes
|
||||||
|
her eligible).
|
||||||
|
- Stage F's mechanism is now in C-b. Its remaining content is evidence only: work
|
||||||
|
proceeds with the console idle, which this relay already shows for one message.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Capsule Block Manifest — Auto-generated
|
# Capsule Block Manifest — Auto-generated
|
||||||
<!-- Generated by mkcapsule --manifest 2026-09-23T20:43:24Z -->
|
<!-- Generated by mkcapsule --manifest 2026-09-23T21:45:34Z -->
|
||||||
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
|
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
|
||||||
<!-- Hand-written justifications and immutability notes live -->
|
<!-- Hand-written justifications and immutability notes live -->
|
||||||
<!-- in MANIFEST.md alongside this auto-generated index. -->
|
<!-- in MANIFEST.md alongside this auto-generated index. -->
|
||||||
|
|||||||
@@ -68,6 +68,8 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include "starkernel/vm_uuid.h"
|
#include "starkernel/vm_uuid.h"
|
||||||
|
|
||||||
|
struct VM;
|
||||||
|
|
||||||
/* Boot-time allocation (FABRIC-3.6.md task 3.1, 2026-09-21): kmalloc's the
|
/* Boot-time allocation (FABRIC-3.6.md task 3.1, 2026-09-21): kmalloc's the
|
||||||
* slot table to stadium_max_vm_count() entries. Must run after
|
* slot table to stadium_max_vm_count() entries. Must run after
|
||||||
* stadium_boot_init() (that bound is 0, and this fails, until Stadium has
|
* stadium_boot_init() (that bound is 0, and this fails, until Stadium has
|
||||||
@@ -97,44 +99,74 @@ int sk_vm_switch_signal_register(VMUuid vm_id);
|
|||||||
* No-op (returns -1) if vm_id was never registered. */
|
* No-op (returns -1) if vm_id was never registered. */
|
||||||
int sk_vm_switch_signal_unregister(VMUuid vm_id);
|
int sk_vm_switch_signal_unregister(VMUuid vm_id);
|
||||||
|
|
||||||
/* Called from heartbeat_tick() (ISR context) every timer tick. Cheap:
|
/*
|
||||||
* iterates only the registered slots (bounded, small). */
|
* FABRIC-3.8.md §3.13 (Stage C-b, 2026-09-23): the ISR half of this module
|
||||||
void sk_vm_switch_signal_tick(void);
|
* (sk_vm_switch_signal_tick() from heartbeat_tick(), readiness accrual
|
||||||
|
* against a fixed 50-tick threshold, a pending flag) is removed -- the
|
||||||
|
* timer no longer decides anything. This module's own header comment above
|
||||||
|
* (not a repurposing of VM-physics heat) is reversed by Captain Bob's
|
||||||
|
* §3.11 ruling: the decision below IS a VM-physics heat comparison, made
|
||||||
|
* workable at switch granularity by Stage C-a's per-beat charge.
|
||||||
|
*/
|
||||||
|
|
||||||
/* Called from the cooperative checkpoint (execute_colon_word(), mainline,
|
/* 1 if vm_id is a registered participant (the idle pump skips these: their
|
||||||
* once per word dispatch). Returns the VMUuid of a VM that should now be
|
* mail is delivered on their own stacks, by a switch). */
|
||||||
* switched to, or vm_uuid_none() if nothing is pending. Clears the
|
int sk_vm_switch_signal_is_participant(VMUuid vm_id);
|
||||||
* pending flag as a side effect -- call at most once per checkpoint. */
|
|
||||||
VMUuid sk_vm_switch_signal_take_pending(void);
|
|
||||||
|
|
||||||
/* Call once, from the same checkpoint, immediately after a switch
|
/* A fresh, unique wait stamp (mainline, single writer). kernel_hermes.c
|
||||||
* sk_vm_switch_signal_take_pending() requested actually executes (not if
|
* stamps each admitted message with one. */
|
||||||
* the target turned out invalid/self) -- feeds the DoE CSV counters
|
uint64_t sk_vm_switch_signal_next_stamp(void);
|
||||||
* below. Also resets `target_id`'s own readiness/has_work directly
|
|
||||||
* (Stage 3 follow-on correction, 2026-09-14) -- see the .c file's own doc
|
/* vm_id just stopped running (sk_vm_context_switch(), for `from`): record
|
||||||
* comment on why this can't be left to tick()'s current-slot polling. */
|
* when, for the "waiting longest" tie-break. */
|
||||||
|
void sk_vm_switch_signal_note_parked(VMUuid vm_id);
|
||||||
|
|
||||||
|
/* Physical events after which the decision may differ: the running VM
|
||||||
|
* produced a beat (vm_tick(), right after its Stage C-a charge), or mail
|
||||||
|
* was admitted (sk_hermes_send_one()). take_decision_due() consumes the
|
||||||
|
* flag at the next owned checkpoint. */
|
||||||
|
void sk_vm_switch_signal_note_beat(void);
|
||||||
|
void sk_vm_switch_signal_note_mail(void);
|
||||||
|
int sk_vm_switch_signal_take_decision_due(void);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* sk_vm_switch_decide - who runs next, by physical comparison (§3.13.3).
|
||||||
|
*
|
||||||
|
* Among registered participants other than `self` that have work -- a
|
||||||
|
* pending kernel-Hermes message, or parked on their own stack mid-task
|
||||||
|
* (top frame not VM_FRAME_IDLE), or (Hera only) pending console input --
|
||||||
|
* returns the one with the lowest VM-physics heat; among equally cool
|
||||||
|
* candidates, the one waiting longest (unique wait stamps). With
|
||||||
|
* self_competes set (the caller still has work), returns a candidate only
|
||||||
|
* if it is strictly cooler than `self`. Returns NULL when nobody should
|
||||||
|
* run instead. A pure function of physical state: it keeps nothing
|
||||||
|
* between calls, and its answer does not depend on table walk order. Not
|
||||||
|
* a scheduler -- no queue, no priority list, nothing on a clock.
|
||||||
|
*/
|
||||||
|
struct VM *sk_vm_switch_decide(struct VM *self, int self_competes);
|
||||||
|
|
||||||
|
/* Reap one participant marked pending_reap (a WIREBIND identity whose
|
||||||
|
* device detached while it was parked), if any. Call only at a safe point
|
||||||
|
* (checkpoint, trampoline, idle point) -- same contract the old Stage 3
|
||||||
|
* checkpoint's inline reap had. */
|
||||||
|
void sk_vm_switch_reap_pending(void);
|
||||||
|
|
||||||
|
/* Call once, immediately before a decided switch executes -- feeds the DoE
|
||||||
|
* CSV counters below. */
|
||||||
void sk_vm_switch_signal_note_switch_performed(VMUuid target_id);
|
void sk_vm_switch_signal_note_switch_performed(VMUuid target_id);
|
||||||
|
|
||||||
/* Message-arrival eligibility hook (FABRIC-3.md §XXVIII Stage 3 follow-on,
|
/* SWITCH-MARK-WORK's body. Stage C-b: no flag to set any more (the queue
|
||||||
* 2026-09-14): mark that VM `vm_id` was just sent a message (the FORTH-side
|
* is the eligibility); only asks for a fresh decision. */
|
||||||
* MSG-SEND hook in capsules/common/messaging.4th calls this via the new
|
|
||||||
* SWITCH-MARK-WORK primitive, mainline, single-writer). Consulted by
|
|
||||||
* sk_vm_switch_signal_tick()'s own readiness->pending decision so a VM
|
|
||||||
* with nothing recently sent to it never becomes a switch target purely by
|
|
||||||
* sitting idle long enough -- closes the wasteful (but, since the Stage 3
|
|
||||||
* stack-ownership fix, no longer corrupting) trampoline-bounce cycle for
|
|
||||||
* an idle participant. No-op for an unregistered vm_id. */
|
|
||||||
void sk_vm_switch_signal_mark_work(VMUuid vm_id);
|
void sk_vm_switch_signal_mark_work(VMUuid vm_id);
|
||||||
|
|
||||||
/* DoE CSV read-only exposure (FABRIC-3.md §XXVIII Stage 3 follow-on,
|
/* DoE CSV read-only exposure (FABRIC-3.md §XXVIII Stage 3 follow-on,
|
||||||
* 2026-09-13) -- all of this state already existed for the switch
|
* 2026-09-13). Stage C-b: ticks_since_switch now counts beats of the
|
||||||
* decision itself; these just make it observable. */
|
* running VM since the last switch (§3.13.4), not ISR ticks; the per-VM
|
||||||
|
* readiness getters are gone with the readiness itself. */
|
||||||
uint64_t sk_vm_switch_signal_switch_count(void); /* cumulative, since boot */
|
uint64_t sk_vm_switch_signal_switch_count(void); /* cumulative, since boot */
|
||||||
uint32_t sk_vm_switch_signal_ticks_since_switch(void);
|
uint32_t sk_vm_switch_signal_ticks_since_switch(void);
|
||||||
int sk_vm_switch_signal_current_slot(void); /* -1 = none/unregistered */
|
int sk_vm_switch_signal_current_slot(void); /* -1 = none/unregistered */
|
||||||
int sk_vm_switch_signal_slot_count(void);
|
int sk_vm_switch_signal_slot_count(void);
|
||||||
uint32_t sk_vm_switch_signal_readiness(int slot); /* 0 if slot out of range */
|
|
||||||
uint32_t sk_vm_switch_signal_readiness_of(VMUuid vm_id); /* 0 if not registered */
|
|
||||||
|
|
||||||
/* FABRIC-3.6.md task 3.1 (2026-09-21): the table's boot-time-computed
|
/* FABRIC-3.6.md task 3.1 (2026-09-21): the table's boot-time-computed
|
||||||
* capacity (0 if sk_vm_switch_signal_boot_init() was never called or
|
* capacity (0 if sk_vm_switch_signal_boot_init() was never called or
|
||||||
|
|||||||
@@ -165,6 +165,10 @@ typedef struct {
|
|||||||
uint32_t orig_type;
|
uint32_t orig_type;
|
||||||
int in_use;
|
int in_use;
|
||||||
VMUuid owner; /* VM whose reservoir funded this message (task 2.7) */
|
VMUuid owner; /* VM whose reservoir funded this message (task 2.7) */
|
||||||
|
uint64_t wait_stamp; /* FABRIC-3.8.md §3.13.3 (Stage C-b): unique
|
||||||
|
* admission stamp (sk_vm_switch_signal_next_stamp())
|
||||||
|
* -- how long the recipient has been waiting,
|
||||||
|
* for the switch decision's tie-break */
|
||||||
} SkHermesMessage;
|
} SkHermesMessage;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -55,6 +55,19 @@ int sk_vm_context_switch(struct VM *from, struct VM *to);
|
|||||||
struct VM *sk_vm_switch_current_vm(void);
|
struct VM *sk_vm_switch_current_vm(void);
|
||||||
void sk_vm_switch_set_current(struct VM *vm);
|
void sk_vm_switch_set_current(struct VM *vm);
|
||||||
|
|
||||||
|
/* FABRIC-3.8.md §3.13.3 (Stage C-b): the word checkpoint's switch point.
|
||||||
|
* Call only where `vm` owns its outermost frame (vm_core.c gates this with
|
||||||
|
* sk_vm_owns_outermost_frame()). Re-decides only if something physical
|
||||||
|
* changed since the last decision (a beat of the running VM, or mail);
|
||||||
|
* switches to a strictly cooler VM with work, if there is one. */
|
||||||
|
void sk_vm_switch_checkpoint(struct VM *vm);
|
||||||
|
|
||||||
|
/* FABRIC-3.8.md §3.13.5 (Stage C-b, ruling C-5): the idle point. Called
|
||||||
|
* from the console's input poll loops (repl.c) each pass with no input.
|
||||||
|
* Acts only when Hera is running with no frame live (between lines); then
|
||||||
|
* switches to the coolest VM with work, if any. */
|
||||||
|
void sk_vm_idle_point(void);
|
||||||
|
|
||||||
#endif /* __STARKERNEL__ */
|
#endif /* __STARKERNEL__ */
|
||||||
|
|
||||||
#endif /* STARKERNEL_VM_SWITCH_H */
|
#endif /* STARKERNEL_VM_SWITCH_H */
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,12 @@
|
|||||||
#include "starkernel/vm/stadium.h" /* stadium_max_vm_count() -- task 3.1's sizing bound */
|
#include "starkernel/vm/stadium.h" /* stadium_max_vm_count() -- task 3.1's sizing bound */
|
||||||
#include "starkernel/kmalloc.h"
|
#include "starkernel/kmalloc.h"
|
||||||
#include "starkernel/console.h"
|
#include "starkernel/console.h"
|
||||||
|
#include "starkernel/capsule_birth.h" /* registry, capsule_vm_force_reap() */
|
||||||
|
#include "starkernel/capsule_vm_physics.h" /* vm_physics_heat_of() -- the decision's heat */
|
||||||
|
#include "starkernel/vm/kernel_hermes.h" /* sk_hermes_pending_peek() -- eligibility is the queue */
|
||||||
|
#include "starkernel/vm/vm_frame.h" /* top frame kind -- in progress vs parked idle */
|
||||||
|
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.h" /* sk_get_mama_vm() */
|
||||||
|
#include "starkernel/repl.h" /* sk_console_key_available() -- Hera's mail (C-5) */
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
|
||||||
/* FABRIC-3.6.md task 3.1 (2026-09-21, ruled B2 / FABRIC-3.5.md §XLV.2):
|
/* FABRIC-3.6.md task 3.1 (2026-09-21, ruled B2 / FABRIC-3.5.md §XLV.2):
|
||||||
@@ -47,36 +53,42 @@
|
|||||||
* feedback_no_hardcoded_small_scale_bounds in project memory -- a fixed
|
* feedback_no_hardcoded_small_scale_bounds in project memory -- a fixed
|
||||||
* cap here was rejected before; this closes the same class of bound. */
|
* cap here was rejected before; this closes the same class of bound. */
|
||||||
|
|
||||||
/* Ticks a non-running participant must accumulate readiness before a
|
/* FABRIC-3.8.md §3.13 (Stage C-b, 2026-09-23): the timer-driven readiness
|
||||||
* switch to it is requested. Simple linear accumulate-then-threshold,
|
* accrual (SK_SWITCH_READINESS_THRESHOLD = 50 ISR ticks, per-slot
|
||||||
* not the fancier relaxation law a later pass might want -- correctness
|
* readiness/has_work, a pending flag armed from heartbeat_tick()) is gone.
|
||||||
* and a clean single-writer/single-reader story mattered more than the
|
* Captain Bob: "the timer is NOT the source of preemption, compudynamics
|
||||||
* exact law for this first cut (this stage's own design-review latitude,
|
* is" and "no scheduler allowed." Whether and to whom a VM switches is now
|
||||||
* per the plan). Reset to 0 the instant a slot becomes current, so a
|
* sk_vm_switch_decide() below: a physical comparison (VM-physics heat,
|
||||||
* VM that just ran needs a full threshold's worth of ticks idle before
|
* charged per beat of work -- Stage C-a) over the VMs that have work,
|
||||||
* it can be selected again -- a crude but real fairness property. */
|
* made only at the moments the physics exposes. Nothing here runs from
|
||||||
#define SK_SWITCH_READINESS_THRESHOLD 50u
|
* interrupt context any more. */
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
VMUuid vm_id;
|
VMUuid vm_id;
|
||||||
uint32_t readiness;
|
uint64_t parked_stamp; /* wait stamp taken when this VM last stopped
|
||||||
int has_work; /* Stage 3 follow-on (2026-09-14): set by
|
* running (sk_vm_switch_signal_note_parked());
|
||||||
* sk_vm_switch_signal_mark_work() (mainline,
|
* 0 = never parked. See sk_vm_switch_decide(). */
|
||||||
* 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;
|
} sk_switch_slot_entry_t;
|
||||||
|
|
||||||
static sk_switch_slot_entry_t *g_slots = (sk_switch_slot_entry_t *)0;
|
static sk_switch_slot_entry_t *g_slots = (sk_switch_slot_entry_t *)0;
|
||||||
static int g_slot_capacity; /* 0 until sk_vm_switch_signal_boot_init() succeeds */
|
static int g_slot_capacity; /* 0 until sk_vm_switch_signal_boot_init() succeeds */
|
||||||
static int g_slot_count;
|
static int g_slot_count;
|
||||||
static int g_pending;
|
|
||||||
static VMUuid g_pending_target;
|
/* Single-writer, mainline-only event count behind every wait stamp (mail
|
||||||
|
* admission and parking). Unique per event, so the "waiting longest"
|
||||||
|
* tie-break (§3.13.3, ruling C-3) always has exactly one answer. Starts
|
||||||
|
* at 1 so 0 can mean "no stamp". */
|
||||||
|
static uint64_t g_stamp_next = 1;
|
||||||
|
|
||||||
|
/* Set when something physical changed that could change the decision --
|
||||||
|
* the running VM produced a beat (and was just charged for it), or mail
|
||||||
|
* was admitted -- and consumed at the next owned checkpoint. Between such
|
||||||
|
* events every decision would repeat the last one (§3.13.3). */
|
||||||
|
static int g_decision_due;
|
||||||
|
|
||||||
/* DoE CSV exposure (FABRIC-3.md §XXVIII Stage 3 follow-on, 2026-09-13). */
|
/* 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 uint64_t g_switch_count;
|
||||||
static uint32_t g_ticks_since_switch;
|
static uint32_t g_beats_since_switch; /* §3.13.4: was ISR ticks, now beats */
|
||||||
|
|
||||||
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading
|
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading
|
||||||
* zeros -- same small helper stadium.c/stadium_blocks.c each carry their
|
* zeros -- same small helper stadium.c/stadium_blocks.c each carry their
|
||||||
@@ -115,9 +127,8 @@ int sk_vm_switch_signal_boot_init(void) {
|
|||||||
if (!slots) return -1;
|
if (!slots) return -1;
|
||||||
|
|
||||||
for (i = 0; i < max_vm_count; i++) {
|
for (i = 0; i < max_vm_count; i++) {
|
||||||
slots[i].vm_id = vm_uuid_none();
|
slots[i].vm_id = vm_uuid_none();
|
||||||
slots[i].readiness = 0;
|
slots[i].parked_stamp = 0;
|
||||||
slots[i].has_work = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
g_slots = slots;
|
g_slots = slots;
|
||||||
@@ -133,9 +144,8 @@ int sk_vm_switch_signal_boot_init(void) {
|
|||||||
|
|
||||||
int sk_vm_switch_signal_register(VMUuid vm_id) {
|
int sk_vm_switch_signal_register(VMUuid vm_id) {
|
||||||
if (g_slot_count >= g_slot_capacity) return -1;
|
if (g_slot_count >= g_slot_capacity) return -1;
|
||||||
g_slots[g_slot_count].vm_id = vm_id;
|
g_slots[g_slot_count].vm_id = vm_id;
|
||||||
g_slots[g_slot_count].readiness = 0;
|
g_slots[g_slot_count].parked_stamp = 0;
|
||||||
g_slots[g_slot_count].has_work = 0;
|
|
||||||
return g_slot_count++;
|
return g_slot_count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,104 +158,140 @@ int sk_vm_switch_signal_unregister(VMUuid vm_id) {
|
|||||||
}
|
}
|
||||||
g_slot_count--;
|
g_slot_count--;
|
||||||
|
|
||||||
/* A pending switch targeting this VM must not survive its removal --
|
/* No pending target to clear any more (Stage C-b): the decision keeps
|
||||||
* the checkpoint in execute_colon_word() would otherwise try to
|
* no state between calls, so a removed VM simply stops being walked. */
|
||||||
* switch into a VM no longer tracked here. */
|
|
||||||
if (g_pending && vm_uuid_equal(g_pending_target, vm_id)) {
|
|
||||||
g_pending = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* g_current_slot_cached is a DoE-observable value only (see its own
|
|
||||||
* declaration comment); if compaction just invalidated it, the very
|
|
||||||
* next tick() recomputes it from scratch via slot_for_vm_id(), so a
|
|
||||||
* momentarily-stale cache here is harmless. */
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void sk_vm_switch_signal_tick(void) {
|
int sk_vm_switch_signal_is_participant(VMUuid vm_id) {
|
||||||
VM *current;
|
return slot_for_vm_id(vm_id) >= 0;
|
||||||
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) {
|
uint64_t sk_vm_switch_signal_next_stamp(void) {
|
||||||
if (!g_pending) return vm_uuid_none();
|
return g_stamp_next++;
|
||||||
g_pending = 0;
|
}
|
||||||
return g_pending_target;
|
|
||||||
|
void sk_vm_switch_signal_note_parked(VMUuid vm_id) {
|
||||||
|
int slot = slot_for_vm_id(vm_id);
|
||||||
|
if (slot >= 0) g_slots[slot].parked_stamp = g_stamp_next++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sk_vm_switch_signal_note_beat(void) {
|
||||||
|
g_beats_since_switch++;
|
||||||
|
g_decision_due = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sk_vm_switch_signal_note_mail(void) {
|
||||||
|
g_decision_due = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sk_vm_switch_signal_take_decision_due(void) {
|
||||||
|
int due = g_decision_due;
|
||||||
|
g_decision_due = 0;
|
||||||
|
return due;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* One candidate's physical standing: its heat, and how long it has been
|
||||||
|
* waiting (the oldest of its first pending message's admission stamp and
|
||||||
|
* the stamp from when it last stopped running). Returns 0 if `v` has no
|
||||||
|
* work at all -- not eligible. */
|
||||||
|
static int candidate_standing(VM *v, VM *mama, uint64_t parked_stamp,
|
||||||
|
uint64_t *heat_out, uint64_t *stamp_out) {
|
||||||
|
SkHermesMessage *m = sk_hermes_pending_peek(v->stadium_vm_id);
|
||||||
|
int eligible = 0;
|
||||||
|
uint64_t stamp = UINT64_MAX;
|
||||||
|
|
||||||
|
if (m) { /* has mail */
|
||||||
|
eligible = 1;
|
||||||
|
stamp = m->wait_stamp;
|
||||||
|
}
|
||||||
|
if (v->native_stack_saved_sp != 0) { /* parked on its own stack */
|
||||||
|
const VMFrameStack *fs = &v->frames;
|
||||||
|
int in_progress = fs->count > 0 &&
|
||||||
|
fs->entries[fs->count - 1].kind != VM_FRAME_IDLE;
|
||||||
|
/* §3.13.5 / ruling C-5: pending console input is Hera's mail. */
|
||||||
|
int console_input = (v == mama) && sk_console_key_available();
|
||||||
|
if (in_progress || console_input) {
|
||||||
|
eligible = 1;
|
||||||
|
if (parked_stamp != 0 && parked_stamp < stamp) stamp = parked_stamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!eligible) return 0;
|
||||||
|
*heat_out = vm_physics_heat_of(v->stadium_vm_id);
|
||||||
|
*stamp_out = stamp;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
VM *sk_vm_switch_decide(VM *self, int self_competes) {
|
||||||
|
VM *mama = (VM *)sk_get_mama_vm();
|
||||||
|
VM *best = (VM *)0;
|
||||||
|
uint64_t best_heat = 0, best_stamp = 0;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
for (i = 0; i < g_slot_count; i++) {
|
||||||
|
VMRegistryEntry e;
|
||||||
|
VM *v;
|
||||||
|
uint64_t heat, stamp;
|
||||||
|
|
||||||
|
if (self && vm_uuid_equal(g_slots[i].vm_id, self->stadium_vm_id)) continue;
|
||||||
|
if (capsule_vm_registry_get(g_slots[i].vm_id, &e) != 0) continue;
|
||||||
|
v = (VM *)e.vm_ptr;
|
||||||
|
if (!v || v == self || e.pending_reap) continue;
|
||||||
|
if (e.state == VM_STATE_DEAD || e.state == VM_STATE_STILLBORN) continue;
|
||||||
|
if (v->native_stack_top == 0) continue;
|
||||||
|
if (!candidate_standing(v, mama, g_slots[i].parked_stamp, &heat, &stamp)) continue;
|
||||||
|
|
||||||
|
/* Coolest wins; among equally cool, the one waiting longest.
|
||||||
|
* Stamps are unique, so the winner doesn't depend on the order
|
||||||
|
* this loop walks the table in -- the "not a scheduler" property
|
||||||
|
* (§3.13.3), checked directly by C-b's walk-order probe. */
|
||||||
|
if (!best || heat < best_heat || (heat == best_heat && stamp < best_stamp)) {
|
||||||
|
best = v;
|
||||||
|
best_heat = heat;
|
||||||
|
best_stamp = stamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A running VM that still has work keeps the CPU unless the best
|
||||||
|
* candidate is strictly cooler: a tie with the incumbent is "no reason
|
||||||
|
* to move," not a coin toss (ruling C-3). */
|
||||||
|
if (best && self_competes && !(best_heat < vm_physics_heat_of(self->stadium_vm_id))) {
|
||||||
|
return (VM *)0;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sk_vm_switch_reap_pending(void) {
|
||||||
|
VM *current = sk_vm_switch_current_vm();
|
||||||
|
int i;
|
||||||
|
|
||||||
|
/* One per call: capsule_vm_force_reap() unregisters the VM, which
|
||||||
|
* compacts this table under the walk. Every safe point calls this, so
|
||||||
|
* any further pending reaps are picked up at the next one. */
|
||||||
|
for (i = 0; i < g_slot_count; i++) {
|
||||||
|
VMRegistryEntry e;
|
||||||
|
if (capsule_vm_registry_get(g_slots[i].vm_id, &e) == 0 &&
|
||||||
|
e.pending_reap && e.vm_ptr != (void *)current) {
|
||||||
|
capsule_vm_force_reap(e.vm_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void sk_vm_switch_signal_note_switch_performed(VMUuid target_id) {
|
void sk_vm_switch_signal_note_switch_performed(VMUuid target_id) {
|
||||||
/* FABRIC-3.md §XXVIII Stage 3 follow-on correction (2026-09-14):
|
/* Stage C-b: counters only -- there is no readiness left to reset. */
|
||||||
* reset the target's own readiness/has_work HERE, deterministically,
|
(void)target_id;
|
||||||
* 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_switch_count++;
|
||||||
g_ticks_since_switch = 0;
|
g_beats_since_switch = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void sk_vm_switch_signal_mark_work(VMUuid vm_id) {
|
void sk_vm_switch_signal_mark_work(VMUuid vm_id) {
|
||||||
int slot = slot_for_vm_id(vm_id);
|
/* Stage C-b: eligibility is read from the kernel-Hermes queue itself,
|
||||||
if (slot < 0) return;
|
* so there is no has_work flag to set. Kept (SWITCH-MARK-WORK still
|
||||||
g_slots[slot].has_work = 1;
|
* calls it) as a "something changed, decide again" hint; Stage I
|
||||||
|
* strips it if nothing calls it. */
|
||||||
|
(void)vm_id;
|
||||||
|
g_decision_due = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t sk_vm_switch_signal_switch_count(void) {
|
uint64_t sk_vm_switch_signal_switch_count(void) {
|
||||||
@@ -253,11 +299,12 @@ uint64_t sk_vm_switch_signal_switch_count(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint32_t sk_vm_switch_signal_ticks_since_switch(void) {
|
uint32_t sk_vm_switch_signal_ticks_since_switch(void) {
|
||||||
return g_ticks_since_switch;
|
return g_beats_since_switch;
|
||||||
}
|
}
|
||||||
|
|
||||||
int sk_vm_switch_signal_current_slot(void) {
|
int sk_vm_switch_signal_current_slot(void) {
|
||||||
return g_current_slot_cached;
|
VM *current = sk_vm_switch_current_vm();
|
||||||
|
return current ? slot_for_vm_id(current->stadium_vm_id) : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int sk_vm_switch_signal_slot_count(void) {
|
int sk_vm_switch_signal_slot_count(void) {
|
||||||
@@ -268,15 +315,4 @@ int sk_vm_switch_signal_slot_capacity(void) {
|
|||||||
return g_slot_capacity;
|
return g_slot_capacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
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__ */
|
#endif /* __STARKERNEL__ */
|
||||||
|
|||||||
@@ -216,16 +216,12 @@ void doe_log_tick_row(VM *vm, const HeartbeatTickSnapshot *snap)
|
|||||||
int switch_slot = sk_vm_switch_signal_current_slot();
|
int switch_slot = sk_vm_switch_signal_current_slot();
|
||||||
unsigned switch_current_slot = (switch_slot >= 0) ? (unsigned)switch_slot : 0xFFFFFFFFu;
|
unsigned switch_current_slot = (switch_slot >= 0) ? (unsigned)switch_slot : 0xFFFFFFFFu;
|
||||||
uint32_t switch_ticks_since = sk_vm_switch_signal_ticks_since_switch();
|
uint32_t switch_ticks_since = sk_vm_switch_signal_ticks_since_switch();
|
||||||
|
/* FABRIC-3.8.md §3.13.4 (Stage C-b): readiness no longer exists (the
|
||||||
|
* timer-driven accrual it measured is removed). The three columns are
|
||||||
|
* kept, always 0, so the CSV's shape and every existing reader stay
|
||||||
|
* valid; Stage I decides whether to drop them. switch_ticks_since above
|
||||||
|
* now counts beats of the running VM since the last switch. */
|
||||||
uint32_t hera_readiness = 0, hermes_readiness = 0, artemis_readiness = 0;
|
uint32_t hera_readiness = 0, hermes_readiness = 0, artemis_readiness = 0;
|
||||||
{
|
|
||||||
VMRegistryEntry e;
|
|
||||||
if (capsule_vm_find_by_name_nocase("Hera", &e) == 0)
|
|
||||||
hera_readiness = sk_vm_switch_signal_readiness_of(e.vm_id);
|
|
||||||
if (capsule_vm_find_by_name_nocase("Hermes", &e) == 0)
|
|
||||||
hermes_readiness = sk_vm_switch_signal_readiness_of(e.vm_id);
|
|
||||||
if (capsule_vm_find_by_name_nocase("Artemis", &e) == 0)
|
|
||||||
artemis_readiness = sk_vm_switch_signal_readiness_of(e.vm_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
char buf[DOE_BUF_SIZE];
|
char buf[DOE_BUF_SIZE];
|
||||||
snprintf(buf, sizeof(buf),
|
snprintf(buf, sizeof(buf),
|
||||||
|
|||||||
@@ -36,7 +36,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "starkernel/timer.h"
|
#include "starkernel/timer.h"
|
||||||
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.md §XXVIII Stage 3 */
|
|
||||||
|
|
||||||
#if defined(ARCH_AMD64)
|
#if defined(ARCH_AMD64)
|
||||||
#define HEARTBEAT_HAS_VARIANCE 1
|
#define HEARTBEAT_HAS_VARIANCE 1
|
||||||
@@ -164,19 +163,11 @@ void heartbeat_tick(void)
|
|||||||
g_heartbeat.ticks++;
|
g_heartbeat.ticks++;
|
||||||
g_pending_valid = 1;
|
g_pending_valid = 1;
|
||||||
|
|
||||||
/* FABRIC-3.md §XXVIII, Stage 3 (2026-09-13): the one deliberate,
|
/* FABRIC-3.8.md §3.13.4 (Stage C-b, 2026-09-23): the FABRIC-3.md
|
||||||
* documented amendment to this file's own top-half/bottom-half
|
* §XXVIII Stage 3 amendment that called sk_vm_switch_signal_tick()
|
||||||
* discipline above. sk_vm_switch_signal_tick() reaches into VM
|
* from here is removed. Captain Bob: "the timer is NOT the source of
|
||||||
* scheduling state from real ISR context -- something this file's
|
* preemption, compudynamics is." This ISR is back to its original
|
||||||
* header comment previously said nothing here did. It stays cheap
|
* minimal top half; no switch decision reads anything it produces. */
|
||||||
* (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)
|
void heartbeat_service(void)
|
||||||
|
|||||||
+15
-1
@@ -46,6 +46,8 @@
|
|||||||
#include "starkernel/capsule_run.h"
|
#include "starkernel/capsule_run.h"
|
||||||
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.h"
|
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.h"
|
||||||
#include "starkernel/vm/kernel_hermes.h" /* FABRIC-3.6.md task 3.8 -- sk_hermes_send_one() */
|
#include "starkernel/vm/kernel_hermes.h" /* FABRIC-3.6.md task 3.8 -- sk_hermes_send_one() */
|
||||||
|
#include "starkernel/vm/switch.h" /* FABRIC-3.8.md §3.13.5 -- sk_vm_idle_point() */
|
||||||
|
#include "starkernel/capsule_vm_switch_signal.h" /* §3.13.5 -- pump skips participants */
|
||||||
#include "starkernel/vm/stadium.h" /* task 3.8 -- stadium_conserved() evidence print */
|
#include "starkernel/vm/stadium.h" /* task 3.8 -- stadium_conserved() evidence print */
|
||||||
#include "block_subsystem.h"
|
#include "block_subsystem.h"
|
||||||
#include "word_source/include/keyboard_words.h"
|
#include "word_source/include/keyboard_words.h"
|
||||||
@@ -912,9 +914,15 @@ static void sk_repl_idle(VM *active_vm)
|
|||||||
|
|
||||||
for (i = 0; i < count; i++) {
|
for (i = 0; i < count; i++) {
|
||||||
VMRegistryEntry ent;
|
VMRegistryEntry ent;
|
||||||
|
/* FABRIC-3.8.md §3.4 / §3.13.5 (Stage C-b, G3): switch
|
||||||
|
* participants are skipped -- their mail is delivered on their
|
||||||
|
* own stacks, by a switch (sk_vm_idle_point() and the trampoline),
|
||||||
|
* never drained here on Hera's. Only VMs not yet participants
|
||||||
|
* (Stage E makes every born VM one) are still pumped here. */
|
||||||
if (capsule_vm_registry_get_by_index(i, &ent) == 0 &&
|
if (capsule_vm_registry_get_by_index(i, &ent) == 0 &&
|
||||||
ent.state == VM_STATE_LIVE &&
|
ent.state == VM_STATE_LIVE &&
|
||||||
ent.vm_ptr != (void *)mama) {
|
ent.vm_ptr != (void *)mama &&
|
||||||
|
!sk_vm_switch_signal_is_participant(ent.vm_id)) {
|
||||||
(void)sk_hermes_drain_checkpoint((VM *)ent.vm_ptr);
|
(void)sk_hermes_drain_checkpoint((VM *)ent.vm_ptr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1081,6 +1089,7 @@ int sk_console_getkey(VM *active_vm)
|
|||||||
VM *live_active = g_repl_active_vm ? g_repl_active_vm : (VM *)sk_get_mama_vm();
|
VM *live_active = g_repl_active_vm ? g_repl_active_vm : (VM *)sk_get_mama_vm();
|
||||||
sk_repl_idle(live_active);
|
sk_repl_idle(live_active);
|
||||||
}
|
}
|
||||||
|
sk_vm_idle_point(); /* FABRIC-3.8.md §3.13.5 -- acts only between lines */
|
||||||
arch_relax();
|
arch_relax();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1104,6 +1113,7 @@ void sk_repl_headless_wait(VM *mama)
|
|||||||
g_last_beat_tick = now;
|
g_last_beat_tick = now;
|
||||||
sk_repl_idle(mama);
|
sk_repl_idle(mama);
|
||||||
}
|
}
|
||||||
|
sk_vm_idle_point(); /* FABRIC-3.8.md §3.13.5 */
|
||||||
arch_relax();
|
arch_relax();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1303,6 +1313,10 @@ int sk_console_readline(char* buf, int size, VM* active_vm, int reanchor_prompt)
|
|||||||
* On real hardware a wfi/hlt would be appropriate; add it here
|
* On real hardware a wfi/hlt would be appropriate; add it here
|
||||||
* under an #ifdef REAL_HARDWARE guard when that path is needed.
|
* under an #ifdef REAL_HARDWARE guard when that path is needed.
|
||||||
*/
|
*/
|
||||||
|
/* FABRIC-3.8.md §3.13.5 (Stage C-b): Hera waiting for a line is
|
||||||
|
* the idle point -- the coolest VM with work runs now, not at
|
||||||
|
* the next SK_IDLE_BEAT_INTERVAL (that cadence is the timer's). */
|
||||||
|
sk_vm_idle_point();
|
||||||
arch_relax(); /* PAUSE — reduce power, maintain tight poll */
|
arch_relax(); /* PAUSE — reduce power, maintain tight poll */
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
#include "starkernel/kmalloc.h"
|
#include "starkernel/kmalloc.h"
|
||||||
#include "console.h"
|
#include "console.h"
|
||||||
#include "vm.h" /* task 3.4 -- VM struct fields, vm_interpret() */
|
#include "vm.h" /* task 3.4 -- VM struct fields, vm_interpret() */
|
||||||
|
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.8.md §3.13.3 -- wait stamps */
|
||||||
|
|
||||||
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading
|
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading
|
||||||
* zeros -- same small helper stadium.c/capsule_vm_switch_signal.c each
|
* zeros -- same small helper stadium.c/capsule_vm_switch_signal.c each
|
||||||
@@ -531,6 +532,12 @@ int sk_hermes_send_one(VMUuid from, VMUuid to, uint32_t type, uint32_t channel_i
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* FABRIC-3.8.md §3.13.3 (Stage C-b): stamped only once admitted, and
|
||||||
|
* the switch decision told something changed -- the recipient may now
|
||||||
|
* be the coolest VM with work. */
|
||||||
|
msg->wait_stamp = sk_vm_switch_signal_next_stamp();
|
||||||
|
sk_vm_switch_signal_note_mail();
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+86
-17
@@ -32,6 +32,8 @@
|
|||||||
#include "starkernel/capsule_birth.h" /* capsule_vm_set_state */
|
#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/vm/kernel_hermes.h" /* FABRIC-3.8.md §3.2 -- sk_hermes_drain_checkpoint() */
|
||||||
#include "starkernel/hal/hal.h" /* sk_hal_panic() */
|
#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 <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
/* Arch-specific (switch.S in each arch dir). sk_vm_switch_prime() builds
|
/* Arch-specific (switch.S in each arch dir). sk_vm_switch_prime() builds
|
||||||
@@ -121,39 +123,105 @@ void sk_vm_switch_set_current(VM *vm) { g_switch_current_vm = vm; }
|
|||||||
* indistinguishable from a switch storm. A failed yield is a kernel
|
* indistinguishable from a switch storm. A failed yield is a kernel
|
||||||
* invariant violation, not something to retry, so it panics loudly.
|
* invariant violation, not something to retry, so it panics loudly.
|
||||||
*
|
*
|
||||||
* Deliberately NOT here yet (FABRIC-3.8.md §3.11, a Stage C design
|
* Stage C-b (below) adds the compudynamic decision at this point between
|
||||||
* question): this point between messages is also a clean switch point
|
* messages, so a VM with a queue of checkpoint-free payloads cannot hold
|
||||||
* (own stack, depth 0), and the compudynamic switch decision should be
|
* the CPU indefinitely.
|
||||||
* consulted here too, 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) {
|
void sk_vm_switch_entry(void) {
|
||||||
VM *self = g_switch_entry_vm;
|
VM *self = g_switch_entry_vm;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (sk_hermes_drain_checkpoint(self) != 0) {
|
VM *next;
|
||||||
continue; /* consumed one message (1 = ok, -1 = payload erred) */
|
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");
|
||||||
}
|
}
|
||||||
/* Stage FR (§3.12): parked-idle is a VM_FRAME_IDLE frame on this VM's
|
sk_vm_switch_reap_pending();
|
||||||
* own stack, pushed only around the yield and popped (by ID) on
|
next = sk_vm_switch_decide(self, has_mail);
|
||||||
* resume, BEFORE any further drain -- so the drain's own frame is
|
if (!next) {
|
||||||
* still the only frame and the ownership gate's exactly-one-frame
|
if (has_mail) continue; /* still coolest with work: keep going */
|
||||||
* rule holds. It records WHERE the VM is parked: an empty stack alone
|
next = (VM *)sk_get_mama_vm(); /* nobody has work: back to the root */
|
||||||
* would also describe Hera parked at an idle point with her REPL call
|
if (!next || next == self) sk_hal_panic("switch trampoline: no root to idle to");
|
||||||
* chain live, which is not "nothing in progress." */
|
}
|
||||||
|
|
||||||
|
/* 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);
|
VMFrameID idle_id = vm_frame_push(&self->frames, self->stadium_vm_id, VM_FRAME_IDLE);
|
||||||
if (idle_id == 0) {
|
if (idle_id == 0) {
|
||||||
sk_hal_panic("switch trampoline: idle frame push failed");
|
sk_hal_panic("switch trampoline: idle frame push failed");
|
||||||
}
|
}
|
||||||
if (!self->native_switch_back_to ||
|
sk_vm_switch_signal_note_switch_performed(next->stadium_vm_id);
|
||||||
sk_vm_context_switch(self, self->native_switch_back_to) != 0) {
|
if (sk_vm_context_switch(self, next) != 0) {
|
||||||
sk_hal_panic("switch trampoline: idle yield failed");
|
sk_hal_panic("switch trampoline: switch failed");
|
||||||
}
|
}
|
||||||
vm_frame_pop(&self->frames, idle_id); /* resumed: someone switched back in */
|
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(void) {
|
||||||
|
VM *self = g_switch_current_vm;
|
||||||
|
VM *next;
|
||||||
|
|
||||||
|
if (!self || self != (VM *)sk_get_mama_vm()) return;
|
||||||
|
if (self->frames.count != 0) return; /* inside a word (KEY/EXPECT): not idle */
|
||||||
|
|
||||||
|
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_context_switch(VM *from, VM *to) {
|
int sk_vm_context_switch(VM *from, VM *to) {
|
||||||
uint64_t new_sp;
|
uint64_t new_sp;
|
||||||
|
|
||||||
@@ -177,6 +245,7 @@ int sk_vm_context_switch(VM *from, VM *to) {
|
|||||||
|
|
||||||
capsule_vm_set_state(to->stadium_vm_id, VM_STATE_LIVE);
|
capsule_vm_set_state(to->stadium_vm_id, VM_STATE_LIVE);
|
||||||
capsule_vm_set_state(from->stadium_vm_id, VM_STATE_SWITCHED_OUT);
|
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 */
|
||||||
g_switch_current_vm = to;
|
g_switch_current_vm = to;
|
||||||
|
|
||||||
sk_vm_switch_to(&from->native_stack_saved_sp, new_sp);
|
sk_vm_switch_to(&from->native_stack_saved_sp, new_sp);
|
||||||
|
|||||||
+17
-25
@@ -967,32 +967,14 @@ void execute_colon_word(VM* vm)
|
|||||||
* stack AND is at its own outermost frame AND is not compiling --
|
* stack AND is at its own outermost frame AND is not compiling --
|
||||||
* instead of the global depth count, which stops being meaningful
|
* instead of the global depth count, which stops being meaningful
|
||||||
* once VMs genuinely interleave on their own native stacks. */
|
* once VMs genuinely interleave on their own native stacks. */
|
||||||
|
/* FABRIC-3.8.md §3.13.3 (Stage C-b, 2026-09-23): no longer a
|
||||||
|
* timer-armed pending target -- the compudynamic decision itself,
|
||||||
|
* re-made only after a beat of this VM or new mail. The pending_reap
|
||||||
|
* handling that lived inline here (FABRIC-3.md §XXVIII Stage 4) is
|
||||||
|
* now sk_vm_switch_reap_pending(), run at every safe point. */
|
||||||
if (sk_vm_owns_outermost_frame(vm)) {
|
if (sk_vm_owns_outermost_frame(vm)) {
|
||||||
VMUuid target_id = sk_vm_switch_signal_take_pending();
|
sk_vm_switch_checkpoint(vm);
|
||||||
if (!vm_uuid_equal(target_id, vm_uuid_none())) {
|
/* Resumes here once something later switches back. */
|
||||||
VMRegistryEntry target_entry;
|
|
||||||
if (capsule_vm_registry_get(target_id, &target_entry) == 0 &&
|
|
||||||
target_entry.vm_ptr && target_entry.vm_ptr != (void *)vm) {
|
|
||||||
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): a target
|
|
||||||
* marked pending_reap is a WIREBIND identity whose
|
|
||||||
* owning device has physically detached while it sat
|
|
||||||
* parked SWITCHED_OUT -- capsule_vm_kill() already
|
|
||||||
* refused to free it when that happened (see
|
|
||||||
* VMRegistryEntry.pending_reap's own doc comment).
|
|
||||||
* This checkpoint is the safe point the mark-and-
|
|
||||||
* defer design calls for: reap instead of resuming,
|
|
||||||
* and skip the switch this tick -- take_pending()
|
|
||||||
* already consumed the pending flag, so no other
|
|
||||||
* checkpoint will try this same dead end. */
|
|
||||||
if (target_entry.pending_reap) {
|
|
||||||
capsule_vm_force_reap(target_id);
|
|
||||||
} else {
|
|
||||||
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. */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -1288,6 +1270,16 @@ void vm_interpret_kind(VM* vm, const char* input, uint32_t kind)
|
|||||||
vm_heartbeat_run_cycle(vm);
|
vm_heartbeat_run_cycle(vm);
|
||||||
vm->heartbeat.check_counter = 0;
|
vm->heartbeat.check_counter = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef __STARKERNEL__
|
||||||
|
/* FABRIC-3.8.md §3.13.3 (Stage C-b): between two top-level words
|
||||||
|
* is a clean switch point too -- without it, a payload made only
|
||||||
|
* of primitives (no colon word, so no execute_colon_word()
|
||||||
|
* checkpoint) could never be displaced. Same ownership gate. */
|
||||||
|
if (!vm->error && !vm->abort_requested && sk_vm_owns_outermost_frame(vm)) {
|
||||||
|
sk_vm_switch_checkpoint(vm);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
g_vm_interpret_depth--;
|
g_vm_interpret_depth--;
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
#include "vm_internal.h"
|
#include "vm_internal.h"
|
||||||
#include "starkernel/capsule_vm_physics.h"
|
#include "starkernel/capsule_vm_physics.h"
|
||||||
#include "starkernel/vm/switch.h" /* sk_vm_switch_current_vm() -- FABRIC-3.8.md §3.13.2 */
|
#include "starkernel/vm/switch.h" /* sk_vm_switch_current_vm() -- FABRIC-3.8.md §3.13.2 */
|
||||||
|
#include "starkernel/capsule_vm_switch_signal.h" /* note_beat() -- §3.13.3 */
|
||||||
#include "starkernel/timer.h"
|
#include "starkernel/timer.h"
|
||||||
|
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
@@ -150,6 +151,9 @@ void vm_tick(VM* vm)
|
|||||||
* executes can ask for, skip, or give back a charge. */
|
* executes can ask for, skip, or give back a charge. */
|
||||||
if (vm == sk_vm_switch_current_vm()) {
|
if (vm == sk_vm_switch_current_vm()) {
|
||||||
vm_physics_charge(vm->stadium_vm_id);
|
vm_physics_charge(vm->stadium_vm_id);
|
||||||
|
/* Stage C-b (§3.13.3): the running VM's heat just changed, so the
|
||||||
|
* next owned checkpoint re-decides who runs. */
|
||||||
|
sk_vm_switch_signal_note_beat();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Plugin 2: System State Monitoring (Future) */
|
/* Plugin 2: System State Monitoring (Future) */
|
||||||
|
|||||||
Reference in New Issue
Block a user