Stage MC-0b: real kernel spinlocks; sf_mutex no longer a no-op (FABRIC-3.10.8 §3.2)

- New spinlock primitive (spinlock.h/.c): CAS on an owner word (core+1),
  interrupts masked while any lock is held (state kept in the per-core
  block, so release order doesn't matter). Recursion, release-not-held,
  held-at-switch and destroyed-while-held are counted and logged, never
  silent. Locks are no-ops until the boot core's block is installed
  (riscv64's monolithic early boot prints before sk_percpu_boot_init()).
- Kernel locks in a documented order: hermes, switch, registry, doe, log,
  console, heap, vmm, pmm. sf_mutex (dict_lock/tuning_lock) uses the same
  lock word.
- Hermes: public functions over *_locked internals; never held across the
  drain's vm_interpret_kind(); switch notifications after dropping the
  lock; event payload moved off static storage. Switch table and registry
  never call out while held (slot/entry copied under the lock).
- No lock crosses a switch: checked at context switch (leave, resume) and
  the idle point.
- Probe (removed): every lock balances on all three ISAs; 0 held at
  switch over 7.5M-51M checks; 0 recursions, 0 bad releases.
- Standard sequence on amd64/aarch64/riscv64: hashes and G byte-identical
  to MC-0a, MINT/WIREBIND/USE ok, conservation true, 0 idle growth.
- Block subsystem split out as MC-0b2 (Captain Bob's ruling).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-25 16:43:19 -04:00
co-authored by Claude Opus 5.5
parent 8409e376cf
commit 5bc7b4dee7
28 changed files with 56110 additions and 274 deletions
+116 -48
View File
@@ -44,6 +44,7 @@
#include "word_source/include/framebuffer_words.h" /* FABRIC-3.6.md task 1.8 -- register_framebuffer_words() */
#include "vm.h"
#include "platform_alloc.h"
#include "starkernel/spinlock.h" /* FABRIC-3.10.8 MC-0b -- g_lock_registry */
/* No LOG_LINE_MAX include-order constraint anymore: vm.h's own
* LOG_LINE_MAX (persistent block-log, 64) and log.h's in-memory line
* length (renamed LOG_MSG_LINE_MAX, 256) are distinct names, so include
@@ -101,7 +102,18 @@ static int vm_name_eq(const char *a, const char *b) {
return *a == *b;
}
/* Internal: return mutable pointer into registry node for vm_id */
/* FABRIC-3.10.8 MC-0b: g_lock_registry covers the node list (head, links,
* count) and every copy out of or single-field write into an entry. It is
* never held while calling out (Stadium, Hermes, the switch table, the
* console, vm_cleanup()): a long operation on an entry -- birth, KILL,
* reap -- runs unlocked and takes the lock only for its reads and final
* writes. Nodes are never freed after boot, so an entry pointer stays
* valid. A birth fills its embryo's entry unlocked: nothing else touches
* an entry before it is LIVE. capsule_vm_kill_all_nonmama() runs only on
* the way to a cold reset and walks unlocked. */
/* Internal: return mutable pointer into registry node for vm_id.
* g_lock_registry held. */
static VMRegistryEntry *vm_find_entry_ptr(VMUuid vm_id) {
vm_node_t *node = vm_registry_head;
while (node) {
@@ -145,16 +157,17 @@ void capsule_vm_registry_init(void *mama_vm_ptr) {
vm_node_t *mama;
/* Free any nodes from a previous init (defensive) */
sk_spin_lock(&g_lock_registry);
node = vm_registry_head;
vm_registry_head = (void *)0;
vm_registry_count = 0;
sk_spin_unlock(&g_lock_registry);
while (node) {
next = node->next;
kfree(node);
node = next;
}
vm_registry_head = (void *)0;
vm_registry_count = 0;
/* Mama is always the reserved, fixed vm_uuid_hera() id (item 3.8) */
mama = (vm_node_t *)kmalloc(sizeof(vm_node_t));
if (!mama) return;
@@ -175,8 +188,10 @@ void capsule_vm_registry_init(void *mama_vm_ptr) {
vm_name_copy(mama->entry.name, "Hera");
mama->next = (void *)0;
sk_spin_lock(&g_lock_registry);
vm_registry_head = mama;
vm_registry_count = 1;
sk_spin_unlock(&g_lock_registry);
/* From this point on all console output is prefixed [Hera] */
console_set_vm_name("Hera");
@@ -218,6 +233,7 @@ static VMRegistryEntry *vm_registry_alloc(void) {
node->next = (void *)0;
/* Append to tail */
sk_spin_lock(&g_lock_registry);
if (!vm_registry_head) {
vm_registry_head = node;
} else {
@@ -227,16 +243,18 @@ static VMRegistryEntry *vm_registry_alloc(void) {
}
vm_registry_count++;
sk_spin_unlock(&g_lock_registry);
return &node->entry;
}
int capsule_vm_registry_get(VMUuid vm_id, VMRegistryEntry *out) {
VMRegistryEntry *entry;
if (!out) return -1;
sk_spin_lock(&g_lock_registry);
entry = vm_find_entry_ptr(vm_id);
if (!entry) return -1;
*out = *entry;
return 0;
if (entry) *out = *entry;
sk_spin_unlock(&g_lock_registry);
return entry ? 0 : -1;
}
uint32_t capsule_vm_registry_count(void) {
@@ -244,15 +262,19 @@ uint32_t capsule_vm_registry_count(void) {
}
int capsule_vm_registry_get_by_index(uint32_t index, VMRegistryEntry *out) {
vm_node_t *node = vm_registry_head;
vm_node_t *node;
uint32_t i = 0;
int rc = -1;
if (!out) return -1;
sk_spin_lock(&g_lock_registry);
node = vm_registry_head;
while (node) {
if (i == index) { *out = node->entry; return 0; }
if (i == index) { *out = node->entry; rc = 0; break; }
node = node->next;
i++;
}
return -1;
sk_spin_unlock(&g_lock_registry);
return rc;
}
/* Live population, distinct from vm_registry_count above: vm_registry_count
@@ -265,27 +287,34 @@ int capsule_vm_registry_get_by_index(uint32_t index, VMRegistryEntry *out) {
* "K soak" check, capsule_vm_physics.c). Not exposed in the public header:
* only capsule_birth_baby's bound check needs it today. */
static uint32_t vm_registry_live_count(void) {
vm_node_t *node = vm_registry_head;
vm_node_t *node;
uint32_t live = 0;
sk_spin_lock(&g_lock_registry);
node = vm_registry_head;
while (node) {
if (node->entry.state == VM_STATE_LIVE) live++;
node = node->next;
}
sk_spin_unlock(&g_lock_registry);
return live;
}
int capsule_vm_find_by_name(const char *name, VMRegistryEntry *out) {
vm_node_t *node;
int rc = -1;
if (!name || !out) return -1;
sk_spin_lock(&g_lock_registry);
node = vm_registry_head;
while (node) {
if (vm_name_eq(node->entry.name, name)) {
*out = node->entry;
return 0;
rc = 0;
break;
}
node = node->next;
}
return -1;
sk_spin_unlock(&g_lock_registry);
return rc;
}
/* Fold ASCII letter to lowercase (no libc) */
@@ -319,26 +348,36 @@ static int vm_name_prefix_eq_nocase(const char *str, const char *prefix) {
int capsule_vm_find_by_name_nocase(const char *name, VMRegistryEntry *out) {
vm_node_t *node;
int rc = -1;
if (!name || !out) return -1;
sk_spin_lock(&g_lock_registry);
node = vm_registry_head;
while (node) {
if (vm_name_eq_nocase(node->entry.name, name)) {
*out = node->entry;
return 0;
rc = 0;
break;
}
node = node->next;
}
return -1;
sk_spin_unlock(&g_lock_registry);
return rc;
}
void capsule_vm_set_state(VMUuid vm_id, uint32_t state) {
VMRegistryEntry *entry = vm_find_entry_ptr(vm_id);
VMRegistryEntry *entry;
sk_spin_lock(&g_lock_registry);
entry = vm_find_entry_ptr(vm_id);
if (entry) entry->state = state;
sk_spin_unlock(&g_lock_registry);
}
void capsule_vm_set_pending_reap(VMUuid vm_id, int pending) {
VMRegistryEntry *entry = vm_find_entry_ptr(vm_id);
VMRegistryEntry *entry;
sk_spin_lock(&g_lock_registry);
entry = vm_find_entry_ptr(vm_id);
if (entry) entry->pending_reap = pending;
sk_spin_unlock(&g_lock_registry);
}
/* A dead VM leaves kernel-Hermes: every channel, and its undelivered mail
@@ -363,21 +402,28 @@ static void capsule_vm_hermes_depart(VMUuid vm_id, const char *name, int was_obs
}
void capsule_vm_force_reap(VMUuid vm_id) {
VMRegistryEntry *entry = vm_find_entry_ptr(vm_id);
if (!entry) return;
VMRegistryEntry *entry;
VM *vm;
size_t patron_cell;
sk_spin_lock(&g_lock_registry);
entry = vm_find_entry_ptr(vm_id);
/* Idempotent, matching capsule_vm_kill()'s own convention -- the
* Stage 3 checkpoint that calls this cannot itself know whether some
* other path (a later re-attach's own teardown, say) already reaped
* this exact VM before the checkpoint got to it. */
if (entry->state == VM_STATE_DEAD) return;
/* Hera is never a switch-signal participant subject to pending_reap
* this exact VM before the checkpoint got to it.
*
* Hera is never a switch-signal participant subject to pending_reap
* in the first place, but refuse on principle anyway -- matches
* capsule_vm_kill()'s own unconditional rule. */
if (vm_uuid_is_hera(entry->vm_id)) return;
VM *vm = (VM *)entry->vm_ptr;
if (!entry || entry->state == VM_STATE_DEAD || vm_uuid_is_hera(entry->vm_id)) {
sk_spin_unlock(&g_lock_registry);
return;
}
vm = (VM *)entry->vm_ptr;
patron_cell = entry->stadium_patron_cell;
entry->stadium_patron_cell = STADIUM_CELL_NONE;
sk_spin_unlock(&g_lock_registry);
/* Deliberately NOT capsule_vm_kill()'s VM_STATE_SWITCHED_OUT refusal
* -- that guard exists precisely for the case this function handles:
@@ -386,9 +432,8 @@ void capsule_vm_force_reap(VMUuid vm_id) {
* native-stack context will never be resumed. Freeing it here is the
* deferred half of the mark-and-defer design (FABRIC-3.md §XXVIII
* Stage 4) -- see VMRegistryEntry.pending_reap's own doc comment. */
if (entry->stadium_patron_cell != STADIUM_CELL_NONE) {
(void)stadium_evict(entry->stadium_patron_cell);
entry->stadium_patron_cell = STADIUM_CELL_NONE;
if (patron_cell != STADIUM_CELL_NONE) {
(void)stadium_evict(patron_cell);
}
if (vm) {
@@ -411,20 +456,30 @@ void capsule_vm_force_reap(VMUuid vm_id) {
console_puts(entry->name[0] ? entry->name : "(unnamed)");
console_println(" tombstone reclaimed");
sk_spin_lock(&g_lock_registry);
entry->vm_ptr = (void *)0;
entry->state = VM_STATE_DEAD;
entry->pending_reap = 0;
for (uint32_t i = 0; i < VM_NAME_MAX; i++) entry->name[i] = '\0';
sk_spin_unlock(&g_lock_registry);
}
void capsule_vm_registry_set_name(VMUuid vm_id, const char *name) {
VMRegistryEntry *entry;
int first_naming;
int first_naming, live;
char named[VM_NAME_MAX];
if (!name) return;
sk_spin_lock(&g_lock_registry);
entry = vm_find_entry_ptr(vm_id);
if (!entry) return;
if (!entry) {
sk_spin_unlock(&g_lock_registry);
return;
}
first_naming = (entry->name[0] == '\0');
vm_name_copy(entry->name, name);
vm_name_copy(named, entry->name);
live = (entry->state == VM_STATE_LIVE);
sk_spin_unlock(&g_lock_registry);
/* FABRIC-3.9.8 Stage M-B (§7.3): EV-BORN. capsule_birth_baby() leaves
* a VM unnamed; every birth path (BIRTH, WORKER-BIRTH, identity births,
@@ -432,9 +487,8 @@ void capsule_vm_registry_set_name(VMUuid vm_id, const char *name) {
* first naming of a LIVE VM is the moment it's born, under the name
* its subscribers know it by. Funded by the newborn (M-5), whose quota
* is granted during birth; participants only (M-4). */
if (first_naming && entry->state == VM_STATE_LIVE &&
!sk_vm_switch_signal_is_observer(vm_id)) {
(void)sk_hermes_event(SK_HERMES_CHANNEL_EV_BORN, vm_id, vm_id, entry->name);
if (first_naming && live && !sk_vm_switch_signal_is_observer(vm_id)) {
(void)sk_hermes_event(SK_HERMES_CHANNEL_EV_BORN, vm_id, vm_id, named);
}
}
@@ -494,8 +548,10 @@ int capsule_vm_kill(const char *name) {
/* Locate by name (case-insensitive) */
{
vm_node_t *node = vm_registry_head;
vm_node_t *node;
entry = (VMRegistryEntry *)0;
sk_spin_lock(&g_lock_registry);
node = vm_registry_head;
while (node) {
if (vm_name_eq_nocase(node->entry.name, name)) {
entry = &node->entry;
@@ -503,6 +559,7 @@ int capsule_vm_kill(const char *name) {
}
node = node->next;
}
sk_spin_unlock(&g_lock_registry);
}
if (!entry) {
@@ -544,16 +601,22 @@ int capsule_vm_kill(const char *name) {
}
}
vm_id = entry->vm_id;
vm = (VM *)entry->vm_ptr;
/* FABRIC-2.md SS B, VM-COOL: reap this VM's own Stadium patron cell for
* real, dispatching COOL. Refusal (already naturally reclaimed by
* unrelated quota pressure, or never admitted) is silently tolerated --
* KILL tears the VM down unconditionally either way. */
if (entry->stadium_patron_cell != STADIUM_CELL_NONE) {
(void)stadium_evict(entry->stadium_patron_cell);
{
size_t patron_cell;
sk_spin_lock(&g_lock_registry);
vm_id = entry->vm_id;
vm = (VM *)entry->vm_ptr;
patron_cell = entry->stadium_patron_cell;
entry->stadium_patron_cell = STADIUM_CELL_NONE;
sk_spin_unlock(&g_lock_registry);
/* FABRIC-2.md SS B, VM-COOL: reap this VM's own Stadium patron cell for
* real, dispatching COOL. Refusal (already naturally reclaimed by
* unrelated quota pressure, or never admitted) is silently tolerated --
* KILL tears the VM down unconditionally either way. */
if (patron_cell != STADIUM_CELL_NONE) {
(void)stadium_evict(patron_cell);
}
}
/* Tear down and free */
@@ -572,9 +635,11 @@ int capsule_vm_kill(const char *name) {
capsule_vm_hermes_depart(vm_id, entry->name, was_observer);
}
sk_spin_lock(&g_lock_registry);
entry->vm_ptr = (void *)0;
entry->state = VM_STATE_DEAD;
for (i = 0; i < VM_NAME_MAX; i++) entry->name[i] = '\0';
sk_spin_unlock(&g_lock_registry);
capsule_parity_log_kill(vm_id, name);
@@ -700,11 +765,14 @@ CapsuleRunResult capsule_birth_mama(
post_dict_hash);
{
VMRegistryEntry *mama_entry = vm_find_entry_ptr(vm_uuid_hera());
VMRegistryEntry *mama_entry;
sk_spin_lock(&g_lock_registry);
mama_entry = vm_find_entry_ptr(vm_uuid_hera());
if (mama_entry) {
mama_entry->birth_capsule_id = mama_cap->capsule_id;
mama_entry->birth_dict_hash = post_dict_hash;
}
sk_spin_unlock(&g_lock_registry);
}
/* item 3.8: seed the vm_uuid pool now that the Mama capsule's content
@@ -995,9 +1063,9 @@ CapsuleRunResult capsule_birth_baby(
* capsule booted twice produces the same dict hash" determinism
* invariant this codebase relies on elsewhere. */
{
VMRegistryEntry *parent_entry = vm_find_entry_ptr(parent);
if (parent_entry && parent_entry->vm_ptr) {
dictionary_snapshot_acl_from_parent(new_vm, (VM *)parent_entry->vm_ptr);
VMRegistryEntry parent_entry;
if (capsule_vm_registry_get(parent, &parent_entry) == 0 && parent_entry.vm_ptr) {
dictionary_snapshot_acl_from_parent(new_vm, (VM *)parent_entry.vm_ptr);
}
}
+104 -39
View File
@@ -39,6 +39,7 @@
#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 "starkernel/spinlock.h" /* FABRIC-3.10.8 MC-0b -- g_lock_switch */
#include <stddef.h>
/* FABRIC-3.6.md task 3.1 (2026-09-21, ruled B2 / FABRIC-3.5.md §XLV.2):
@@ -113,6 +114,15 @@ static void console_put_u64(uint64_t v) {
console_puts(&buf[i]);
}
/* FABRIC-3.10.8 MC-0b: g_lock_switch covers the slot table, the stamp
* counter, the decision-due flag and the switch counters. Nothing here
* calls out while holding it: the walks (the decision, the Stage H floor,
* the reap sweep) copy one slot under the lock with slot_copy() and judge
* it -- registry, Hermes queues, heat -- outside. A slot removed or shifted
* between two copies is simply judged at its new index or missed until
* the next decision, the same as a VM unregistered just before a walk. */
/* g_lock_switch held. */
static int slot_for_vm_id(VMUuid vm_id) {
int i;
for (i = 0; i < g_slot_count; i++) {
@@ -121,6 +131,16 @@ static int slot_for_vm_id(VMUuid vm_id) {
return -1;
}
/* Copies slot i out; 0 once i is past the end. */
static int slot_copy(int i, sk_switch_slot_entry_t *out) {
int ok;
sk_spin_lock(&g_lock_switch);
ok = i < g_slot_count;
if (ok) *out = g_slots[i];
sk_spin_unlock(&g_lock_switch);
return ok;
}
int sk_vm_switch_signal_boot_init(void) {
size_t max_vm_count = stadium_max_vm_count();
sk_switch_slot_entry_t *slots;
@@ -137,9 +157,11 @@ int sk_vm_switch_signal_boot_init(void) {
slots[i].observer = 0;
}
sk_spin_lock(&g_lock_switch);
g_slots = slots;
g_slot_capacity = (int)max_vm_count;
g_slot_count = 0;
sk_spin_unlock(&g_lock_switch);
console_puts("Switch-signal: ");
console_put_u64((uint64_t)max_vm_count);
@@ -152,64 +174,96 @@ int sk_vm_switch_signal_register(VMUuid vm_id) {
/* FABRIC-3.8.md §3.6 (Stage E): idempotent. Registration now happens
* once, at birth; a second call returns the existing slot rather than
* allocating a duplicate that the decision would walk twice. */
int existing = slot_for_vm_id(vm_id);
if (existing >= 0) return existing;
if (g_slot_count >= g_slot_capacity) return -1;
g_slots[g_slot_count].vm_id = vm_id;
g_slots[g_slot_count].parked_stamp = 0;
g_slots[g_slot_count].observer = 0;
return g_slot_count++;
int slot;
sk_spin_lock(&g_lock_switch);
slot = slot_for_vm_id(vm_id);
if (slot < 0 && g_slot_count < g_slot_capacity) {
g_slots[g_slot_count].vm_id = vm_id;
g_slots[g_slot_count].parked_stamp = 0;
g_slots[g_slot_count].observer = 0;
slot = g_slot_count++;
}
sk_spin_unlock(&g_lock_switch);
return slot;
}
void sk_vm_switch_signal_set_observer(VMUuid vm_id) {
int slot = slot_for_vm_id(vm_id);
int slot;
sk_spin_lock(&g_lock_switch);
slot = slot_for_vm_id(vm_id);
if (slot >= 0) g_slots[slot].observer = 1;
sk_spin_unlock(&g_lock_switch);
}
int sk_vm_switch_signal_is_observer(VMUuid vm_id) {
int slot = slot_for_vm_id(vm_id);
return slot >= 0 && g_slots[slot].observer;
int slot, obs;
sk_spin_lock(&g_lock_switch);
slot = slot_for_vm_id(vm_id);
obs = slot >= 0 && g_slots[slot].observer;
sk_spin_unlock(&g_lock_switch);
return obs;
}
int sk_vm_switch_signal_unregister(VMUuid vm_id) {
int slot = slot_for_vm_id(vm_id);
if (slot < 0) return -1;
for (int i = slot; i < g_slot_count - 1; i++) {
g_slots[i] = g_slots[i + 1];
int slot;
sk_spin_lock(&g_lock_switch);
slot = slot_for_vm_id(vm_id);
if (slot >= 0) {
for (int i = slot; i < g_slot_count - 1; i++) {
g_slots[i] = g_slots[i + 1];
}
g_slot_count--;
}
g_slot_count--;
sk_spin_unlock(&g_lock_switch);
/* No pending target to clear any more (Stage C-b): the decision keeps
* no state between calls, so a removed VM simply stops being walked. */
return 0;
return slot >= 0 ? 0 : -1;
}
int sk_vm_switch_signal_is_participant(VMUuid vm_id) {
return slot_for_vm_id(vm_id) >= 0;
int slot;
sk_spin_lock(&g_lock_switch);
slot = slot_for_vm_id(vm_id);
sk_spin_unlock(&g_lock_switch);
return slot >= 0;
}
uint64_t sk_vm_switch_signal_next_stamp(void) {
return g_stamp_next++;
uint64_t s;
sk_spin_lock(&g_lock_switch);
s = g_stamp_next++;
sk_spin_unlock(&g_lock_switch);
return s;
}
void sk_vm_switch_signal_note_parked(VMUuid vm_id) {
int slot = slot_for_vm_id(vm_id);
int slot;
sk_spin_lock(&g_lock_switch);
slot = slot_for_vm_id(vm_id);
if (slot >= 0) g_slots[slot].parked_stamp = g_stamp_next++;
sk_spin_unlock(&g_lock_switch);
}
void sk_vm_switch_signal_note_beat(void) {
sk_spin_lock(&g_lock_switch);
g_beats_since_switch++;
g_decision_due = 1;
sk_spin_unlock(&g_lock_switch);
}
void sk_vm_switch_signal_note_mail(void) {
sk_spin_lock(&g_lock_switch);
g_decision_due = 1;
sk_spin_unlock(&g_lock_switch);
}
int sk_vm_switch_signal_take_decision_due(void) {
int due = g_decision_due;
int due;
sk_spin_lock(&g_lock_switch);
due = g_decision_due;
g_decision_due = 0;
sk_spin_unlock(&g_lock_switch);
return due;
}
@@ -274,6 +328,7 @@ 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;
sk_switch_slot_entry_t s;
int i, self_obs = self && sk_vm_switch_signal_is_observer(self->stadium_vm_id);
/* FABRIC-3.9.0.md §2.4, ruling O-1 (Stage O-A): an observer acting on
@@ -287,38 +342,38 @@ VM *sk_vm_switch_decide(VM *self, int self_competes) {
/* Observers with events go ahead of every participant, at this clean
* boundary; among themselves, the one waiting longest (unique stamps,
* so still independent of walk order). No heat involved. */
for (i = 0; i < g_slot_count; i++) {
for (i = 0; slot_copy(i, &s); i++) {
VMRegistryEntry e;
VM *v;
uint64_t stamp;
if (!g_slots[i].observer) continue;
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;
if (!s.observer) continue;
if (self && vm_uuid_equal(s.vm_id, self->stadium_vm_id)) continue;
if (capsule_vm_registry_get(s.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 (!observer_event(v, mama, g_slots[i].parked_stamp, &stamp)) continue;
if (!observer_event(v, mama, s.parked_stamp, &stamp)) continue;
if (!best || stamp < best_stamp) { best = v; best_stamp = stamp; }
}
if (best) return best;
/* Participants: the FABRIC-3.8 compudynamics, unchanged -- observers
* are never heat candidates. */
for (i = 0; i < g_slot_count; i++) {
for (i = 0; slot_copy(i, &s); i++) {
VMRegistryEntry e;
VM *v;
uint64_t heat, stamp;
if (g_slots[i].observer) continue;
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;
if (s.observer) continue;
if (self && vm_uuid_equal(s.vm_id, self->stadium_vm_id)) continue;
if (capsule_vm_registry_get(s.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;
if (!candidate_standing(v, mama, s.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
@@ -347,6 +402,7 @@ void sk_vm_switch_signal_note_work_gained(VMUuid vm_id) {
VM *mama = (VM *)sk_get_mama_vm();
VMRegistryEntry e;
VM *v;
sk_switch_slot_entry_t s;
uint64_t floor_q48 = 0;
int have = 0, i;
@@ -371,17 +427,17 @@ void sk_vm_switch_signal_note_work_gained(VMUuid vm_id) {
floor_q48 = vm_physics_work_heat_of(current->stadium_vm_id);
have = 1;
}
for (i = 0; i < g_slot_count; i++) {
for (i = 0; slot_copy(i, &s); i++) {
VMRegistryEntry o;
VM *ov;
uint64_t heat, stamp;
if (g_slots[i].observer) continue;
if (vm_uuid_equal(g_slots[i].vm_id, vm_id)) continue;
if (capsule_vm_registry_get(g_slots[i].vm_id, &o) != 0) continue;
if (s.observer) continue;
if (vm_uuid_equal(s.vm_id, vm_id)) continue;
if (capsule_vm_registry_get(s.vm_id, &o) != 0) continue;
ov = (VM *)o.vm_ptr;
if (!ov || ov == current || o.pending_reap) continue;
if (o.state == VM_STATE_DEAD || o.state == VM_STATE_STILLBORN) continue;
if (!candidate_standing(ov, mama, g_slots[i].parked_stamp, &heat, &stamp)) continue;
if (!candidate_standing(ov, mama, s.parked_stamp, &heat, &stamp)) continue;
if (!have || heat < floor_q48) { floor_q48 = heat; have = 1; }
}
if (have) vm_physics_work_heat_floor(vm_id, floor_q48);
@@ -389,14 +445,15 @@ void sk_vm_switch_signal_note_work_gained(VMUuid vm_id) {
void sk_vm_switch_reap_pending(void) {
VM *current = sk_vm_switch_current_vm();
sk_switch_slot_entry_t s;
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++) {
for (i = 0; slot_copy(i, &s); i++) {
VMRegistryEntry e;
if (capsule_vm_registry_get(g_slots[i].vm_id, &e) == 0 &&
if (capsule_vm_registry_get(s.vm_id, &e) == 0 &&
e.pending_reap && e.vm_ptr != (void *)current) {
capsule_vm_force_reap(e.vm_id);
return;
@@ -407,8 +464,10 @@ void sk_vm_switch_reap_pending(void) {
void sk_vm_switch_signal_note_switch_performed(VMUuid target_id) {
/* Stage C-b: counters only -- there is no readiness left to reset. */
(void)target_id;
sk_spin_lock(&g_lock_switch);
g_switch_count++;
g_beats_since_switch = 0;
sk_spin_unlock(&g_lock_switch);
}
uint64_t sk_vm_switch_signal_switch_count(void) {
@@ -421,7 +480,13 @@ uint32_t sk_vm_switch_signal_ticks_since_switch(void) {
int sk_vm_switch_signal_current_slot(void) {
VM *current = sk_vm_switch_current_vm();
return current ? slot_for_vm_id(current->stadium_vm_id) : -1;
int slot = -1;
if (current) {
sk_spin_lock(&g_lock_switch);
slot = slot_for_vm_id(current->stadium_vm_id);
sk_spin_unlock(&g_lock_switch);
}
return slot;
}
int sk_vm_switch_signal_slot_count(void) {
+30 -6
View File
@@ -36,6 +36,7 @@
#include <string.h>
#include "block_subsystem.h" /* blk_meta_zone_read/write, compute_crc64 */
#include "starkernel/spinlock.h" /* FABRIC-3.10.8 MC-0b -- g_lock_doe */
static uint64_t doe_region_ctrl_compute_crc(const doe_region_ctrl_t *ctrl) {
size_t crc_span = offsetof(doe_region_ctrl_t, hdr_crc);
@@ -119,11 +120,13 @@ static int doe_region_read_slot(uint32_t slot_index, doe_trial_slot_t *out) {
return 0;
}
int doe_region_append(uint32_t run_id, uint32_t cfg, uint32_t rep, uint32_t nw,
uint32_t mode, uint32_t fail_count,
uint64_t fleet_k_q48, uint32_t fleet_conserved,
uint64_t switch_count_cumulative,
const char *isa, uint32_t isa_len) {
/* FABRIC-3.10.8 MC-0b: append and count run under g_lock_doe (the public
* functions below); these are their bodies. */
static int doe_region_append_locked(uint32_t run_id, uint32_t cfg, uint32_t rep, uint32_t nw,
uint32_t mode, uint32_t fail_count,
uint64_t fleet_k_q48, uint32_t fleet_conserved,
uint64_t switch_count_cumulative,
const char *isa, uint32_t isa_len) {
doe_region_ctrl_t ctrl;
if (doe_region_ctrl_read(&ctrl) != 0) {
doe_region_ctrl_init_fresh(&ctrl);
@@ -174,7 +177,20 @@ int doe_region_append(uint32_t run_id, uint32_t cfg, uint32_t rep, uint32_t nw,
return doe_region_ctrl_write(&ctrl) == 0 ? 0 : -1;
}
uint32_t doe_region_count_for_isa(const char *isa, uint32_t isa_len) {
int doe_region_append(uint32_t run_id, uint32_t cfg, uint32_t rep, uint32_t nw,
uint32_t mode, uint32_t fail_count,
uint64_t fleet_k_q48, uint32_t fleet_conserved,
uint64_t switch_count_cumulative,
const char *isa, uint32_t isa_len) {
int rc;
sk_spin_lock(&g_lock_doe);
rc = doe_region_append_locked(run_id, cfg, rep, nw, mode, fail_count, fleet_k_q48,
fleet_conserved, switch_count_cumulative, isa, isa_len);
sk_spin_unlock(&g_lock_doe);
return rc;
}
static uint32_t doe_region_count_for_isa_locked(const char *isa, uint32_t isa_len) {
doe_region_ctrl_t ctrl;
if (doe_region_ctrl_read(&ctrl) != 0) return 0;
@@ -202,3 +218,11 @@ uint32_t doe_region_count_for_isa(const char *isa, uint32_t isa_len) {
}
return count;
}
uint32_t doe_region_count_for_isa(const char *isa, uint32_t isa_len) {
uint32_t n;
sk_spin_lock(&g_lock_doe);
n = doe_region_count_for_isa_locked(isa, isa_len);
sk_spin_unlock(&g_lock_doe);
return n;
}
+54 -10
View File
@@ -49,6 +49,7 @@
#include "framebuffer.h"
#include "vt100.h"
#include "arch.h"
#include "starkernel/spinlock.h" /* FABRIC-3.10.8 MC-0b -- g_lock_console */
#if defined(__x86_64__) || defined(__i386__)
#define SERIAL_SUPPORTED 1
@@ -178,17 +179,31 @@ static const char *g_active_vm_name = (void *)0;
static int g_line_start = 1;
static uint64_t g_console_tx_count; /* monotonic console_putc() counter (BSS) */
/* FABRIC-3.10.8 MC-0b: g_lock_console covers the output path and the line
* state above (name, line start, pending line close, the framebuffer
* terminal behind it). A string goes out under one hold, so lines from two
* cores never interleave mid-call. Unlocked on purpose:
* - console_get_vm_name(): a pointer read, and the prefix provider
* (repl.c) calls it from inside emit_prefix(), i.e. under the lock;
* - console_fb_init()/console_fb_enable_ttf(): boot-time, and they log
* (log_message() prints through this lock);
* - input (console_poll()/console_getc()). */
void console_set_vm_name(const char *name) {
/* Empty string treated the same as NULL: console_save_vm_name()
* writes "" for "there was no active name," so this keeps that
* round-trip correct (save-empty then restore-empty must mean
* "still no prefix," not "prefix is now the empty string"). */
if (!name || !name[0]) { g_active_vm_name = (void *)0; return; }
size_t i;
for (i = 0; i < CONSOLE_VM_NAME_BUF - 1u && name[i]; i++)
g_active_vm_name_buf[i] = name[i];
g_active_vm_name_buf[i] = '\0';
g_active_vm_name = g_active_vm_name_buf;
sk_spin_lock(&g_lock_console);
if (!name || !name[0]) {
g_active_vm_name = (void *)0;
} else {
size_t i;
for (i = 0; i < CONSOLE_VM_NAME_BUF - 1u && name[i]; i++)
g_active_vm_name_buf[i] = name[i];
g_active_vm_name_buf[i] = '\0';
g_active_vm_name = g_active_vm_name_buf;
}
sk_spin_unlock(&g_lock_console);
}
const char *console_get_vm_name(void) {
@@ -210,10 +225,12 @@ void console_set_user_prefix_provider(console_user_prefix_fn fn) {
void console_save_vm_name(char *out, size_t cap) {
if (!out || cap == 0) return;
size_t i = 0;
sk_spin_lock(&g_lock_console);
if (g_active_vm_name) {
for (; i < cap - 1u && g_active_vm_name[i]; i++)
out[i] = g_active_vm_name[i];
}
sk_spin_unlock(&g_lock_console);
out[i] = '\0';
}
@@ -302,7 +319,8 @@ static void console_putc_inner(char c);
* its own. */
static int g_pending_line_close = 0;
void console_putc(char c) {
/* console_putc() with g_lock_console already held. */
static void console_putc_locked(char c) {
if (g_pending_line_close) {
g_pending_line_close = 0;
g_console_tx_count++;
@@ -312,6 +330,12 @@ void console_putc(char c) {
console_putc_inner(c);
}
void console_putc(char c) {
sk_spin_lock(&g_lock_console);
console_putc_locked(c);
sk_spin_unlock(&g_lock_console);
}
static void console_putc_inner(char c)
{
/* --- serial UART path (always on) --- */
@@ -337,9 +361,11 @@ static void console_putc_inner(char c)
void console_puts(const char *s) {
if (!s) return;
sk_spin_lock(&g_lock_console);
while (*s) {
console_putc(*s++);
console_putc_locked(*s++);
}
sk_spin_unlock(&g_lock_console);
}
uint64_t console_tx_count(void)
@@ -349,23 +375,31 @@ uint64_t console_tx_count(void)
void console_ensure_line_start(void)
{
sk_spin_lock(&g_lock_console);
if (!g_line_start)
{
g_pending_line_close = 1;
}
sk_spin_unlock(&g_lock_console);
}
void console_cancel_deferred_line_start(void)
{
sk_spin_lock(&g_lock_console);
g_pending_line_close = 0;
sk_spin_unlock(&g_lock_console);
}
/**
* Write a string with newline to serial console
*/
void console_println(const char *s) {
console_puts(s);
console_putc('\n');
sk_spin_lock(&g_lock_console);
if (s) {
while (*s) console_putc_locked(*s++);
}
console_putc_locked('\n');
sk_spin_unlock(&g_lock_console);
}
/**
@@ -409,37 +443,47 @@ void console_fb_enable_ttf(void)
void console_fb_scroll_back(uint32_t n)
{
sk_spin_lock(&g_lock_console);
if (fb_is_available()) {
vt100_scroll_back(n);
}
sk_spin_unlock(&g_lock_console);
}
void console_fb_scroll_fwd(uint32_t n)
{
sk_spin_lock(&g_lock_console);
if (fb_is_available()) {
vt100_scroll_fwd(n);
}
sk_spin_unlock(&g_lock_console);
}
void console_fb_toggle_graphics(void)
{
sk_spin_lock(&g_lock_console);
if (fb_is_available()) {
vt100_toggle_graphics();
}
sk_spin_unlock(&g_lock_console);
}
void console_fb_draw_cursor(void)
{
sk_spin_lock(&g_lock_console);
if (fb_is_available()) {
vt100_draw_cursor();
}
sk_spin_unlock(&g_lock_console);
}
void console_fb_erase_cursor(void)
{
sk_spin_lock(&g_lock_console);
if (fb_is_available()) {
vt100_erase_cursor();
}
sk_spin_unlock(&g_lock_console);
}
/**
+7
View File
@@ -31,10 +31,17 @@
* Secondary cores get theirs as they come online (MC-1). */
static sk_percpu_t g_percpu_boot;
volatile int sk_percpu_ready;
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;
g_percpu_boot.lock_depth = 0;
g_percpu_boot.saved_irq = 0;
g_percpu_boot.lock_acquired = 0;
g_percpu_boot.lock_released = 0;
sk_percpu_install(&g_percpu_boot);
sk_percpu_ready = 1;
}
+227
View File
@@ -0,0 +1,227 @@
/*
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.
*/
/**
* spinlock.c - Kernel spinlocks (FABRIC-3.10.8 Stage MC-0b). See
* starkernel/spinlock.h for the rules and the lock order.
*/
#include "starkernel/spinlock.h"
#include "starkernel/percpu.h"
#include "starkernel/arch.h"
#include "log.h"
sk_spinlock_t g_lock_registry = SK_SPINLOCK_INIT;
sk_spinlock_t g_lock_switch = SK_SPINLOCK_INIT;
sk_spinlock_t g_lock_hermes = SK_SPINLOCK_INIT;
sk_spinlock_t g_lock_doe = SK_SPINLOCK_INIT;
sk_spinlock_t g_lock_log = SK_SPINLOCK_INIT;
sk_spinlock_t g_lock_console = SK_SPINLOCK_INIT;
sk_spinlock_t g_lock_heap = SK_SPINLOCK_INIT;
sk_spinlock_t g_lock_pmm = SK_SPINLOCK_INIT;
sk_spinlock_t g_lock_vmm = SK_SPINLOCK_INIT;
/* Failure counters (spinlock.h). Written with atomics: a failure can be
* counted on any core. The first site of each kind is kept for the log. */
static uint64_t g_recursions;
static uint64_t g_bad_releases;
static uint64_t g_held_at_switch;
static const volatile uint32_t *g_recursion_word;
static void *g_recursion_site;
static const volatile uint32_t *g_bad_release_word;
static void *g_bad_release_site;
static const char *g_held_at_switch_where;
static uint64_t g_reported_rec, g_reported_bad, g_reported_held;
/* ---- interrupt state: save-and-mask / restore, per ISA ---------------- */
static inline uint64_t irq_save_mask(void)
{
uint64_t f;
#if defined(__x86_64__)
__asm__ volatile ("pushfq; popq %0; cli" : "=r"(f) : : "memory");
#elif defined(__aarch64__)
__asm__ volatile ("mrs %0, daif; msr daifset, #2" : "=r"(f) : : "memory");
#elif defined(__riscv)
__asm__ volatile ("csrrci %0, sstatus, 2" : "=r"(f) : : "memory");
#else
#error "spinlock.c: unsupported architecture"
#endif
return f;
}
static inline void irq_restore(uint64_t f)
{
#if defined(__x86_64__)
if (f & 0x200u) __asm__ volatile ("sti" : : : "memory"); /* IF */
#elif defined(__aarch64__)
__asm__ volatile ("msr daif, %0" : : "r"(f) : "memory");
#elif defined(__riscv)
if (f & 0x2u) __asm__ volatile ("csrsi sstatus, 2" : : : "memory"); /* SIE */
#endif
}
/* ---- the lock word ---------------------------------------------------- */
static void lockword_acquire(volatile uint32_t *w, void *site)
{
sk_percpu_t *pc;
uint32_t me;
uint64_t f;
if (!sk_percpu_ready) return; /* early boot: one core, interrupts off */
pc = sk_percpu();
me = pc->core_id + 1u;
f = irq_save_mask();
if (pc->lock_depth++ == 0) pc->saved_irq = f;
pc->lock_acquired++;
if (*w == me) {
/* Already ours: spinning would hang this core for good. Let it
* through and count it (spinlock.h). */
if (__atomic_fetch_add(&g_recursions, 1, __ATOMIC_RELAXED) == 0) {
g_recursion_word = w;
g_recursion_site = site;
}
return;
}
for (;;) {
uint32_t expected = 0;
if (__atomic_compare_exchange_n(w, &expected, me, 0,
__ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) {
return;
}
while (*w != 0) arch_relax();
}
}
static void lockword_release(volatile uint32_t *w, void *site)
{
sk_percpu_t *pc;
uint32_t me;
if (!sk_percpu_ready) return; /* early boot: see lockword_acquire() */
pc = sk_percpu();
me = pc->core_id + 1u;
if (*w == me) {
__atomic_store_n(w, 0u, __ATOMIC_RELEASE);
} else if (__atomic_fetch_add(&g_bad_releases, 1, __ATOMIC_RELAXED) == 0) {
g_bad_release_word = w;
g_bad_release_site = site;
}
pc->lock_released++;
if (pc->lock_depth == 0) return; /* counted above: nothing was held */
if (--pc->lock_depth == 0) irq_restore(pc->saved_irq);
}
void sk_lockword_acquire(volatile uint32_t *word)
{
lockword_acquire(word, __builtin_return_address(0));
}
void sk_lockword_release(volatile uint32_t *word)
{
lockword_release(word, __builtin_return_address(0));
}
void sk_spin_lock(sk_spinlock_t *l)
{
lockword_acquire(&l->owner, __builtin_return_address(0));
l->acquired++;
}
void sk_spin_unlock(sk_spinlock_t *l)
{
l->released++;
lockword_release(&l->owner, __builtin_return_address(0));
}
int sk_spin_held_here(const sk_spinlock_t *l)
{
return sk_percpu_ready && l->owner == sk_percpu()->core_id + 1u;
}
void sk_lock_check_none_held(const char *where)
{
if (!sk_percpu_ready || sk_percpu()->lock_depth == 0) return;
if (__atomic_fetch_add(&g_held_at_switch, 1, __ATOMIC_RELAXED) == 0) {
g_held_at_switch_where = where;
}
}
uint64_t sk_lock_recursions(void) { return g_recursions; }
uint64_t sk_lock_bad_releases(void) { return g_bad_releases; }
uint64_t sk_lock_held_at_switch(void) { return g_held_at_switch; }
/* ---- reporting -------------------------------------------------------- */
static const char *lock_name(const volatile uint32_t *w)
{
if (w == &g_lock_registry.owner) return "registry";
if (w == &g_lock_switch.owner) return "switch";
if (w == &g_lock_hermes.owner) return "hermes";
if (w == &g_lock_doe.owner) return "doe";
if (w == &g_lock_log.owner) return "log";
if (w == &g_lock_console.owner) return "console";
if (w == &g_lock_heap.owner) return "heap";
if (w == &g_lock_pmm.owner) return "pmm";
if (w == &g_lock_vmm.owner) return "vmm";
return "vm sf_mutex";
}
/* Freestanding kvsnprintf has no %llx: hex by hand. */
static void hex64(char out[19], uint64_t v)
{
int i;
out[0] = '0'; out[1] = 'x';
for (i = 17; i >= 2; i--) {
uint32_t n = (uint32_t)(v & 0xFu);
out[i] = (char)(n < 10u ? '0' + n : 'a' + (n - 10u));
v >>= 4;
}
out[18] = '\0';
}
void sk_lock_report(void)
{
char a[19];
if (g_recursions != g_reported_rec) {
g_reported_rec = g_recursions;
hex64(a, (uint64_t)(uintptr_t)g_recursion_site);
log_message(LOG_ERROR, "LOCK: recursion x%llu (first: %s lock, taken again from %s)",
(unsigned long long)g_recursions, lock_name(g_recursion_word), a);
}
if (g_bad_releases != g_reported_bad) {
g_reported_bad = g_bad_releases;
hex64(a, (uint64_t)(uintptr_t)g_bad_release_site);
log_message(LOG_ERROR, "LOCK: release of a lock not held x%llu (first: %s lock, from %s)",
(unsigned long long)g_bad_releases, lock_name(g_bad_release_word), a);
}
if (g_held_at_switch != g_reported_held) {
g_reported_held = g_held_at_switch;
log_message(LOG_ERROR, "LOCK: lock held at a switch or idle point x%llu (first: %s)",
(unsigned long long)g_held_at_switch,
g_held_at_switch_where ? g_held_at_switch_where : "?");
}
}
+16 -3
View File
@@ -37,6 +37,7 @@
#include <string.h>
#include "block_subsystem.h" /* blk_meta_zone_read/write, compute_crc64 */
#include "starkernel/spinlock.h" /* FABRIC-3.10.8 MC-0b -- g_lock_log */
static uint64_t log_region_ctrl_compute_crc(const log_region_ctrl_t *ctrl) {
size_t crc_span = offsetof(log_region_ctrl_t, hdr_crc);
@@ -106,9 +107,11 @@ static int log_region_write_slot(uint32_t slot_index, const log_slot_t *slot) {
return blk_meta_zone_write(dft, devblock_buf) == 0 ? 0 : -1;
}
int log_region_append(uint8_t level, uint64_t timestamp,
const char *source, uint32_t source_len,
const char *msg, uint32_t msg_len) {
/* FABRIC-3.10.8 MC-0b: the control block's read-modify-write and the slot
* write are one step under g_lock_log (log_region_append() below). */
static int log_region_append_locked(uint8_t level, uint64_t timestamp,
const char *source, uint32_t source_len,
const char *msg, uint32_t msg_len) {
log_region_ctrl_t ctrl;
if (log_region_ctrl_read(&ctrl) != 0) {
log_region_ctrl_init_fresh(&ctrl);
@@ -176,3 +179,13 @@ int log_region_append(uint8_t level, uint64_t timestamp,
return log_region_ctrl_write(&ctrl);
}
int log_region_append(uint8_t level, uint64_t timestamp,
const char *source, uint32_t source_len,
const char *msg, uint32_t msg_len) {
int rc;
sk_spin_lock(&g_lock_log);
rc = log_region_append_locked(level, timestamp, source, source_len, msg, msg_len);
sk_spin_unlock(&g_lock_log);
return rc;
}
+30 -23
View File
@@ -52,6 +52,7 @@
#include <stddef.h>
#include "kmalloc.h"
#include "pmm.h"
#include "starkernel/spinlock.h" /* FABRIC-3.10.8 MC-0b -- g_lock_heap */
#define KMALLOC_DEFAULT_HEAP_SIZE (2ULL * 1024ULL * 1024ULL * 1024ULL) /* 2 GiB — floor for 256+ baby VMs */
#define KMALLOC_MIN_ALIGN 16u
@@ -292,20 +293,19 @@ void *kmalloc_aligned(size_t size, size_t align)
return NULL;
}
heap_block_t *cur = heap_head;
heap_block_t *cur;
void *ptr = NULL;
size_t effective_align = normalize_alignment(align);
while (cur) {
sk_spin_lock(&g_lock_heap);
for (cur = heap_head; cur; cur = cur->next) {
if (cur->free) {
void *ptr = allocate_from_block(cur, size, effective_align);
if (ptr) {
return ptr;
}
ptr = allocate_from_block(cur, size, effective_align);
if (ptr) break;
}
cur = cur->next;
}
return NULL;
sk_spin_unlock(&g_lock_heap);
return ptr;
}
void kfree(void *ptr)
@@ -320,31 +320,34 @@ void kfree(void *ptr)
kmalloc_prefix_t *prefix =
(kmalloc_prefix_t *)((uint8_t *)ptr - sizeof(kmalloc_prefix_t));
if (prefix->magic != KMALLOC_MAGIC || !prefix->block) {
return;
}
heap_block_t *block = prefix->block;
if (block->free) {
return;
sk_spin_lock(&g_lock_heap);
if (prefix->magic == KMALLOC_MAGIC && prefix->block && !prefix->block->free) {
heap_block_t *block = prefix->block;
account_free(block->requested);
block->requested = 0;
block->free = 1;
coalesce_neighbors(block);
}
account_free(block->requested);
block->requested = 0;
block->free = 1;
coalesce_neighbors(block);
sk_spin_unlock(&g_lock_heap);
}
kmalloc_stats_t kmalloc_get_stats(void)
{
return heap_stats;
kmalloc_stats_t s;
sk_spin_lock(&g_lock_heap);
s = heap_stats;
sk_spin_unlock(&g_lock_heap);
return s;
}
void kmalloc_debug_census(size_t *out_largest_free, size_t *out_free_count,
size_t *out_used_count, size_t *out_total_blocks)
{
size_t largest = 0, free_count = 0, used_count = 0, total = 0;
heap_block_t *cur = heap_head;
heap_block_t *cur;
sk_spin_lock(&g_lock_heap);
cur = heap_head;
while (cur) {
total++;
if (cur->free) {
@@ -355,6 +358,7 @@ void kmalloc_debug_census(size_t *out_largest_free, size_t *out_free_count,
}
cur = cur->next;
}
sk_spin_unlock(&g_lock_heap);
if (out_largest_free) *out_largest_free = largest;
if (out_free_count) *out_free_count = free_count;
if (out_used_count) *out_used_count = used_count;
@@ -364,7 +368,9 @@ void kmalloc_debug_census(size_t *out_largest_free, size_t *out_free_count,
void kmalloc_debug_census_bytes(size_t *out_total_free_bytes, size_t *out_total_used_bytes)
{
size_t free_bytes = 0, used_bytes = 0;
heap_block_t *cur = heap_head;
heap_block_t *cur;
sk_spin_lock(&g_lock_heap);
cur = heap_head;
while (cur) {
if (cur->free) {
free_bytes += cur->size;
@@ -373,6 +379,7 @@ void kmalloc_debug_census_bytes(size_t *out_total_free_bytes, size_t *out_total_
}
cur = cur->next;
}
sk_spin_unlock(&g_lock_heap);
if (out_total_free_bytes) *out_total_free_bytes = free_bytes;
if (out_total_used_bytes) *out_total_used_bytes = used_bytes;
}
+14 -20
View File
@@ -51,6 +51,7 @@
#include "pmm.h"
#include "console.h"
#include "starkernel/spinlock.h" /* FABRIC-3.10.8 MC-0b -- g_lock_pmm */
#define PMM_MAX_BITMAP_BYTES (2u * 1024u * 1024u) /* Tracks up to ~64 GiB */
#define PMM_BITMAP_BITS (PMM_MAX_BITMAP_BYTES * 8ull)
@@ -313,38 +314,27 @@ int pmm_is_initialized(void) {
}
uint64_t pmm_alloc_page(void) {
if (!pmm_initialized) {
return 0;
}
uint64_t start = find_contiguous_free(1);
if (start == UINT64_MAX) {
return 0;
}
bitmap_mark_range(start, 1, 1);
return start * (uint64_t)PMM_PAGE_SIZE;
return pmm_alloc_contiguous(1);
}
uint64_t pmm_alloc_contiguous(uint64_t num_pages) {
uint64_t start;
if (!pmm_initialized || num_pages == 0) {
return 0;
}
uint64_t start = find_contiguous_free(num_pages);
if (start == UINT64_MAX) {
return 0;
sk_spin_lock(&g_lock_pmm);
start = find_contiguous_free(num_pages);
if (start != UINT64_MAX) {
bitmap_mark_range(start, num_pages, 1);
}
sk_spin_unlock(&g_lock_pmm);
bitmap_mark_range(start, num_pages, 1);
return start * (uint64_t)PMM_PAGE_SIZE;
return start == UINT64_MAX ? 0 : start * (uint64_t)PMM_PAGE_SIZE;
}
void pmm_free_page(uint64_t paddr) {
if (!pmm_initialized || (paddr % PMM_PAGE_SIZE) != 0) {
return;
}
pmm_free_contiguous(paddr, 1);
}
@@ -362,13 +352,17 @@ void pmm_free_contiguous(uint64_t paddr, uint64_t num_pages) {
num_pages = pmm_tracked_pages - start_page;
}
sk_spin_lock(&g_lock_pmm);
bitmap_mark_range(start_page, num_pages, 0);
sk_spin_unlock(&g_lock_pmm);
}
pmm_stats_t pmm_get_stats(void) {
pmm_stats_t stats = {0};
sk_spin_lock(&g_lock_pmm);
stats.total_pages = pmm_total_pages;
stats.free_pages = pmm_free_pages;
sk_spin_unlock(&g_lock_pmm);
stats.used_pages = (pmm_total_pages > pmm_free_pages) ? (pmm_total_pages - pmm_free_pages) : 0;
stats.total_bytes = stats.total_pages * (uint64_t)PMM_PAGE_SIZE;
stats.used_bytes = stats.used_pages * (uint64_t)PMM_PAGE_SIZE;
+44 -10
View File
@@ -53,6 +53,7 @@
#include "pmm.h"
#include "console.h"
#include "uefi.h"
#include "starkernel/spinlock.h" /* FABRIC-3.10.8 MC-0b -- g_lock_vmm */
#define PTE_PRESENT (1ull << 0)
#define PTE_WRITABLE (1ull << 1)
@@ -176,7 +177,11 @@ static uint64_t *resolve_pt(uint64_t vaddr, uint16_t *idx_out) {
return pt;
}
int vmm_map_page(uint64_t vaddr, uint64_t paddr, uint64_t flags) {
/* FABRIC-3.10.8 MC-0b: every table walk runs under g_lock_vmm. The walk
* bodies are the *_locked functions; the public ones take the lock around
* them. -2 from map_page_locked means a mapping conflict, printed by the
* caller once the lock is dropped (the console lock sits above this one). */
static int map_page_locked(uint64_t vaddr, uint64_t paddr, uint64_t flags) {
if (vmm_root_pml4_phys == 0) {
return -1;
}
@@ -204,8 +209,7 @@ int vmm_map_page(uint64_t vaddr, uint64_t paddr, uint64_t flags) {
return 0;
}
}
console_println("VMM map conflict");
return -1;
return -2;
}
pt[idx] = make_pte(paddr, flags | VMM_FLAG_PRESENT);
@@ -213,7 +217,19 @@ int vmm_map_page(uint64_t vaddr, uint64_t paddr, uint64_t flags) {
return 0;
}
int vmm_unmap_page(uint64_t vaddr) {
int vmm_map_page(uint64_t vaddr, uint64_t paddr, uint64_t flags) {
int rc;
sk_spin_lock(&g_lock_vmm);
rc = map_page_locked(vaddr, paddr, flags);
sk_spin_unlock(&g_lock_vmm);
if (rc == -2) {
console_println("VMM map conflict");
rc = -1;
}
return rc;
}
static int unmap_page_locked(uint64_t vaddr) {
if (vmm_root_pml4_phys == 0) {
return -1;
}
@@ -238,7 +254,15 @@ int vmm_unmap_page(uint64_t vaddr) {
return 0;
}
uint64_t vmm_get_paddr(uint64_t vaddr) {
int vmm_unmap_page(uint64_t vaddr) {
int rc;
sk_spin_lock(&g_lock_vmm);
rc = unmap_page_locked(vaddr);
sk_spin_unlock(&g_lock_vmm);
return rc;
}
static uint64_t get_paddr_locked(uint64_t vaddr) {
if (vmm_root_pml4_phys == 0) {
return 0;
}
@@ -261,6 +285,14 @@ uint64_t vmm_get_paddr(uint64_t vaddr) {
return (pt[idx] & ADDR_MASK) | (vaddr & 0xFFF);
}
uint64_t vmm_get_paddr(uint64_t vaddr) {
uint64_t pa;
sk_spin_lock(&g_lock_vmm);
pa = get_paddr_locked(vaddr);
sk_spin_unlock(&g_lock_vmm);
return pa;
}
int vmm_map_range(uint64_t vaddr, uint64_t paddr, uint64_t size, uint64_t flags) {
uint64_t pages = (size + VMM_PAGE_SIZE - 1) / VMM_PAGE_SIZE;
for (uint64_t i = 0; i < pages; ++i) {
@@ -279,12 +311,14 @@ int vmm_query_page(uint64_t vaddr, vmm_page_info_t *info) {
}
uint16_t idx = 0;
uint64_t *pt = resolve_pt(vaddr, &idx);
if (!pt) {
return 0;
}
uint64_t entry = 0;
uint64_t *pt;
sk_spin_lock(&g_lock_vmm);
pt = resolve_pt(vaddr, &idx);
if (pt) entry = pt[idx];
sk_spin_unlock(&g_lock_vmm);
uint64_t entry = pt[idx];
if (!(entry & PTE_PRESENT)) {
return 0;
}
+34 -30
View File
@@ -58,6 +58,7 @@
#endif
#include "platform_time.h"
#include "platform_lock.h"
#include "starkernel/spinlock.h" /* FABRIC-3.10.8 MC-0b -- sf_mutex is a real lock */
/* No LOG_LINE_MAX include-order constraint anymore: vm.h's own
* LOG_LINE_MAX (persistent block-log line size, 64) and log.h's in-memory
* message line length (renamed LOG_MSG_LINE_MAX, 256) no longer share a
@@ -381,14 +382,18 @@ void log_test_result(const char *word_name, TestResult result) {
void log_set_vm(struct VM *vm) { (void)vm; }
/* -----------------------------------------------------------------------------
* Mutex stubs (single-threaded kernel)
* Mutexes: kernel spinlocks (FABRIC-3.10.8 Stage MC-0b)
* ---------------------------------------------------------------------------*/
/* Until MC-0b these were no-ops ("single-threaded kernel"). They are now
* the kernel spinlock (starkernel/spinlock.h) on sf_mutex_t's one lock
* word: 0 = free, core_id + 1 = held. Interrupts are masked on the owning
* core while held, which also settles the old concern recorded here
* (FABRIC-0.md §21.2, §25.1 item 0.9): an interrupt can no longer arrive
* on a core inside one of these sections, so it can't spin against its
* own core. */
/**
* @brief Initialise a kernel mutex (sets state to 0 — unlocked).
*
* The kernel VM runs on a single thread with no preemption at the VM level,
* so the full spinlock/pthread path is unnecessary. This stub zeros the
* @c state field in @p mutex to leave it in a consistent unlocked state.
* @brief Initialise a kernel mutex (unlocked).
*
* @param mutex Mutex to initialise; returns -1 immediately if @c NULL.
* @return 0 on success, -1 if @p mutex is @c NULL.
@@ -399,47 +404,46 @@ int sf_mutex_init(sf_mutex_t *mutex) {
return 0;
}
static uint64_t g_mutex_destroyed_held;
/**
* @brief Destroy a kernel mutex (no-op).
* @brief Destroy a kernel mutex.
*
* Nothing to release in the kernel stub — the mutex holds only a single
* @c int field and has no OS resources.
* Nothing to release. A mutex destroyed while held is counted and logged
* (the VM it belongs to is going away with a critical section open).
*
* @param mutex Ignored.
* @param mutex Mutex to destroy; ignored if @c NULL.
*/
void sf_mutex_destroy(sf_mutex_t *mutex) {
(void)mutex;
if (!mutex) return;
if (mutex->state != 0) {
g_mutex_destroyed_held++;
log_message(LOG_ERROR, "LOCK: VM mutex destroyed while held x%llu",
(unsigned long long)g_mutex_destroyed_held);
}
mutex->state = 0;
}
uint64_t sf_mutex_destroyed_held_count(void) { return g_mutex_destroyed_held; }
/**
* @brief Acquire a kernel mutex (no-op).
* @brief Acquire a kernel mutex (spins; interrupts masked while held).
*
* Single-threaded kernel: no contention is possible at the VM level, so
* acquiring the mutex is always immediate and guaranteed to succeed.
*
* This no-op is correct only as long as nothing running in interrupt
* context mutates the structure a given lock protects (FABRIC-0.md §21.2,
* §25.1 item 0.9). If that ever stops being true, making this a real
* spinlock is not the fix: a single hart taking an interrupt while
* already holding the lock would spin against itself and deadlock. The
* correct fix in that case is disabling interrupts around the critical
* section, not adding a spinlock here.
*
* @param mutex Ignored.
* @param mutex Mutex to lock; ignored if @c NULL.
*/
void sf_mutex_lock(sf_mutex_t *mutex) {
(void)mutex;
if (!mutex) return;
sk_lockword_acquire(&mutex->state);
}
/**
* @brief Release a kernel mutex (no-op).
* @brief Release a kernel mutex.
*
* Single-threaded kernel: matching @c sf_mutex_lock() no-op.
*
* @param mutex Ignored.
* @param mutex Mutex to unlock; ignored if @c NULL.
*/
void sf_mutex_unlock(sf_mutex_t *mutex) {
(void)mutex;
if (!mutex) return;
sk_lockword_release(&mutex->state);
}
/* -----------------------------------------------------------------------------
+231 -85
View File
@@ -54,6 +54,21 @@
#include "log.h" /* Stage M-0: grow failures are logged at LOG_ERROR */
#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 */
#include "starkernel/spinlock.h" /* FABRIC-3.10.8 MC-0b -- g_lock_hermes */
/* FABRIC-3.10.8 MC-0b: g_lock_hermes covers everything in this file -- the
* arena directory and segments, the ledger and event counters, the channel
* table and member lists, the pending queues. Public functions take it;
* the *_locked statics run with it held and are what public functions
* compose from, so it is never taken twice. It is never held across:
* - vm_interpret_kind() (a drained message's FORTH can SEND straight
* back in) or a policy word (sk_hermes_channel_open_policy());
* - the switch module's notifications after a send (the Stage H floor
* reads Hermes queues back) -- sk_hermes_send_one() makes them once
* the lock is dropped. The wait stamp itself is taken under this lock
* (the switch lock sits below it, spinlock.h).
* Publish and event walk a channel's members one copied member at a time,
* sending each through sk_hermes_send_one(). */
#include "starkernel/capsule_run.h" /* VM_NAME_MAX -- Stage M-B event payloads */
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading
@@ -138,10 +153,12 @@ static uint64_t sk_hermes_returned = 0;
static uint64_t sk_hermes_consumed = 0;
void sk_hermes_ledger(uint64_t *held, uint64_t *pulled, uint64_t *returned, uint64_t *consumed) {
sk_spin_lock(&g_lock_hermes);
if (held) *held = sk_hermes_held;
if (pulled) *pulled = sk_hermes_pulled;
if (returned) *returned = sk_hermes_returned;
if (consumed) *consumed = sk_hermes_consumed;
sk_spin_unlock(&g_lock_hermes);
}
int sk_hermes_audit_values(uint64_t held, uint64_t pulled, uint64_t returned, uint64_t consumed) {
@@ -151,7 +168,7 @@ int sk_hermes_audit_values(uint64_t held, uint64_t pulled, uint64_t returned, ui
static uint64_t sk_hermes_audit_failures = 0;
int sk_hermes_audit(void) {
static int audit_locked(void) {
int ok = sk_hermes_audit_values(sk_hermes_held, sk_hermes_pulled,
sk_hermes_returned, sk_hermes_consumed);
if (!ok) {
@@ -161,11 +178,19 @@ int sk_hermes_audit(void) {
return ok;
}
int sk_hermes_audit(void) {
int ok;
sk_spin_lock(&g_lock_hermes);
ok = audit_locked();
sk_spin_unlock(&g_lock_hermes);
return ok;
}
uint64_t sk_hermes_audit_failure_count(void) {
return sk_hermes_audit_failures;
}
uint64_t sk_hermes_scan_held(size_t *live_count) {
static uint64_t scan_held_locked(size_t *live_count) {
uint64_t sum = 0;
size_t n = 0;
size_t i, len = sk_hermes_arena_capacity();
@@ -180,8 +205,20 @@ uint64_t sk_hermes_scan_held(size_t *live_count) {
return sum;
}
uint64_t sk_hermes_scan_held(size_t *live_count) {
uint64_t sum;
sk_spin_lock(&g_lock_hermes);
sum = scan_held_locked(live_count);
sk_spin_unlock(&g_lock_hermes);
return sum;
}
int sk_hermes_scan_check(void) {
return sk_hermes_scan_held((size_t *)0) == sk_hermes_held;
int ok;
sk_spin_lock(&g_lock_hermes);
ok = scan_held_locked((size_t *)0) == sk_hermes_held;
sk_spin_unlock(&g_lock_hermes);
return ok;
}
/* Lowest free index; grows the arena by a segment when every slot is in
@@ -195,7 +232,7 @@ static long sk_hermes_find_free_slot(void) {
return (long)len; /* first slot of the new segment */
}
int sk_hermes_alloc(VMUuid vm_id, SkHermesMessage **out_msg) {
static int alloc_locked(VMUuid vm_id, SkHermesMessage **out_msg) {
long slot;
SkHermesMessage *m;
uint64_t pulled;
@@ -260,12 +297,20 @@ int sk_hermes_alloc(VMUuid vm_id, SkHermesMessage **out_msg) {
* once every refusal path above has already returned. */
sk_hermes_held += pulled;
sk_hermes_pulled += pulled;
sk_hermes_audit();
audit_locked();
return 0;
}
int sk_hermes_release(SkHermesMessage *msg) {
int sk_hermes_alloc(VMUuid vm_id, SkHermesMessage **out_msg) {
int rc;
sk_spin_lock(&g_lock_hermes);
rc = alloc_locked(vm_id, out_msg);
sk_spin_unlock(&g_lock_hermes);
return rc;
}
static int release_locked(SkHermesMessage *msg) {
int rc;
uint64_t remaining;
@@ -302,18 +347,30 @@ int sk_hermes_release(SkHermesMessage *msg) {
if (rc == 0) {
sk_hermes_held -= remaining;
sk_hermes_returned += remaining;
sk_hermes_audit();
audit_locked();
}
memset(msg, 0, sizeof(*msg));
return rc;
}
int sk_hermes_release(SkHermesMessage *msg) {
int rc;
sk_spin_lock(&g_lock_hermes);
rc = release_locked(msg);
sk_spin_unlock(&g_lock_hermes);
return rc;
}
int sk_hermes_decay(SkHermesMessage *msg) {
uint64_t before, after;
StadiumCell *cell;
if (!msg || !msg->in_use || msg->stadium_cell < 0) return -1;
sk_spin_lock(&g_lock_hermes);
if (!msg || !msg->in_use || msg->stadium_cell < 0) {
sk_spin_unlock(&g_lock_hermes);
return -1;
}
cell = &stadium_cells()[msg->stadium_cell];
before = cell->header.heat;
@@ -326,7 +383,8 @@ int sk_hermes_decay(SkHermesMessage *msg) {
sk_hermes_consumed += before - after;
sk_hermes_held -= before - after;
stadium_consumed_record(msg->owner, before - after);
sk_hermes_audit();
audit_locked();
sk_spin_unlock(&g_lock_hermes);
return 0;
}
@@ -361,8 +419,10 @@ int sk_hermes_channels_boot_init(void) {
table[SK_HERMES_CHANNEL_EV_DIED].in_use = 1;
}
sk_spin_lock(&g_lock_hermes);
sk_hermes_channels = table;
sk_hermes_channel_capacity_val = (int)max_vm_count;
sk_spin_unlock(&g_lock_hermes);
console_puts("Kernel-Hermes: ");
console_put_u64((uint64_t)max_vm_count);
@@ -383,40 +443,47 @@ static int channel_valid(int channel_id) {
}
int sk_hermes_channel_create(void) {
int i;
if (!sk_hermes_channels) return -1;
int i, ch = -1;
sk_spin_lock(&g_lock_hermes);
/* Index 0 is always in_use (the common channel), so this scan never
* returns it -- start at 1 purely as a scan-cost micro-optimisation,
* not for correctness (the in_use check alone would already skip it). */
for (i = 1; i < sk_hermes_channel_capacity_val; i++) {
for (i = 1; sk_hermes_channels && i < sk_hermes_channel_capacity_val; i++) {
if (!sk_hermes_channels[i].in_use) {
memset(&sk_hermes_channels[i], 0, sizeof(SkHermesChannel));
sk_hermes_channels[i].in_use = 1;
return i;
ch = i;
break;
}
}
return -1;
sk_spin_unlock(&g_lock_hermes);
return ch;
}
int sk_hermes_channel_destroy(int channel_id) {
int rc = -1;
/* Permanent: channel 0 (SXLV.1) and, since Stage M-B, the event
* channels 1..3. */
if (channel_id >= SK_HERMES_CHANNEL_COMMON &&
channel_id <= SK_HERMES_CHANNEL_PERMANENT_LAST) return -1;
if (!channel_valid(channel_id)) return -1;
/* Stage M-0: the member list is heap-grown -- free it, or create()'s
* memset of this slot would leak it. */
if (sk_hermes_channels[channel_id].membership.members) {
kfree(sk_hermes_channels[channel_id].membership.members);
sk_spin_lock(&g_lock_hermes);
if (channel_valid(channel_id)) {
/* Stage M-0: the member list is heap-grown -- free it, or create()'s
* memset of this slot would leak it. */
if (sk_hermes_channels[channel_id].membership.members) {
kfree(sk_hermes_channels[channel_id].membership.members);
}
memset(&sk_hermes_channels[channel_id], 0, sizeof(SkHermesChannel));
rc = 0;
}
memset(&sk_hermes_channels[channel_id], 0, sizeof(SkHermesChannel));
return 0;
sk_spin_unlock(&g_lock_hermes);
return rc;
}
int sk_hermes_channel_subscribe(int channel_id, VMUuid vm_id) {
static int subscribe_locked(int channel_id, VMUuid vm_id) {
SkHermesMembership *m;
size_t i;
@@ -446,7 +513,7 @@ int sk_hermes_channel_subscribe(int channel_id, VMUuid vm_id) {
return 0;
}
int sk_hermes_channel_unsubscribe(int channel_id, VMUuid vm_id) {
static int unsubscribe_locked(int channel_id, VMUuid vm_id) {
SkHermesMembership *m;
size_t i;
@@ -466,29 +533,56 @@ int sk_hermes_channel_unsubscribe(int channel_id, VMUuid vm_id) {
return -1; /* not a member */
}
int sk_hermes_channel_subscribe(int channel_id, VMUuid vm_id) {
int rc;
sk_spin_lock(&g_lock_hermes);
rc = subscribe_locked(channel_id, vm_id);
sk_spin_unlock(&g_lock_hermes);
return rc;
}
int sk_hermes_channel_unsubscribe(int channel_id, VMUuid vm_id) {
int rc;
sk_spin_lock(&g_lock_hermes);
rc = unsubscribe_locked(channel_id, vm_id);
sk_spin_unlock(&g_lock_hermes);
return rc;
}
int sk_hermes_channel_is_member(int channel_id, VMUuid vm_id) {
SkHermesMembership *m;
size_t i;
int member = 0;
if (!channel_valid(channel_id)) return 0;
m = &sk_hermes_channels[channel_id].membership;
for (i = 0; i < m->count; i++) {
if (vm_uuid_equal(m->members[i], vm_id)) return 1;
sk_spin_lock(&g_lock_hermes);
if (channel_valid(channel_id)) {
m = &sk_hermes_channels[channel_id].membership;
for (i = 0; i < m->count; i++) {
if (vm_uuid_equal(m->members[i], vm_id)) { member = 1; break; }
}
}
return 0;
sk_spin_unlock(&g_lock_hermes);
return member;
}
int sk_hermes_channel_member_count(int channel_id) {
if (!channel_valid(channel_id)) return -1;
return (int)sk_hermes_channels[channel_id].membership.count;
int n = -1;
sk_spin_lock(&g_lock_hermes);
if (channel_valid(channel_id)) n = (int)sk_hermes_channels[channel_id].membership.count;
sk_spin_unlock(&g_lock_hermes);
return n;
}
int sk_hermes_channel_member_at(int channel_id, size_t i, VMUuid *out) {
if (!channel_valid(channel_id) || !out) return -1;
if (i >= sk_hermes_channels[channel_id].membership.count) return -1;
*out = sk_hermes_channels[channel_id].membership.members[i];
return 0;
int rc = -1;
if (!out) return -1;
sk_spin_lock(&g_lock_hermes);
if (channel_valid(channel_id) && i < sk_hermes_channels[channel_id].membership.count) {
*out = sk_hermes_channels[channel_id].membership.members[i];
rc = 0;
}
sk_spin_unlock(&g_lock_hermes);
return rc;
}
/* Task 3.3: per-subscriber pending queue table. See kernel_hermes.h's own
@@ -526,8 +620,10 @@ int sk_hermes_queues_boot_init(void) {
memset(&table[i], 0, sizeof(SkHermesPendingQueue));
}
sk_spin_lock(&g_lock_hermes);
sk_hermes_queues = table;
sk_hermes_queue_capacity_val = (int)max_vm_count;
sk_spin_unlock(&g_lock_hermes);
console_puts("Kernel-Hermes: ");
console_put_u64((uint64_t)max_vm_count);
@@ -606,19 +702,15 @@ static int queue_push(SkHermesPendingQueue *q, uint32_t msg_index) {
return 0;
}
int sk_hermes_send_one(VMUuid from, VMUuid to, uint32_t type, uint32_t channel_id,
void *payload_addr, uint32_t payload_len) {
/* sk_hermes_send_one() under g_lock_hermes. *first_out: the recipient's
* queue went from empty to one message (the Stage H floor applies). */
static int send_one_locked(VMUuid from, VMUuid to, uint32_t type, uint32_t channel_id,
void *payload_addr, uint32_t payload_len, int *first_out) {
SkHermesMessage *msg;
SkHermesPendingQueue *q;
uint32_t idx;
/* Task 3.5: the one-block bound, enforced uniformly here whether or
* not payload_addr happens to be a chunk carrier -- see
* kernel_hermes.h's own sizing note on why SK_HERMES_CHUNK_MAX_SLICE
* (not SK_HERMES_CHUNK_MAX_PAYLOAD) is a chunk's own content size. */
if (payload_len > SK_HERMES_CHUNK_MAX_PAYLOAD) return -1;
if (sk_hermes_alloc(from, &msg) != 0) return -1;
if (alloc_locked(from, &msg) != 0) return -1;
/* Real defect, found 2026-09-22 (FABRIC-3.6.md task 3.8's own
* findings log): this used to store the caller's own payload_addr
@@ -645,21 +737,41 @@ int sk_hermes_send_one(VMUuid from, VMUuid to, uint32_t type, uint32_t channel_i
q = find_or_create_queue(to);
if (!q) {
sk_hermes_release(msg); /* roll back -- no destination queue */
release_locked(msg); /* roll back -- no destination queue */
return -1;
}
idx = msg->index;
if (queue_push(q, idx) != 0) {
sk_hermes_release(msg); /* roll back -- queue couldn't grow (logged, counted) */
release_locked(msg); /* roll back -- queue couldn't grow (logged, counted) */
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. */
/* FABRIC-3.8.md §3.13.3 (Stage C-b): stamped only once admitted. */
msg->wait_stamp = sk_vm_switch_signal_next_stamp();
if (q->count == 1) sk_vm_switch_signal_note_work_gained(to); /* Stage H floor */
*first_out = (q->count == 1);
return 0;
}
int sk_hermes_send_one(VMUuid from, VMUuid to, uint32_t type, uint32_t channel_id,
void *payload_addr, uint32_t payload_len) {
int rc, first = 0;
/* Task 3.5: the one-block bound, enforced uniformly here whether or
* not payload_addr happens to be a chunk carrier -- see
* kernel_hermes.h's own sizing note on why SK_HERMES_CHUNK_MAX_SLICE
* (not SK_HERMES_CHUNK_MAX_PAYLOAD) is a chunk's own content size. */
if (payload_len > SK_HERMES_CHUNK_MAX_PAYLOAD) return -1;
sk_spin_lock(&g_lock_hermes);
rc = send_one_locked(from, to, type, channel_id, payload_addr, payload_len, &first);
sk_spin_unlock(&g_lock_hermes);
if (rc != 0) return rc;
/* The switch decision told something changed -- the recipient may now
* be the coolest VM with work. After the lock: the floor reads Hermes
* queues back. */
if (first) sk_vm_switch_signal_note_work_gained(to); /* Stage H floor */
sk_vm_switch_signal_note_mail();
return 0;
@@ -667,20 +779,19 @@ int sk_hermes_send_one(VMUuid from, VMUuid to, uint32_t type, uint32_t channel_i
int sk_hermes_publish(VMUuid from, int channel_id, uint32_t type,
void *payload_addr, uint32_t payload_len) {
SkHermesMembership *m;
VMUuid to;
size_t i;
int delivered = 0;
if (!channel_valid(channel_id)) return -1;
if (sk_hermes_channel_member_count(channel_id) < 0) return -1; /* not a channel */
if (payload_len > SK_HERMES_CHUNK_MAX_PAYLOAD) return -1; /* see sk_hermes_send_one()'s
* own check -- duplicated here
* so publish() itself returns
* -1 (not a silent 0) on an
* oversized payload */
m = &sk_hermes_channels[channel_id].membership;
for (i = 0; i < m->count; i++) {
if (sk_hermes_send_one(from, m->members[i], type, (uint32_t)channel_id,
for (i = 0; sk_hermes_channel_member_at(channel_id, i, &to) == 0; i++) {
if (sk_hermes_send_one(from, to, type, (uint32_t)channel_id,
payload_addr, payload_len) == 0) {
delivered++;
}
@@ -696,9 +807,11 @@ uint64_t sk_hermes_events_delivered(void) { return sk_hermes_ev_delivered; }
uint64_t sk_hermes_events_dropped(void) { return sk_hermes_ev_dropped; }
int sk_hermes_event(int ev_channel, VMUuid about, VMUuid funder, const char *name) {
static char payload[VM_NAME_MAX + 32];
/* MC-0b: on this stack, not static -- two cores can publish events at
* once. */
char payload[VM_NAME_MAX + 32];
const char *handler;
SkHermesMembership *m;
VMUuid to;
size_t i;
int len, delivered = 0, dropped = 0;
@@ -708,17 +821,15 @@ int sk_hermes_event(int ev_channel, VMUuid about, VMUuid funder, const char *nam
case SK_HERMES_CHANNEL_EV_DIED: handler = "ON-DIED"; break;
default: return 0;
}
if (!channel_valid(ev_channel) || !name) return 0;
if (sk_hermes_channel_member_count(ev_channel) < 0 || !name) return 0;
len = snprintf(payload, sizeof payload, "S\" %s\" %s", name, handler);
if (len <= 0 || (size_t)len >= sizeof payload) return 0;
/* Copied by sk_hermes_send_one(), so one static payload serves every
* member. The member list is read by index each time: a send never
* changes channel membership. */
m = &sk_hermes_channels[ev_channel].membership;
for (i = 0; i < m->count; i++) {
VMUuid to = m->members[i];
/* Copied by sk_hermes_send_one(), so one payload serves every member.
* The member list is read by index each time: a send never changes
* channel membership. */
for (i = 0; sk_hermes_channel_member_at(ev_channel, i, &to) == 0; i++) {
if (vm_uuid_equal(to, about)) continue; /* never to the VM it's about */
if (sk_hermes_send_one(funder, to, SK_HERMES_MSG_TYPE_EVENT, (uint32_t)ev_channel,
payload, (uint32_t)(len + 1)) == 0) {
@@ -728,10 +839,12 @@ int sk_hermes_event(int ev_channel, VMUuid about, VMUuid funder, const char *nam
}
}
sk_spin_lock(&g_lock_hermes);
sk_hermes_ev_delivered += (uint64_t)delivered;
sk_hermes_ev_dropped += (uint64_t)dropped;
sk_spin_unlock(&g_lock_hermes);
if (dropped > 0) {
/* M-6: dropped, counted, logged -- never blocking. */
sk_hermes_ev_dropped += (uint64_t)dropped;
console_puts("Kernel-Hermes: event ");
console_puts(handler);
console_puts(" for ");
@@ -744,24 +857,13 @@ int sk_hermes_event(int ev_channel, VMUuid about, VMUuid funder, const char *nam
return delivered;
}
int sk_hermes_pending_count(VMUuid vm_id) {
SkHermesPendingQueue *q = find_queue(vm_id);
return q ? (int)q->count : 0;
}
SkHermesMessage *sk_hermes_pending_peek(VMUuid vm_id) {
static SkHermesMessage *pending_peek_locked(VMUuid vm_id) {
SkHermesPendingQueue *q = find_queue(vm_id);
if (!q || q->count == 0) return (SkHermesMessage *)0;
return msg_at(q->slots[q->head]);
}
SkHermesMessage *sk_hermes_pending_at(VMUuid vm_id, size_t i) {
SkHermesPendingQueue *q = find_queue(vm_id);
if (!q || i >= q->count) return (SkHermesMessage *)0;
return msg_at(q->slots[(q->head + i) % q->cap]);
}
int sk_hermes_pending_pop(VMUuid vm_id) {
static int pending_pop_locked(VMUuid vm_id) {
SkHermesPendingQueue *q = find_queue(vm_id);
if (!q || q->count == 0) return -1;
q->head = (q->head + 1) % q->cap;
@@ -770,14 +872,53 @@ int sk_hermes_pending_pop(VMUuid vm_id) {
return 0;
}
int sk_hermes_pending_count(VMUuid vm_id) {
SkHermesPendingQueue *q;
int n;
sk_spin_lock(&g_lock_hermes);
q = find_queue(vm_id);
n = q ? (int)q->count : 0;
sk_spin_unlock(&g_lock_hermes);
return n;
}
/* The message pointer stays valid after the lock drops (messages never
* move, kernel_hermes.h); only the queue's owner pops it. */
SkHermesMessage *sk_hermes_pending_peek(VMUuid vm_id) {
SkHermesMessage *m;
sk_spin_lock(&g_lock_hermes);
m = pending_peek_locked(vm_id);
sk_spin_unlock(&g_lock_hermes);
return m;
}
SkHermesMessage *sk_hermes_pending_at(VMUuid vm_id, size_t i) {
SkHermesPendingQueue *q;
SkHermesMessage *m = (SkHermesMessage *)0;
sk_spin_lock(&g_lock_hermes);
q = find_queue(vm_id);
if (q && i < q->count) m = msg_at(q->slots[(q->head + i) % q->cap]);
sk_spin_unlock(&g_lock_hermes);
return m;
}
int sk_hermes_pending_pop(VMUuid vm_id) {
int rc;
sk_spin_lock(&g_lock_hermes);
rc = pending_pop_locked(vm_id);
sk_spin_unlock(&g_lock_hermes);
return rc;
}
int sk_hermes_vm_departed(VMUuid vm_id, int *channels_left, int *released) {
SkHermesPendingQueue *q;
SkHermesMessage *m;
int ch, left = 0, rel = 0;
sk_spin_lock(&g_lock_hermes);
/* Every channel it belonged to, channel 0 included. */
for (ch = 0; ch < sk_hermes_channel_capacity_val; ch++) {
if (channel_valid(ch) && sk_hermes_channel_unsubscribe(ch, vm_id) == 0) left++;
if (channel_valid(ch) && unsubscribe_locked(ch, vm_id) == 0) left++;
}
/* Its undelivered mail: each message released (heat back to whoever
@@ -785,12 +926,13 @@ int sk_hermes_vm_departed(VMUuid vm_id, int *channels_left, int *released) {
* freed for reuse, keeping its grown ring (find_or_create_queue()). */
q = find_queue(vm_id);
if (q) {
while ((m = sk_hermes_pending_peek(vm_id)) != (SkHermesMessage *)0) {
if (sk_hermes_release(m) == 0) rel++;
sk_hermes_pending_pop(vm_id);
while ((m = pending_peek_locked(vm_id)) != (SkHermesMessage *)0) {
if (release_locked(m) == 0) rel++;
pending_pop_locked(vm_id);
}
q->in_use = 0;
}
sk_spin_unlock(&g_lock_hermes);
if (channels_left) *channels_left = left;
if (released) *released = rel;
@@ -896,8 +1038,10 @@ int sk_hermes_drain_checkpoint(VM *vm) {
* enclosing execution */
vm->abort_requested = saved_abort;
sk_hermes_release(msg);
sk_hermes_pending_pop(self);
sk_spin_lock(&g_lock_hermes);
release_locked(msg);
pending_pop_locked(self);
sk_spin_unlock(&g_lock_hermes);
return drain_error ? -1 : 1;
}
@@ -925,7 +1069,9 @@ int sk_hermes_reassemble(SkHermesMessage **chunks, int n_chunks,
present = (int *)kmalloc((size_t)n_chunks * sizeof(int));
if (!present) {
sk_spin_lock(&g_lock_hermes); /* the failure counter */
sk_hermes_grow_failed("reassembly scratch");
sk_spin_unlock(&g_lock_hermes);
return -1;
}
rc = reassemble_with(chunks, n_chunks, out_buf, out_buf_cap, out_len, present);
+9
View File
@@ -35,6 +35,7 @@
#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/percpu.h" /* FABRIC-3.10.8 MC-0a -- this core's current VM */
#include "starkernel/spinlock.h" /* FABRIC-3.10.8 MC-0b -- no lock crosses a switch */
#include <stdint.h>
/* Arch-specific (switch.S in each arch dir). sk_vm_switch_prime() builds
@@ -226,6 +227,11 @@ void sk_vm_idle_point(int may_drain_self) {
VM *self = sk_percpu()->current_vm;
VM *next;
/* FABRIC-3.10.8 MC-0b: nothing is held on return to the idle point,
* which makes this the place to log any lock failure counted since. */
sk_lock_check_none_held("idle point");
sk_lock_report();
if (!self || self != (VM *)sk_get_mama_vm()) return;
if (self->frames.count != 0) return; /* inside a word (KEY/EXPECT): not idle */
@@ -280,7 +286,10 @@ int sk_vm_context_switch(VM *from, VM *to) {
sk_vm_switch_signal_note_parked(from->stadium_vm_id); /* §3.13.3 wait stamp */
sk_percpu()->current_vm = to;
/* FABRIC-3.10.8 MC-0b: no lock crosses a switch (spinlock.h). */
sk_lock_check_none_held("context switch, leaving");
sk_vm_switch_to(&from->native_stack_saved_sp, new_sp);
sk_lock_check_none_held("context switch, resumed");
/* Resumes here only once something later switches back to `from` --
* from `from`'s own point of view, this call simply took a while.