An x86-64 operating system built from xv6, now largely rewritten: SMP, a copy-on-write root filesystem (SchFS), PCI/USB/virtio drivers, a TTF rasteriser, and a Linux-numbered syscall ABI. No history before this point -- the tree was never under version control. This commit exists so the rename that follows has something to undo to.
299 lines
11 KiB
C
299 lines
11 KiB
C
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// SchFS volume layer: the uberblock ring.
|
|
//
|
|
// A ring of SCHFS_UBER_COUNT slots is written round-robin, each stamped with a
|
|
// transaction number and a checksum. Mounting reads every slot and adopts the
|
|
// highest-numbered one that validates.
|
|
//
|
|
// That single rule replaces journalling entirely. A commit writes one block; it
|
|
// either lands whole or it does not. If it does not, its checksum fails, the
|
|
// previous slot is still the highest valid one, and the volume comes up at the
|
|
// previous transaction with no replay and no fsck. Rotating through many slots
|
|
// rather than overwriting one means a run of bad sectors under the current slot
|
|
// cannot take the volume with it.
|
|
|
|
#include "schfs_int.h"
|
|
|
|
#ifdef SCHFS_HOSTED
|
|
#include <string.h>
|
|
#else
|
|
#include "types.h"
|
|
#include "defs.h"
|
|
#endif
|
|
|
|
// Checksum an uberblock with its own crc field zeroed, the same convention the
|
|
// tree nodes use.
|
|
static uint32_t
|
|
uber_crc(struct schfs_uberblock *u)
|
|
{
|
|
uint32_t saved = u->crc;
|
|
u->crc = 0;
|
|
uint32_t c = schfs_crc32c(0, u, sizeof(*u));
|
|
u->crc = saved;
|
|
return c;
|
|
}
|
|
|
|
static int
|
|
uber_valid(struct schfs_uberblock *u)
|
|
{
|
|
if (u->magic != SCHFS_MAGIC)
|
|
return 0;
|
|
if (u->version != SCHFS_VERSION)
|
|
return 0;
|
|
// A volume made with a different block size would otherwise be read with
|
|
// every offset wrong, which looks exactly like corruption.
|
|
if (u->blocksize != SCHFS_BLKSIZE)
|
|
return 0;
|
|
if (uber_crc(u) != u->crc)
|
|
return 0;
|
|
// Self-consistency: a root inside the ring or past the end of the device is
|
|
// a corrupt uberblock that happens to checksum, not a usable one.
|
|
if (u->root < SCHFS_FIRST_BLOCK || u->root >= u->total_blocks)
|
|
return 0;
|
|
return 1;
|
|
}
|
|
|
|
static void
|
|
vol_from_uber(struct schfs_vol *v, struct schfs_uberblock *u, int slot)
|
|
{
|
|
v->tree.dev = &v->dev;
|
|
v->tree.root = u->root;
|
|
v->tree.root_crc = u->root_crc;
|
|
v->tree.root_gen = u->root_generation;
|
|
// The tree's `generation` is the transaction group being *built*, one past
|
|
// the newest committed one. Setting it to the committed root's generation
|
|
// instead would tell the tree that every block of the mounted filesystem
|
|
// belongs to the open transaction, and it would then rewrite the live tree
|
|
// in place — losing the very thing copy-on-write exists to preserve.
|
|
v->tree.generation = u->generation + 1;
|
|
|
|
v->total_blocks = u->total_blocks;
|
|
v->free_blocks = u->free_blocks;
|
|
v->next_objectid = u->next_objectid;
|
|
v->root_objectid = u->root_objectid;
|
|
v->generation = u->generation;
|
|
v->uber_slot = slot;
|
|
|
|
memmove(v->uuid, u->uuid, sizeof(v->uuid));
|
|
memmove(v->label, u->label, sizeof(v->label));
|
|
v->label[SCHFS_LABEL_MAX - 1] = '\0';
|
|
}
|
|
|
|
int
|
|
schfs_vol_open(struct schfs_vol *v, struct schfs_bdev *dev,
|
|
struct schfs_ctx *x, void *(*palloc)(void))
|
|
{
|
|
struct schfs_uberblock *u = (struct schfs_uberblock *)x->a;
|
|
int best_slot = -1;
|
|
uint64_t best_gen = 0;
|
|
int io_errors = 0;
|
|
|
|
v->dev = *dev;
|
|
v->ctx = *x;
|
|
v->tree.dev = &v->dev;
|
|
|
|
// Remember which slot won, not a copy of it.
|
|
//
|
|
// An uberblock is a whole SCHFS_BLKSIZE block, and keeping one as a local
|
|
// put 4096 bytes on a kernel stack that is 4096 bytes in total — the canary
|
|
// in kalloc'd stacks caught it as "kstack overflow" on the very first boot.
|
|
// Re-reading the winner costs one block read, once, at mount.
|
|
for (int i = 0; i < SCHFS_UBER_COUNT; i++) {
|
|
if (v->dev.bread(v->dev.ctx, SCHFS_UBER_START + i, u) != 0) {
|
|
io_errors++;
|
|
continue; // an unreadable slot is not fatal: try the rest
|
|
}
|
|
if (!uber_valid(u))
|
|
continue;
|
|
if (best_slot < 0 || u->generation > best_gen) {
|
|
best_gen = u->generation;
|
|
best_slot = i;
|
|
}
|
|
}
|
|
|
|
if (best_slot < 0)
|
|
return io_errors == SCHFS_UBER_COUNT ? -2 : -1;
|
|
|
|
if (v->dev.bread(v->dev.ctx, SCHFS_UBER_START + best_slot, u) != 0)
|
|
return -2;
|
|
if (!uber_valid(u))
|
|
return -2; /* changed under us, or a flaky device */
|
|
|
|
vol_from_uber(v, u, best_slot);
|
|
v->readonly = 1; // until an allocation map exists, below
|
|
|
|
// The newest uberblock passing its own checksum does not prove the tree it
|
|
// names is readable — a misdirected write could have landed on the root.
|
|
// Check now, while there is still the option of falling back, rather than
|
|
// discovering it on the first lookup.
|
|
if (v->dev.bread(v->dev.ctx, v->tree.root, x->a) != 0)
|
|
return -2;
|
|
{
|
|
struct schfs_node_hdr *h = (struct schfs_node_hdr *)x->a;
|
|
if (h->magic != SCHFS_NODE_MAGIC || h->blocknr != v->tree.root)
|
|
return -2;
|
|
}
|
|
|
|
// Rebuild the allocation map from the tree just adopted. Without a palloc
|
|
// the volume stays read-only, which is exactly right for a tool that only
|
|
// inspects an image: it can then never allocate, rather than allocating
|
|
// from a map that says everything is free.
|
|
if (palloc) {
|
|
int sr = schfs_space_init(&v->space, v->total_blocks, palloc);
|
|
if (sr != 0)
|
|
return sr;
|
|
if (schfs_space_scan(v) != 0)
|
|
return -2;
|
|
v->free_blocks = v->space.free_blocks;
|
|
v->readonly = 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
schfs_vol_commit(struct schfs_vol *v)
|
|
{
|
|
struct schfs_uberblock *u = (struct schfs_uberblock *)v->ctx.a;
|
|
|
|
v->generation++;
|
|
v->uber_slot = (v->uber_slot + 1) % SCHFS_UBER_COUNT;
|
|
|
|
memset(u, 0, sizeof(*u));
|
|
u->magic = SCHFS_MAGIC;
|
|
u->version = SCHFS_VERSION;
|
|
u->blocksize = SCHFS_BLKSIZE;
|
|
u->generation = v->generation;
|
|
u->root = v->tree.root;
|
|
u->root_crc = v->tree.root_crc;
|
|
u->root_generation = v->tree.root_gen;
|
|
u->total_blocks = v->total_blocks;
|
|
// Blocks this group released count as free *as of this uberblock*: nothing
|
|
// the root below reaches touches them. They are only still marked in use in
|
|
// memory because the group being superseded is what a crash would land on
|
|
// until this very block is written.
|
|
u->free_blocks = v->space.nblocks
|
|
? v->space.free_blocks + v->space.pending
|
|
: v->free_blocks;
|
|
u->next_objectid = v->next_objectid;
|
|
u->root_objectid = v->root_objectid;
|
|
memmove(u->uuid, v->uuid, sizeof(u->uuid));
|
|
memmove(u->label, v->label, sizeof(u->label));
|
|
u->crc = uber_crc(u);
|
|
|
|
if (v->dev.bwrite(v->dev.ctx, SCHFS_UBER_START + v->uber_slot, u) != 0)
|
|
return -2;
|
|
|
|
// Order matters here, and it is the whole crash-safety argument in three
|
|
// lines. Only now that the new uberblock is on the disk is the group this
|
|
// freed blocks in superseded, so only now may those blocks be handed out
|
|
// again; releasing them before the write would let a crash come back to a
|
|
// tree whose blocks had already been reused.
|
|
if (v->space.nblocks) {
|
|
schfs_space_commit(&v->space);
|
|
v->free_blocks = v->space.free_blocks;
|
|
}
|
|
|
|
// Everything written from here on belongs to the next transaction group, so
|
|
// the blocks just published become read-only to the tree again and the
|
|
// copy-on-write rule resumes. Advancing this is the whole reason a commit
|
|
// is a barrier rather than just another block write.
|
|
v->tree.generation = v->generation + 1;
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
schfs_vol_format(struct schfs_vol *v, struct schfs_bdev *dev,
|
|
uint64_t total_blocks, const char *label,
|
|
struct schfs_ctx *x, void *(*palloc)(void))
|
|
{
|
|
if (total_blocks <= SCHFS_FIRST_BLOCK)
|
|
return -3;
|
|
|
|
memset(v, 0, sizeof(*v));
|
|
v->dev = *dev;
|
|
v->ctx = *x;
|
|
v->tree.dev = &v->dev;
|
|
v->total_blocks = total_blocks;
|
|
v->root_objectid = SCHFS_ROOT_OBJECTID;
|
|
|
|
// Nothing is allocated on a fresh volume beyond the boot block and the
|
|
// ring, so the map starts empty rather than being scanned into existence.
|
|
{
|
|
int sr = schfs_space_init(&v->space, total_blocks, palloc);
|
|
if (sr != 0)
|
|
return sr;
|
|
}
|
|
v->free_blocks = v->space.free_blocks;
|
|
// The root's object id is consumed by the root itself, so allocation must
|
|
// start *past* it. Starting at SCHFS_FIRST_OBJECTID instead handed the
|
|
// first directory created the same id as the root: that directory then
|
|
// silently became the root, so it contained itself and any tree walk
|
|
// recursed until the stack ran out.
|
|
v->next_objectid = SCHFS_ROOT_OBJECTID + 1;
|
|
v->generation = 0; /* nothing committed yet */
|
|
v->tree.generation = 1; /* ...so this is the group being built */
|
|
v->uber_slot = -1;
|
|
v->readonly = 0;
|
|
|
|
if (label) {
|
|
unsigned i = 0;
|
|
for (; i < SCHFS_LABEL_MAX - 1 && label[i]; i++)
|
|
v->label[i] = label[i];
|
|
v->label[i] = '\0';
|
|
}
|
|
|
|
// A UUID from the block count and a fixed salt. Not random — mkfs is
|
|
// deliberately reproducible, so two runs over the same tree produce
|
|
// byte-identical images and a diff means a real change.
|
|
{
|
|
uint32_t a = schfs_crc32c(0, &total_blocks, sizeof(total_blocks));
|
|
uint32_t b = schfs_crc32c(a, v->label, SCHFS_LABEL_MAX);
|
|
for (int i = 0; i < 16; i++)
|
|
v->uuid[i] = (uint8_t)((i & 1 ? a : b) >> ((i % 4) * 8));
|
|
}
|
|
|
|
// Zero the whole ring so a stale uberblock from a previous filesystem on
|
|
// this device cannot outrank ours and be mounted instead.
|
|
memset(x->a, 0, SCHFS_BLKSIZE);
|
|
for (int i = 0; i < SCHFS_UBER_COUNT; i++)
|
|
if (v->dev.bwrite(v->dev.ctx, SCHFS_UBER_START + i, x->a) != 0)
|
|
return -2;
|
|
|
|
if (schfs_btree_create(&v->tree, &v->dev, x) != 0)
|
|
return -2;
|
|
|
|
return schfs_vol_commit(v);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Allocation */
|
|
/* */
|
|
/* Thin adapters over the space manager, so callers wiring up a */
|
|
/* schfs_bdev have something with the right shape to point at. The */
|
|
/* readonly check lives here rather than in space.c because "this */
|
|
/* volume may not be written" is a property of the mount, not of the */
|
|
/* bitmap. */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
uint64_t
|
|
schfs_vol_alloc(struct schfs_vol *v)
|
|
{
|
|
if (v->readonly)
|
|
return 0;
|
|
return schfs_space_alloc(&v->space);
|
|
}
|
|
|
|
uint64_t
|
|
schfs_vol_alloc_run(struct schfs_vol *v, uint64_t n)
|
|
{
|
|
if (v->readonly)
|
|
return 0;
|
|
return schfs_space_alloc_run(&v->space, n);
|
|
}
|
|
|
|
void
|
|
schfs_vol_free(struct schfs_vol *v, uint64_t blk)
|
|
{
|
|
schfs_space_free(&v->space, blk);
|
|
}
|