ramdisk: the live root grows into half of memory

The live root was a fixed 96 MB image in the kernel: 60 MB of zeros on
the ISO, twice, and room for 60 MB of packages whatever the machine had.

Now the image is only what the root holds, plus slack, measured by
building it once large. Past its end the ramdisk goes on to half of the
memory free at mount, as pages taken the first time each block is
written; an unwritten block reads as zeros and costs nothing. SchFS
keeps no free-space map on disk, so growing a volume is a larger total
before open builds the map: grow_to, set for the ramdisk root.

8 GB machine: a 4 GB root, Xorg installed into it in 77 s with 7.6 GB
still free. The ISO is 172 MB instead of 344.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
Nexoniarz 2026-10-24 17:20:43 +02:00
commit d0941780f5
7 changed files with 120 additions and 13 deletions

View file

@ -14,6 +14,19 @@
//
// Enabled by building with RAMROOT=1. Without it this file still compiles, the
// device reports zero size, and the kernel boots from the IDE disk as before.
//
// The device is larger than the image. The image is only what the root holds,
// with a little slack; past its end the device goes on to half of the memory
// free at mount time, and those blocks are pages taken from kalloc the first
// time each is written -- a block nobody has written reads as zeros and costs
// nothing. SchFS grows the volume to the device's size when it mounts (see
// grow_to in kernel/fs/schfs/vol.c), so `apk add` on a live system has as
// much room as the machine has memory to give, rather than whatever empty
// space was built into the ISO. The ISO used to carry 60 MB of zeros for it.
//
// Pages are not given back when SchFS frees a block: there is no discard.
// A freed block is reused by the next write, so the cost is the high-water
// mark, not a leak.
#include "core/types.h"
#include "core/defs.h"
@ -33,11 +46,23 @@ extern uchar _binary_ramdisk_size[];
static uchar *rd_base;
static uint64 rd_size; // bytes
static uint rd_blocks; // rd_size / BSIZE
static uint rd_blocks; // rd_size / BSIZE: the image, served in place
// Past the image: rd_dir[i] is a page of pointers, entry j the page holding
// block rd_blocks + i*RD_PTRS + j, or 0 for one never written. 32768 pages of
// 512 pointers reach 64 GB, which is as far as half of any machine this runs
// on is likely to go, and costs 256 KB of pointers here.
#define RD_PTRS (PGSIZE / sizeof(uchar *))
#define RD_DIRS 32768
static uchar **rd_dir[RD_DIRS];
static uint64 rd_total; // blocks, image and the rest; 0 until sized
static uint64 rd_pages; // pages taken for blocks past the image
static struct spinlock rd_lock;
void
ramdiskinit(void)
{
initlock(&rd_lock, "ramdisk");
#ifdef RAMDISK_ROOT
rd_base = _binary_ramdisk_start;
rd_size = (uint64)_binary_ramdisk_size;
@ -66,6 +91,53 @@ ramdisk_present(void)
return rd_blocks != 0;
}
// The device's size in blocks, fixed the first time anyone asks -- which is
// SchFS mounting the root, after every page has reached the allocator. Asked
// earlier, during ramdiskinit, the free count would be the first few
// megabytes kinit1 handed over and nothing more.
uint64
ramdisk_blocks(void)
{
if (rd_blocks == 0)
return 0;
acquire(&rd_lock);
if (rd_total == 0) {
uint64 extra = kalloc_free_bytes() / 2 / BSIZE;
if (extra > (uint64)RD_DIRS * RD_PTRS)
extra = (uint64)RD_DIRS * RD_PTRS;
rd_total = rd_blocks + extra;
cprintf("ramdisk: %u MB image, %u MB of memory to grow into\n",
(uint)(rd_size >> 20), (uint)(extra * BSIZE >> 20));
}
release(&rd_lock);
return rd_total;
}
// The page behind a block past the image, made when `make` says so.
static uchar *
rd_page(uint64 blk, int make)
{
uint64 i = (blk - rd_blocks) / RD_PTRS, j = (blk - rd_blocks) % RD_PTRS;
uchar *pg = 0;
acquire(&rd_lock);
if (rd_dir[i] == 0 && make) {
if ((rd_dir[i] = (uchar **)kalloc()) != 0)
memset(rd_dir[i], 0, PGSIZE);
}
if (rd_dir[i]) {
pg = rd_dir[i][j];
if (pg == 0 && make && (pg = (uchar *)kalloc()) != 0) {
rd_dir[i][j] = pg;
rd_pages++;
}
}
release(&rd_lock);
if (make && pg == 0)
panic("ramdisk: out of memory for the live root");
return pg;
}
// Sync buf with the in-memory image. Mirrors iderw()'s contract: on a read,
// fill b->data and set B_VALID; on a write, copy out and clear B_DIRTY.
void
@ -77,8 +149,21 @@ ramdiskrw(struct buf *b)
panic("ramdiskrw: nothing to do");
if(rd_base == 0)
panic("ramdiskrw: no ramdisk");
if(b->blockno >= rd_blocks)
panic("ramdiskrw: block out of range");
if(b->blockno >= rd_blocks){
if(b->blockno >= ramdisk_blocks())
panic("ramdiskrw: block out of range");
uchar *pg = rd_page(b->blockno, (b->flags & B_DIRTY) != 0);
if(b->flags & B_DIRTY){
memmove(pg, b->data, BSIZE);
b->flags &= ~B_DIRTY;
} else if(pg){
memmove(b->data, pg, BSIZE);
} else {
memset(b->data, 0, BSIZE); // never written
}
b->flags |= B_VALID;
return;
}
uchar *p = rd_base + (uint64)b->blockno * BSIZE;

View file

@ -641,6 +641,9 @@ kmount_slot(int slot, uint bdev)
// Opening with a page allocator also builds the allocation bitmap, by walking
// the tree — the only mount-time cost SchFS has, and what buys it a mount
// that cannot disagree with the filesystem it just adopted.
// The live root fills its ramdisk, however much memory that turned out to
// be. See kernel/dev/ramdisk.c.
m->vol.grow_to = (bdev == RAMDISK_DEV) ? ramdisk_blocks() : 0;
int r = schfs_vol_open(&m->vol, &bd, &cx, kd_palloc, kd_pfree);
if (r == -1)
return -EINVAL; /* not a SchFS volume at all */

View file

@ -104,6 +104,13 @@ struct schfs_vol {
uint64_t total_blocks;
uint64_t free_blocks;
// Set by the opener before schfs_vol_open: the device's size, when the
// volume should grow to fill it. Nothing on disk records which blocks are
// free -- open rebuilds that from the tree -- so growing is only a larger
// total before the map is built, and the next commit writes it down. The
// live root uses it to fill a ramdisk sized from memory at boot. 0, and
// anything not larger than the volume, leaves it alone.
uint64_t grow_to;
uint64_t next_objectid;
uint64_t root_objectid;
uint64_t generation; // last *committed* transaction group

View file

@ -215,6 +215,8 @@ schfs_vol_open(struct schfs_vol *v, struct schfs_bdev *dev,
}
vol_from_uber(v, u, best_slot);
if (v->grow_to > v->total_blocks)
v->total_blocks = v->grow_to;
v->readonly = 1; // until an allocation map exists, below
if (v->dev.bread(v->dev.ctx, v->tree.root, x->a) != 0)