Stage MC-0a: per-core block on one core -- current VM moves behind the per-CPU register (FABRIC-3.10.8 §3.1)

- New sk_percpu_t { self, core_id, current_vm }, reached via GS base
  (amd64, %gs:0), TPIDR_EL1 (aarch64), tp (riscv64). Boot core's block is
  one static object installed right after arch_early_init(); secondary
  cores' blocks come with MC-1.
- switch.c's global g_switch_current_vm is gone; current VM is
  sk_percpu()->current_vm, re-read after resume in the context switch.
- Probe (removed): normal path and every heartbeat ISR found core 0's
  block on all three ISAs (ISR ok 889/1877/1625, bad 0).
- Standard sequence on amd64/aarch64/riscv64: hashes unchanged from O-C,
  G A=40 B=40, MINT/WIREBIND/USE ok, conservation true, 0 idle growth.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-25 13:56:48 -04:00
co-authored by Claude Opus 5.5
parent 8d33f7a959
commit 8409e376cf
17 changed files with 55013 additions and 8 deletions
+37 -1
View File
@@ -78,4 +78,40 @@ Everything that would break if two cores ran kernel or VM code at the same momen
## 3. Execution log ## 3. Execution log
*(none yet)* ### 3.1 MC-0a — the per-core block, on one core (2026-09-25)
**Change.**
- `include/starkernel/percpu.h` (new): `sk_percpu_t { self, core_id, current_vm }`, with `self` first.
- `sk_percpu()` reads this core's block: `movq %gs:0` on amd64, `mrs tpidr_el1` on aarch64, `mv tp` on riscv64.
- `sk_percpu_install()` writes the register: `IA32_GS_BASE` via `wrmsr`, `TPIDR_EL1`, or `tp`.
- `src/starkernel/hal/percpu.c` (new): the boot core's block is one static object, not a table sized to a core count. Secondary cores get theirs as they come online (MC-1).
- `kernel_main.c`: `sk_percpu_boot_init()` runs straight after `arch_early_init()`, because on amd64 that call reloads GS, which would reset the base.
- `switch.c`: the global `g_switch_current_vm` is gone.
- `sk_vm_switch_current_vm()`, the setter, the idle point and `sk_vm_context_switch()` use `sk_percpu()->current_vm`.
- After resume, the context switch re-reads the block rather than trusting a pointer cached before the switch.
**Probe** (temporary; removed before commit).
- `kernel_main.c` checked, from the normal path after the Tripod came up, that:
- `sk_percpu()->self == sk_percpu()`;
- `core_id == 0`;
- `current_vm` is Hera;
- `sk_vm_switch_current_vm()` agrees.
- `heartbeat_tick()` ran the same self-check from interrupt context on every beat.
| ISA | log | normal path | core_id | current_vm is Hera | switch.c agrees | interrupt path ok / bad |
|---|---|---|---|---|---|---|
| amd64 | `logs/20260925-130335` | ok | 0 | 1 | 1 | 889 / 0 |
| aarch64 | `logs/20260925-130905` | ok | 0 | 1 | 1 | 1877 / 0 |
| riscv64 | `logs/20260925-131337` | ok | 0 | 1 | 1 | 1625 / 0 |
**Standard sequence** (clean build, with the probe reverted; the tree was checked byte-for-byte against the saved stage diff).
| ISA | log | O-A (G, PREC, LOAD) | M7.1a hash | MINT / WIREBIND / USE / `1 2 + .` | idle growth | UNKNOWN / faults |
|---|---|---|---|---|---|---|
| amd64 | `logs/20260925-131805` | A=40 B=40, HHH, ok | `0x99b01134c3d3049c` | all ok, `3` | 0 B / 20 s | 0 / 0 |
| aarch64 | `logs/20260925-133049` | A=40 B=40, HHH, ok | `0x99b01134c3d3049c` | all ok, `3` | 0 B / 20 s | 0 / 0 |
| riscv64 | `logs/20260925-134357` | A=40 B=40, HHH, ok | `0x99b01134c3d3049c` | all ok, `3` | 0 B / 20 s | 0 / 0 |
- The `dict_hash` values (Hera `0x00340a8b2c842eb7`, Hestia `0x78bf28439e4a643f`, Artemis `0x66886d174d5f3b5a`) are identical across ISAs and unchanged from O-C.
- The ledger line matches across ISAs: held=2048, pulled=258048, returned=255263, consumed=737, conserved=true.
- No stop condition was hit. **MC-0a done.**
+89
View File
@@ -0,0 +1,89 @@
/*
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.
*/
/**
* percpu.h - Per-core state (FABRIC-3.10.8 Stage MC-0a).
*
* Each core owns one sk_percpu_t, reached through that ISA's per-CPU
* register, so code running on a core -- including an interrupt handler --
* finds its own block without a lookup:
* - amd64: the GS base (IA32_GS_BASE); the block's first field points
* back at itself, so `mov %gs:0` reads the pointer.
* - aarch64: TPIDR_EL1 (reachable from EL2 as well, so either boot EL).
* - riscv64: the tp register (the kernel owns it: no user mode, no TLS;
* the trap entry never touches it).
*
* MC-0a: only the boot core exists, and its block is a single statically
* allocated object (not a table of cores -- secondary cores' blocks are
* allocated as they come online, MC-1). What moves in first is the switch
* mechanism's "who is running on this core" (was switch.c's global
* g_switch_current_vm).
*/
#ifndef STARKERNEL_PERCPU_H
#define STARKERNEL_PERCPU_H
#include <stdint.h>
struct VM;
typedef struct sk_percpu {
struct sk_percpu *self; /* must stay first: amd64 reads it as %gs:0 */
uint32_t core_id; /* 0 = the boot core (the system core, MC-R1) */
struct VM *current_vm; /* who is running on this core (switch.c) */
} sk_percpu_t;
/* Installs the boot core's block in this core's per-CPU register. Call
* once, as early as possible (kernel_main, straight after
* arch_early_init(): on amd64 that reloads GS, which would reset the base). */
void sk_percpu_boot_init(void);
/* This core's block. Valid from sk_percpu_boot_init() on. */
static inline sk_percpu_t *sk_percpu(void)
{
sk_percpu_t *p;
#if defined(__x86_64__)
__asm__ volatile ("movq %%gs:0, %0" : "=r"(p));
#elif defined(__aarch64__)
__asm__ volatile ("mrs %0, tpidr_el1" : "=r"(p));
#elif defined(__riscv)
__asm__ volatile ("mv %0, tp" : "=r"(p));
#else
#error "percpu.h: unsupported architecture"
#endif
return p;
}
/* Architecture hook: point this core's per-CPU register at `p`. */
static inline void sk_percpu_install(sk_percpu_t *p)
{
#if defined(__x86_64__)
uint64_t v = (uint64_t)(uintptr_t)p;
__asm__ volatile ("wrmsr" : : "c"(0xC0000101u) /* IA32_GS_BASE */,
"a"((uint32_t)v), "d"((uint32_t)(v >> 32)) : "memory");
#elif defined(__aarch64__)
__asm__ volatile ("msr tpidr_el1, %0" : : "r"(p) : "memory");
#elif defined(__riscv)
__asm__ volatile ("mv tp, %0" : : "r"(p) : "memory");
#endif
}
#endif /* STARKERNEL_PERCPU_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
+40
View File
@@ -0,0 +1,40 @@
/*
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.
*/
/**
* percpu.c - The boot core's per-core block (FABRIC-3.10.8 Stage MC-0a).
* See starkernel/percpu.h.
*/
#include "starkernel/percpu.h"
/* The boot core's block: one object, not a table sized to a core count.
* Secondary cores get theirs as they come online (MC-1). */
static sk_percpu_t g_percpu_boot;
void sk_percpu_boot_init(void)
{
g_percpu_boot.self = &g_percpu_boot;
g_percpu_boot.core_id = 0;
g_percpu_boot.current_vm = (struct VM *)0;
sk_percpu_install(&g_percpu_boot);
}
+7
View File
@@ -66,6 +66,7 @@ EFI_RUNTIME_SERVICES *g_sk_runtime_services = NULL;
#include "starkernel/capsule_zuse_boot.h" /* capsule_zuse_boot_load_root_pubkey */ #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/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/vm/switch.h" /* sk_vm_switch_set_current() -- Stage 3 follow-on */
#include "starkernel/percpu.h" /* FABRIC-3.10.8 MC-0a -- sk_percpu_boot_init() */
#include "starkernel/artemis_sig.h" /* artemis_sig_check/genesis_stamp */ #include "starkernel/artemis_sig.h" /* artemis_sig_check/genesis_stamp */
#include "starkernel/kmalloc.h" #include "starkernel/kmalloc.h"
#include "starkernel/repl.h" #include "starkernel/repl.h"
@@ -393,6 +394,12 @@ void kernel_main(BootInfo *boot_info) {
*/ */
arch_early_init(); arch_early_init();
/* FABRIC-3.10.8 Stage MC-0a: this core's per-core block, installed in
* the per-CPU register. Straight after arch_early_init() -- on amd64 its
* GS reload resets the GS base -- and before anything that can switch or
* take an interrupt. */
sk_percpu_boot_init();
/* M1: Console initialization — serial UART first */ /* M1: Console initialization — serial UART first */
console_init(); console_init();
print_banner(); print_banner();
+13 -7
View File
@@ -34,6 +34,7 @@
#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/capsule_vm_switch_signal.h" /* FABRIC-3.8.md §3.13.3 -- the decision */
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.h" /* sk_get_mama_vm() -- the idle root */ #include "starkernel/vm/bootstrap/sk_vm_bootstrap.h" /* sk_get_mama_vm() -- the idle root */
#include "starkernel/percpu.h" /* FABRIC-3.10.8 MC-0a -- this core's current VM */
#include <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
@@ -70,11 +71,14 @@ static VM *g_switch_entry_vm;
* Seeded once at boot via sk_vm_switch_set_current() for whichever VM is * 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 * running before any switch has ever happened (Hera, at Stage 3
* registration time -- kernel_main.c). */ * registration time -- kernel_main.c). */
static VM *g_switch_current_vm; /* FABRIC-3.10.8 Stage MC-0a: "who is running" is per core now, held in this
* core's per-core block (starkernel/percpu.h) instead of the file-scope
* global g_switch_current_vm this comment used to describe. Same semantics
* on one core; with more, each core answers for itself. */
VM *sk_vm_switch_current_vm(void) { return g_switch_current_vm; } VM *sk_vm_switch_current_vm(void) { return sk_percpu()->current_vm; }
void sk_vm_switch_set_current(VM *vm) { g_switch_current_vm = vm; } void sk_vm_switch_set_current(VM *vm) { sk_percpu()->current_vm = vm; }
/** /**
* sk_vm_switch_entry - trampoline for a VM's first-ever entry. * sk_vm_switch_entry - trampoline for a VM's first-ever entry.
@@ -219,7 +223,7 @@ void sk_vm_switch_checkpoint(VM *vm) {
* mail or pending console input, or is returned to by a trampoline with * mail or pending console input, or is returned to by a trampoline with
* nothing left to do. */ * nothing left to do. */
void sk_vm_idle_point(int may_drain_self) { void sk_vm_idle_point(int may_drain_self) {
VM *self = g_switch_current_vm; VM *self = sk_percpu()->current_vm;
VM *next; VM *next;
if (!self || self != (VM *)sk_get_mama_vm()) return; if (!self || self != (VM *)sk_get_mama_vm()) return;
@@ -274,14 +278,16 @@ 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 */ sk_vm_switch_signal_note_parked(from->stadium_vm_id); /* §3.13.3 wait stamp */
g_switch_current_vm = to; sk_percpu()->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);
/* Resumes here only once something later switches back to `from` -- /* Resumes here only once something later switches back to `from` --
* from `from`'s own point of view, this call simply took a while. */ * from `from`'s own point of view, this call simply took a while.
* sk_percpu() is read fresh here, not cached from before the switch:
* once workers migrate (MC-3), `from` may resume on a different core. */
capsule_vm_set_state(from->stadium_vm_id, VM_STATE_LIVE); capsule_vm_set_state(from->stadium_vm_id, VM_STATE_LIVE);
g_switch_current_vm = from; sk_percpu()->current_vm = from;
return 0; return 0;
} }