ACPI, DMI, SMP, xHCI, USB, I2C, NVMe, r8169 and ramdisk progress lines are kinfo now: in dmesg and on a verbose boot, off the screen of a working machine. Failures stay cprintf. vt_repaint also paints the strip right of the last column and below the last row, which kept the bottom of the X desktop after Ctrl+Alt+F-key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
291 lines
10 KiB
C
291 lines
10 KiB
C
// Memory-backed block device.
|
|
//
|
|
// Serves an entire SCHFS image out of RAM, so the system can boot and run a
|
|
// shell with the physical disk untouched. That is what makes destructive work
|
|
// on the real disk survivable: reformatting, running mkfs, or bringing up a new
|
|
// on-disk format cannot take away the root you are running from. It is also the
|
|
// first piece of the installer -- an install environment is exactly "a root in
|
|
// RAM plus a disk you are allowed to destroy".
|
|
//
|
|
// The image is linked into the kernel as a binary blob (see the ramdisk_obj
|
|
// rule in the Makefile) and is served in place: the blob lives in writable
|
|
// kernel memory, so writes to the ramdisk simply modify it. They are lost on
|
|
// reboot, which is the expected behaviour for this kind of root.
|
|
//
|
|
// 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.
|
|
//
|
|
// **A compressed image.** The blob can also be SCFF (include/fs/scff.h): the
|
|
// image cut into chunks, each compressed with zstd. Then no block is served in
|
|
// place. The page directory covers the image too, and a block with no page
|
|
// has not been unpacked yet: reading it decompresses its whole chunk, and
|
|
// every block of that chunk nobody has written gets a page of its own. The
|
|
// kernel and the ISO are the compressed size -- the 360 MB root with Xorg in
|
|
// it is 104 MB -- and the root costs memory only for what has been read or
|
|
// written. A block of zeros points at one shared zero page and costs nothing.
|
|
|
|
#include "core/types.h"
|
|
#include "core/defs.h"
|
|
#include "core/param.h"
|
|
#include "arch/memlayout.h"
|
|
#include "arch/mmu.h"
|
|
#include "core/spinlock.h"
|
|
#include "core/sleeplock.h"
|
|
#include "fs/fs.h"
|
|
#include "fs/buf.h"
|
|
#include "fs/scff.h"
|
|
#include "../scff/scff_zstd.h"
|
|
|
|
#ifdef RAMDISK_ROOT
|
|
// Supplied by `ld -r -b binary` over build/fs-ram.img, renamed by objcopy.
|
|
extern uchar _binary_ramdisk_start[];
|
|
extern uchar _binary_ramdisk_size[];
|
|
#endif
|
|
|
|
static uchar *rd_base;
|
|
static uint64 rd_size; // bytes
|
|
static uint rd_blocks; // blocks in the image
|
|
static uint rd_first; // the first block with a page: rd_blocks, or 0 for SCFF
|
|
|
|
// SCFF only. rd_zlock is held across a chunk's decompression, and makes
|
|
// rd_chunk -- the one chunk-sized buffer -- its holder's.
|
|
static struct scff_header *rd_scff;
|
|
static uint64 *rd_index;
|
|
static struct sleeplock rd_zlock;
|
|
static uchar rd_chunk[SCFF_CHUNK_MAX] __attribute__((aligned(PGSIZE)));
|
|
static uchar rd_zero[BSIZE] __attribute__((aligned(PGSIZE)));
|
|
static uint64 rd_unpacked; // chunks decompressed
|
|
|
|
// rd_dir[i] is a page of pointers, entry j the page holding block
|
|
// rd_first + 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;
|
|
rd_blocks = (uint)(rd_size / BSIZE);
|
|
rd_first = rd_blocks;
|
|
|
|
if(rd_size >= sizeof(struct scff_header) && memcmp(rd_base, SCFF_MAGIC, 4) == 0){
|
|
struct scff_header *h = (struct scff_header *)rd_base;
|
|
uint64 chunks = h->chunk_bytes ? (h->image_bytes + h->chunk_bytes - 1) / h->chunk_bytes : 0;
|
|
if(h->version != SCFF_VERSION || h->codec != SCFF_CODEC_ZSTD ||
|
|
h->chunk_bytes < BSIZE || h->chunk_bytes > SCFF_CHUNK_MAX ||
|
|
(h->chunk_bytes & (h->chunk_bytes - 1)) || h->image_bytes % BSIZE ||
|
|
chunks != h->nchunks || h->stored_bytes > rd_size ||
|
|
h->index_off + (h->nchunks + 1) * sizeof(uint64) > rd_size)
|
|
panic("ramdiskinit: the SCFF header does not describe this image");
|
|
rd_index = (uint64 *)(rd_base + h->index_off);
|
|
for(uint i = 0; i < h->nchunks; i++)
|
|
if(rd_index[i] > rd_index[i + 1] || rd_index[i + 1] > h->stored_bytes ||
|
|
rd_index[i + 1] - rd_index[i] > h->chunk_bytes)
|
|
panic("ramdiskinit: the SCFF index is damaged");
|
|
if(scff_zstd_init() < 0)
|
|
panic("ramdiskinit: no room for the zstd decoder");
|
|
initsleeplock(&rd_zlock, "scff");
|
|
rd_scff = h;
|
|
rd_blocks = (uint)(h->image_bytes / BSIZE);
|
|
rd_first = 0;
|
|
kinfo("ramdisk: SCFF, %u MB in %u MB, %u chunks of %u KB\n",
|
|
(uint)(h->image_bytes >> 20), (uint)(rd_size >> 20),
|
|
h->nchunks, h->chunk_bytes >> 10);
|
|
}
|
|
|
|
if(rd_blocks == 0)
|
|
panic("ramdiskinit: embedded image is empty");
|
|
|
|
// A truncated image is worth catching here rather than as a confusing
|
|
// "invalid file system" panic later: the superblock lives in block 1.
|
|
if(rd_blocks < 2)
|
|
panic("ramdiskinit: embedded image smaller than a superblock");
|
|
|
|
if(!rd_scff)
|
|
kinfo("ramdisk: %u blocks (%u KB) at %p\n",
|
|
rd_blocks, (uint)(rd_size / 1024), rd_base);
|
|
#else
|
|
rd_base = 0;
|
|
rd_size = 0;
|
|
rd_blocks = 0;
|
|
#endif
|
|
}
|
|
|
|
int
|
|
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;
|
|
uint64 room = (uint64)RD_DIRS * RD_PTRS - (rd_blocks - rd_first);
|
|
if (extra > room)
|
|
extra = room;
|
|
rd_total = rd_blocks + extra;
|
|
kinfo("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, made when `make` says so. One that is only the
|
|
// shared zero page gets a page of its own before anything writes to it.
|
|
static uchar *
|
|
rd_page(uint64 blk, int make)
|
|
{
|
|
uint64 i = (blk - rd_first) / RD_PTRS, j = (blk - rd_first) % 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 || pg == rd_zero) && make && (pg = (uchar *)kalloc()) != 0) {
|
|
if (rd_dir[i][j] == rd_zero)
|
|
memset(pg, 0, BSIZE);
|
|
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;
|
|
}
|
|
|
|
// Give every block of SCFF chunk c that has no page one: its contents, or the
|
|
// zero page. Blocks already there -- written since boot, or unpacked by
|
|
// whoever held the lock before us -- are left as they are.
|
|
static void
|
|
rd_unpack(uint c)
|
|
{
|
|
uint per = rd_scff->chunk_bytes / BSIZE;
|
|
uint64 first = (uint64)c * per;
|
|
uint n = rd_blocks - first < per ? (uint)(rd_blocks - first) : per;
|
|
uint64 len = rd_index[c + 1] - rd_index[c];
|
|
uchar *src = rd_base + rd_index[c];
|
|
uint64 want = (uint64)n * BSIZE;
|
|
uchar *data = rd_chunk;
|
|
|
|
acquiresleep(&rd_zlock);
|
|
if (len == 0) {
|
|
data = 0; // all zeros
|
|
} else if (len == want) {
|
|
data = src; // stored as it is
|
|
} else {
|
|
long got = scff_zstd_decompress(rd_chunk, sizeof(rd_chunk), src, len);
|
|
if (got != (long)want)
|
|
panic("ramdisk: an SCFF chunk does not decompress");
|
|
rd_unpacked++;
|
|
}
|
|
|
|
for (uint k = 0; k < n; k++) {
|
|
uint64 blk = first + k, i = blk / RD_PTRS, j = blk % RD_PTRS;
|
|
uchar *b = data ? data + (uint64)k * BSIZE : 0;
|
|
int zero = 1;
|
|
if (b)
|
|
for (uint w = 0; w < BSIZE / 8 && zero; w++)
|
|
zero = ((uint64 *)b)[w] == 0;
|
|
|
|
uchar *pg = zero ? rd_zero : (uchar *)kalloc();
|
|
if (pg == 0)
|
|
panic("ramdisk: out of memory for the live root");
|
|
if (!zero)
|
|
memmove(pg, b, BSIZE);
|
|
|
|
acquire(&rd_lock);
|
|
if (rd_dir[i] == 0 && (rd_dir[i] = (uchar **)kalloc()) != 0)
|
|
memset(rd_dir[i], 0, PGSIZE);
|
|
if (rd_dir[i] == 0)
|
|
panic("ramdisk: out of memory for the live root");
|
|
if (rd_dir[i][j] == 0) {
|
|
rd_dir[i][j] = pg;
|
|
if (!zero)
|
|
rd_pages++;
|
|
pg = 0;
|
|
}
|
|
release(&rd_lock);
|
|
if (pg && pg != rd_zero)
|
|
kfree((char *)pg); // someone was there first
|
|
}
|
|
releasesleep(&rd_zlock);
|
|
}
|
|
|
|
// 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
|
|
ramdiskrw(struct buf *b)
|
|
{
|
|
if(!holdingsleep(&b->lock))
|
|
panic("ramdiskrw: buf not locked");
|
|
if((b->flags & (B_VALID|B_DIRTY)) == B_VALID)
|
|
panic("ramdiskrw: nothing to do");
|
|
if(rd_base == 0)
|
|
panic("ramdiskrw: no ramdisk");
|
|
if(b->blockno >= rd_first){
|
|
if(b->blockno >= ramdisk_blocks())
|
|
panic("ramdiskrw: block out of range");
|
|
uchar *pg = rd_page(b->blockno, (b->flags & B_DIRTY) != 0);
|
|
if(pg == 0 && rd_scff && b->blockno < rd_blocks && !(b->flags & B_DIRTY)){
|
|
rd_unpack((uint)(b->blockno / (rd_scff->chunk_bytes / BSIZE)));
|
|
pg = rd_page(b->blockno, 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;
|
|
|
|
if(b->flags & B_DIRTY){
|
|
memmove(p, b->data, BSIZE);
|
|
b->flags &= ~B_DIRTY;
|
|
} else {
|
|
memmove(b->data, p, BSIZE);
|
|
}
|
|
b->flags |= B_VALID;
|
|
}
|