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.
215 lines
10 KiB
C
215 lines
10 KiB
C
// SchFS copy-on-write B-tree.
|
|
//
|
|
// This is the load-bearing piece of the whole filesystem: inodes, directory
|
|
// entries, extent maps and indexes are all just records in this one tree, so a
|
|
// subtle bug here is a bug in everything. It is therefore written as portable C
|
|
// against an abstract block device, and exercised on the host by
|
|
// tools/schfs_test.c before it is ever run inside the kernel.
|
|
//
|
|
// Copy-on-write means no block that is part of the *committed* tree is ever
|
|
// modified. An update copies the path from the leaf back up to the root, and
|
|
// the caller then publishes the new root. A crash therefore lands on either the
|
|
// old tree or the new one, never a mixture — which is why there is no journal
|
|
// and no fsck.
|
|
//
|
|
// "Committed" is the operative word: a block first written during the
|
|
// transaction group now being built is not reachable from any published root,
|
|
// so it may be rewritten in place. See schfs_btree.generation.
|
|
|
|
#ifndef SCHFS_BTREE_H
|
|
#define SCHFS_BTREE_H
|
|
|
|
#include "schfs.h"
|
|
|
|
// Block device the tree lives on. The kernel supplies one backed by the buffer
|
|
// cache; the test harness supplies one backed by a plain file or by memory.
|
|
struct schfs_bdev {
|
|
void *ctx;
|
|
int (*bread)(void *ctx, uint64_t blk, void *buf);
|
|
int (*bwrite)(void *ctx, uint64_t blk, const void *buf);
|
|
// Allocate a block for copy-on-write. Must never return a block that is
|
|
// reachable from the last committed root.
|
|
uint64_t (*balloc)(void *ctx);
|
|
// Release a block that the new tree no longer references. Freeing may only
|
|
// take effect at commit, since the old tree must stay readable until then.
|
|
void (*bfree)(void *ctx, uint64_t blk);
|
|
// Optional: allocate n physically contiguous blocks, returning the first,
|
|
// or 0 if that cannot be satisfied. This is what lets a file become one
|
|
// extent rather than one per block, and it has to be a separate entry point
|
|
// because an allocator with a free list will happily return scattered
|
|
// blocks from balloc. NULL means unsupported; callers then fall back to
|
|
// allocating singly and accept the extra extent records.
|
|
uint64_t (*balloc_run)(void *ctx, uint64_t n);
|
|
};
|
|
|
|
struct schfs_btree {
|
|
struct schfs_bdev *dev;
|
|
uint64_t root; // block number of the current root
|
|
uint32_t root_crc;
|
|
uint64_t root_gen; // generation stamped in the root block itself
|
|
// The transaction group being built. Every block written by a mutation is
|
|
// stamped with this, and a block already carrying it is rewritten in place
|
|
// rather than copied — it cannot be part of a committed tree. The volume
|
|
// layer advances it on every commit; nothing else may touch it.
|
|
uint64_t generation;
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Scratch buffers
|
|
//
|
|
// Nothing in this file may put a block on the stack. A SchFS block is 4096
|
|
// bytes and so is the entire kernel stack (KSTACKSIZE), so even one such local
|
|
// overflows it — the kstack canary caught exactly that on the first boot of the
|
|
// read path. Mutations need three blocks live at once (the node being edited,
|
|
// the sibling a split produces, and a repacking buffer), so callers hand in
|
|
// three and the tree borrows them.
|
|
//
|
|
// The three must not alias, and their contents do not survive across calls.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
struct schfs_ctx {
|
|
void *a; // node currently being edited
|
|
void *b; // sibling produced by a split
|
|
void *c; // repacking scratch for leaves
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Cursor
|
|
//
|
|
// The read path is driven through a cursor for the same reason: a cursor holds
|
|
// exactly one block buffer, supplied by the caller, and remembers the path as
|
|
// block numbers, re-reading a parent on the way back up. Walking a tree of any
|
|
// depth therefore costs one page rather than one page per level.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
struct schfs_path_ent {
|
|
uint64_t blocknr;
|
|
uint32_t crc;
|
|
int index; // internal: child descended into; leaf: item
|
|
};
|
|
|
|
struct schfs_cursor {
|
|
struct schfs_btree *t;
|
|
void *blk; // caller-owned, SCHFS_BLKSIZE bytes
|
|
int depth; // path[depth] is the leaf
|
|
int valid; // 0 once the walk has run off either end
|
|
struct schfs_path_ent path[SCHFS_MAX_LEVEL + 1];
|
|
};
|
|
|
|
// `blk` must point at SCHFS_BLKSIZE bytes that outlive the cursor.
|
|
void schfs_cursor_init(struct schfs_cursor *c, struct schfs_btree *t, void *blk);
|
|
|
|
// Position at the first key >= start, or at the very first key if start is
|
|
// NULL. Returns 0 if positioned, -1 if the tree holds no such key, -2 on
|
|
// corruption.
|
|
int schfs_cursor_seek(struct schfs_cursor *c, const struct schfs_key *start);
|
|
|
|
// Step. Return 0, -1 at the end of the tree, -2 on corruption.
|
|
int schfs_cursor_next(struct schfs_cursor *c);
|
|
int schfs_cursor_prev(struct schfs_cursor *c);
|
|
|
|
// Current record. Both pointers are invalidated by the next seek/next/prev.
|
|
// Return NULL when the cursor is not positioned.
|
|
const struct schfs_key *schfs_cursor_key(struct schfs_cursor *c);
|
|
const void *schfs_cursor_val(struct schfs_cursor *c, unsigned *len);
|
|
|
|
// Ordering over keys: (objectid, type, offset). Returns <0, 0, >0.
|
|
int schfs_key_cmp(const struct schfs_key *a, const struct schfs_key *b);
|
|
|
|
uint32_t schfs_crc32c(uint32_t crc, const void *buf, unsigned len);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Node accessors
|
|
//
|
|
// Plain views over the on-disk layout in schfs.h. Exposed because the space
|
|
// manager walks the tree by block number rather than by key, and a second,
|
|
// slightly different copy of these three lines is exactly the sort of thing
|
|
// that ends up disagreeing with the first.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#define SCHFS_HDR(b) ((struct schfs_node_hdr *)(b))
|
|
#define SCHFS_KPTRS(b) ((struct schfs_kptr *) \
|
|
((char *)(b) + sizeof(struct schfs_node_hdr)))
|
|
#define SCHFS_ITEMS(b) ((struct schfs_item *) \
|
|
((char *)(b) + sizeof(struct schfs_node_hdr)))
|
|
|
|
// Read one tree block into `out` and validate it completely: magic, the block's
|
|
// self-pointer, its checksum, and — when want_crc is non-zero — that the
|
|
// checksum is the one its parent recorded. Returns 0 or -2.
|
|
int schfs_node_read(struct schfs_bdev *dev, uint64_t blk, uint32_t want_crc,
|
|
void *out);
|
|
|
|
// Create an empty tree (a single empty leaf) and set t->root.
|
|
int schfs_btree_create(struct schfs_btree *t, struct schfs_bdev *dev,
|
|
struct schfs_ctx *x);
|
|
|
|
// Look a key up. On success copies at most *len bytes of the value into buf and
|
|
// stores the true size in *len. Returns 0 on success, -1 if absent, -2 on a
|
|
// corrupt or unreadable block. Uses x->a as its block buffer.
|
|
int schfs_btree_lookup(struct schfs_btree *t, struct schfs_ctx *x,
|
|
const struct schfs_key *k, void *buf, unsigned *len);
|
|
|
|
// Same, through a cursor the caller already has positioned somewhere else.
|
|
int schfs_btree_lookup_cur(struct schfs_cursor *c, const struct schfs_key *k,
|
|
void *buf, unsigned *len);
|
|
|
|
// Greatest key <= k, which is how an extent covering a file offset is found:
|
|
// extents are keyed by the file offset they start at, so the record governing
|
|
// byte N is the last one at or before (ino, EXTENT, N). Leaves the cursor
|
|
// positioned on it. Returns 0, -1 if every key in the tree is greater, -2 on
|
|
// corruption.
|
|
int schfs_btree_lookup_le(struct schfs_cursor *c, const struct schfs_key *k);
|
|
|
|
// Insert or replace. Copies the path, so t->root changes on success.
|
|
// Returns 0, -1 if the value is too large to store inline, -2 on I/O or
|
|
// corruption, -3 if the device is out of blocks.
|
|
int schfs_btree_insert(struct schfs_btree *t, struct schfs_ctx *x,
|
|
const struct schfs_key *k, const void *val, unsigned len);
|
|
|
|
// Remove a key. Emptied nodes are freed and the root is collapsed when it drops
|
|
// to a single child, so a tree that is emptied returns to one block.
|
|
// Returns 0, -1 if absent, -2 on corruption, -3 out of space.
|
|
int schfs_btree_delete(struct schfs_btree *t, struct schfs_ctx *x,
|
|
const struct schfs_key *k);
|
|
|
|
// Walk records from `start` (inclusive) onward. This is what a directory read
|
|
// uses: every entry of directory D is a key in [(D,DIRENT,0), (D,DIRENT,~0)],
|
|
// so listing a directory costs the size of that directory rather than the size
|
|
// of the filesystem. fn returns non-zero to stop.
|
|
//
|
|
// WARNING: the `val` pointer handed to fn points *into the cursor's block
|
|
// buffer*. Any other tree operation the callback performs may overwrite it —
|
|
// including one on a different cursor, if the two were given the same buffer.
|
|
// A callback that looks anything up must copy what it needs first. The key is
|
|
// already copied out for you; the value is not, because it can be a kilobyte
|
|
// and most callbacks consume it immediately.
|
|
int schfs_btree_range(struct schfs_cursor *c, const struct schfs_key *start,
|
|
int (*fn)(const struct schfs_key *k,
|
|
const void *val, unsigned len, void *arg),
|
|
void *arg);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Host-only facilities
|
|
//
|
|
// These recurse and keep a block per level, which is fine in a tool and fatal
|
|
// in the kernel. Compiling them out of the kernel makes that a link error
|
|
// rather than a comment nobody reads.
|
|
// ---------------------------------------------------------------------------
|
|
#ifdef SCHFS_HOSTED
|
|
|
|
// Walk every record in key order, calling fn for each. A non-zero return from
|
|
// fn stops the walk and is passed back.
|
|
int schfs_btree_iterate(struct schfs_btree *t,
|
|
int (*fn)(const struct schfs_key *k,
|
|
const void *val, unsigned len, void *arg),
|
|
void *arg);
|
|
|
|
// Full structural check: magic, self-pointer, checksum, level agreement, key
|
|
// ordering within and across nodes, that values lie inside their block, and
|
|
// that every child's first key is exactly the key its parent files it under.
|
|
// Returns 0 if the tree is sound, non-zero on the first problem found.
|
|
int schfs_btree_check(struct schfs_btree *t);
|
|
|
|
#endif /* SCHFS_HOSTED */
|
|
|
|
#endif /* SCHFS_BTREE_H */
|