Stage MC-0b2: block subsystem behind g_lock_block (FABRIC-3.10.8 §3.3)

- block_subsystem.c (vendored): every public function's body becomes a
  static *_locked function; bodies call each other only through the
  _locked forms, breaking the internal cycles (relocate, meta relocate,
  migration scan, firsttouch, the meta accessors). Thin public wrappers
  take g_lock_block under __STARKERNEL__; hosted build has no lock and
  still builds clean.
- Lock order: hermes, switch, registry, doe, log, block, console, heap,
  vmm, pmm (Stadium, MC-0d, above block).
- Documented, not coverable by this lock: blk_get_buffer() pointers are
  used after release -- block-buffer ownership across cores is open for
  MC-3.
- Probe (removed): block lock balances on all three ISAs (2,057-2,379
  acquisitions), 0 recursions, 0 held at switch; vblk_io timeouts
  (0/0/4 probe, 0/0/2 acceptance) within the pre-lock riscv64 baseline
  of 0-6.
- Standard sequence on amd64/aarch64/riscv64: hashes and G byte-identical
  to MC-0b, MINT/WIREBIND/USE ok, conservation true, 0 idle growth.
- FABRIC-3.10.8 §3.2 wording fix; lock order noted as checked by
  inspection only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-25 18:57:38 -04:00
co-authored by Claude Opus 5.5
parent 5bc7b4dee7
commit 33f4090d6d
16 changed files with 55305 additions and 82 deletions
+63 -2
View File
@@ -143,7 +143,7 @@ Everything that would break if two cores ran kernel or VM code at the same momen
That crashed run's log was deleted as flawed.
- `sf_mutex_*` (the vendored VM's `dict_lock` and `tuning_lock`, `shim.c`) now use the same lock word. They are no longer no-ops.
**The kernel locks, in acquisition order** (spinlock.h; MC-3's deadlock freedom rests on this order):
**The kernel locks, in acquisition order** (spinlock.h; MC-3's deadlock freedom rests on this order; checked by reading the code, not yet by a counted check; MC-0b2 inserts `block` between `log` and `console`, §3.3):
1. `hermes`
2. `switch`
3. `registry`
@@ -186,7 +186,7 @@ It ran at Hera's prompt (after O-A's G, PREC and LOAD), and again on rajames aft
- Every lock balances individually on every ISA, at both readings.
- The amd64 and aarch64 probe runs predate the `sk_percpu_ready` guard. That guard only changes behaviour before `sk_percpu_boot_init()`, and the acceptance runs below are on the final code.
- The riscv64 `vblk_io timed out` count, which the advisor flagged (defect 3 sits in the region now masked), is 2 in the probe run, the same as MC-0a's acceptance run. It is 0 in this stage's acceptance run.
- The riscv64 `vblk_io timed out` count, watched because known defect 3 sits in code that now runs with interrupts masked, is 2 in the probe run, the same as MC-0a's acceptance run. It is 0 in this stage's acceptance run.
**Finding, for MC-3.** Hera's input-poll idle loop runs the switch decision without pause, and every pass takes the registry, switch and Hermes locks once per slot. That is the hundreds of millions of acquisitions above (amd64: registry 379 M, switch 858 M, Hermes 266 M). It is harmless on one core. With several cores, core 0 spinning through these same locks would contend with every worker's claim. MC-3 should settle how that loop waits before cores share these locks.
@@ -201,3 +201,64 @@ It ran at Hera's prompt (after O-A's G, PREC and LOAD), and again on rajames aft
- The G string is byte-identical to MC-0a on all three ISAs, so masking interrupts under the locks didn't move the interleave.
- The `dict_hash` set and the ledger line are unchanged from MC-0a.
- No stop condition was hit. **MC-0b done** (the block subsystem follows as MC-0b2).
### 3.3 MC-0b2 — the block subsystem's lock (2026-09-25)
**Change.** `src/block_subsystem.c` (vendored) is split into bodies and wrappers:
- Every public function's body is now a static `*_locked` function.
- Bodies call each other only through the `_locked` forms. That breaks the internal cycles:
- relocate → get buffer / update / mark free;
- meta relocate → relocate;
- migration scan → meta relocate;
- firsttouch → get meta / set meta / mark allocated;
- the six meta accessors.
- A block of thin public wrappers at the end of the file takes the lock around each body.
- Under `__STARKERNEL__` that lock is `g_lock_block`.
- The hosted build has no lock, and the vendored hosted `make` still builds clean.
- The lock covers the device chain, every slot's cache and BAM, the relocation table, and the device I/O issued on their behalf.
- Its place in the order is **below `log` and `doe`** (both regions write through the block subsystem) and **above `console`** (block logs), `heap` (`calloc`/`free`), `vmm` and `pmm`:
1. `hermes`
2. `switch`
3. `registry`
4. `doe`
5. `log`
6. `block`
7. `console`
8. `heap`
9. `vmm`
10. `pmm`
Stadium (MC-0d) sits above `block`, because `STADIUM_BEHAVIOUR_MIGRATE` calls `blk_flush()`.
**Not covered, and not coverable by this lock:** `blk_get_buffer()` returns a pointer into RAM, a ramdrive, or a device cache slot. The caller (`BLOCK`/`BUFFER`, Artemis) reads and writes through that pointer after the lock is released. On one core nothing can evict the slot in between; with worker cores (MC-3) another core's block access can. Block-buffer ownership across cores is an open item for MC-3.
**The masking risk, measured.** `virtio_blk.c` records (2026-09-09) that masking interrupts around `vblk_io()` once made missed completions worse. All block I/O now runs with interrupts masked (the MC-0b primitive), so the probe runs counted `vblk_io timed out` on every ISA:
| Where | amd64 | aarch64 | riscv64 |
|---|---|---|---|
| This stage's probe runs | 0 | 0 | 4 |
| This stage's acceptance runs | 0 | 0 | 2 |
The riscv64 counts sit inside the pre-lock baseline: the 24 most recent riscv64 logs, from 2026-09-24 (no locks at all) through this stage, range from 0 to 6, with 4, 5 and 6 all seen before MC-0b. No regression is visible at this sample size. The masking stays as MC-0b ruled.
**Probe** (temporary, removed before commit). This is MC-0b's `MC0B-PROBE` word, extended with the block lock. Same sequence: O-A, then Hera's prompt, then MINT, WIREBIND, `USE` and `1 2 + .`, then rajames. The table shows the final (rajames) reading:
| ISA | log | block acquired = released | total acquired = released | depth | switch/idle checks | held at switch | recursions / bad releases / destroyed held |
|---|---|---|---|---|---|---|---|
| amd64 | `logs/20260925-173901` | 2,379 | 1,387,861,111 | 0 | 46,795,001 | 0 | 0 / 0 / 0 |
| aarch64 | `logs/20260925-175215` | 2,057 | 247,761,816 | 0 | 8,014,413 | 0 | 0 / 0 / 0 |
| riscv64 | `logs/20260925-180546` | 2,300 | 555,650,314 | 0 | 18,447,619 | 0 | 0 / 0 / 0 |
Every lock balances individually on every ISA, at both readings. 0 recursions means no public-to-public call inside `block_subsystem.c` was missed.
**Standard sequence** (clean build, with the probe reverted; the tree is byte-identical to the saved stage diff).
| ISA | log | O-A (G, PREC, LOAD) | M7.1a hash | MINT / WIREBIND / USE / `1 2 + .` | idle growth | UNKNOWN / faults |
|---|---|---|---|---|---|---|
| amd64 | `logs/20260925-181908` | A=40 B=40, HHH, ok | `0x99b01134c3d3049c` | all ok, `3` | 0 B / 20 s | 0 / 0 |
| aarch64 | `logs/20260925-183151` | A=40 B=40, HHH, ok | `0x99b01134c3d3049c` | all ok, `3` | 0 B / 20 s | 0 / 0 |
| riscv64 | `logs/20260925-184454` | A=40 B=40, HHH, ok | `0x99b01134c3d3049c` | all ok, `3` | 0 B / 20 s | 0 / 0 |
- The G string is byte-identical to MC-0b and MC-0a on all three ISAs.
- The `dict_hash` set and the ledger line are unchanged.
- No stop condition was hit. **MC-0b2 done.**