Stage M-0: kernel-Hermes arena, pending queues and member lists grow on demand; Q_SLOT its own constant (FABRIC-3.9.0 §7.0, M-0/M-0b)
- The static sk_hermes_msgs[32] is replaced by a directory of fixed-size segments (SK_HERMES_ARENA_SEGMENT, growth granularity only). Messages never move -- callers, including the drain across vm_interpret_kind(), hold message pointers across later allocations, so copy-on-grow (as §7.0 first proposed) would dangle them. Each message carries its index; numbering unchanged. - Pending-queue rings and channel member lists heap-grown (doubling); member lists freed on destroy; reassembly scratch sized per call. SK_HERMES_MSG_MAX / _PENDING_MAX / _MEMBER_MAX are gone. - Q_SLOT = Q48_ONE / 32 as its own constant (M-0b), value unchanged. - Every grow failure is printed, logged at LOG_ERROR, counted (sk_hermes_grow_failures()) and refused whole; a send whose queue can't grow releases its message. Probe (reverted), identical on amd64/aarch64/riscv64: 70 messages in one queue (arena 32 -> 96), sender refused by its reservoir, 72 popped intact in order, 100 members; a live Hera drain on a full arena grew it mid-drain and released/popped the right message; each grow path's injected failure refused whole and counted. 3-ISA acceptance passes; boot parity, dict_hash set and the live ledger line byte-identical to before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
316be9fc4b
commit
d8358a3011
+83
-1
@@ -1,6 +1,6 @@
|
||||
# FABRIC-3.9.8.md — Observers and participants: execution plan and log
|
||||
|
||||
**Status: IN PROGRESS.** Done: O-A, the stack-convention fix, S-1a, S-1b and S-1c (§2). The design and
|
||||
**Status: IN PROGRESS.** Done: O-A, the stack-convention fix, S-1a, S-1b, S-1c and M-0 (§2). The design and
|
||||
its rulings (O-1 to O-4, the discard, and §7/§8's M- and S- rulings) are in `FABRIC-3.9.0.md`.
|
||||
|
||||
Each stage:
|
||||
@@ -413,3 +413,85 @@ Evidence is named **before** each stage runs.
|
||||
2. **`PAD` overlaps the block I/O window.** It returns `VM_MEMORY_SIZE - 512`, which is inside
|
||||
the top block slot (`BLK_VM_WINDOW_BASE` = `VM_MEMORY_SIZE - 4096`), so `PAD` and the
|
||||
4th `BLOCK` buffer share bytes.
|
||||
- [x] **Stage M-0 — kernel-Hermes grows on demand (`FABRIC-3.9.0.md` §7.0, rulings M-0 and M-0b).**
|
||||
2026-09-24. Captain Bob: *"go ahead with M-0."*
|
||||
|
||||
**Corrected before starting: the mechanism, not the ruling.** §7.0 proposed the arena "grow by
|
||||
doubling … the same allocate-copy-free pattern `vm_frame.c` already uses". That would move
|
||||
messages on every grow, but callers hold `SkHermesMessage *` across later allocations:
|
||||
- the boot self-tests hold up to 32;
|
||||
- the switch decision peeks;
|
||||
- **the drain holds one across `vm_interpret_kind()`**, and a payload that `SEND`s can grow
|
||||
the arena mid-drain.
|
||||
|
||||
A copy-on-grow arena would leave all of them dangling. So the arena is a **directory of
|
||||
fixed-size segments**: messages never move once allocated, and only the directory is
|
||||
allocate-copy-free (nothing holds pointers into it). It still grows on demand, from the
|
||||
kernel heap, with RAM the only limit, as ruled.
|
||||
|
||||
**The change** (`kernel_hermes.c/.h`, `kernel_main.c`'s self-test):
|
||||
- **The arena.** `sk_hermes_msgs[32]` is gone.
|
||||
- `SK_HERMES_ARENA_SEGMENT` (32) is the growth *granularity*, not a limit. A full arena
|
||||
adds a segment.
|
||||
- Each message carries its own `index`, its identity. Queues hold indices, never pointers.
|
||||
- Numbering is unchanged: lowest free index first.
|
||||
- **The pending queues.** Each queue's ring is heap-grown on push (doubling, unwrapped into
|
||||
FIFO order). `SK_HERMES_PENDING_MAX` is gone. A reused queue-table entry keeps its ring
|
||||
rather than leaking it.
|
||||
- **The channel members.** `SkHermesMembership` is a pointer plus a capacity, grown on
|
||||
subscribe and freed by `sk_hermes_channel_destroy()`. `SK_HERMES_MEMBER_MAX` (64) is gone.
|
||||
- **Reassembly.** `present[]` is allocated to `n_chunks` per call, with a single exit path.
|
||||
That also removes the old cap of 32 chunks.
|
||||
- **M-0b.** `SK_HERMES_Q_SLOT` is `Q48_ONE / SK_HERMES_Q_SLOT_DIVISOR` (32): its own
|
||||
constant, with the same value, no longer derived from the arena size. How many messages a
|
||||
VM can have in flight is now purely a heat question.
|
||||
- **Failures follow §8.1.** Each grow path (the arena directory, a segment, a queue ring, a
|
||||
member list, the reassembly scratch):
|
||||
- prints `Kernel-Hermes: can't grow <what>` in full;
|
||||
- logs at `LOG_ERROR`;
|
||||
- counts (`sk_hermes_grow_failures()`);
|
||||
- refuses whole. A send whose queue can't grow releases the message it already allocated,
|
||||
so the ledger stays exact.
|
||||
|
||||
A reservoir refusal is a physics outcome, not a grow failure, and stays uncounted.
|
||||
- **The `kernel_main.c` alloc/release self-test.** Its `msgs[]` is sized from `expected_n + 1`,
|
||||
and every loop is bounded by that. The refusal after `expected_n` must now come from the
|
||||
reservoir. It prints `arena_capacity` and `grow_failures`.
|
||||
- **Not in scope:** the queue table and the channel table are still RAM-sized at boot. Those
|
||||
are S-3's per-VM-tables item.
|
||||
|
||||
**Evidence** (probe, reverted; synthetic, quota-granted sender ids, not Tripod VMs;
|
||||
byte-identical on all three ISAs, 86 lines; `logs/20260924-170043/amd64/`,
|
||||
`-170507/aarch64/`, `-170940/riscv64/`):
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| Boot self-tests | All PASS; `expected_n = got_n = 32`, `arena_capacity=32`, `grow_failures=0`. |
|
||||
| Past 32, system-wide and in one queue | Three funded senders put **70** messages into one queue. 70 are live system-wide, and the arena grew 32 → 96. |
|
||||
| Refused by the reservoir | The sender with 30 in flight managed **2** more, then was refused with its reservoir at **0**. Grow failures: 0. |
|
||||
| Intact across the grows | All 72 popped in send order with the right payloads, and every release succeeded. |
|
||||
| Past 64 members | 100 subscribed, `member_count` 100, member 99 present; one unsubscribed gives 99; destroy OK. |
|
||||
| **Live drain across a grow** (the discriminating case) | The arena was filled to exactly 96/96. Hera's queue held a `SEND` payload (`: PZD 10 0 DO S" 1 DROP" S" Hestia" SEND DROP LOOP ; PZD`) followed by a marker. `sk_hermes_drain_checkpoint(Hera)` returned 1, the arena grew to **128 mid-drain**, and Hestia received **10**. The next head was the marker, **intact**; Hera's pending count went 2 → 1, and the marker then drained too. Audit failures 0. |
|
||||
| Forced failure, arena | With the arena **full, 128/128**, one injected segment failure: refused, +1 counted, reservoir untouched, capacity and ledger unchanged. The retry grows and succeeds. |
|
||||
| Forced failure, queue | With 8 queued, a 9th with an injected ring failure: refused, +1, still 8 pending, the message rolled back (held unchanged). The retry gives 9. |
|
||||
| Forced failure, members | The 9th subscribe with an injected failure: refused, +1, still 8. The retry gives 9. |
|
||||
| Forced failure, reassembly | Injected scratch failure: refused, +1. Separately, bad chunks are refused **without** counting as a grow failure. |
|
||||
| At rest | Grow failures exactly 4 (the injected ones). Audit failures 0, `scan_check` 1. Live messages are 10, Hestia's pending, and `held` delta = 10 × Q_SLOT. `stadium_conserved` true for every sender, the receiver and Hera. |
|
||||
|
||||
*(The first two probe runs filled the arena short of full, so the arena-failure check found a
|
||||
free slot and never tried to grow. They were flawed probe runs, not M-0 failures. The fill was
|
||||
fixed and those logs deleted.)*
|
||||
|
||||
**Acceptance** on the clean tree (`logs/20260924-171403/amd64/`, `-172648/aarch64/`,
|
||||
`-173952/riscv64/`):
|
||||
- Boot M7.1a and the `dict_hash` set are unchanged.
|
||||
- **The live ledger line is byte-identical to before M-0:** `held=2048 pulled=258048
|
||||
returned=255263 consumed=737`. Q_SLOT's value and the message numbering did not move.
|
||||
- The O-A checks pass: 40/40/48, `HHH`, and `3` mid-stream.
|
||||
- The relay answers `3`, idle log growth is 0, there are no faults, and QEMU exits cleanly.
|
||||
- Clean builds on all three ISAs plus the hosted build.
|
||||
|
||||
**Found, reported, not fixed:** the boot self-tests print `Kernel-Hermes drain self-test:
|
||||
SKIPPED (Hermes not live)` and `channel-open policy self-test: SKIPPED (Hera/Hermes not
|
||||
available)`. Both gates still look for the Hermes VM, which was removed in FABRIC-3.6 Phase 4,
|
||||
so two self-tests have silently never run since then.
|
||||
|
||||
@@ -64,14 +64,22 @@
|
||||
#include "starkernel/q48_16.h" /* Q48_ONE */
|
||||
|
||||
/*
|
||||
* SK_HERMES_MSG_MAX / SK_HERMES_MEMBER_MAX - sizing. Mirrors
|
||||
* messaging.4th's own MSG-MAX (32) and MBR-MAX (64) as a starting point --
|
||||
* kernel-Hermes is a single central pool rather than N per-VM arenas, so
|
||||
* these may need revisiting once real traffic exists to size against. Not
|
||||
* a ruling, just where the FORTH precedent already was.
|
||||
* FABRIC-3.9.0.md §7.0 (Stage M-0): kernel-Hermes has no fixed counts any
|
||||
* more. SK_HERMES_MSG_MAX (32 messages system-wide), SK_HERMES_PENDING_MAX
|
||||
* (32 per queue) and SK_HERMES_MEMBER_MAX (64 per channel) are gone: the
|
||||
* message arena, each pending queue and each channel's member list grow on
|
||||
* demand from the kernel heap, and a grow that fails is logged, counted
|
||||
* (sk_hermes_grow_failures()) and refused whole.
|
||||
*
|
||||
* SK_HERMES_ARENA_SEGMENT - the arena's GROWTH GRANULARITY, not a limit.
|
||||
* The arena is a directory of fixed-size segments; running out adds a
|
||||
* segment. Messages never move once allocated -- callers hold
|
||||
* SkHermesMessage pointers across later allocations (the drain holds one
|
||||
* across interpreting a payload that may itself SEND), so a copy-on-grow
|
||||
* arena would leave them dangling. Only the segment directory is
|
||||
* allocate-copy-free, and nothing holds pointers into it.
|
||||
*/
|
||||
#define SK_HERMES_MSG_MAX 32
|
||||
#define SK_HERMES_MEMBER_MAX 64
|
||||
#define SK_HERMES_ARENA_SEGMENT 32
|
||||
|
||||
/* SK_HERMES_CHUNK_MAX_PAYLOAD - one block (1024 bytes, 64x16, SXLIV.1),
|
||||
* moved up here (was originally defined much further down, next to
|
||||
@@ -91,8 +99,15 @@
|
||||
* floor to protect. Deliberately simpler here rather than carrying the
|
||||
* old formula's now-unmotivated term forward: reservoir split evenly
|
||||
* across message slots only.
|
||||
*
|
||||
* FABRIC-3.9.0.md §7.0, ruling M-0b (Stage M-0): Q_SLOT is its own
|
||||
* constant now, 1/32 of a reservoir -- the same value as before, no longer
|
||||
* derived from the arena size (which no longer exists as a number). How
|
||||
* many messages a VM can have in flight is a heat question (its reservoir
|
||||
* over Q_SLOT), not a memory one.
|
||||
*/
|
||||
#define SK_HERMES_Q_SLOT ((uint64_t)Q48_ONE / SK_HERMES_MSG_MAX)
|
||||
#define SK_HERMES_Q_SLOT_DIVISOR 32u
|
||||
#define SK_HERMES_Q_SLOT ((uint64_t)Q48_ONE / SK_HERMES_Q_SLOT_DIVISOR)
|
||||
|
||||
/*
|
||||
* SkHermesMessage - one message slot, field-for-field mirror of
|
||||
@@ -169,6 +184,8 @@ typedef struct {
|
||||
* admission stamp (sk_vm_switch_signal_next_stamp())
|
||||
* -- how long the recipient has been waiting,
|
||||
* for the switch decision's tie-break */
|
||||
uint32_t index; /* Stage M-0: this message's arena index (its
|
||||
* identity -- queues hold indices, never pointers) */
|
||||
} SkHermesMessage;
|
||||
|
||||
/*
|
||||
@@ -176,12 +193,16 @@ typedef struct {
|
||||
* has joined, no per-member state beyond identity. Replaces the 28-word
|
||||
* channel abstraction; see this file's own top comment for why.
|
||||
*
|
||||
* @field members Member VM identities, valid for indices < count.
|
||||
* @field members Member VM identities, valid for indices < count. Stage
|
||||
* M-0: heap-grown on subscribe (was a fixed [64]); freed by
|
||||
* sk_hermes_channel_destroy().
|
||||
* @field count Number of valid entries in members[].
|
||||
* @field cap Allocated length of members[].
|
||||
*/
|
||||
typedef struct {
|
||||
VMUuid members[SK_HERMES_MEMBER_MAX];
|
||||
size_t count;
|
||||
VMUuid *members;
|
||||
size_t count;
|
||||
size_t cap;
|
||||
} SkHermesMembership;
|
||||
|
||||
/*
|
||||
@@ -302,7 +323,7 @@ uint64_t sk_hermes_audit_failure_count(void);
|
||||
|
||||
/*
|
||||
* Task 2.8 (item 28), SXXXVII.4: the scan that verifies the counters
|
||||
* themselves. DIAGNOSTIC ONLY -- O(SK_HERMES_MSG_MAX), never called from
|
||||
* themselves. DIAGNOSTIC ONLY -- O(arena capacity), never called from
|
||||
* alloc/release/decay.
|
||||
*
|
||||
* sk_hermes_scan_held - walks the message arena and sums the live Stadium
|
||||
@@ -385,8 +406,8 @@ int sk_hermes_channel_destroy(int channel_id);
|
||||
|
||||
/* sk_hermes_channel_subscribe - add vm_id to channel_id's membership.
|
||||
* Refuses (-1, no effect) if channel_id is invalid/not in use, vm_id is
|
||||
* already a member, or the channel's membership is already at
|
||||
* SK_HERMES_MEMBER_MAX. Idempotent in effect (a second call with the
|
||||
* already a member, or the member list can't grow (Stage M-0: logged and
|
||||
* counted, sk_hermes_grow_failures()). Idempotent in effect (a second call with the
|
||||
* same args refuses rather than duplicating), not in return value. */
|
||||
int sk_hermes_channel_subscribe(int channel_id, VMUuid vm_id);
|
||||
|
||||
@@ -506,12 +527,17 @@ int sk_hermes_send_one(VMUuid from, VMUuid to, uint32_t type, uint32_t channel_i
|
||||
int sk_hermes_publish(VMUuid from, int channel_id, uint32_t type,
|
||||
void *payload_addr, uint32_t payload_len);
|
||||
|
||||
/* SK_HERMES_PENDING_MAX - per-subscriber pending-queue depth. The global
|
||||
* message arena (SK_HERMES_MSG_MAX) is the real ceiling on how many
|
||||
* messages can ever be in flight system-wide, so sizing each VM's own
|
||||
* queue to that same bound is a safe, simple upper limit rather than a
|
||||
* new number to justify. */
|
||||
#define SK_HERMES_PENDING_MAX SK_HERMES_MSG_MAX
|
||||
/* Stage M-0: each subscriber's pending queue grows on demand (was a fixed
|
||||
* SK_HERMES_PENDING_MAX = 32). A queue that can't grow refuses the send,
|
||||
* which rolls the message back (released, ledger exact). */
|
||||
|
||||
/* sk_hermes_grow_failures - arena, queue, member-list and reassembly-scratch
|
||||
* grows that failed (each logged at LOG_ERROR and refused whole). */
|
||||
uint64_t sk_hermes_grow_failures(void);
|
||||
|
||||
/* sk_hermes_arena_capacity - messages the arena can currently hold without
|
||||
* growing (diagnostic; grows in SK_HERMES_ARENA_SEGMENT steps). */
|
||||
size_t sk_hermes_arena_capacity(void);
|
||||
|
||||
/* sk_hermes_pending_count - number of messages currently queued for
|
||||
* vm_id (0 if vm_id has no queue yet -- never having received a message
|
||||
@@ -664,9 +690,9 @@ uint32_t sk_hermes_chunk_count(uint32_t len);
|
||||
* seq order regardless of the order chunks[] itself is passed in.
|
||||
*
|
||||
* Refuses (-1, *out_len untouched, no partial copy ever lands in
|
||||
* out_buf) if: n_chunks is 0 or exceeds SK_HERMES_MSG_MAX (the system-
|
||||
* wide message arena size -- chunking can never need more chunk-carrier
|
||||
* messages than the whole arena holds); any chunk is NULL, has no
|
||||
* out_buf) if: n_chunks is 0; its per-call seq scratch can't be allocated
|
||||
* (Stage M-0: was a fixed [SK_HERMES_MSG_MAX] array; now sized to n_chunks,
|
||||
* a failure logged and counted); any chunk is NULL, has no
|
||||
* payload, or its payload is shorter than sizeof(SkHermesChunkHeader);
|
||||
* the chunks' msg_ids disagree; seq values are not exactly
|
||||
* {0..n_chunks-1} (a duplicate or a gap); is_last is not set on exactly
|
||||
|
||||
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
@@ -883,9 +883,14 @@ static void kernel_main_deep(BootInfo *boot_info) {
|
||||
uint64_t reservoir0 = stadium_reservoir_peek(alloc_test_id);
|
||||
uint64_t expected_n = reservoir0 / SK_HERMES_Q_SLOT;
|
||||
uint64_t got_n = 0;
|
||||
SkHermesMessage *msgs[SK_HERMES_MSG_MAX];
|
||||
/* Stage M-0: the arena has no fixed size any more, so this list
|
||||
* is sized from the reservoir (expected_n) plus one, and every
|
||||
* loop below is bounded by msgs_cap -- the refusal past
|
||||
* expected_n must now come from the reservoir, not the arena. */
|
||||
uint64_t msgs_cap = expected_n + 1;
|
||||
SkHermesMessage **msgs = (SkHermesMessage **)kmalloc((size_t)msgs_cap * sizeof(SkHermesMessage *));
|
||||
SkHermesMessage *msg;
|
||||
int ok = 1;
|
||||
int ok = msgs ? 1 : 0;
|
||||
size_t i;
|
||||
uint64_t held0, pulled0, returned0, consumed0;
|
||||
uint64_t held1, pulled1, returned1, consumed1;
|
||||
@@ -896,7 +901,7 @@ static void kernel_main_deep(BootInfo *boot_info) {
|
||||
* only thing that has ever called these functions. */
|
||||
sk_hermes_ledger(&held0, &pulled0, &returned0, &consumed0);
|
||||
|
||||
while (got_n < SK_HERMES_MSG_MAX && sk_hermes_alloc(alloc_test_id, &msg) == 0) {
|
||||
while (msgs && got_n < msgs_cap && sk_hermes_alloc(alloc_test_id, &msg) == 0) {
|
||||
msgs[got_n] = msg;
|
||||
got_n++;
|
||||
}
|
||||
@@ -958,7 +963,7 @@ static void kernel_main_deep(BootInfo *boot_info) {
|
||||
uint64_t held3, pulled3, returned3, consumed3;
|
||||
uint64_t held4, pulled4, returned4, consumed4;
|
||||
uint64_t n2 = 0;
|
||||
while (n2 < SK_HERMES_MSG_MAX && sk_hermes_alloc(alloc_test_id, &msg) == 0) {
|
||||
while (msgs && n2 < msgs_cap && sk_hermes_alloc(alloc_test_id, &msg) == 0) {
|
||||
msgs[n2] = msg;
|
||||
n2++;
|
||||
}
|
||||
@@ -998,7 +1003,7 @@ static void kernel_main_deep(BootInfo *boot_info) {
|
||||
uint64_t expected_b = stadium_reservoir_peek(alloc_test_id) / SK_HERMES_Q_SLOT;
|
||||
int sb = 1;
|
||||
if (!stadium_conserved(alloc_test_id)) sb = 0; /* before */
|
||||
while (nb < SK_HERMES_MSG_MAX && sk_hermes_alloc(alloc_test_id, &msg) == 0) {
|
||||
while (msgs && nb < msgs_cap && sk_hermes_alloc(alloc_test_id, &msg) == 0) {
|
||||
msgs[nb++] = msg;
|
||||
}
|
||||
if (nb != expected_b || nb == 0) sb = 0;
|
||||
@@ -1025,7 +1030,7 @@ static void kernel_main_deep(BootInfo *boot_info) {
|
||||
int sc = 1;
|
||||
uint64_t k = 0;
|
||||
if (!sk_hermes_scan_check() || sk_hermes_scan_held(&live) != 0 || live != 0) sc = 0;
|
||||
while (k < 4 && sk_hermes_alloc(alloc_test_id, &msg) == 0) msgs[k++] = msg;
|
||||
while (msgs && k < 4 && k < msgs_cap && sk_hermes_alloc(alloc_test_id, &msg) == 0) msgs[k++] = msg;
|
||||
if (k != 4) sc = 0;
|
||||
s1 = sk_hermes_scan_held(&live);
|
||||
sk_hermes_ledger(&h, &p_, &r, &c);
|
||||
@@ -1073,6 +1078,9 @@ static void kernel_main_deep(BootInfo *boot_info) {
|
||||
print_uint(" pulled(final)=", pulled2);
|
||||
print_uint(" returned(final)=", returned2);
|
||||
print_uint(" consumed(final)=", consumed2);
|
||||
print_uint(" arena_capacity=", (uint64_t)sk_hermes_arena_capacity());
|
||||
print_uint(" grow_failures=", sk_hermes_grow_failures());
|
||||
if (msgs) kfree(msgs);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
#include "starkernel/q48_16.h"
|
||||
#include "starkernel/kmalloc.h"
|
||||
#include "console.h"
|
||||
#include "log.h" /* Stage M-0: grow failures are logged at LOG_ERROR */
|
||||
#include "vm.h" /* task 3.4 -- VM struct fields, vm_interpret() */
|
||||
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.8.md §3.13.3 -- wait stamps */
|
||||
|
||||
@@ -72,7 +73,60 @@ static void console_put_u64(uint64_t v) {
|
||||
console_puts(&buf[i]);
|
||||
}
|
||||
|
||||
static SkHermesMessage sk_hermes_msgs[SK_HERMES_MSG_MAX];
|
||||
/* FABRIC-3.9.0.md §7.0 (Stage M-0): the message arena grows on demand (was
|
||||
* a static sk_hermes_msgs[32]). A directory of SK_HERMES_ARENA_SEGMENT-sized
|
||||
* segments: messages never move once allocated (see kernel_hermes.h), only
|
||||
* the directory is allocate-copy-free. A message's identity is its arena
|
||||
* index, lowest free first -- the same numbering as the fixed array. */
|
||||
static SkHermesMessage **sk_hermes_segs = (SkHermesMessage **)0;
|
||||
static size_t sk_hermes_nsegs = 0;
|
||||
static size_t sk_hermes_segs_cap = 0;
|
||||
static uint64_t sk_hermes_grow_fail = 0;
|
||||
|
||||
uint64_t sk_hermes_grow_failures(void) { return sk_hermes_grow_fail; }
|
||||
|
||||
size_t sk_hermes_arena_capacity(void) { return sk_hermes_nsegs * SK_HERMES_ARENA_SEGMENT; }
|
||||
|
||||
/* Every grow failure: logged (persisted), printed in full, counted. The
|
||||
* caller then refuses whole. */
|
||||
static void sk_hermes_grow_failed(const char *what) {
|
||||
sk_hermes_grow_fail++;
|
||||
console_puts("Kernel-Hermes: can't grow ");
|
||||
console_println(what);
|
||||
log_message(LOG_ERROR, "Kernel-Hermes: can't grow %s", what);
|
||||
}
|
||||
|
||||
static SkHermesMessage *msg_at(size_t idx) {
|
||||
return &sk_hermes_segs[idx / SK_HERMES_ARENA_SEGMENT][idx % SK_HERMES_ARENA_SEGMENT];
|
||||
}
|
||||
|
||||
/* Add one segment (and grow the directory first if it's full). */
|
||||
static int sk_hermes_arena_grow(void) {
|
||||
SkHermesMessage *seg;
|
||||
|
||||
if (sk_hermes_nsegs == sk_hermes_segs_cap) {
|
||||
size_t ncap = sk_hermes_segs_cap ? sk_hermes_segs_cap * 2 : 4;
|
||||
SkHermesMessage **dir = (SkHermesMessage **)kmalloc(ncap * sizeof(SkHermesMessage *));
|
||||
size_t i;
|
||||
if (!dir) {
|
||||
sk_hermes_grow_failed("message arena directory");
|
||||
return -1;
|
||||
}
|
||||
for (i = 0; i < sk_hermes_nsegs; i++) dir[i] = sk_hermes_segs[i];
|
||||
if (sk_hermes_segs) kfree(sk_hermes_segs);
|
||||
sk_hermes_segs = dir;
|
||||
sk_hermes_segs_cap = ncap;
|
||||
}
|
||||
|
||||
seg = (SkHermesMessage *)kmalloc(SK_HERMES_ARENA_SEGMENT * sizeof(SkHermesMessage));
|
||||
if (!seg) {
|
||||
sk_hermes_grow_failed("message arena");
|
||||
return -1;
|
||||
}
|
||||
memset(seg, 0, SK_HERMES_ARENA_SEGMENT * sizeof(SkHermesMessage));
|
||||
sk_hermes_segs[sk_hermes_nsegs++] = seg;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Task 2.4 (item 28), FABRIC-3.5.md SXXXVII.3/SXL.4's four counters.
|
||||
* `consumed` is declared now but touched nowhere yet -- task 2.5 adds
|
||||
@@ -113,11 +167,12 @@ uint64_t sk_hermes_audit_failure_count(void) {
|
||||
uint64_t sk_hermes_scan_held(size_t *live_count) {
|
||||
uint64_t sum = 0;
|
||||
size_t n = 0;
|
||||
int i;
|
||||
size_t i, len = sk_hermes_arena_capacity();
|
||||
|
||||
for (i = 0; i < SK_HERMES_MSG_MAX; i++) {
|
||||
if (!sk_hermes_msgs[i].in_use || sk_hermes_msgs[i].stadium_cell < 0) continue;
|
||||
sum += stadium_cells()[sk_hermes_msgs[i].stadium_cell].header.heat;
|
||||
for (i = 0; i < len; i++) {
|
||||
SkHermesMessage *m = msg_at(i);
|
||||
if (!m->in_use || m->stadium_cell < 0) continue;
|
||||
sum += stadium_cells()[m->stadium_cell].header.heat;
|
||||
n++;
|
||||
}
|
||||
if (live_count) *live_count = n;
|
||||
@@ -128,16 +183,20 @@ int sk_hermes_scan_check(void) {
|
||||
return sk_hermes_scan_held((size_t *)0) == sk_hermes_held;
|
||||
}
|
||||
|
||||
static int sk_hermes_find_free_slot(void) {
|
||||
int i;
|
||||
for (i = 0; i < SK_HERMES_MSG_MAX; i++) {
|
||||
if (!sk_hermes_msgs[i].in_use) return i;
|
||||
/* Lowest free index; grows the arena by a segment when every slot is in
|
||||
* use. -1 only if that grow fails (logged, counted). */
|
||||
static long sk_hermes_find_free_slot(void) {
|
||||
size_t i, len = sk_hermes_arena_capacity();
|
||||
for (i = 0; i < len; i++) {
|
||||
if (!msg_at(i)->in_use) return (long)i;
|
||||
}
|
||||
return -1;
|
||||
if (sk_hermes_arena_grow() != 0) return -1;
|
||||
return (long)len; /* first slot of the new segment */
|
||||
}
|
||||
|
||||
int sk_hermes_alloc(VMUuid vm_id, SkHermesMessage **out_msg) {
|
||||
int slot;
|
||||
long slot;
|
||||
SkHermesMessage *m;
|
||||
uint64_t pulled;
|
||||
StadiumPatronHeader candidate;
|
||||
size_t cell;
|
||||
@@ -150,7 +209,7 @@ int sk_hermes_alloc(VMUuid vm_id, SkHermesMessage **out_msg) {
|
||||
if (stadium_reservoir_peek(vm_id) < SK_HERMES_Q_SLOT) return -1;
|
||||
|
||||
slot = sk_hermes_find_free_slot();
|
||||
if (slot < 0) return -1; /* arena full, reservoir untouched */
|
||||
if (slot < 0) return -1; /* arena couldn't grow (logged, counted), reservoir untouched */
|
||||
|
||||
pulled = stadium_reservoir_pull(vm_id, SK_HERMES_Q_SLOT);
|
||||
if (pulled < SK_HERMES_Q_SLOT) {
|
||||
@@ -174,7 +233,7 @@ int sk_hermes_alloc(VMUuid vm_id, SkHermesMessage **out_msg) {
|
||||
candidate.ttl = 0; /* decay/TTL is task 2.5's scope, not this one's */
|
||||
candidate.link = 0;
|
||||
candidate.contains = STADIUM_CONTAINS_NONE;
|
||||
candidate.mass = 1; /* message body lives in sk_hermes_msgs[], not here */
|
||||
candidate.mass = 1; /* message body lives in the arena, not here */
|
||||
candidate.flags = 0;
|
||||
candidate.behaviour = (uint8_t)STADIUM_BEHAVIOUR_DELIVER;
|
||||
|
||||
@@ -188,11 +247,13 @@ int sk_hermes_alloc(VMUuid vm_id, SkHermesMessage **out_msg) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(&sk_hermes_msgs[slot], 0, sizeof(SkHermesMessage));
|
||||
sk_hermes_msgs[slot].in_use = 1;
|
||||
sk_hermes_msgs[slot].stadium_cell = (int32_t)cell;
|
||||
sk_hermes_msgs[slot].owner = vm_id;
|
||||
*out_msg = &sk_hermes_msgs[slot];
|
||||
m = msg_at((size_t)slot);
|
||||
memset(m, 0, sizeof(SkHermesMessage));
|
||||
m->in_use = 1;
|
||||
m->stadium_cell = (int32_t)cell;
|
||||
m->owner = vm_id;
|
||||
m->index = (uint32_t)slot;
|
||||
*out_msg = m;
|
||||
|
||||
/* Task 2.4: the one site where held/pulled increment -- only reached
|
||||
* once every refusal path above has already returned. */
|
||||
@@ -336,6 +397,11 @@ int sk_hermes_channel_destroy(int channel_id) {
|
||||
if (channel_id == SK_HERMES_CHANNEL_COMMON) return -1; /* permanent, SXLV.1 */
|
||||
if (!channel_valid(channel_id)) return -1;
|
||||
|
||||
/* Stage M-0: the member list is heap-grown -- free it, or create()'s
|
||||
* memset of this slot would leak it. */
|
||||
if (sk_hermes_channels[channel_id].membership.members) {
|
||||
kfree(sk_hermes_channels[channel_id].membership.members);
|
||||
}
|
||||
memset(&sk_hermes_channels[channel_id], 0, sizeof(SkHermesChannel));
|
||||
return 0;
|
||||
}
|
||||
@@ -350,7 +416,20 @@ int sk_hermes_channel_subscribe(int channel_id, VMUuid vm_id) {
|
||||
for (i = 0; i < m->count; i++) {
|
||||
if (vm_uuid_equal(m->members[i], vm_id)) return -1; /* already a member */
|
||||
}
|
||||
if (m->count >= SK_HERMES_MEMBER_MAX) return -1;
|
||||
if (m->count == m->cap) {
|
||||
/* Stage M-0: grow the member list (was a fixed [64]) -- doubling,
|
||||
* allocate-copy-free, all-or-nothing. */
|
||||
size_t ncap = m->cap ? m->cap * 2 : 8;
|
||||
VMUuid *grown = (VMUuid *)kmalloc(ncap * sizeof(VMUuid));
|
||||
if (!grown) {
|
||||
sk_hermes_grow_failed("channel member list");
|
||||
return -1;
|
||||
}
|
||||
for (i = 0; i < m->count; i++) grown[i] = m->members[i];
|
||||
if (m->members) kfree(m->members);
|
||||
m->members = grown;
|
||||
m->cap = ncap;
|
||||
}
|
||||
|
||||
m->members[m->count] = vm_id;
|
||||
m->count++;
|
||||
@@ -396,17 +475,21 @@ int sk_hermes_channel_member_count(int channel_id) {
|
||||
}
|
||||
|
||||
/* Task 3.3: per-subscriber pending queue table. See kernel_hermes.h's own
|
||||
* doc comments on sk_hermes_queues_boot_init()/SK_HERMES_PENDING_MAX for
|
||||
* scope and sizing reasoning. A queue is found-or-created lazily by
|
||||
* doc comment on sk_hermes_queues_boot_init() for scope and sizing
|
||||
* reasoning (each queue's depth grows on demand since Stage M-0; the table
|
||||
* of queues is still RAM-sized at boot -- S-3's per-VM-tables item). A
|
||||
* queue is found-or-created lazily by
|
||||
* vm_id, same shape as stadium.c's quota_slot_for_vm() and session.c's
|
||||
* session_find() -- not pre-populated at birth like channel membership
|
||||
* is, since not every VM ever receives a message. */
|
||||
typedef struct {
|
||||
VMUuid vm_id;
|
||||
int in_use;
|
||||
int slots[SK_HERMES_PENDING_MAX]; /* sk_hermes_msgs[] indices, circular FIFO */
|
||||
size_t head;
|
||||
size_t count;
|
||||
VMUuid vm_id;
|
||||
int in_use;
|
||||
uint32_t *slots; /* arena indices, circular FIFO; Stage M-0: heap-grown
|
||||
* on push (was a fixed [32]) */
|
||||
size_t cap;
|
||||
size_t head;
|
||||
size_t count;
|
||||
} SkHermesPendingQueue;
|
||||
|
||||
static SkHermesPendingQueue *sk_hermes_queues = (SkHermesPendingQueue *)0;
|
||||
@@ -460,7 +543,15 @@ static SkHermesPendingQueue *find_or_create_queue(VMUuid vm_id) {
|
||||
}
|
||||
if (free_slot < 0) return (SkHermesPendingQueue *)0; /* table full */
|
||||
|
||||
memset(&sk_hermes_queues[free_slot], 0, sizeof(SkHermesPendingQueue));
|
||||
{
|
||||
/* Stage M-0: a reused table entry keeps its grown ring buffer
|
||||
* (empty, count 0) rather than the memset leaking it. */
|
||||
uint32_t *keep = sk_hermes_queues[free_slot].slots;
|
||||
size_t keep_cap = sk_hermes_queues[free_slot].cap;
|
||||
memset(&sk_hermes_queues[free_slot], 0, sizeof(SkHermesPendingQueue));
|
||||
sk_hermes_queues[free_slot].slots = keep;
|
||||
sk_hermes_queues[free_slot].cap = keep_cap;
|
||||
}
|
||||
sk_hermes_queues[free_slot].vm_id = vm_id;
|
||||
sk_hermes_queues[free_slot].in_use = 1;
|
||||
return &sk_hermes_queues[free_slot];
|
||||
@@ -473,10 +564,25 @@ static SkHermesPendingQueue *find_or_create_queue(VMUuid vm_id) {
|
||||
* nothing is pending anywhere, the overwhelmingly common case. */
|
||||
static int sk_hermes_pending_total = 0;
|
||||
|
||||
static int queue_push(SkHermesPendingQueue *q, int msg_index) {
|
||||
static int queue_push(SkHermesPendingQueue *q, uint32_t msg_index) {
|
||||
size_t tail;
|
||||
if (q->count >= SK_HERMES_PENDING_MAX) return -1;
|
||||
tail = (q->head + q->count) % SK_HERMES_PENDING_MAX;
|
||||
if (q->count == q->cap) {
|
||||
/* Stage M-0: grow the ring (doubling), unwrapped into FIFO order
|
||||
* with head = 0. All-or-nothing: the old ring stays if this fails. */
|
||||
size_t ncap = q->cap ? q->cap * 2 : 8;
|
||||
uint32_t *grown = (uint32_t *)kmalloc(ncap * sizeof(uint32_t));
|
||||
size_t i;
|
||||
if (!grown) {
|
||||
sk_hermes_grow_failed("pending queue");
|
||||
return -1;
|
||||
}
|
||||
for (i = 0; i < q->count; i++) grown[i] = q->slots[(q->head + i) % q->cap];
|
||||
if (q->slots) kfree(q->slots);
|
||||
q->slots = grown;
|
||||
q->cap = ncap;
|
||||
q->head = 0;
|
||||
}
|
||||
tail = (q->head + q->count) % q->cap;
|
||||
q->slots[tail] = msg_index;
|
||||
q->count++;
|
||||
sk_hermes_pending_total++;
|
||||
@@ -487,7 +593,7 @@ int sk_hermes_send_one(VMUuid from, VMUuid to, uint32_t type, uint32_t channel_i
|
||||
void *payload_addr, uint32_t payload_len) {
|
||||
SkHermesMessage *msg;
|
||||
SkHermesPendingQueue *q;
|
||||
int idx;
|
||||
uint32_t idx;
|
||||
|
||||
/* Task 3.5: the one-block bound, enforced uniformly here whether or
|
||||
* not payload_addr happens to be a chunk carrier -- see
|
||||
@@ -526,9 +632,9 @@ int sk_hermes_send_one(VMUuid from, VMUuid to, uint32_t type, uint32_t channel_i
|
||||
return -1;
|
||||
}
|
||||
|
||||
idx = (int)(msg - sk_hermes_msgs);
|
||||
idx = msg->index;
|
||||
if (queue_push(q, idx) != 0) {
|
||||
sk_hermes_release(msg); /* roll back -- destination queue full */
|
||||
sk_hermes_release(msg); /* roll back -- queue couldn't grow (logged, counted) */
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -573,13 +679,13 @@ int sk_hermes_pending_count(VMUuid vm_id) {
|
||||
SkHermesMessage *sk_hermes_pending_peek(VMUuid vm_id) {
|
||||
SkHermesPendingQueue *q = find_queue(vm_id);
|
||||
if (!q || q->count == 0) return (SkHermesMessage *)0;
|
||||
return &sk_hermes_msgs[q->slots[q->head]];
|
||||
return msg_at(q->slots[q->head]);
|
||||
}
|
||||
|
||||
int sk_hermes_pending_pop(VMUuid vm_id) {
|
||||
SkHermesPendingQueue *q = find_queue(vm_id);
|
||||
if (!q || q->count == 0) return -1;
|
||||
q->head = (q->head + 1) % SK_HERMES_PENDING_MAX;
|
||||
q->head = (q->head + 1) % q->cap;
|
||||
q->count--;
|
||||
sk_hermes_pending_total--;
|
||||
return 0;
|
||||
@@ -692,18 +798,40 @@ uint32_t sk_hermes_chunk_count(uint32_t len) {
|
||||
return (len + SK_HERMES_CHUNK_MAX_SLICE - 1) / SK_HERMES_CHUNK_MAX_SLICE;
|
||||
}
|
||||
|
||||
static int reassemble_with(SkHermesMessage **chunks, int n_chunks,
|
||||
uint8_t *out_buf, uint32_t out_buf_cap,
|
||||
uint32_t *out_len, int *present);
|
||||
|
||||
/* Stage M-0: present[] is sized to n_chunks per call (was a fixed
|
||||
* [SK_HERMES_MSG_MAX], which also capped n_chunks at 32). One exit path so
|
||||
* the scratch is always freed; the validation itself is unchanged, in
|
||||
* reassemble_with() below. */
|
||||
int sk_hermes_reassemble(SkHermesMessage **chunks, int n_chunks,
|
||||
uint8_t *out_buf, uint32_t out_buf_cap,
|
||||
uint32_t *out_len) {
|
||||
int present[SK_HERMES_MSG_MAX];
|
||||
int *present;
|
||||
int rc;
|
||||
|
||||
if (!chunks || n_chunks <= 0 || !out_buf || !out_len) return -1;
|
||||
|
||||
present = (int *)kmalloc((size_t)n_chunks * sizeof(int));
|
||||
if (!present) {
|
||||
sk_hermes_grow_failed("reassembly scratch");
|
||||
return -1;
|
||||
}
|
||||
rc = reassemble_with(chunks, n_chunks, out_buf, out_buf_cap, out_len, present);
|
||||
kfree(present);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int reassemble_with(SkHermesMessage **chunks, int n_chunks,
|
||||
uint8_t *out_buf, uint32_t out_buf_cap,
|
||||
uint32_t *out_len, int *present) {
|
||||
int i;
|
||||
uint32_t msg_id;
|
||||
uint32_t total;
|
||||
uint32_t last_slice_len = 0;
|
||||
|
||||
if (!chunks || n_chunks <= 0 || !out_buf || !out_len) return -1;
|
||||
if (n_chunks > SK_HERMES_MSG_MAX) return -1; /* guard before indexing present[] */
|
||||
|
||||
for (i = 0; i < n_chunks; i++) present[i] = 0;
|
||||
|
||||
if (!chunks[0] || !chunks[0]->payload_addr ||
|
||||
|
||||
Reference in New Issue
Block a user