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:
co-authored by
Claude Sonnet 5
parent
073dae4f56
commit
36d832ff47
+52
-7
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Capsule Block Manifest — Auto-generated
|
||||
<!-- Generated by mkcapsule --manifest 2026-08-25T18:44:46Z -->
|
||||
<!-- Generated by mkcapsule --manifest 2026-08-25T23:11:30Z -->
|
||||
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
|
||||
<!-- Hand-written justifications and immutability notes live -->
|
||||
<!-- in MANIFEST.md alongside this auto-generated index. -->
|
||||
|
||||
@@ -71,6 +71,17 @@ carrying timestamp noise in git history.
|
||||
silently replaying the old device's cached (all-zero) content, which is
|
||||
exactly the class of bug a same-LBN-range device swap can cause if the
|
||||
VM block window cache (`vm->blk_vm_cbuf[]`) isn't re-validated on a hit.
|
||||
- `artemis-reloc-test.img` — 64MB raw image, added 2026-08-25 for single-block
|
||||
relocation (`RELOCATE-BLOCK`) persistence verification. Blank at creation;
|
||||
a genuinely *fresh* volume was required (not `artemis.img`) because
|
||||
relocation-table capacity (`reloc_start`/`reloc_devblocks` in
|
||||
`blk_volume_meta_t`) is only reserved by `blk_compute_fresh_geometry()` on
|
||||
a fresh format — `artemis.img` predates the feature and correctly reads
|
||||
back `reloc_devblocks=0` from what was previously unused header padding.
|
||||
Attach via `make -f Makefile.starkernel ARTDISK=disk/artemis-reloc-test.img
|
||||
...` (the Makefile's `ARTDISK` var is `?=`-overridable). Not yet
|
||||
`BLK-CONFIRM-FORMAT`-committed in the repo copy — commit that step live if
|
||||
reusing this fixture for further reloc-table testing.
|
||||
|
||||
**Convention, standing as of 2026-08-22: every virtual disk/thumb-drive image
|
||||
used for testing — Artemis persistence disks above, and USB Mass Storage
|
||||
|
||||
Binary file not shown.
@@ -113,7 +113,7 @@ typedef struct {
|
||||
/* BAM placement (external 1-bit bitmap region, stored in 4 KiB pages) */
|
||||
uint32_t bam_start; /* usually 1 */
|
||||
uint32_t bam_devblocks; /* number of 4 KiB pages used by BAM */
|
||||
uint32_t devblock_base; /* first payload devblock = bam_start + bam_devblocks */
|
||||
uint32_t devblock_base; /* first payload devblock = reloc_start + reloc_devblocks */
|
||||
|
||||
/* Capacity modeling (Forth 1 KiB blocks tracked/usable) */
|
||||
uint64_t tracked_blocks; /* 32768 * bam_devblocks (bits per 4 KiB page) */
|
||||
@@ -135,6 +135,15 @@ typedef struct {
|
||||
/* Optional integrity (unused yet) */
|
||||
uint64_t hdr_crc;
|
||||
|
||||
/* Relocation-exception table placement (Milestone 2h+ single-block relocation).
|
||||
* Appended after hdr_crc, carved out of what was previously _pad[] -- appending
|
||||
* (not inserting) preserves every earlier field's byte offset, so a pre-existing
|
||||
* formatted volume's zeroed padding reads back here as reloc_devblocks=0 ("no
|
||||
* reloc capacity"), gracefully, not a format-breaking change. See
|
||||
* block_subsystem.c's reloc_flush_to_disk()/reloc_load_from_disk(). */
|
||||
uint32_t reloc_start; /* usually bam_start + bam_devblocks */
|
||||
uint32_t reloc_devblocks; /* number of 4 KiB pages used by the reloc table (0 = none) */
|
||||
|
||||
/* Padding to keep header ≤ 4096 bytes */
|
||||
uint8_t _pad[4096 - (
|
||||
4 + 4 + /* magic, version */
|
||||
@@ -145,7 +154,8 @@ typedef struct {
|
||||
8 + 8 + /* first_free, last_allocated */
|
||||
4 + 4 + /* reserved ranges */
|
||||
8 + 8 + /* timestamps */
|
||||
8 /* hdr_crc */
|
||||
8 + /* hdr_crc */
|
||||
4 + 4 /* reloc_start, reloc_devblocks */
|
||||
)];
|
||||
} blk_volume_meta_t;
|
||||
|
||||
@@ -236,15 +246,39 @@ int blk_subsys_detach_device(struct blkio_dev *dev);
|
||||
|
||||
/* Monotonic counter, bumped on every attach/detach (Milestone 2h). A raw
|
||||
* pointer comparison against a blk_get_buffer() result cannot reliably
|
||||
* detect a same-address device swap (glibc's allocator can hand back the
|
||||
* exact address just free()'d by a detach to the very next attach's
|
||||
* calloc()) -- callers that cache a blk_get_buffer() result across calls
|
||||
* detect a same-address device swap (this kernel's own first-fit kmalloc,
|
||||
* src/starkernel/memory/kmalloc.c, can hand back the exact address just
|
||||
* free()'d by a detach to the very next attach's calloc() -- confirmed
|
||||
* live) -- callers that cache a blk_get_buffer() result across calls
|
||||
* (block_words.c's VM block window) must instead compare this epoch
|
||||
* against the value they last observed, invalidating their whole cache on
|
||||
* any change rather than trusting a stored pointer's identity.
|
||||
*/
|
||||
uint64_t blk_subsys_epoch(void);
|
||||
|
||||
/* Single-block relocation. Copies home_lbn's current content to target_lbn
|
||||
* (both must already be valid LBNs -- target_lbn is expected to be a block
|
||||
* the caller's own identity already owns, on whichever device it's being
|
||||
* relocated to; this function does not itself validate ownership, that's
|
||||
* policy, left to the caller -- see this session's ACL-owns-policy
|
||||
* direction in FABRIC-2.md), frees home_lbn's original BAM entry, records
|
||||
* an LBN->LBN redirect so every future access to home_lbn transparently
|
||||
* resolves to target_lbn instead, and bumps blk_subsys_epoch() so any VM's
|
||||
* cached block window correctly invalidates. Persisted immediately to the
|
||||
* relocation-owner device's on-disk table (see block_subsystem.c's
|
||||
* reloc_flush_to_disk()) if one exists.
|
||||
*
|
||||
* Returns BLK_OK on success.
|
||||
* Returns BLK_EINVAL if home_lbn == target_lbn, or home_lbn is already
|
||||
* relocated (call again with a different target to re-relocate --
|
||||
* not supported by simply calling this twice on the same home_lbn).
|
||||
* Returns BLK_ERANGE if either LBN doesn't resolve to a valid device.
|
||||
* Returns BLK_EIO if the content copy fails (e.g. target device refuses
|
||||
* the write -- a read-only backend like blkio_usb.c today).
|
||||
* Returns BLK_ENOMEM if the in-memory relocation table is full.
|
||||
*/
|
||||
int blk_subsys_relocate_block(uint32_t home_lbn, uint32_t target_lbn);
|
||||
|
||||
int blk_subsys_shutdown(void);
|
||||
|
||||
uint8_t *blk_get_buffer(uint32_t block_num, int writable);
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+192
-5
@@ -71,6 +71,20 @@
|
||||
#define META_PER_BLOCK (META_REGION_SIZE / BLK_PACK_RATIO)
|
||||
#define DISK_CACHE_SLOTS 8
|
||||
|
||||
/* Single-block relocation (Milestone 2h+). One reserved 4 KiB devblock:
|
||||
* a 4-byte count prefix plus up to BLK_RELOC_MAX_ENTRIES 8-byte
|
||||
* {home_lbn, actual_lbn} pairs (500*8+4 = 4004 <= 4096). Relocations are
|
||||
* expected rare, not routine -- see FABRIC-2.md's own design writeup --
|
||||
* so a linear-scanned fixed array is deliberate, matching this file's
|
||||
* existing tolerance for small bounded scans (DISK_CACHE_SLOTS above,
|
||||
* BLK_VM_SLOTS in block_words.c are the same shape). */
|
||||
#define BLK_RELOC_MAX_ENTRIES 500u
|
||||
|
||||
typedef struct {
|
||||
uint32_t home_lbn;
|
||||
uint32_t actual_lbn;
|
||||
} blk_reloc_entry_t;
|
||||
|
||||
static inline size_t minzu(size_t a, size_t b) { return a < b ? a : b; }
|
||||
static inline uint32_t udiv_floor(uint32_t a, uint32_t b) { return a / b; }
|
||||
|
||||
@@ -154,9 +168,10 @@ static struct {
|
||||
|
||||
/* Bumped on every attach/detach (Milestone 2h). Freeing a slot then
|
||||
* immediately allocating a new one for a same-LBN re-attach can hand
|
||||
* back the *same* heap address (confirmed live: glibc's allocator does
|
||||
* exactly this for a free() followed immediately by a same-size
|
||||
* calloc(), with nothing else allocated in between) -- so a raw
|
||||
* back the *same* heap address (confirmed live: this kernel's own
|
||||
* first-fit kmalloc, src/starkernel/memory/kmalloc.c, does exactly
|
||||
* this for a free() followed immediately by a same-size calloc(),
|
||||
* with nothing else allocated in between) -- so a raw
|
||||
* pointer comparison against a cached blk_get_buffer() result cannot
|
||||
* reliably detect "this LBN's device changed underneath a caller
|
||||
* holding a stale cached pointer." A monotonic epoch can't be fooled
|
||||
@@ -165,9 +180,32 @@ static struct {
|
||||
* caches a blk_get_buffer() result across calls). */
|
||||
uint64_t epoch;
|
||||
|
||||
/* Single-block relocation exception table (Milestone 2h+, in-memory
|
||||
* mirror of whichever attached disk's own on-disk copy is canonical
|
||||
* -- see reloc_flush_to_disk()/reloc_load_from_disk() below).
|
||||
* Subsystem-global, not per-device: an LBN can be relocated to any
|
||||
* other LBN regardless of which devices happen to be involved. */
|
||||
blk_reloc_entry_t reloc[BLK_RELOC_MAX_ENTRIES];
|
||||
uint32_t reloc_count;
|
||||
|
||||
int initialized;
|
||||
} g = {0};
|
||||
|
||||
/* Redirect lbn through the relocation table if it's been moved elsewhere.
|
||||
* The single choke point every public LBN-consuming entry point below
|
||||
* calls first -- see FABRIC-2.md's design writeup for why this is an
|
||||
* LBN->LBN redirect rather than a new storage-allocation mechanism, and
|
||||
* why it's safe for every downstream function (BAM offset math, cache
|
||||
* lookup, lbn_to_slot() itself) to stay completely unaware a substitution
|
||||
* happened. Linear scan -- see BLK_RELOC_MAX_ENTRIES's own doc comment
|
||||
* for why that's the right tradeoff here. */
|
||||
static uint32_t resolve_lbn(uint32_t lbn) {
|
||||
for (uint32_t i = 0; i < g.reloc_count; i++) {
|
||||
if (g.reloc[i].home_lbn == lbn) return g.reloc[i].actual_lbn;
|
||||
}
|
||||
return lbn;
|
||||
}
|
||||
|
||||
/* ===== slot routing ===== */
|
||||
static blk_dev_slot_t *lbn_to_slot(uint32_t lbn) {
|
||||
blk_dev_slot_t *s = g.head;
|
||||
@@ -323,8 +361,9 @@ static uint32_t choose_B(uint64_t total_devblocks_4k) {
|
||||
|
||||
static void compute_totals_from_B(blk_volume_meta_t *m) {
|
||||
uint64_t B = m->bam_devblocks;
|
||||
uint64_t R = m->reloc_devblocks; /* reserved relocation-table region, see blk_volume_meta_t's own doc comment */
|
||||
m->tracked_blocks = 32768ULL * B;
|
||||
uint64_t payload4k = (m->total_devblocks > (1+B)) ? (m->total_devblocks - 1 - B) : 0;
|
||||
uint64_t payload4k = (m->total_devblocks > (1+B+R)) ? (m->total_devblocks - 1 - B - R) : 0;
|
||||
uint64_t storable = 3ULL * payload4k;
|
||||
m->total_blocks = (m->tracked_blocks < storable) ? m->tracked_blocks : storable;
|
||||
uint64_t reserved = (uint64_t) BLK_DISK_SYS_RESERVED;
|
||||
@@ -409,6 +448,58 @@ static int bam_flush_to_disk(blk_dev_slot_t *slot) {
|
||||
return BLK_OK;
|
||||
}
|
||||
|
||||
/* ===== relocation-exception table I/O (Milestone 2h+, mirrors the BAM I/O
|
||||
* functions' own absolute-devblock-addressing shape above) =====
|
||||
*
|
||||
* Wire format: raw byte layout of the in-memory g.reloc[]/g.reloc_count
|
||||
* state -- a 4-byte count prefix, then that many packed {uint32_t home_lbn;
|
||||
* uint32_t actual_lbn;} pairs, all within one 4 KiB devblock (only the
|
||||
* first is ever read/written -- blk_compute_fresh_geometry() always
|
||||
* reserves exactly one; a volume with reloc_devblocks > 1 isn't produced
|
||||
* by this driver today, so the rest would silently go unused, matching
|
||||
* "won't migrate too much" scale). slot is the relocation-table owner
|
||||
* (first_disk_slot(), see blk_subsys_attach_device()) -- both functions
|
||||
* are no-ops (not errors) when there's no owner yet, or the owner
|
||||
* predates reloc capacity (reloc_devblocks == 0, e.g. disk/artemis.img's
|
||||
* existing fixture, formatted before this feature existed). */
|
||||
static int reloc_load_from_disk(blk_dev_slot_t *slot) {
|
||||
if (!slot || !slot->dev) return BLK_EINVAL;
|
||||
blk_volume_meta_t *m = &slot->vol_meta;
|
||||
if (m->reloc_devblocks == 0) { g.reloc_count = 0; return BLK_OK; }
|
||||
|
||||
uint8_t buf4k[4096];
|
||||
uint32_t base1k = m->reloc_start * 4u;
|
||||
for (uint32_t k = 0; k < 4; k++) {
|
||||
if (blkio_read(slot->dev, base1k + k, buf4k + k*1024u) != BLKIO_OK)
|
||||
memset(buf4k + k*1024u, 0, 1024u);
|
||||
}
|
||||
|
||||
uint32_t count;
|
||||
memcpy(&count, buf4k, sizeof(count));
|
||||
if (count > BLK_RELOC_MAX_ENTRIES) count = 0; /* corrupt/foreign data guard */
|
||||
memcpy(g.reloc, buf4k + sizeof(count), (size_t) count * sizeof(blk_reloc_entry_t));
|
||||
g.reloc_count = count;
|
||||
return BLK_OK;
|
||||
}
|
||||
|
||||
static int reloc_flush_to_disk(blk_dev_slot_t *slot) {
|
||||
if (!slot || !slot->dev) return BLK_OK; /* no owner yet -- nothing to persist to */
|
||||
blk_volume_meta_t *m = &slot->vol_meta;
|
||||
if (m->reloc_devblocks == 0) return BLK_OK; /* this device predates reloc capacity */
|
||||
|
||||
uint8_t buf4k[4096] = {0};
|
||||
memcpy(buf4k, &g.reloc_count, sizeof(g.reloc_count));
|
||||
memcpy(buf4k + sizeof(g.reloc_count), g.reloc,
|
||||
(size_t) g.reloc_count * sizeof(blk_reloc_entry_t));
|
||||
|
||||
uint32_t base1k = m->reloc_start * 4u;
|
||||
for (uint32_t k = 0; k < 4; k++) {
|
||||
if (blkio_write(slot->dev, base1k + k, buf4k + k*1024u) != BLKIO_OK) return BLK_EIO;
|
||||
}
|
||||
blkio_flush(slot->dev);
|
||||
return BLK_OK;
|
||||
}
|
||||
|
||||
/* ===== volume format / load (per disk slot) ===== */
|
||||
|
||||
/* Compute fresh volume geometry in memory only. Pure function of device
|
||||
@@ -426,7 +517,15 @@ static void blk_compute_fresh_geometry(blk_dev_slot_t *slot) {
|
||||
slot->vol_meta.total_devblocks = (uint64_t) udiv_floor(slot->total_blkio_blocks_1k, 4);
|
||||
slot->vol_meta.bam_start = 1;
|
||||
slot->vol_meta.bam_devblocks = choose_B(slot->vol_meta.total_devblocks);
|
||||
slot->vol_meta.devblock_base = slot->vol_meta.bam_start + slot->vol_meta.bam_devblocks;
|
||||
slot->vol_meta.reloc_start = slot->vol_meta.bam_start + slot->vol_meta.bam_devblocks;
|
||||
/* One devblock (4 KiB): a 4-byte count prefix + up to BLK_RELOC_MAX_ENTRIES
|
||||
* 8-byte {home_lbn, actual_lbn} pairs -- see this file's own doc comment
|
||||
* on BLK_RELOC_MAX_ENTRIES. Reserved unconditionally on every fresh
|
||||
* format, not sized to demand -- relocations are rare, but knowing in
|
||||
* advance whether a volume *can* ever receive one is simpler than a
|
||||
* variable-size region that might need to grow later. */
|
||||
slot->vol_meta.reloc_devblocks = 1;
|
||||
slot->vol_meta.devblock_base = slot->vol_meta.reloc_start + slot->vol_meta.reloc_devblocks;
|
||||
compute_totals_from_B(&slot->vol_meta);
|
||||
|
||||
if (sf_has_rtc()) slot->vol_meta.created_time = sf_realtime_ns();
|
||||
@@ -452,6 +551,12 @@ static int blk_commit_format(blk_dev_slot_t *slot) {
|
||||
uint32_t base1k = (slot->vol_meta.bam_start + i) * 4u;
|
||||
for (uint32_t k = 0; k < 4; k++) (void) blkio_write(slot->dev, base1k + k, z);
|
||||
}
|
||||
/* Zero the reloc region too -- a zeroed 4-byte count prefix reads back
|
||||
* as "0 entries", the correct empty-table default. */
|
||||
for (uint32_t i = 0; i < slot->vol_meta.reloc_devblocks; i++) {
|
||||
uint32_t base1k = (slot->vol_meta.reloc_start + i) * 4u;
|
||||
for (uint32_t k = 0; k < 4; k++) (void) blkio_write(slot->dev, base1k + k, z);
|
||||
}
|
||||
uint8_t hdr[BLK_DEVICE_SECTOR];
|
||||
volmeta_to_buf(&slot->vol_meta, hdr);
|
||||
(void) write_header_4k(slot, hdr);
|
||||
@@ -553,6 +658,9 @@ int blk_subsys_add_raw_device(uint8_t *buf, uint32_t nblocks) {
|
||||
return BLK_OK;
|
||||
}
|
||||
|
||||
static blk_dev_slot_t *first_disk_slot(void); /* defined below; used here to identify the
|
||||
* relocation-table owner right after attach */
|
||||
|
||||
int blk_subsys_attach_device(struct blkio_dev *dev) {
|
||||
if (!g.initialized) return BLK_ENODEV;
|
||||
if (!dev) return BLK_EINVAL;
|
||||
@@ -578,6 +686,19 @@ int blk_subsys_attach_device(struct blkio_dev *dev) {
|
||||
g.total_user_lbn += slot->user_blocks;
|
||||
g.epoch++;
|
||||
|
||||
/* Milestone 2h+: load the relocation-exception table from whichever
|
||||
* disk-backed device is now the canonical owner (first_disk_slot(),
|
||||
* the same "which device is canonical" answer blk_get_volume_meta()/
|
||||
* blk_set_volume_meta() already use) -- but only the first time that
|
||||
* device becomes the owner. A later-attached second disk-backed
|
||||
* device (e.g. a USB drive attaching after Artemis's own disk) must
|
||||
* NOT overwrite the already-loaded table with its own (likely empty)
|
||||
* one. See FABRIC-2.md's design writeup for why "first attached wins"
|
||||
* is a pragmatic default, not a general multi-primary-device answer. */
|
||||
if (slot->dev && first_disk_slot() == slot) {
|
||||
(void) reloc_load_from_disk(slot);
|
||||
}
|
||||
|
||||
log_message(LOG_INFO,
|
||||
"blk: disk '%s' v2 LBN %u..%u (%u user blocks); "
|
||||
"devblocks=%llu bam=%u base=%u total=%llu free=%llu",
|
||||
@@ -636,6 +757,63 @@ int blk_subsys_detach_device(struct blkio_dev *dev) {
|
||||
return BLK_OK;
|
||||
}
|
||||
|
||||
/* Milestone 2h+ single-block relocation -- see FABRIC-2.md's design
|
||||
* writeup for the full reasoning. Mechanical primitive only: this
|
||||
* function does not decide *whether* a relocation should happen (ACL's
|
||||
* job) or validate that target_lbn is genuinely owned by whoever is
|
||||
* asking (also ACL's job) -- it just executes one, correctly, once told
|
||||
* to. */
|
||||
int blk_subsys_relocate_block(uint32_t home_lbn, uint32_t target_lbn) {
|
||||
if (!g.initialized) return BLK_ENODEV;
|
||||
if (home_lbn == target_lbn) return BLK_EINVAL;
|
||||
|
||||
/* Refuse re-relocating an already-relocated home_lbn, or relocating
|
||||
* onto an LBN that's itself someone else's relocation source --
|
||||
* both would need chain-following this function deliberately doesn't
|
||||
* support; call blk_subsys_unrelocate()-style bookkeeping (not yet
|
||||
* needed, not yet built) first if that's ever required. */
|
||||
for (uint32_t i = 0; i < g.reloc_count; i++) {
|
||||
if (g.reloc[i].home_lbn == home_lbn || g.reloc[i].home_lbn == target_lbn)
|
||||
return BLK_EINVAL;
|
||||
}
|
||||
if (g.reloc_count >= BLK_RELOC_MAX_ENTRIES) return BLK_ENOMEM;
|
||||
|
||||
if (!blk_is_valid(home_lbn) || !blk_is_valid(target_lbn)) return BLK_ERANGE;
|
||||
|
||||
/* Stage through a local buffer rather than copying directly from one
|
||||
* blk_get_buffer() result to another -- obtaining the target buffer
|
||||
* can trigger a cache eviction (cache_get_slot()'s FIFO shift) that
|
||||
* silently invalidates a pointer already held into the *same*
|
||||
* device's cache array, if home_lbn and target_lbn happen to share a
|
||||
* device. Not a hypothetical: this is exactly the class of stale
|
||||
* pointer this file's own blk_vm_evict() comment already warns about. */
|
||||
uint8_t *src = blk_get_buffer(home_lbn, 0);
|
||||
if (!src) return BLK_EIO;
|
||||
uint8_t staged[BLK_FORTH_SIZE];
|
||||
memcpy(staged, src, BLK_FORTH_SIZE);
|
||||
|
||||
uint8_t *dst = blk_get_buffer(target_lbn, 1);
|
||||
if (!dst) return BLK_EIO;
|
||||
memcpy(dst, staged, BLK_FORTH_SIZE);
|
||||
if (blk_update(target_lbn) != BLK_OK) return BLK_EIO;
|
||||
|
||||
/* Free home_lbn's original backing block -- must happen before the
|
||||
* redirect is inserted below, while resolve_lbn(home_lbn) still
|
||||
* resolves to itself; inserting the redirect first would make this
|
||||
* call free target_lbn instead. */
|
||||
(void) blk_mark_free(home_lbn);
|
||||
|
||||
g.reloc[g.reloc_count].home_lbn = home_lbn;
|
||||
g.reloc[g.reloc_count].actual_lbn = target_lbn;
|
||||
g.reloc_count++;
|
||||
g.epoch++;
|
||||
|
||||
(void) reloc_flush_to_disk(first_disk_slot());
|
||||
|
||||
log_message(LOG_INFO, "blk: relocated LBN %u -> %u", home_lbn, target_lbn);
|
||||
return BLK_OK;
|
||||
}
|
||||
|
||||
uint64_t blk_subsys_epoch(void) {
|
||||
return g.epoch;
|
||||
}
|
||||
@@ -685,6 +863,7 @@ int blk_subsys_confirm_format(uint32_t lbn) {
|
||||
|
||||
uint8_t *blk_get_buffer(uint32_t block_num, int writable) {
|
||||
if (!g.initialized) return NULL;
|
||||
block_num = resolve_lbn(block_num);
|
||||
|
||||
/* RAM */
|
||||
if (block_num < g.ram_user) {
|
||||
@@ -727,6 +906,7 @@ uint8_t *blk_get_empty_buffer(uint32_t block_num) {
|
||||
|
||||
int blk_update(uint32_t block_num) {
|
||||
if (!g.initialized) return BLK_ENODEV;
|
||||
block_num = resolve_lbn(block_num);
|
||||
|
||||
/* RAM */
|
||||
if (block_num < g.ram_user) {
|
||||
@@ -775,6 +955,7 @@ int blk_update(uint32_t block_num) {
|
||||
|
||||
int blk_flush(uint32_t block_num) {
|
||||
if (!g.initialized) return BLK_ENODEV;
|
||||
if (block_num > 0) block_num = resolve_lbn(block_num); /* 0 is the "flush all" sentinel */
|
||||
|
||||
if (block_num > 0) {
|
||||
if (block_num < g.ram_user) {
|
||||
@@ -829,6 +1010,7 @@ int blk_flush(uint32_t block_num) {
|
||||
|
||||
int blk_is_allocated(uint32_t block_num) {
|
||||
if (!g.initialized) return BLK_ENODEV;
|
||||
block_num = resolve_lbn(block_num);
|
||||
if (block_num < g.ram_user) return BLK_EINVAL;
|
||||
blk_dev_slot_t *slot = lbn_to_slot(block_num);
|
||||
if (!slot) return 0;
|
||||
@@ -837,6 +1019,7 @@ int blk_is_allocated(uint32_t block_num) {
|
||||
|
||||
int blk_mark_allocated(uint32_t block_num) {
|
||||
if (!g.initialized) return BLK_ENODEV;
|
||||
block_num = resolve_lbn(block_num);
|
||||
if (block_num < g.ram_user) return BLK_EINVAL;
|
||||
blk_dev_slot_t *slot = lbn_to_slot(block_num);
|
||||
if (!slot) return BLK_ERANGE;
|
||||
@@ -855,6 +1038,7 @@ int blk_mark_allocated(uint32_t block_num) {
|
||||
|
||||
int blk_mark_free(uint32_t block_num) {
|
||||
if (!g.initialized) return BLK_ENODEV;
|
||||
block_num = resolve_lbn(block_num);
|
||||
if (block_num < g.ram_user) return BLK_EINVAL;
|
||||
blk_dev_slot_t *slot = lbn_to_slot(block_num);
|
||||
if (!slot) return BLK_ERANGE;
|
||||
@@ -933,6 +1117,7 @@ int blk_set_volume_meta(const blk_volume_meta_t *meta) {
|
||||
|
||||
int blk_is_valid(uint32_t block_num) {
|
||||
if (!g.initialized) return 0;
|
||||
block_num = resolve_lbn(block_num);
|
||||
if (block_num < g.ram_user) return 1;
|
||||
return lbn_to_slot(block_num) ? 1 : 0;
|
||||
}
|
||||
@@ -945,6 +1130,7 @@ uint32_t blk_get_total_blocks(void) {
|
||||
int blk_get_meta(uint32_t block_num, blk_meta_t *meta) {
|
||||
if (!meta) return BLK_EINVAL;
|
||||
if (!g.initialized) return BLK_ENODEV;
|
||||
block_num = resolve_lbn(block_num);
|
||||
if (block_num < g.ram_user) {
|
||||
memset(meta, 0, sizeof(*meta));
|
||||
meta->magic = 0x424C4B5F5354524BULL;
|
||||
@@ -967,6 +1153,7 @@ int blk_get_meta(uint32_t block_num, blk_meta_t *meta) {
|
||||
int blk_set_meta(uint32_t block_num, const blk_meta_t *meta) {
|
||||
if (!meta) return BLK_EINVAL;
|
||||
if (!g.initialized) return BLK_ENODEV;
|
||||
block_num = resolve_lbn(block_num);
|
||||
if (block_num < g.ram_user) return BLK_OK;
|
||||
blk_dev_slot_t *slot = lbn_to_slot(block_num);
|
||||
if (!slot) return BLK_ERANGE;
|
||||
|
||||
@@ -159,9 +159,10 @@ void empty_all_buffers(VM *vm) {
|
||||
* it was last validated (Milestone 2h). A device hot-detach followed by a
|
||||
* later re-attach can reuse both the exact same LBN range
|
||||
* (block_subsystem.c's chain always appends at the current tail) *and* the
|
||||
* exact same blk_get_buffer() return address (confirmed live: glibc's
|
||||
* allocator hands the just-freed slot straight back to the very next
|
||||
* same-size calloc()), so neither LBN nor a cached pointer is a reliable
|
||||
* exact same blk_get_buffer() return address (confirmed live: this
|
||||
* kernel's own first-fit kmalloc, src/starkernel/memory/kmalloc.c, hands
|
||||
* the just-freed slot straight back to the very next same-size
|
||||
* calloc()), so neither LBN nor a cached pointer is a reliable
|
||||
* "still the same device" signal on its own. Any epoch change discards
|
||||
* every cached slot outright -- no flush attempt, matching
|
||||
* blk_subsys_detach_device()'s own reasoning: whatever device the stale
|
||||
@@ -353,6 +354,22 @@ void block_word_confirm_format(VM *vm) {
|
||||
if (blk_subsys_confirm_format((uint32_t) blk) != BLK_OK) { vm->error = 1; return; }
|
||||
}
|
||||
|
||||
/* RELOCATE-BLOCK ( home target -- ) : relocate home's content to target,
|
||||
* redirecting all future access to home transparently through target
|
||||
* from now on. See blk_subsys_relocate_block()'s own doc comment
|
||||
* (block_subsystem.h) for the full contract -- this word performs no
|
||||
* policy validation of its own (is target actually free? does the
|
||||
* caller actually own it?), matching this project's "ACL owns policy,
|
||||
* this is a mechanical primitive" direction. */
|
||||
void block_word_relocate(VM *vm) {
|
||||
if (vm->dsp < 1) { vm->error = 1; return; }
|
||||
cell_t target = vm_pop(vm);
|
||||
cell_t home = vm_pop(vm);
|
||||
if (blk_subsys_relocate_block((uint32_t) home, (uint32_t) target) != BLK_OK) {
|
||||
vm->error = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* SAVE-BUFFERS ( -- ) : sync and write all dirty blocks */
|
||||
void block_word_save_buffers(VM *vm) {
|
||||
blk_vm_flush_all(vm);
|
||||
@@ -532,6 +549,7 @@ void register_block_words(VM *vm) {
|
||||
register_word(vm, "BUFFER", block_word_buffer);
|
||||
register_word(vm, "UPDATE", block_word_update);
|
||||
register_word(vm, "BLK-CONFIRM-FORMAT", block_word_confirm_format);
|
||||
register_word(vm, "RELOCATE-BLOCK", block_word_relocate);
|
||||
register_word(vm, "SAVE-BUFFERS", block_word_save_buffers);
|
||||
register_word(vm, "EMPTY-BUFFERS", block_word_empty_buffers);
|
||||
register_word(vm, "FLUSH", block_word_flush);
|
||||
|
||||
Reference in New Issue
Block a user