Stage MC-0d: Stadium partitioned by quota slot, one lock per slot (FABRIC-3.10.8 §3.5)

- Ruling (Captain Bob): per-slot locks, not "own path needs no lock" --
  a lock only on the cross-slot side can't exclude an unlocked owner
  (Hermes release evicting a sender's cell while the sender admits).
  FABRIC-3.10.0 §3.6 amended.
- StadiumVMQuota.lock covers the slot's lists, reservoir, consumed, word
  row and its cells. Word dispatch holds only its own slot lock for the
  whole dispatch (*_in_slot forms); peek-then-pull is one step.
- g_lock_stadium: grant slot allocation (new slot published with release
  in_use) and the shared block table (ruled: the one shared-lock Stadium
  path). Taken before a slot lock.
- Bitmap bytes set/cleared atomically (8 cells, possibly 8 slots).
- Cross-slot writers moved under the owner's lock: Hermes decay
  (stadium_cell_decay), Hermes release ledger (stadium_evict_credit),
  session pin (stadium_cell_set_pin). stadium_conserved() one hold.
- Word and block cell lookups check the cell is still this slot's.
- Probe (removed), identical on all three ISAs: 10,746,599 of 10,746,616
  word dispatches took exactly one lock (own slot), the 17 others all row
  growths; every live VM conserved; Hermes audit failures 0.
- Findings reported: births/USE run a VM's words while another VM is the
  core's current VM (MC-3 claim must cover it); quota slots never
  released; owner admission can evict a live message cell (latent, 0).
- Standard sequence on amd64/aarch64/riscv64: dict_hash set and G
  byte-identical, 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 22:40:33 -04:00
co-authored by Claude Opus 5.5
parent 355909d46f
commit 02c4c7e620
22 changed files with 55429 additions and 133 deletions
+3 -1
View File
@@ -31,6 +31,7 @@
#include "log.h"
sk_spinlock_t g_lock_registry = SK_SPINLOCK_INIT;
sk_spinlock_t g_lock_stadium = 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;
@@ -180,6 +181,7 @@ uint64_t sk_lock_held_at_switch(void) { return g_held_at_switch; }
static const char *lock_name(const volatile uint32_t *w)
{
if (w == &g_lock_registry.owner) return "registry";
if (w == &g_lock_stadium.owner) return "stadium";
if (w == &g_lock_switch.owner) return "switch";
if (w == &g_lock_hermes.owner) return "hermes";
if (w == &g_lock_doe.owner) return "doe";
@@ -189,7 +191,7 @@ static const char *lock_name(const volatile uint32_t *w)
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";
return "vm sf_mutex or Stadium slot";
}
/* Freestanding kvsnprintf has no %llx: hex by hand. */
+26 -28
View File
@@ -321,24 +321,20 @@ static int release_locked(SkHermesMessage *msg) {
return -1;
}
/* Read the patron's current heat BEFORE eviction -- stadium_evict()
* zeroes the header as part of returning the cell to the free list,
* and its own return value is a success code, not the amount
* credited. Today (before task 2.5's decay exists) this always
* equals exactly what was pulled at allocation; once decay exists,
* this is the message's true remaining heat, which is what actually
* flows back to the reservoir -- reading it here rather than
* assuming the original pulled amount is what keeps this correct
* without changes once task 2.5 lands. */
remaining = stadium_cells()[msg->stadium_cell].header.heat;
/* stadium_evict() returns the departing patron's remaining heat to
* its owner's reservoir itself (stadium.c: "the departing patron's
* remaining heat must flow back to its owner's reservoir before the
* cell returns to the free list") -- release does not touch the
* reservoir directly, the eviction path does it, matching
* MSG-FREE-NODE's own shape ("DUP 5 CELLS + @ STADIUM-EVICT DROP"). */
rc = stadium_evict((size_t)msg->stadium_cell);
/* The patron's remaining heat is what flows back to the reservoir:
* what was pulled at allocation, less any decay. FABRIC-3.10.8 MC-0d:
* stadium_evict_credit() reports exactly what it credited, read under
* the owning slot's lock in the same step as the eviction -- was read
* here beforehand, separately, straight out of the cell.
*
* The eviction returns that heat to its owner's reservoir itself
* (stadium.c: "the departing patron's remaining heat must flow back to
* its owner's reservoir before the cell returns to the free list") --
* release does not touch the reservoir directly, the eviction path does
* it, matching MSG-FREE-NODE's own shape ("DUP 5 CELLS + @ STADIUM-EVICT
* DROP"). */
remaining = 0;
rc = stadium_evict_credit((size_t)msg->stadium_cell, &remaining);
/* Task 2.4: the one site where held/returned change on release. Only
* on successful eviction -- a refused eviction leaves the patron
@@ -363,8 +359,7 @@ int sk_hermes_release(SkHermesMessage *msg) {
}
int sk_hermes_decay(SkHermesMessage *msg) {
uint64_t before, after;
StadiumCell *cell;
uint64_t delta;
sk_spin_lock(&g_lock_hermes);
if (!msg || !msg->in_use || msg->stadium_cell < 0) {
@@ -372,17 +367,20 @@ int sk_hermes_decay(SkHermesMessage *msg) {
return -1;
}
cell = &stadium_cells()[msg->stadium_cell];
before = cell->header.heat;
after = (uint64_t)q48_mul((q48_16_t)before, (q48_16_t)SK_HERMES_Q_DECAY);
cell->header.heat = after;
/* FABRIC-3.10.8 MC-0d: the decay itself (heat *= SK_HERMES_Q_DECAY, the
* difference recorded as the sender's consumed heat) happens in Stadium,
* under the sender's slot lock -- was a direct write into the sender's
* cell from here. Refused (-1, ledger untouched) if the cell isn't
* resident. */
if (stadium_cell_decay((size_t)msg->stadium_cell, (uint64_t)SK_HERMES_Q_DECAY, &delta) != 0) {
sk_spin_unlock(&g_lock_hermes);
return -1;
}
/* Task 2.5: the one site where consumed increments (and where held
* pays for it). SXXXVII.3: decay's delta is exact and in hand here. */
sk_hermes_consumed += before - after;
sk_hermes_held -= before - after;
stadium_consumed_record(msg->owner, before - after);
sk_hermes_consumed += delta;
sk_hermes_held -= delta;
audit_locked();
sk_spin_unlock(&g_lock_hermes);
+3 -5
View File
@@ -140,11 +140,9 @@ void session_set_pinned(VMUuid vm_id, int pinned)
cells = stadium_cells();
if (!cells) return;
if (pinned) {
cells[s->stadium_cell].header.flags |= STADIUM_FLAG_PIN;
} else {
cells[s->stadium_cell].header.flags &= (uint8_t)~STADIUM_FLAG_PIN;
}
/* FABRIC-3.10.8 MC-0d: under the cell's owning quota-slot lock (was a
* direct write into the cell header from here). */
(void)stadium_cell_set_pin((size_t)s->stadium_cell, pinned);
}
int session_is_pinned(VMUuid vm_id)
+237 -55
View File
@@ -43,6 +43,7 @@
#include "vm.h" /* VM_MEMORY_SIZE -- the per-VM footprint stadium_max_vm_count() budgets against */
#include "block_subsystem.h" /* blk_flush() -- STADIUM_BEHAVIOUR_MIGRATE's real write-back action */
#include "starkernel/session.h" /* session_register()/session_set_pinned() -- FABRIC-2.md §H.12 step 4 */
#include "starkernel/spinlock.h" /* FABRIC-3.10.8 MC-0d -- per-slot locks, g_lock_stadium */
static StadiumCell *stadium_cell_array = (StadiumCell *)0;
static uint8_t *stadium_bitmap = (uint8_t *)0;
@@ -112,13 +113,39 @@ typedef struct {
* have decayed away (recorded by kernel-Hermes,
* FABRIC-3.6.md task 2.7). Full invariant:
* Σ(resident) + reservoir + consumed == Q48_ONE. */
volatile uint32_t lock; /* FABRIC-3.10.8 MC-0d: this slot's own lock (a
* spinlock.h lock word). Covers every field above,
* and this slot's cells: their headers, owner and
* resident-list entries. Taken by the owning VM's
* dispatch and by anything else touching the slot --
* another VM's dispatch never contends it. */
} StadiumVMQuota;
/* FABRIC-3.10.8 MC-0d (Captain Bob, 2026-09-25: per-slot locks).
*
* Stadium is partitioned by quota slot. The word-dispatch path takes only its
* own VM's slot lock, so it takes no shared lock. Cross-slot work takes the
* other slot's lock:
* - kernel-Hermes evicting or decaying a sender's message cell from the
* receiver's drain;
* - KILL/reap evicting a dead VM's cell;
* - conservation audits of another VM.
* g_lock_stadium serializes what is truly global:
* - quota-slot allocation (grants);
* - stadium_blocks.c's one block table, shared by every VM (ruled: a shared
* lock on the block path, recorded for MC-3 to measure).
* Order: g_lock_stadium, then at most one slot lock (spinlock.h). The lock
* helpers, slot_lock()/slot_unlock(), follow the quota table below. */
/* kmalloc'd at stadium_boot_init() to stadium_max_vm_count_val entries,
* replacing the old static StadiumVMQuota stadium_quotas[STADIUM_MAX_VM_COUNT]
* (2026-08-15: the bound is computed from RAM, not a compile-time constant). */
static StadiumVMQuota *stadium_quotas = (StadiumVMQuota *)0;
/* MC-0d: one quota slot's lock (see StadiumVMQuota.lock). */
static void slot_lock(int slot) { sk_lockword_acquire(&stadium_quotas[slot].lock); }
static void slot_unlock(int slot) { sk_lockword_release(&stadium_quotas[slot].lock); }
/* Doubly-linked resident-list threading, one entry per cell, parallel to
* stadium_owner[] -- kmalloc'd at stadium_boot_init() to stadium_ncells
* entries each. Only meaningful for cells currently resident (bitmap bit
@@ -162,7 +189,10 @@ static void resident_list_remove(int slot, size_t cell_index) {
static int quota_slot_for_vm(VMUuid vm_id) {
size_t i;
for (i = 0; i < stadium_max_vm_count_val; i++) {
if (stadium_quotas[i].in_use && vm_uuid_equal(stadium_quotas[i].vm_id, vm_id)) return i;
/* MC-0d: in_use is published last, with release (stadium_grant_quota()),
* so a slot seen in use has its vm_id and lists already in place. */
if (__atomic_load_n(&stadium_quotas[i].in_use, __ATOMIC_ACQUIRE) &&
vm_uuid_equal(stadium_quotas[i].vm_id, vm_id)) return i;
}
return -1;
}
@@ -281,6 +311,7 @@ int stadium_boot_init(void) {
quotas[i].resident_head = STADIUM_CELL_NONE;
quotas[i].reservoir = 0;
quotas[i].consumed = 0;
quotas[i].lock = 0;
}
}
quotas[0].vm_id = vm_uuid_hera();
@@ -424,16 +455,22 @@ uint64_t stadium_density(size_t cell_index) {
/* STADIUM_FLAG_PIN moved to stadium.h (FABRIC-2.md §H.12 step 3) -- see its
* doc comment there for why. */
/* FABRIC-3.10.8 MC-0d: one bitmap byte covers 8 cells, which can belong to
* different quota slots, and each slot's lock covers only its own cells --
* so the byte is changed with atomic OR/AND, never a plain read-modify-write. */
static int bitmap_get(size_t cell_index) {
return (stadium_bitmap[cell_index / 8u] >> (cell_index % 8u)) & 1u;
return (__atomic_load_n(&stadium_bitmap[cell_index / 8u], __ATOMIC_ACQUIRE)
>> (cell_index % 8u)) & 1u;
}
static void bitmap_set(size_t cell_index) {
stadium_bitmap[cell_index / 8u] |= (uint8_t)(1u << (cell_index % 8u));
__atomic_fetch_or(&stadium_bitmap[cell_index / 8u],
(uint8_t)(1u << (cell_index % 8u)), __ATOMIC_RELEASE);
}
static void bitmap_clear(size_t cell_index) {
stadium_bitmap[cell_index / 8u] &= (uint8_t)~(1u << (cell_index % 8u));
__atomic_fetch_and(&stadium_bitmap[cell_index / 8u],
(uint8_t)~(1u << (cell_index % 8u)), __ATOMIC_RELEASE);
}
/*
@@ -447,7 +484,9 @@ static void bitmap_clear(size_t cell_index) {
* purpose, so a wrongly-cleared pin cannot silently swallow the violation
* via the ordinary refusal path instead of surfacing it.
*/
int stadium_evict(size_t cell_index) {
/* MC-0d: the body, with the cell's owning slot locked. *credited (if given)
* receives the heat returned to the owner's reservoir. */
static int evict_locked(size_t cell_index, uint64_t *credited) {
StadiumPatronHeader *header;
uint16_t slot;
@@ -473,6 +512,7 @@ int stadium_evict(size_t cell_index) {
* Q48_ONE. Captured BEFORE the zero-fill below, which would otherwise
* destroy it. */
stadium_quotas[slot].reservoir += header->heat;
if (credited) *credited = header->heat;
{
uint8_t *raw = (uint8_t *)header;
@@ -493,6 +533,34 @@ int stadium_evict(size_t cell_index) {
return 0;
}
/* Locks the slot that owns cell_index. A resident cell's owner never changes
* (grants move only free cells), and a free cell's only under its donor's
* lock -- so the owner is re-read once locked, and the lock retaken if it
* moved in between. Returns the locked slot, or -1 (out of range). */
static int lock_cell_owner(size_t cell_index) {
int slot;
if (!stadium_initialized || cell_index >= stadium_ncells) return -1;
for (;;) {
slot = (int)__atomic_load_n(&stadium_owner[cell_index], __ATOMIC_ACQUIRE);
slot_lock(slot);
if ((int)stadium_owner[cell_index] == slot) return slot;
slot_unlock(slot);
}
}
int stadium_evict(size_t cell_index) {
return stadium_evict_credit(cell_index, (uint64_t *)0);
}
int stadium_evict_credit(size_t cell_index, uint64_t *credited) {
int slot = lock_cell_owner(cell_index);
int rc;
if (slot < 0) return -1;
rc = evict_locked(cell_index, credited);
slot_unlock(slot);
return rc;
}
/*
* FABRIC-0.md §19.3, §22.3, item 3.7: admit into vm_id's own quota. Pops that
* VM's free-list head first (O(1), no comparison needed -- §19.3's density
@@ -503,13 +571,26 @@ int stadium_evict(size_t cell_index) {
* evicting it only if the candidate is strictly denser.
*/
size_t stadium_admit(VMUuid vm_id, const StadiumPatronHeader *candidate) {
int slot;
int slot;
size_t idx;
if (!stadium_initialized || !candidate) return STADIUM_CELL_NONE;
slot = quota_slot_for_vm(vm_id);
if (slot < 0) return STADIUM_CELL_NONE;
slot_lock(slot);
idx = stadium_admit_in_slot(slot, candidate);
slot_unlock(slot);
return idx;
}
/* MC-0d: stadium_admit() with `slot` already locked by the caller. */
size_t stadium_admit_in_slot(int slot, const StadiumPatronHeader *candidate) {
size_t idx;
size_t least_dense_index = STADIUM_CELL_NONE;
uint64_t least_dense_value = 0;
uint64_t candidate_density;
if (!stadium_initialized || !candidate) return STADIUM_CELL_NONE;
if (!stadium_initialized || !candidate || slot < 0) return STADIUM_CELL_NONE;
/* Multi-cell patrons need a continuation chain, and no header field is
* documented as carrying one's first index -- see this function's doc
@@ -524,9 +605,6 @@ size_t stadium_admit(VMUuid vm_id, const StadiumPatronHeader *candidate) {
if (candidate->contains != STADIUM_CONTAINS_NONE &&
candidate->contains >= stadium_ncells) return STADIUM_CELL_NONE;
slot = quota_slot_for_vm(vm_id);
if (slot < 0) return STADIUM_CELL_NONE;
if (stadium_quotas[slot].free_head != STADIUM_CELL_NONE) {
idx = stadium_quotas[slot].free_head;
stadium_quotas[slot].free_head = link_to_size(stadium_cell_array[idx].header.link);
@@ -582,13 +660,12 @@ size_t stadium_admit(VMUuid vm_id, const StadiumPatronHeader *candidate) {
candidate_density = candidate->heat / (uint64_t)candidate->mass; /* mass == 1, guaranteed above */
if (candidate_density <= least_dense_value) return STADIUM_CELL_NONE;
if (stadium_evict(least_dense_index) != 0) return STADIUM_CELL_NONE;
if (evict_locked(least_dense_index, (uint64_t *)0) != 0) return STADIUM_CELL_NONE;
/* stadium_evict() just pushed least_dense_index onto quotas[slot]'s free
/* evict_locked() just pushed least_dense_index onto quotas[slot]'s free
* list -- its owner is `slot`, the same quota we scoped the search to.
* Single-threaded today (§21.1/§21.2: real concurrency is step-one, not
* yet built), so nothing else can have touched the list meanwhile; pop
* it straight back off. */
* MC-0d: the slot has stayed locked throughout, so nothing else can have
* touched the list meanwhile; pop it straight back off. */
idx = stadium_quotas[slot].free_head;
stadium_quotas[slot].free_head = link_to_size(stadium_cell_array[idx].header.link);
stadium_quotas[slot].free_count--;
@@ -616,19 +693,28 @@ int stadium_grant_quota(VMUuid new_vm_id, VMUuid from_vm_id) {
size_t half, idx, last_new, new_head, remainder_head;
if (!stadium_initialized) return -1;
if (quota_slot_for_vm(new_vm_id) >= 0) return -1;
/* MC-0d: slot allocation is global (g_lock_stadium: two grants must not
* pick the same free slot); the split is the donor's (its slot lock). */
sk_spin_lock(&g_lock_stadium);
if (quota_slot_for_vm(new_vm_id) >= 0) { sk_spin_unlock(&g_lock_stadium); return -1; }
from_slot = quota_slot_for_vm(from_vm_id);
if (from_slot < 0) return -1;
if (from_slot < 0) { sk_spin_unlock(&g_lock_stadium); return -1; }
new_slot = -1;
for (i = 0; i < (int)stadium_max_vm_count_val; i++) {
if (!stadium_quotas[i].in_use) { new_slot = i; break; }
}
if (new_slot < 0) return -1;
if (new_slot < 0) { sk_spin_unlock(&g_lock_stadium); return -1; }
slot_lock(from_slot);
half = stadium_quotas[from_slot].free_count / 2;
if (half == 0) return -1; /* fewer than 2 free cells -- nothing to split */
if (half == 0) { /* fewer than 2 free cells -- nothing to split */
slot_unlock(from_slot);
sk_spin_unlock(&g_lock_stadium);
return -1;
}
new_head = stadium_quotas[from_slot].free_head;
idx = new_head;
@@ -644,15 +730,19 @@ int stadium_grant_quota(VMUuid new_vm_id, VMUuid from_vm_id) {
stadium_cell_array[last_new].header.link = size_to_link(STADIUM_CELL_NONE);
stadium_quotas[from_slot].free_head = remainder_head;
stadium_quotas[from_slot].free_count -= half;
slot_unlock(from_slot);
/* Not yet visible to anyone (in_use is 0 until the release store below). */
stadium_quotas[new_slot].vm_id = new_vm_id;
stadium_quotas[new_slot].in_use = 1;
stadium_quotas[new_slot].free_head = new_head;
stadium_quotas[new_slot].free_count = half;
stadium_quotas[new_slot].resident_head = STADIUM_CELL_NONE;
stadium_quotas[new_slot].reservoir = Q48_ONE;
stadium_quotas[new_slot].consumed = 0;
stadium_quotas[new_slot].lock = 0;
__atomic_store_n(&stadium_quotas[new_slot].in_use, 1, __ATOMIC_RELEASE);
sk_spin_unlock(&g_lock_stadium);
return 0;
}
@@ -703,69 +793,143 @@ static int owned_resident_slot(VMUuid vm_id, size_t cell_index) {
}
uint64_t stadium_cell_heat_get(VMUuid vm_id, size_t cell_index) {
if (owned_resident_slot(vm_id, cell_index) < 0) return 0;
return stadium_cell_array[cell_index].header.heat;
int slot = quota_slot_for_vm(vm_id);
uint64_t heat = 0;
if (slot < 0) return 0;
slot_lock(slot);
if (owned_resident_slot(vm_id, cell_index) >= 0) heat = stadium_cell_array[cell_index].header.heat;
slot_unlock(slot);
return heat;
}
int stadium_cell_heat_set(VMUuid vm_id, size_t cell_index, uint64_t new_heat) {
int slot = owned_resident_slot(vm_id, cell_index);
int slot = quota_slot_for_vm(vm_id);
uint64_t old_heat, delta, pulled;
int rc = 0;
if (slot < 0) return -1;
slot_lock(slot);
if (owned_resident_slot(vm_id, cell_index) < 0) { slot_unlock(slot); return -1; }
old_heat = stadium_cell_array[cell_index].header.heat;
if (new_heat == old_heat) return 0;
if (new_heat > old_heat) {
delta = new_heat - old_heat;
pulled = (delta > stadium_quotas[slot].reservoir) ? stadium_quotas[slot].reservoir : delta;
if (pulled < delta) return -1; /* insufficient -- no partial credit, no mutation */
stadium_quotas[slot].reservoir -= pulled;
} else {
if (pulled < delta) {
rc = -1; /* insufficient -- no partial credit, no mutation */
} else {
stadium_quotas[slot].reservoir -= pulled;
stadium_cell_array[cell_index].header.heat = new_heat;
}
} else if (new_heat < old_heat) {
delta = old_heat - new_heat;
stadium_quotas[slot].reservoir += delta;
stadium_cell_array[cell_index].header.heat = new_heat;
}
slot_unlock(slot);
return rc;
}
stadium_cell_array[cell_index].header.heat = new_heat;
/* MC-0d: kernel-Hermes's message decay (was a direct write to the sender's
* cell from kernel_hermes.c). Under the owning slot's lock: the cell's heat
* becomes q48_mul(heat, factor_q48), and the difference is recorded as that
* slot's consumed heat -- the same arithmetic sk_hermes_decay() did inline.
* *consumed_out receives the difference. -1 if the cell isn't resident. */
int stadium_cell_decay(size_t cell_index, uint64_t factor_q48, uint64_t *consumed_out) {
int slot = lock_cell_owner(cell_index);
uint64_t before, after;
if (slot < 0) return -1;
if (!bitmap_get(cell_index)) { slot_unlock(slot); return -1; }
before = stadium_cell_array[cell_index].header.heat;
after = (uint64_t)q48_mul((q48_16_t)before, (q48_16_t)factor_q48);
stadium_cell_array[cell_index].header.heat = after;
stadium_quotas[slot].consumed += before - after;
slot_unlock(slot);
if (consumed_out) *consumed_out = before - after;
return 0;
}
uint64_t stadium_reservoir_pull(VMUuid vm_id, uint64_t amount) {
int slot = quota_slot_for_vm(vm_id);
uint64_t pulled;
/* MC-0d: session.c's pin (was a direct write into the cell header). */
int stadium_cell_set_pin(size_t cell_index, int pinned) {
int slot = lock_cell_owner(cell_index);
if (slot < 0) return -1;
if (pinned) stadium_cell_array[cell_index].header.flags |= STADIUM_FLAG_PIN;
else stadium_cell_array[cell_index].header.flags &= (uint8_t)~STADIUM_FLAG_PIN;
slot_unlock(slot);
return 0;
}
if (slot < 0) return 0;
pulled = (amount > stadium_quotas[slot].reservoir) ? stadium_quotas[slot].reservoir : amount;
/* The *_in_slot forms: caller holds `slot`'s lock (stadium_slot_lock()). */
uint64_t stadium_reservoir_pull_in_slot(int slot, uint64_t amount) {
uint64_t pulled = (amount > stadium_quotas[slot].reservoir) ? stadium_quotas[slot].reservoir : amount;
stadium_quotas[slot].reservoir -= pulled;
return pulled;
}
void stadium_reservoir_push(VMUuid vm_id, uint64_t amount) {
int slot = quota_slot_for_vm(vm_id);
if (slot < 0) return;
void stadium_reservoir_push_in_slot(int slot, uint64_t amount) {
stadium_quotas[slot].reservoir += amount;
}
uint64_t stadium_reservoir_peek(VMUuid vm_id) {
int slot = quota_slot_for_vm(vm_id);
if (slot < 0) return 0;
uint64_t stadium_reservoir_peek_in_slot(int slot) {
return stadium_quotas[slot].reservoir;
}
int stadium_evict_in_slot(int slot, size_t cell_index) {
if (cell_index >= stadium_ncells || (int)stadium_owner[cell_index] != slot) return -1;
return evict_locked(cell_index, (uint64_t *)0);
}
int stadium_cell_owner_slot(size_t cell_index) {
if (cell_index >= stadium_ncells) return -1;
return (int)__atomic_load_n(&stadium_owner[cell_index], __ATOMIC_ACQUIRE);
}
int stadium_slot_lock_vm(VMUuid vm_id) {
int slot;
if (!stadium_initialized) return -1;
slot = quota_slot_for_vm(vm_id);
if (slot >= 0) slot_lock(slot);
return slot;
}
void stadium_slot_unlock(int slot) {
if (slot >= 0) slot_unlock(slot);
}
uint64_t stadium_reservoir_pull(VMUuid vm_id, uint64_t amount) {
int slot = stadium_slot_lock_vm(vm_id);
uint64_t pulled;
if (slot < 0) return 0;
pulled = stadium_reservoir_pull_in_slot(slot, amount);
slot_unlock(slot);
return pulled;
}
void stadium_reservoir_push(VMUuid vm_id, uint64_t amount) {
int slot = stadium_slot_lock_vm(vm_id);
if (slot < 0) return;
stadium_reservoir_push_in_slot(slot, amount);
slot_unlock(slot);
}
uint64_t stadium_reservoir_peek(VMUuid vm_id) {
int slot = stadium_slot_lock_vm(vm_id);
uint64_t r;
if (slot < 0) return 0;
r = stadium_reservoir_peek_in_slot(slot);
slot_unlock(slot);
return r;
}
int stadium_quota_slot_for_vm(VMUuid vm_id) {
return quota_slot_for_vm(vm_id);
}
uint64_t stadium_resident_sum(VMUuid vm_id) {
int slot = quota_slot_for_vm(vm_id);
/* The full-array scan (the stronger audit: it doesn't trust the resident
* list). Caller holds `slot`'s lock. */
static uint64_t resident_sum_locked(int slot) {
uint64_t sum = 0;
size_t i;
if (slot < 0) return 0;
for (i = 0; i < stadium_ncells; i++) {
if (!bitmap_get(i)) continue;
if (stadium_owner[i] != (uint16_t)slot) continue;
@@ -774,23 +938,41 @@ uint64_t stadium_resident_sum(VMUuid vm_id) {
return sum;
}
void stadium_consumed_record(VMUuid vm_id, uint64_t amount) {
int slot = quota_slot_for_vm(vm_id);
uint64_t stadium_resident_sum(VMUuid vm_id) {
int slot = stadium_slot_lock_vm(vm_id);
uint64_t sum;
if (slot < 0) return 0;
sum = resident_sum_locked(slot);
slot_unlock(slot);
return sum;
}
void stadium_consumed_record(VMUuid vm_id, uint64_t amount) {
int slot = stadium_slot_lock_vm(vm_id);
if (slot < 0) return;
stadium_quotas[slot].consumed += amount;
slot_unlock(slot);
}
uint64_t stadium_consumed_peek(VMUuid vm_id) {
int slot = quota_slot_for_vm(vm_id);
int slot = stadium_slot_lock_vm(vm_id);
uint64_t c;
if (slot < 0) return 0;
return stadium_quotas[slot].consumed;
c = stadium_quotas[slot].consumed;
slot_unlock(slot);
return c;
}
/* MC-0d: the three terms read under one hold of the slot's lock, so the sum
* is of one instant (read separately, a concurrent pull or eviction between
* them could make a conserved slot read as drifted). */
int stadium_conserved(VMUuid vm_id) {
return (stadium_resident_sum(vm_id) + stadium_reservoir_peek(vm_id) +
stadium_consumed_peek(vm_id)) == (uint64_t)Q48_ONE;
int slot = stadium_slot_lock_vm(vm_id);
uint64_t total;
if (slot < 0) return 0; /* no quota: 0 + 0 + 0 != Q48_ONE, as before */
total = resident_sum_locked(slot) + stadium_quotas[slot].reservoir + stadium_quotas[slot].consumed;
slot_unlock(slot);
return total == (uint64_t)Q48_ONE;
}
/*
+47 -18
View File
@@ -34,6 +34,7 @@
#include "starkernel/console.h"
#include "starkernel/kmalloc.h"
#include "starkernel/q48_16.h" /* Q48_ONE */
#include "starkernel/spinlock.h" /* FABRIC-3.10.8 MC-0d -- g_lock_stadium (the shared block table) */
#include "starforth_config.h" /* STADIUM_BLOCK_HEAT_QUANTUM, STADIUM_BLOCK_COOL_RATE_Q48,
STADIUM_BLOCK_TRACK_CAP_MULT */
@@ -196,24 +197,40 @@ static size_t table_probe(int16_t quota_slot, uint32_t lbn, int *out_found) {
* pulls) depend on. Own copy, not shared code across files -- same
* decision stadium_words.c's own version already made.
*/
static uint64_t block_dispatch_pull(VMUuid vm_id, uint64_t want) {
uint64_t available = stadium_reservoir_peek(vm_id);
static uint64_t block_dispatch_pull(int slot, uint64_t want) {
/* MC-0d: `slot` is locked by the caller, so the peek and the pull are
* one step. */
uint64_t available = stadium_reservoir_peek_in_slot(slot);
uint64_t floor = Q48_ONE / 3;
uint64_t pullable = (available > floor) ? (available - floor) : 0;
uint64_t capped = (want < pullable) ? want : pullable;
return stadium_reservoir_pull(vm_id, capped);
return stadium_reservoir_pull_in_slot(slot, capped);
}
/* FABRIC-3.10.8 MC-0d: block_slots[] is one table shared by every VM, so a
* block dispatch takes g_lock_stadium for the table (the one shared-lock
* Stadium path, as ruled 2026-09-25 -- block access is rare next to word
* dispatch), then this VM's own slot lock for its cells and reservoir. */
static void block_dispatch_locked(int slot, uint32_t lbn, uint64_t heartbeat_ticks);
void stadium_block_dispatch(VMUuid vm_id, uint32_t lbn, uint64_t heartbeat_ticks) {
int slot;
int found;
size_t idx;
int stale = 0;
int slot;
if (!blocks_initialized) return;
slot = stadium_quota_slot_for_vm(vm_id);
if (slot < 0) return;
sk_spin_lock(&g_lock_stadium);
slot = stadium_slot_lock_vm(vm_id);
if (slot >= 0) {
block_dispatch_locked(slot, lbn, heartbeat_ticks);
stadium_slot_unlock(slot);
}
sk_spin_unlock(&g_lock_stadium);
}
static void block_dispatch_locked(int slot, uint32_t lbn, uint64_t heartbeat_ticks) {
int found;
size_t idx;
int stale = 0;
idx = table_probe((int16_t)slot, lbn, &found);
@@ -223,7 +240,8 @@ void stadium_block_dispatch(VMUuid vm_id, uint32_t lbn, uint64_t heartbeat_ticks
if (cell == STADIUM_CELL_NONE || cell >= stadium_cell_count() || !cell_is_resident(cell)) {
stale = 1;
} else if (stadium_cells()[cell].header.identity != (uint64_t)lbn) {
} else if (stadium_cell_owner_slot(cell) != slot || /* MC-0d: still this slot's cell */
stadium_cells()[cell].header.identity != (uint64_t)lbn) {
stale = 1;
}
@@ -237,12 +255,12 @@ void stadium_block_dispatch(VMUuid vm_id, uint32_t lbn, uint64_t heartbeat_ticks
if (cooled > h->heat) cooled = h->heat;
if (cooled > 0) {
h->heat -= cooled;
stadium_reservoir_push(vm_id, cooled);
stadium_reservoir_push_in_slot(slot, cooled);
}
bs->last_decay_tick = heartbeat_ticks;
}
h->heat += block_dispatch_pull(vm_id, (uint64_t)STADIUM_BLOCK_HEAT_QUANTUM);
h->heat += block_dispatch_pull(slot, (uint64_t)STADIUM_BLOCK_HEAT_QUANTUM);
return;
}
@@ -254,7 +272,7 @@ void stadium_block_dispatch(VMUuid vm_id, uint32_t lbn, uint64_t heartbeat_ticks
if (idx >= block_slots_cap) return; /* table full: skip tracking this touch */
{
uint64_t pulled = block_dispatch_pull(vm_id, (uint64_t)STADIUM_BLOCK_HEAT_QUANTUM);
uint64_t pulled = block_dispatch_pull(slot, (uint64_t)STADIUM_BLOCK_HEAT_QUANTUM);
StadiumPatronHeader candidate;
uint8_t *raw = (uint8_t *)&candidate;
size_t i;
@@ -270,9 +288,9 @@ void stadium_block_dispatch(VMUuid vm_id, uint32_t lbn, uint64_t heartbeat_ticks
candidate.flags = 0; /* unpinned */
candidate.behaviour = (uint8_t)STADIUM_BEHAVIOUR_MIGRATE;
cell_idx = stadium_admit(vm_id, &candidate);
cell_idx = stadium_admit_in_slot(slot, &candidate);
if (cell_idx == STADIUM_CELL_NONE) {
stadium_reservoir_push(vm_id, pulled); /* rollback: preserve conservation */
stadium_reservoir_push_in_slot(slot, pulled); /* rollback: preserve conservation */
return;
}
@@ -304,13 +322,22 @@ static void console_put_u64(uint64_t v) {
}
void stadium_blocks_print_boot_diagnostics(VMUuid vm_id) {
int slot = stadium_quota_slot_for_vm(vm_id);
int slot;
uint64_t promotions = 0, evictions = 0;
uint64_t resident_sum = 0;
uint64_t reservoir;
size_t i;
if (!blocks_initialized || slot < 0) {
if (!blocks_initialized) {
console_println("Stadium blocks: not initialized or vm_id holds no quota");
return;
}
/* MC-0d: the table (g_lock_stadium), then this slot's cells. Printing
* waits until both are released. */
sk_spin_lock(&g_lock_stadium);
slot = stadium_slot_lock_vm(vm_id);
if (slot < 0) {
sk_spin_unlock(&g_lock_stadium);
console_println("Stadium blocks: not initialized or vm_id holds no quota");
return;
}
@@ -334,7 +361,9 @@ void stadium_blocks_print_boot_diagnostics(VMUuid vm_id) {
resident_sum += stadium_cells()[cell].header.heat;
}
reservoir = stadium_reservoir_peek(vm_id);
reservoir = stadium_reservoir_peek_in_slot(slot);
stadium_slot_unlock(slot);
sk_spin_unlock(&g_lock_stadium);
console_puts("Stadium blocks: promotions=");
console_put_u64(promotions);
+44 -24
View File
@@ -92,7 +92,7 @@ static int grow_row(int slot, uint32_t word_id) {
}
row = (StadiumWordSlot *)kmalloc((size_t)new_cap * sizeof(StadiumWordSlot));
if (!row) {
row_grow_failures++;
__atomic_add_fetch(&row_grow_failures, 1, __ATOMIC_RELAXED); /* MC-0d: any slot's dispatch */
log_message(LOG_ERROR, "Stadium word row can't grow: not admitted");
return -1;
}
@@ -168,12 +168,14 @@ static int cell_is_resident(size_t idx) {
* NOT floored -- only word-execution admission respects this ceiling on
* its own consumption.
*/
static uint64_t word_dispatch_pull(VMUuid vm_id, uint64_t want) {
uint64_t available = stadium_reservoir_peek(vm_id);
static uint64_t word_dispatch_pull(int slot, uint64_t want) {
/* MC-0d: `slot` is locked by the caller, so the peek and the pull are
* one step. */
uint64_t available = stadium_reservoir_peek_in_slot(slot);
uint64_t floor = Q48_ONE / 3;
uint64_t pullable = (available > floor) ? (available - floor) : 0;
uint64_t capped = (want < pullable) ? want : pullable;
return stadium_reservoir_pull(vm_id, capped);
return stadium_reservoir_pull_in_slot(slot, capped);
}
/*
@@ -195,8 +197,14 @@ static size_t resolve_resident_cell(int slot, uint32_t word_id) {
stat_evictions[slot]++;
return STADIUM_CELL_NONE;
}
/* FABRIC-3.10.8 MC-0d: the cell must also still be this slot's. A word
* cell evicted to this slot's free list can move to another VM in a
* grant and be admitted there -- identity is a word id, which another
* VM's words share -- and this row would then heat the other VM's cell
* (and, with worker cores, touch it without that slot's lock). */
cells = stadium_cells();
if (cells[cell].header.identity != (uint64_t)word_id) {
if (stadium_cell_owner_slot(cell) != slot ||
cells[cell].header.identity != (uint64_t)word_id) {
word_slots[slot][word_id].cell_index = STADIUM_CELL_NONE;
stat_evictions[slot]++;
return STADIUM_CELL_NONE;
@@ -211,7 +219,7 @@ uint64_t stadium_words_resident_heat(VMUuid vm_id) {
StadiumCell *cells;
if (!words_initialized) return 0;
slot = stadium_quota_slot_for_vm(vm_id);
slot = stadium_slot_lock_vm(vm_id);
if (slot < 0) return 0;
cells = stadium_cells();
@@ -220,18 +228,30 @@ uint64_t stadium_words_resident_heat(VMUuid vm_id) {
if (cell == STADIUM_CELL_NONE) continue;
sum += cells[cell].header.heat;
}
stadium_slot_unlock(slot);
return sum;
}
/* FABRIC-3.10.8 MC-0d: the whole dispatch runs under this VM's own quota
* slot lock and nothing else (the row, the cell, the reservoir, admission
* and its eviction fallback are all this slot's) -- no shared lock on the
* word path. A row grow's kmalloc is the one exception (the heap lock). */
static void word_dispatch_locked(int slot, uint32_t word_id, uint64_t heartbeat_ticks);
void stadium_word_dispatch(VMUuid vm_id, uint32_t word_id, uint64_t heartbeat_ticks) {
int slot;
size_t cell;
int slot;
if (!words_initialized) return;
if (word_id == WORD_ID_INVALID) return;
slot = stadium_quota_slot_for_vm(vm_id);
slot = stadium_slot_lock_vm(vm_id);
if (slot < 0) return;
word_dispatch_locked(slot, word_id, heartbeat_ticks);
stadium_slot_unlock(slot);
}
static void word_dispatch_locked(int slot, uint32_t word_id, uint64_t heartbeat_ticks) {
size_t cell;
if (word_id >= row_cap[slot] && grow_row(slot, word_id) != 0) return;
@@ -251,19 +271,19 @@ void stadium_word_dispatch(VMUuid vm_id, uint32_t word_id, uint64_t heartbeat_ti
if (cooled > h->heat) cooled = h->heat;
if (cooled > 0) {
h->heat -= cooled;
stadium_reservoir_push(vm_id, cooled);
stadium_reservoir_push_in_slot(slot, cooled);
}
word_slots[slot][word_id].last_decay_tick = heartbeat_ticks;
}
h->heat += word_dispatch_pull(vm_id, (uint64_t)STADIUM_WORD_HEAT_QUANTUM);
h->heat += word_dispatch_pull(slot, (uint64_t)STADIUM_WORD_HEAT_QUANTUM);
return;
}
/* Not resident: Option B starter-grant admission (§17.7). execution_heat
* plays no role -- density is decided entirely by the pulled quantum. */
{
uint64_t pulled = word_dispatch_pull(vm_id, (uint64_t)STADIUM_WORD_HEAT_QUANTUM);
uint64_t pulled = word_dispatch_pull(slot, (uint64_t)STADIUM_WORD_HEAT_QUANTUM);
StadiumPatronHeader candidate;
uint8_t *raw = (uint8_t *)&candidate;
size_t i;
@@ -279,9 +299,9 @@ void stadium_word_dispatch(VMUuid vm_id, uint32_t word_id, uint64_t heartbeat_ti
candidate.flags = 0; /* unpinned -- words carry no pin exception (§17.3) */
candidate.behaviour = (uint8_t)STADIUM_BEHAVIOUR_COOL;
idx = stadium_admit(vm_id, &candidate);
idx = stadium_admit_in_slot(slot, &candidate);
if (idx == STADIUM_CELL_NONE) {
stadium_reservoir_push(vm_id, pulled); /* rollback: preserve conservation */
stadium_reservoir_push_in_slot(slot, pulled); /* rollback: preserve conservation */
return;
}
@@ -298,25 +318,25 @@ void stadium_word_forget(VMUuid vm_id, uint32_t word_id) {
if (!words_initialized) return;
if (word_id == WORD_ID_INVALID) return;
slot = stadium_quota_slot_for_vm(vm_id);
slot = stadium_slot_lock_vm(vm_id);
if (slot < 0) return;
if (word_id >= row_cap[slot]) return; /* never dispatched here -- can't be resident */
cell = resolve_resident_cell(slot, word_id);
if (cell == STADIUM_CELL_NONE) return;
if (stadium_evict(cell) == 0) {
word_slots[slot][word_id].cell_index = STADIUM_CELL_NONE;
stat_evictions[slot]++;
if (word_id < row_cap[slot]) { /* else never dispatched here -- can't be resident */
cell = resolve_resident_cell(slot, word_id);
if (cell != STADIUM_CELL_NONE && stadium_evict_in_slot(slot, cell) == 0) {
word_slots[slot][word_id].cell_index = STADIUM_CELL_NONE;
stat_evictions[slot]++;
}
}
stadium_slot_unlock(slot);
}
void stadium_words_stats(VMUuid vm_id, uint64_t *promotions, uint64_t *evictions) {
int slot = stadium_quota_slot_for_vm(vm_id);
int slot = stadium_slot_lock_vm(vm_id);
if (promotions) *promotions = (slot >= 0) ? stat_promotions[slot] : 0;
if (evictions) *evictions = (slot >= 0) ? stat_evictions[slot] : 0;
stadium_slot_unlock(slot);
}
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading