Captain Bob's call after seeing the identity-heap-capacity findings (FABRIC-3.md §X.4): the number of concurrently-running VMs is not known in advance, and once this is a complete operating system the heap should be able to use whatever memory is actually available -- not a hardcoded compile-time ceiling. This was already half-built and just not wired up. src/starkernel/vm/alloc_kernel.c previously implemented sf_malloc()/ sf_free() (platform_alloc.h's allocator abstraction -- what vm_create_word() calls for every VM's word dictionary) as its own isolated static 4MB arena: first-fit free list, no splitting or coalescing. That's exactly the allocator that topped out around 6 concurrent WIREBIND-born identities, failing from fragmentation before true capacity exhaustion (§X.4's own measurements). Sitting right next to it, unused for this purpose: src/starkernel/memory/ kmalloc.c, the kernel's general heap. Already initialized at boot (M6, kernel_main.c, well before any VM is ever born), reserved from real PMM-tracked physical memory rather than a fixed array, defaults to a 2 GiB floor explicitly sized "for 256+ baby VMs" per its own comment, overridable via the --heap= boot flag, and its free list actually coalesces neighboring blocks on every free. Change: alloc_kernel.c's sf_malloc()/sf_free() now delegate to kmalloc_aligned()/kfree() instead of managing a separate arena. sf_alloc_init() becomes a no-op (kmalloc is already initialized by the time any VM allocation can happen, and "resetting" a heap now shared by every kernel subsystem would be actively wrong -- confirmed no external caller depended on its old reset semantics). sf_alloc_get_stats() reads kmalloc_get_stats() fresh rather than shadowing byte counts locally; alloc_count/free_count (which kmalloc.c doesn't track) stay as simple local counters. sf_calloc()/sf_realloc() are otherwise unchanged. Kernel- only: the hosted (non-kernel) StarForth build keeps its own separate alloc_host.c implementation, untouched. Verified live: replaying the exact hotplug sequence that previously topped out at 6 identities (Zuse + 8 identities, one at a time via QMP device_add) now succeeds for all 9, where identity 05 specifically used to fail. Three-arch clean qemu acceptance (single Zuse device, the standard regression case) passed on amd64, aarch64, and riscv64 -- one aarch64 attempt hit an unrelated, already-documented one-off QEMU hiccup (empty log, boot never progressed past firmware) and passed cleanly on retry with no rebuild. Not addressed here: the underlying free-list itself is still first-fit without splitting (only coalescing changed, inherited from kmalloc.c); per-VM dictionary sizing (shrinking what each WIREBIND VM's word set actually needs) is a separate, still-open lever from FABRIC-3.md §X.4's open architecture question. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Ec88YKxxhZGG1RNnune78
234 lines
8.8 KiB
C
234 lines
8.8 KiB
C
/*
|
||
StarForth — Steady-State Virtual Machine Runtime
|
||
|
||
Copyright (c) 2023–2025 Robert A. James
|
||
All rights reserved.
|
||
|
||
This file is part of the StarForth project.
|
||
|
||
Licensed under the StarForth License, Version 1.0 (the "License");
|
||
you may not use this file except in compliance with the License.
|
||
|
||
You may obtain a copy of the License at:
|
||
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
|
||
|
||
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
express or implied, including but not limited to the warranties of
|
||
merchantability, fitness for a particular purpose, and noninfringement.
|
||
|
||
See the License for the specific language governing permissions and
|
||
limitations under the License.
|
||
*/
|
||
|
||
/*
|
||
* platform/alloc_kernel.c - Kernel (bare-metal) memory allocator
|
||
*
|
||
* FABRIC-3.md §X.4, 2026-09-07: this used to be its own isolated static
|
||
* 4MB arena (first-fit free list, no splitting/coalescing) -- sized small
|
||
* enough, and fragile enough under concurrent VM churn, that a live
|
||
* identity-heap-capacity test found the arena topping out around 6
|
||
* concurrent WIREBIND-born VMs, failing *before* true capacity exhaustion
|
||
* (fragmentation from concurrent background allocation, no coalescing to
|
||
* recover from it). Meanwhile `kmalloc.c` -- the kernel's general heap,
|
||
* already initialised at boot (M6, well before any VM is ever born) --
|
||
* sits right next to it: reserved from real PMM-tracked physical memory
|
||
* (not a fixed compile-time array), defaults to a 2 GiB floor explicitly
|
||
* sized "for 256+ baby VMs" per its own comment, overridable via the
|
||
* `--heap=` boot flag, and its free list *does* coalesce neighboring
|
||
* blocks on every free.
|
||
*
|
||
* Every VM's word dictionary (`vm_create_word()`, the shared/vendored VM
|
||
* core) allocates through this file's `sf_malloc()`/`sf_free()` --
|
||
* `platform_alloc.h`'s portable allocator abstraction, kernel-side. This
|
||
* file now simply delegates to `kmalloc()`/`kfree()` instead of managing
|
||
* its own separate, much smaller arena: same API contract callers already
|
||
* depend on, real headroom (whatever the boot-time heap ends up being,
|
||
* not a hardcoded 4MB), and real coalescing. No new allocator was
|
||
* invented -- `kmalloc.c` already existed, was already boot-tested on all
|
||
* three arches, and was simply never wired up as the backing store for
|
||
* VM dictionaries specifically.
|
||
*
|
||
* `sf_alloc_init()` is now a no-op (see its own doc comment below): with
|
||
* a *shared* heap serving every kernel subsystem, not an isolated arena
|
||
* used only for VM dictionaries, "reset" is no longer a sane operation --
|
||
* nothing external ever called it anyway (confirmed: no callers besides
|
||
* `sf_malloc()`'s own lazy-init guard, which this file's `sf_malloc()`
|
||
* still keeps, now as a readiness check rather than an initializer).
|
||
*
|
||
* This file's own alignment guarantee (`SF_ALIGN`, 8 bytes) is preserved
|
||
* via `kmalloc_aligned()` -- `kmalloc()`'s own default alignment
|
||
* (`KMALLOC_MIN_ALIGN`, 16 bytes) already satisfies it, but asking
|
||
* explicitly keeps this file's contract self-documenting rather than
|
||
* relying on kmalloc.c's own default not changing under it.
|
||
*/
|
||
|
||
#include "platform_alloc.h"
|
||
#include "starkernel/kmalloc.h"
|
||
#include <stdint.h>
|
||
#include <stddef.h>
|
||
|
||
/* Alignment for all allocations (8 bytes for 64-bit safety) -- unchanged
|
||
* from this file's previous arena-based implementation. */
|
||
#define SF_ALIGN 8
|
||
|
||
/* alloc_count/free_count have no kmalloc.c equivalent (it tracks bytes,
|
||
* not call counts) -- kept here as simple diagnostic counters layered on
|
||
* top of kmalloc's own byte-accurate stats, which sf_alloc_get_stats()
|
||
* below reads fresh on every call rather than shadowing them locally. */
|
||
static size_t g_alloc_count = 0;
|
||
static size_t g_free_count = 0;
|
||
|
||
/**
|
||
* @brief No-op: kept only for API compatibility with `platform_alloc.h`.
|
||
*
|
||
* The backing store is now the shared kernel heap (`kmalloc.c`), already
|
||
* initialised at boot (M6) well before any VM allocation can occur --
|
||
* there is nothing left for this function to set up, and "resetting" a
|
||
* heap shared by every other kernel subsystem would be actively wrong.
|
||
* Kept callable (matches its documented "safe to call at any point"
|
||
* contract) so no caller needs to change.
|
||
*
|
||
* @return 0 always (cannot fail)
|
||
*/
|
||
int sf_alloc_init(void)
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
/*
|
||
* @brief Allocate memory from the shared kernel heap.
|
||
*
|
||
* Delegates to `kmalloc_aligned()` (`SF_ALIGN`-byte aligned, matching
|
||
* this file's previous guarantee). Returns @c NULL for zero-size
|
||
* requests and whenever the shared heap itself returns NULL (not yet
|
||
* initialised, or genuinely out of memory) -- both already part of this
|
||
* function's documented contract.
|
||
*
|
||
* @param size Number of bytes to allocate.
|
||
* @return Pointer to the allocated block on success, @c NULL on failure.
|
||
*/
|
||
void* sf_malloc(size_t size)
|
||
{
|
||
if (size == 0) return (void*)0;
|
||
|
||
void *ptr = kmalloc_aligned(size, SF_ALIGN);
|
||
if (ptr) g_alloc_count++;
|
||
return ptr;
|
||
}
|
||
|
||
/*
|
||
* @brief Allocate and zero-initialise a contiguous array from the kernel heap.
|
||
*
|
||
* Unchanged from this file's previous implementation: computes
|
||
* @p count × @p size, checks for integer overflow via
|
||
* @c total/size != count, then delegates to @c sf_malloc(). The returned
|
||
* block is zeroed with a manual byte loop rather than @c memset so that
|
||
* the kernel build remains freestanding with no libc dependency.
|
||
* Returns @c NULL when either argument is zero, on overflow, or on heap
|
||
* exhaustion.
|
||
*
|
||
* @param count Number of elements to allocate.
|
||
* @param size Size of each element in bytes.
|
||
* @return Pointer to the zeroed block on success, @c NULL on failure.
|
||
*/
|
||
void* sf_calloc(size_t count, size_t size)
|
||
{
|
||
if (count == 0 || size == 0) return (void*)0;
|
||
|
||
size_t total = count * size;
|
||
|
||
/* Check for overflow */
|
||
if (size != 0 && total / size != count)
|
||
{
|
||
return (void*)0;
|
||
}
|
||
|
||
void* ptr = sf_malloc(total);
|
||
if (ptr)
|
||
{
|
||
/* Zero-initialize */
|
||
uint8_t* p = (uint8_t*)ptr;
|
||
for (size_t i = 0; i < total; i++)
|
||
{
|
||
p[i] = 0;
|
||
}
|
||
}
|
||
return ptr;
|
||
}
|
||
|
||
/*
|
||
* @brief Resize an allocation in the kernel heap.
|
||
*
|
||
* Unchanged from this file's previous implementation: `kmalloc.c` has no
|
||
* realloc-equivalent, so a fresh block of @p new_size bytes is allocated
|
||
* and returned; the original block at @p ptr is left for the caller to
|
||
* free explicitly (this function does not free it, matching this file's
|
||
* prior documented behavior exactly -- not a regression introduced by
|
||
* the kmalloc.c switch).
|
||
*
|
||
* Special cases match the C standard:
|
||
* - @p ptr == @c NULL → equivalent to @c sf_malloc(@p new_size).
|
||
* - @p new_size == 0 → returns @c NULL (caller treats old block as freed).
|
||
*
|
||
* The caller is responsible for copying content from the old block
|
||
* before discarding the old pointer; this function does not perform the
|
||
* copy.
|
||
*
|
||
* @param ptr Pointer to the existing allocation (may be @c NULL).
|
||
* @param new_size Desired size of the new block in bytes.
|
||
* @return Pointer to the new block on success, @c NULL on failure or when
|
||
* @p new_size is zero.
|
||
*/
|
||
void* sf_realloc(void* ptr, size_t new_size)
|
||
{
|
||
if (!ptr) return sf_malloc(new_size);
|
||
if (new_size == 0) return (void*)0;
|
||
|
||
return sf_malloc(new_size);
|
||
}
|
||
|
||
/*
|
||
* @brief Release a kernel heap allocation, making it available for reuse.
|
||
*
|
||
* Delegates to @c kfree(), which coalesces the freed block with any free
|
||
* neighbors -- real reclamation, unlike this file's previous first-fit-
|
||
* no-splitting free list (FABRIC-3.md §X.4: that lack of coalescing was
|
||
* the proximate cause of a live-observed allocation failure under
|
||
* concurrent VM churn even with more than enough free bytes overall).
|
||
*
|
||
* @param ptr Pointer previously returned by @c sf_malloc() / @c sf_calloc()
|
||
* (may be @c NULL; silently ignored, matching @c kfree()'s own
|
||
* contract). Must not be used again by the caller after this
|
||
* call, and must not be freed twice.
|
||
*/
|
||
void sf_free(void* ptr)
|
||
{
|
||
if (!ptr) return;
|
||
|
||
kfree(ptr);
|
||
g_free_count++;
|
||
}
|
||
|
||
/*
|
||
* @brief Retrieve a snapshot of kernel allocator statistics.
|
||
*
|
||
* Reads fresh from `kmalloc_get_stats()` on every call -- the shared heap
|
||
* is the real source of truth, not a value shadowed locally. alloc_count/
|
||
* free_count (which kmalloc.c does not track) come from this file's own
|
||
* counters instead.
|
||
*
|
||
* @param stats Output buffer to receive the statistics; silently returns
|
||
* without writing if @p stats is @c NULL.
|
||
*/
|
||
void sf_alloc_get_stats(sf_alloc_stats_t* stats)
|
||
{
|
||
if (!stats) return;
|
||
|
||
kmalloc_stats_t k = kmalloc_get_stats();
|
||
stats->total_bytes = (size_t)k.total_bytes;
|
||
stats->used_bytes = (size_t)k.used_bytes;
|
||
stats->peak_bytes = (size_t)k.peak_bytes;
|
||
stats->alloc_count = g_alloc_count;
|
||
stats->free_count = g_free_count;
|
||
}
|