starkernel: item 3.7 -- per-VM free lists (Phase 3 core complete, for real)
Punch list §25 item 3.7 complete. Added to §25.4 after starting item 4.1 surfaced it as an unbuilt prerequisite -- 3.6's earlier "Phase 3 core complete" claim is corrected in this same commit. StadiumVMQuota table (size STADIUM_MAX_VM_COUNT, linearly searched by vm_id -- capsule_birth.c's vm_id is monotonic and never reused, so it cannot index a table directly, and a 4-entry scan costs nothing). New per-cell stadium_owner byte array records which quota a cell belongs to, needed so eviction returns a freed cell to the correct VM's list and so eviction search stays scoped to the evicting VM's own residents (quota isolation). Free-list linkage reuses each cell's `link` field as a next-free pointer while unresident -- link is documented only as generic "index into the Stadium, not a pointer," so this is a repurposing, not a header change. Does not answer the separate, still-open question of which field carries a multi-cell patron's first continuation-cell index; item 3.5's mass != 1 refusal stands exactly as it was. Boot-time: every cell chained into one list in ascending index order, granted whole to vm_id 0 (Hera), the only VM that exists. Ascending order preserves item 3.6's "Hera is patron zero" invariant once real birth-wiring lands. stadium_admit()'s signature changed to take vm_id -- a change to code shipped in item 3.5, amended there. Pops the calling VM's free-list head first (O(1)); only falls back to a same-VM-scoped eviction search if empty. Caught a real bug before the boot run: the header zero-fill on eviction (and the initial free-list build) both left contains == 0, but 0 is Hera's valid index -- the same collision item 3.1's STADIUM_CONTAINS_NONE fix addressed, recurring at a new site. Fixed by explicitly setting contains = STADIUM_CONTAINS_NONE at both free-list sites. Explicitly out of scope, reported not invented: granting quota to any VM other than Hera is capacity arbitration (item 1.3 left "how much moves per transfer" open). stadium_owner is set once at boot and never rewritten, so quota_slot_for_vm() refuses every vm_id != 0 permanently until item 4.2 adds the grant path and owner-array writes. Verified: three-architecture boot (amd64, aarch64, riscv64), all reaching ok> with identical dict_hash=0x3d4e1daf289da94f matching the item-3.6 baseline. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
72487e7fff
commit
e55111c2c5
+113
-19
@@ -41,9 +41,49 @@
|
||||
|
||||
static StadiumCell *stadium_cell_array = (StadiumCell *)0;
|
||||
static uint8_t *stadium_bitmap = (uint8_t *)0;
|
||||
static uint8_t *stadium_owner = (uint8_t *)0;
|
||||
static size_t stadium_ncells = 0;
|
||||
static int stadium_initialized = 0;
|
||||
|
||||
/* Sentinel for the header's `link` field while it is reused as a free-list
|
||||
* next-pointer (item 3.7): link is uint32_t, but STADIUM_CELL_NONE is
|
||||
* (size_t)-1 -- 64 bits wide on this target. Casting (size_t)-1 down to
|
||||
* uint32_t truncates to the same bit pattern as this constant (safe), but
|
||||
* casting THIS constant back up to size_t does not sign-extend to
|
||||
* STADIUM_CELL_NONE (unsafe) -- hence the explicit link_to_size()/
|
||||
* size_to_link() conversions below rather than a raw cast either direction. */
|
||||
#define STADIUM_LINK_NONE ((uint32_t)-1)
|
||||
|
||||
static uint32_t size_to_link(size_t v) {
|
||||
return (v == STADIUM_CELL_NONE) ? STADIUM_LINK_NONE : (uint32_t)v;
|
||||
}
|
||||
|
||||
static size_t link_to_size(uint32_t v) {
|
||||
return (v == STADIUM_LINK_NONE) ? STADIUM_CELL_NONE : (size_t)v;
|
||||
}
|
||||
|
||||
/*
|
||||
* StadiumVMQuota - one VM's ownership record (item 3.7, FABRIC.md §22.3).
|
||||
* See stadium.h's stadium_admit() doc for why vm_id needs a linear search
|
||||
* rather than direct indexing.
|
||||
*/
|
||||
typedef struct {
|
||||
uint32_t vm_id;
|
||||
int in_use;
|
||||
size_t free_head;
|
||||
} StadiumVMQuota;
|
||||
|
||||
static StadiumVMQuota stadium_quotas[STADIUM_MAX_VM_COUNT];
|
||||
|
||||
/* Returns the quota slot index for vm_id, or -1 if none is granted. */
|
||||
static int quota_slot_for_vm(uint32_t vm_id) {
|
||||
int i;
|
||||
for (i = 0; i < STADIUM_MAX_VM_COUNT; i++) {
|
||||
if (stadium_quotas[i].in_use && stadium_quotas[i].vm_id == vm_id) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading zeros. */
|
||||
static void console_put_u64(uint64_t v) {
|
||||
char buf[21];
|
||||
@@ -73,10 +113,12 @@ int stadium_boot_init(void) {
|
||||
|
||||
StadiumCell *cells = (StadiumCell *)kmalloc(ncells * STADIUM_CELL_BYTES);
|
||||
uint8_t *bitmap = (uint8_t *)kmalloc(bitmap_bytes);
|
||||
if (!cells || !bitmap) {
|
||||
uint8_t *owner = (uint8_t *)kmalloc(ncells);
|
||||
if (!cells || !bitmap || !owner) {
|
||||
console_println("Stadium: kmalloc failed for boot-time allocation");
|
||||
if (cells) kfree(cells);
|
||||
if (bitmap) kfree(bitmap);
|
||||
if (owner) kfree(owner);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -91,8 +133,34 @@ int stadium_boot_init(void) {
|
||||
for (i = 0; i < bitmap_bytes; i++) bitmap[i] = 0;
|
||||
}
|
||||
|
||||
/* Item 3.7: chain every cell into one free list, ascending index order
|
||||
* (so the first-ever pop returns cell 0, per item 3.6), granted whole
|
||||
* to vm_id 0 (Hera) -- the only VM that exists (item 0.1). Reuses each
|
||||
* cell's own `link` field as the next-free-cell pointer while
|
||||
* unresident; see stadium_admit()'s doc for the scope of that reuse. */
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0; i < ncells; i++) {
|
||||
cells[i].header.link = size_to_link((i + 1 < ncells) ? (i + 1) : STADIUM_CELL_NONE);
|
||||
cells[i].header.contains = STADIUM_CONTAINS_NONE;
|
||||
owner[i] = 0;
|
||||
}
|
||||
}
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < STADIUM_MAX_VM_COUNT; i++) {
|
||||
stadium_quotas[i].vm_id = 0;
|
||||
stadium_quotas[i].in_use = 0;
|
||||
stadium_quotas[i].free_head = STADIUM_CELL_NONE;
|
||||
}
|
||||
}
|
||||
stadium_quotas[0].vm_id = 0;
|
||||
stadium_quotas[0].in_use = 1;
|
||||
stadium_quotas[0].free_head = 0;
|
||||
|
||||
stadium_cell_array = cells;
|
||||
stadium_bitmap = bitmap;
|
||||
stadium_owner = owner;
|
||||
stadium_ncells = ncells;
|
||||
stadium_initialized = 1;
|
||||
|
||||
@@ -197,6 +265,7 @@ static void bitmap_clear(size_t cell_index) {
|
||||
*/
|
||||
int stadium_evict(size_t cell_index) {
|
||||
StadiumPatronHeader *header;
|
||||
uint8_t slot;
|
||||
|
||||
if (cell_index >= stadium_ncells) return -1;
|
||||
if (!bitmap_get(cell_index)) return -1;
|
||||
@@ -211,6 +280,7 @@ int stadium_evict(size_t cell_index) {
|
||||
|
||||
stadium_dispatch(cell_index, (StadiumBehaviour)header->behaviour);
|
||||
bitmap_clear(cell_index);
|
||||
slot = stadium_owner[cell_index];
|
||||
|
||||
{
|
||||
uint8_t *raw = (uint8_t *)header;
|
||||
@@ -218,27 +288,40 @@ int stadium_evict(size_t cell_index) {
|
||||
for (i = 0; i < sizeof(*header); i++) raw[i] = 0;
|
||||
}
|
||||
|
||||
/* Item 3.7: return the freed cell to its owning VM's free list. contains
|
||||
* must be set to the real "none" sentinel here, not left at the zero
|
||||
* the fill above just wrote -- 0 is Hera's valid index, so a stray zero
|
||||
* would make this freed cell look permanently "contains Hera" to the
|
||||
* very next admit() that pops it. */
|
||||
header->link = size_to_link(stadium_quotas[slot].free_head);
|
||||
header->contains = STADIUM_CONTAINS_NONE;
|
||||
stadium_quotas[slot].free_head = cell_index;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* FABRIC.md §19.3: admit if denser than the least-dense resident. Free-cell
|
||||
* placement first (no comparison needed -- §19.3 only governs the full
|
||||
* case); otherwise finds the least-dense evictable resident (pinned and
|
||||
* FABRIC.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
|
||||
* rule only governs the full case). Only if that list is empty does this
|
||||
* fall back to eviction, scoped to that SAME VM's own residents (quota
|
||||
* isolation), finding the least-dense evictable one (pinned and
|
||||
* contains-gated residents are skipped, never eviction candidates) and
|
||||
* evicts it only if the candidate is strictly denser.
|
||||
* evicting it only if the candidate is strictly denser.
|
||||
*/
|
||||
size_t stadium_admit(const StadiumPatronHeader *candidate) {
|
||||
size_t stadium_admit(uint32_t vm_id, const StadiumPatronHeader *candidate) {
|
||||
int slot;
|
||||
size_t i;
|
||||
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;
|
||||
|
||||
/* Multi-cell patrons need their continuation chain allocated too, which
|
||||
* needs the per-VM free lists item 3.2 deferred (§22.3) -- not this
|
||||
* item's scope. Refuse rather than admit only the header and leak the
|
||||
/* Multi-cell patrons need a continuation chain, and no header field is
|
||||
* documented as carrying one's first index -- see this function's doc
|
||||
* in stadium.h. Refuse rather than admit only the header and leak the
|
||||
* rest, which would break capacity conservation. */
|
||||
if (candidate->mass != 1) return STADIUM_CELL_NONE;
|
||||
|
||||
@@ -249,18 +332,22 @@ size_t stadium_admit(const StadiumPatronHeader *candidate) {
|
||||
if (candidate->contains != STADIUM_CONTAINS_NONE &&
|
||||
candidate->contains >= stadium_ncells) return STADIUM_CELL_NONE;
|
||||
|
||||
for (i = 0; i < stadium_ncells; i++) {
|
||||
if (!bitmap_get(i) && stadium_cell_array[i].header.mass == 0) {
|
||||
stadium_cell_array[i].header = *candidate;
|
||||
bitmap_set(i);
|
||||
return i;
|
||||
}
|
||||
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);
|
||||
stadium_cell_array[idx].header = *candidate;
|
||||
bitmap_set(idx);
|
||||
return idx;
|
||||
}
|
||||
|
||||
for (i = 0; i < stadium_ncells; i++) {
|
||||
StadiumPatronHeader *h;
|
||||
|
||||
if (!bitmap_get(i)) continue;
|
||||
if (stadium_owner[i] != (uint8_t)slot) continue;
|
||||
h = &stadium_cell_array[i].header;
|
||||
if (h->flags & STADIUM_FLAG_PIN) continue;
|
||||
if (h->contains != STADIUM_CONTAINS_NONE) continue;
|
||||
@@ -276,14 +363,21 @@ size_t stadium_admit(const StadiumPatronHeader *candidate) {
|
||||
|
||||
if (least_dense_index == STADIUM_CELL_NONE) return STADIUM_CELL_NONE;
|
||||
|
||||
candidate_density = candidate->mass ? (candidate->heat / (uint64_t)candidate->mass) : 0;
|
||||
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;
|
||||
|
||||
stadium_cell_array[least_dense_index].header = *candidate;
|
||||
bitmap_set(least_dense_index);
|
||||
return least_dense_index;
|
||||
/* stadium_evict() 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. */
|
||||
idx = stadium_quotas[slot].free_head;
|
||||
stadium_quotas[slot].free_head = link_to_size(stadium_cell_array[idx].header.link);
|
||||
stadium_cell_array[idx].header = *candidate;
|
||||
bitmap_set(idx);
|
||||
return idx;
|
||||
}
|
||||
|
||||
#endif /* __STARKERNEL__ */
|
||||
Reference in New Issue
Block a user