FABRIC-3.md §I.2: FIRSTTOUCH + migration state machine (blk_meta_relocate_devblock)

Closes the block-subsystem punch-list item -- built exactly to §F.11's
already-decided algorithm after re-verifying it against current
blk_meta_t (a 2026-09-03 re-scoping note had wrongly claimed the chain
fields no longer existed; they do, untouched by BMAPFMT).

blk_firsttouch_claim(): one linear scan of Artemis's own device
(new blk_get_first_disk_range(), correctly bounding the scan instead of
the global multi-device LBN space), scattered-chain claim via
prev_block/next_block/chain_length, owner_fp stamped on every member
devblock, fails outright with no partial claim.

blk_meta_relocate_devblock(): the real migration primitive -- bridges
the existing FORTH-block-granularity blk_subsys_relocate_block() up to
devblock granularity (BLK_PACK_RATIO=3, corrected mid-design), running
it 3x and transferring blk_meta_t ownership fields. The "migration
state machine" turned out to be just the 2 states BLK_FLAG_MIGRATING
already reserved; the real design work was the trigger. Two were
scoped in conversation (overflow onto Artemis; heat-based wear
leveling); heat/wear-leveling is built and wired into sk_repl_idle()
via blk_meta_t.write_count. Overflow is deliberately left open,
precisely scoped (needs a slot-lookup-by-device-pointer call site
threaded from WIREBIND) rather than guessed at.

Also flagged, not fixed: BMAPFMT's owner_fp/CLAIMED and the pre-existing
BAM allocator are two parallel, unreconciled accounting systems --
FIRSTTOUCH/relocate only touch the former.

Verified 3-arch boot to ok> (amd64/aarch64/riscv64, each in the
foreground); logs and DoE CSVs from this session's verification runs
included per this repo's own audit-artifact convention.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YcT3H2PQeyujrzjqS3Var
This commit is contained in:
Robert Allan James
2026-09-04 10:33:47 -04:00
co-authored by Claude Sonnet 5
parent 1d468a65b1
commit 4018fe8b04
13 changed files with 27992 additions and 17 deletions
+198
View File
@@ -1136,6 +1136,44 @@ int blk_set_volume_meta(const blk_volume_meta_t *meta) {
return BLK_OK;
}
/* FABRIC-3.md §I.2, 2026-09-04: same dev-pointer slot lookup
* blk_subsys_detach_device() already does internally, exposed publicly
* for the first time so a caller can scope a scan/query to one specific
* attached device. */
static blk_dev_slot_t *slot_by_dev(struct blkio_dev *dev) {
blk_dev_slot_t *s = g.head;
while (s) { if (s->dev == dev) return s; s = s->next; }
return NULL;
}
int blk_get_device_range(struct blkio_dev *dev, uint32_t *out_start_lbn, uint32_t *out_count) {
if (!dev || !out_start_lbn || !out_count) return BLK_EINVAL;
blk_dev_slot_t *slot = slot_by_dev(dev);
if (!slot) return BLK_ENODEV;
*out_start_lbn = slot->start_lbn;
*out_count = slot->user_blocks;
return BLK_OK;
}
int blk_get_device_free_blocks(struct blkio_dev *dev, uint64_t *out_free, uint64_t *out_total) {
if (!dev || !out_free || !out_total) return BLK_EINVAL;
blk_dev_slot_t *slot = slot_by_dev(dev);
if (!slot) return BLK_ENODEV;
if (slot->raw_base) { *out_free = 0; *out_total = 0; return BLK_OK; }
*out_free = slot->vol_meta.free_blocks;
*out_total = slot->vol_meta.total_blocks;
return BLK_OK;
}
int blk_get_first_disk_range(uint32_t *out_start_lbn, uint32_t *out_count) {
if (!out_start_lbn || !out_count) return BLK_EINVAL;
blk_dev_slot_t *slot = first_disk_slot();
if (!slot) return BLK_ENODEV;
*out_start_lbn = slot->start_lbn;
*out_count = slot->user_blocks;
return BLK_OK;
}
/*
* Top-of-device system-metadata fence I/O (Phase 8, 2026-08-26).
* Raw, unpacked 4 KiB devblocks -- no Forth-block packing, same shape as
@@ -1317,6 +1355,166 @@ int blk_flags_set(uint32_t block_num, uint64_t flags) {
return blk_set_meta(block_num, &meta);
}
/* FABRIC-3.md §F.11/§I.2, built 2026-09-04. is_lbn_zero() -- BLK_FLAG_
* CLAIMED clear AND owner_fp all-zero is "unowned"; either alone would
* misclassify a claimed-but-not-yet-fp-stamped or fp-stamped-but-
* evicted devblock, neither of which should exist given owner_fp is
* always stamped/cleared alongside the flag below, but checking both is
* one extra memcmp for a real safety margin, not paranoia without cost. */
static int devblock_is_free(const blk_meta_t *m) {
static const uint8_t zero_fp[8] = {0};
return !(m->flags & BLK_FLAG_CLAIMED) && memcmp(m->owner_fp, zero_fp, 8) == 0;
}
/* Sane upper bound on one claim, independent of how large count is --
* same "generous headroom, not a real constraint" reasoning as
* capsule_wirebind.c's WIREBIND_CERT_MAX_DEVBLOCKS: a stack-allocated
* scratch array of found-devblock LBNs needs a fixed bound, and no
* caller of this session's own scope needs more than a handful of
* devblocks in one claim. */
#define BLK_FIRSTTOUCH_MAX_CLAIM 256u
int blk_firsttouch_claim(const uint8_t owner_fp[8], uint32_t count, uint32_t *out_chain_head) {
if (!owner_fp || !out_chain_head || count == 0) return BLK_EINVAL;
blk_dev_slot_t *slot = first_disk_slot();
if (!slot) return BLK_ENODEV;
/* One linear scan, full range, no cached index (§F.11 decision 2).
* Walks every LBN in the slot's own range but only *acts* on the one
* representative LBN per devblock (slot_pbn_pack_offset() == 0) --
* blk_get_meta()/blk_set_meta() already resolve any of a devblock's
* BLK_PACK_RATIO member LBNs to the same shared blk_meta_t, so
* visiting the others would just re-read the identical struct. */
uint32_t found[BLK_FIRSTTOUCH_MAX_CLAIM];
uint32_t nfound = 0;
if (count > BLK_FIRSTTOUCH_MAX_CLAIM) return BLK_ENOSPC;
for (uint32_t lbn = slot->start_lbn;
lbn < slot->start_lbn + slot->user_blocks && nfound < count;
lbn++) {
uint32_t rel_pbn = lbn_to_slot_pbn(slot, lbn);
if (slot_pbn_pack_offset(rel_pbn) != 0) continue; /* not this devblock's representative LBN */
blk_meta_t meta;
if (blk_get_meta(lbn, &meta) != BLK_OK) continue;
if (devblock_is_free(&meta)) found[nfound++] = lbn;
}
if (nfound < count) return BLK_ENOSPC; /* fail outright, no partial claim (§F.11 decision 3) */
/* Link the scattered chain and stamp owner_fp onto every member
* (§F.11 decision 1) -- not just the head, so ownership reads
* locally from any member without walking the chain. */
for (uint32_t i = 0; i < nfound; i++) {
blk_meta_t meta;
if (blk_get_meta(found[i], &meta) != BLK_OK) return BLK_EIO;
memcpy(meta.owner_fp, owner_fp, 8);
meta.flags |= BLK_FLAG_CLAIMED;
meta.prev_block = (i == 0) ? 0 : found[i - 1];
meta.next_block = (i + 1 == nfound) ? 0 : found[i + 1];
meta.chain_length = nfound;
if (blk_set_meta(found[i], &meta) != BLK_OK) return BLK_EIO;
}
*out_chain_head = found[0];
return BLK_OK;
}
int blk_meta_relocate_devblock(uint32_t home_devblock, uint32_t target_devblock) {
if (home_devblock == target_devblock) return BLK_EINVAL;
blk_meta_t home_meta;
int rc = blk_get_meta(home_devblock, &home_meta);
if (rc != BLK_OK) return rc;
home_meta.flags |= BLK_FLAG_MIGRATING;
if (blk_set_meta(home_devblock, &home_meta) != BLK_OK) return BLK_EIO;
/* Devblock granularity is BLK_PACK_RATIO FORTH blocks -- move each
* one via the existing, only, FORTH-block-granularity relocation
* primitive. NOT atomic across this loop -- see this function's own
* doc comment (block_subsystem.h) for what a mid-loop failure leaves
* behind (BLK_FLAG_MIGRATING still set, ownership not yet
* transferred -- a real, documented limitation, not silently
* dropped). */
for (uint32_t i = 0; i < BLK_PACK_RATIO; i++) {
rc = blk_subsys_relocate_block(home_devblock + i, target_devblock + i);
if (rc != BLK_OK) return rc;
}
blk_meta_t target_meta;
rc = blk_get_meta(target_devblock, &target_meta);
if (rc != BLK_OK) return rc;
memcpy(target_meta.owner_fp, home_meta.owner_fp, sizeof(target_meta.owner_fp));
target_meta.acl_allow = home_meta.acl_allow;
target_meta.acl_ttl = home_meta.acl_ttl;
target_meta.flags |= BLK_FLAG_CLAIMED;
if (blk_set_meta(target_devblock, &target_meta) != BLK_OK) return BLK_EIO;
memset(home_meta.owner_fp, 0, sizeof(home_meta.owner_fp));
home_meta.acl_allow = 0;
home_meta.acl_ttl = 0;
home_meta.flags &= ~(BLK_FLAG_CLAIMED | BLK_FLAG_MIGRATING);
return blk_set_meta(home_devblock, &home_meta);
}
/* FABRIC-3.md §I.2, 2026-09-04: heat/wear-leveling migration trigger.
* Uses blk_meta_t.write_count -- already present, already documented
* for exactly this purpose ("Number of writes (wear leveling)"), no new
* cross-subsystem query needed (Stadium's own compudynamics block heat,
* stadium_blocks.c, has no per-LBN getter exposed and is a different
* granularity/subsystem -- the wrong tool here, not reused). Runs one
* linear scan of Artemis's own device (first_disk_slot()) per call,
* same discovery discipline as blk_firsttouch_claim(); a resident
* devblock whose write_count crosses MIGRATION_WEAR_THRESHOLD and isn't
* already MIGRATING gets relocated to the first free devblock found in
* the same scan. Fixed threshold, not yet DoE-measured or Kconfig-tuned
* -- same "fixed first, adaptive later" sequencing this project already
* uses elsewhere (e.g. capsule_zuse_boot.c's ZUSE_SESSION_TTL_SECONDS).
*
* Overflow-triggered migration (a specific *user's* device running low
* on space) is deliberately NOT built here -- it needs a slot-lookup-by-
* device-pointer call site threaded from wherever the currently-attached
* user identity's own blkio_dev is known (WIREBIND, capsule_wirebind.c),
* not decided in this pass. blk_firsttouch_claim()/blk_meta_relocate_
* devblock() are already the mechanism it would call -- only the
* trigger-detection call site is the remaining gap. */
#define MIGRATION_WEAR_THRESHOLD 10000u
void blk_migration_idle_check(void) {
blk_dev_slot_t *slot = first_disk_slot();
if (!slot) return;
uint32_t hot_lbn = 0, free_lbn = 0;
int have_hot = 0, have_free = 0;
for (uint32_t lbn = slot->start_lbn;
lbn < slot->start_lbn + slot->user_blocks && !(have_hot && have_free);
lbn++) {
uint32_t rel_pbn = lbn_to_slot_pbn(slot, lbn);
if (slot_pbn_pack_offset(rel_pbn) != 0) continue;
blk_meta_t meta;
if (blk_get_meta(lbn, &meta) != BLK_OK) continue;
if (!have_hot && (meta.flags & BLK_FLAG_CLAIMED) &&
!(meta.flags & BLK_FLAG_MIGRATING) &&
meta.write_count >= MIGRATION_WEAR_THRESHOLD) {
hot_lbn = lbn;
have_hot = 1;
}
if (!have_free && devblock_is_free(&meta)) {
free_lbn = lbn;
have_free = 1;
}
}
if (have_hot && have_free && hot_lbn != free_lbn) {
(void) blk_meta_relocate_devblock(hot_lbn, free_lbn);
}
}
/* ===== weak hook (for main.c) ===== */
#if defined(__GNUC__) || defined(__clang__)
__attribute__((weak))
+10
View File
@@ -325,6 +325,16 @@ static void sk_repl_idle(VM *active_vm)
* did not survive a reboot until this fix. */
blk_vm_flush_all(active_vm);
/* FABRIC-3.md §I.2, built 2026-09-04: heat/wear-leveling migration
* trigger -- one linear scan of Artemis's own device per idle tick
* (same ~1 Hz SK_IDLE_BEAT_INTERVAL cadence this whole function
* already runs at, chosen so a hot devblock is caught proactively
* rather than only on a failed write). See block_subsystem.c's own
* doc comment on blk_migration_idle_check() for what's built (heat-
* based relocation) vs. deliberately left open (overflow-triggered
* migration, needs a call site threaded from WIREBIND). */
blk_migration_idle_check();
/* FABRIC-3.md Phase C (2026-08-28): distributed messaging pump. Every
* live VM except Hera herself now owns its own MSG-ARENA/CH-ARENA and
* MSG-TICK word (see capsules/common/messaging.4th) instead of only