Single-block relocation: RELOCATE-BLOCK, resolve_lbn(), persisted exception table

Implements the full design from the prior commit in one pass. resolve_lbn()
is the single choke point threaded through the ten public LBN-consuming
entry points (blk_get_buffer, blk_update, blk_flush, blk_is_allocated,
blk_mark_allocated, blk_mark_free, blk_is_valid, blk_get_meta, blk_set_meta,
plus blk_get_empty_buffer covered via delegation) -- an LBN->LBN redirect,
not a new storage allocator, since the LBN space is already unified across
every attached blkio_dev backend. VM window cache staleness across a
relocation reuses the existing blk_vm_check_epoch() mechanism from
Milestone 2h's hot-detach fix for free -- g.epoch bumps on relocation too.

Persistence lands in the same pass: two new uint32_t fields
(reloc_start/reloc_devblocks) appended after hdr_crc in blk_volume_meta_t,
carved from existing padding without moving any earlier field's byte
offset -- an old formatted volume's zeroed padding reads back as
reloc_devblocks=0 ("no reloc capacity"), gracefully, not a format-breaking
change. compute_totals_from_B() generalized to account for the new
reserved region. reloc_flush_to_disk()/reloc_load_from_disk() mirror the
BAM I/O functions' own absolute-devblock-addressing shape; the persisted
copy's owner is first_disk_slot() (already existed, already used for this
exact "which device is canonical" question by blk_get_volume_meta()).
blk_subsys_relocate_block() is a mechanical primitive only -- copies
content (staged through a local buffer, since obtaining the target's
blk_get_buffer() result can evict and invalidate the source's cache
pointer if they share a device), frees the source BAM entry, appends the
exception entry, bumps the epoch, flushes to disk. RELOCATE-BLOCK exposes
it to FORTH, no policy of its own (ACL's job, per this session's direction).

A first live-test attempt gave a false negative against disk/artemis.img
(predates reloc capacity, so relocation only ever existed in memory that
boot) -- traced to the test's own setup before being mistaken for a bug,
then re-verified correctly against a fresh volume (new fixture,
disk/artemis-reloc-test.img): relocated a RAMDRIVE block to the fresh
disk, confirmed live resolution through the redirect, then confirmed both
the redirect and the relocated content survived an abrupt QEMU kill and
full reboot. Also fixed three lingering "glibc" doc-comment
misattributions from Milestone 2h (the actual allocator is this kernel's
own kmalloc) that survived an earlier FABRIC-2.md-only correction. All
three architectures re-verified clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
This commit is contained in:
Robert Allan James
2026-08-25 19:14:01 -04:00
co-authored by Claude Sonnet 5
parent 073dae4f56
commit 36d832ff47
22 changed files with 63612 additions and 21 deletions
+52 -7
View File
@@ -4139,19 +4139,64 @@ mechanical primitive only — copies content, updates BAM/exception-table state,
signal) and *validating* that a target LBN is genuinely owned by the right identity are ACL's
job, per this session's own direction — not something this function enforces on its own.
- [ ] Add `reloc_start`/`reloc_devblocks` to `blk_volume_meta_t`, adjust fresh-format geometry
- [x] Add `reloc_start`/`reloc_devblocks` to `blk_volume_meta_t`, adjust fresh-format geometry
to reserve one devblock for the reloc table
- [ ] Implement `resolve_lbn()` and thread it through the ten public entry points listed above
- [ ] Implement `reloc_flush_to_disk()`/`reloc_load_from_disk()` (mirroring the BAM I/O
- [x] Implement `resolve_lbn()` and thread it through the ten public entry points listed above
- [x] Implement `reloc_flush_to_disk()`/`reloc_load_from_disk()` (mirroring the BAM I/O
functions' own shape), wired into `blk_subsys_attach_device()` (load once, from
`first_disk_slot()`) and `blk_commit_format()` (zero the region on fresh format)
- [ ] Implement `blk_subsys_relocate_block(home_lbn, target_lbn)`: copy content, free the
- [x] Implement `blk_subsys_relocate_block(home_lbn, target_lbn)`: copy content, free the
source BAM entry, append the exception entry, bump `blk_subsys_epoch()`, flush to disk
- [ ] Add a FORTH word exposing it (matching this project's "compose in FORTH first"
convention — ACL/higher-level policy code needs a callable entry point)
- [ ] Live-verify: relocate a block between two writable devices (Artemis disk ↔ RAMDRIVE),
- [x] Add a FORTH word exposing it (matching this project's "compose in FORTH first"
convention — ACL/higher-level policy code needs a callable entry point). `RELOCATE-BLOCK
( home target -- )`
- [x] Live-verify: relocate a block between two writable devices (Artemis disk ↔ RAMDRIVE),
confirm content and the redirect both survive an abrupt-kill/reboot cycle (same
methodology as Section V item 6's own persistence test)
**All done 2026-08-25, in one pass (design → implementation → live verification), all three
architectures re-verified. Full writeup:**
Implemented exactly as designed above, with one addition found necessary mid-implementation:
`compute_totals_from_B()` (the function that derives `total_blocks`/`tracked_blocks` from a
device's geometry) hardcoded its payload-region math as `total_devblocks - 1 - bam_devblocks`
(the `1` being the header devblock) — didn't yet know about the new reloc region at all.
Generalized to `total_devblocks - 1 - bam_devblocks - reloc_devblocks`, reading
`reloc_devblocks` off the same `blk_volume_meta_t` the caller already populated; single call
site, no other changes needed.
**A first live-test attempt gave a false negative, caught before being mistaken for a bug.**
The first persistence run used `disk/artemis.img` as the relocation target: attach, write,
relocate (LBN 2048 → 25000), live read-back correctly showed the written byte (`119`), abrupt
kill, reboot — read-back came back `0`, not `119`. Before concluding the mechanism was broken,
traced it to the test's own setup: `artemis.img` is a volume formatted *before* this session's
work, so its on-disk `reloc_devblocks` reads back as `0` from what was previously unused header
padding — exactly the documented, intentional "old volumes gracefully lack reloc capacity"
behavior from this design's own persistence section, not a defect. `reloc_flush_to_disk()`
correctly no-op'd (nothing to write to); the relocation only ever existed in memory for that one
boot, which is exactly why it didn't survive a reboot. Re-ran against a genuinely fresh volume
instead (`disk/artemis-reloc-test.img`, new fixture — see `disk/README.md`): confirmed the fresh
format via `BLK-CONFIRM-FORMAT`, wrote a marker byte to LBN 2048 (the volatile RAMDRIVE), ran
`2048 3200 RELOCATE-BLOCK`, confirmed live read-back through the redirect (`119`), killed QEMU
abruptly via QMP (no `BYE`, no `SAVE-BUFFERS`), rebooted with the same disk image, and read LBN
2048 back again: `119` — both the redirect and the relocated content survived the reboot.
Because LBN 2048's *original* backing (RAMDRIVE) is volatile and resets to zero every boot, a
correct `119` read-back specifically proves the *persisted* redirect was followed, not any
leftover RAMDRIVE state. `logs/20260825-185704/amd64/` + `logs/20260825-185849/amd64/` are the
false-negative pair against `artemis.img` (kept as evidence the "old volumes lack capacity"
behavior is real and was actually hit, not just theorized); `logs/20260825-190040/amd64/` +
`logs/20260825-190503/amd64/` are the successful pair against the fresh volume.
Also fixed while in the neighborhood: the "glibc's allocator" misattribution from Milestone 2h's
epoch-based cache-invalidation fix was still present in three in-code doc comments
(`block_subsystem.h`, `block_subsystem.c`, `block_words.c`) even after being corrected in this
file's own prose earlier in the session — corrected all three to name the actual allocator
(this kernel's own first-fit `kmalloc`, `src/starkernel/memory/kmalloc.c`).
All three architectures re-verified clean with the default `disk/artemis.img` (no relocation
performed on these boots — just confirming the final code path boots clean with an old,
reloc-incapable volume attached, the common case going forward until volumes get reformatted):
`logs/20260825-190851/amd64/`, `logs/20260825-191007/aarch64/`, `logs/20260825-191200/riscv64/`.
- [x] Implement the `sk_repl_idle()` body — the cheap "anything dirty? no? done" check
(Section V confirmed this hook is empty and ready right now, doesn't even need
Milestone 2 to be written, only to be *tested end to end*). **Done 2026-08-25**, see