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:
co-authored by
Claude Sonnet 5
parent
1d468a65b1
commit
4018fe8b04
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user