The last three phases of the filesystem, which were written together and
touch the same files throughout -- kmount.c, objw.c and vfs.c are each
rewritten by all three -- so they land as one commit rather than three
that could not be built separately.
Durability. A transaction that reaches the platter out of order is worse
than one that never reached it: the uberblock can name a tree whose
blocks are still in the drive's write cache. bdirty/bflush give the
buffer layer an explicit barrier, ideflush issues ATA FLUSH CACHE and
usb_storage_flush the SCSI SYNCHRONIZE CACHE that means the same thing,
and a commit now waits for the data before it writes the root that
points at it. The uberblock groups are read newest-first with fallback,
so a volume whose newest roots are torn mounts at an older one and says
which generations were lost.
Reflink. Extents carry a refcount, so two objects can name the same
blocks and neither frees them while the other holds them. Exposed as
FICLONE -- Linux's ioctl value, so `cp --reflink` works unmodified --
and as cmd/reflink for the case where there is no cp to hand.
Attributes, and the one genuinely new idea here. attr.c stores named
values on an object, reachable through setxattr/getxattr/removexattr at
Linux's syscall numbers. On top of that, schfs_index declares an index
over an attribute name and schfs_query answers "which objects carry this
value" from the B-tree rather than by walking. A query against an
unindexed attribute is refused with an error, never silently answered by
a full scan: a query that quietly becomes a scan is worse than one that
admits it cannot help. cmd/schattr drives both.
Three bugs fixed on the way, each found by the numbers being wrong
rather than by inspection:
- statfs's f_fsid was declared int64[2]. Linux's fsid_t is two *ints*,
so every field after it landed one slot late: userland read f_frsize
out of f_namelen, got 255, and df reported a 32 MB root filesystem
as 1992 KB. It hid for as long as it did because the fields used to
be invented constants, so wrong numbers looked like the usual ones.
- struct inode's nlink was a short against SchFS's uint32, so a file
past 32767 names came back negative -- and iput frees an object
whose count reaches zero. It is an int now, signed on purpose, so an
underflow lands somewhere a check can see rather than wrapping.
- filewrite split every write to fit v1's log. That limit belongs to
the journal, not to writing, and SchFS makes one transaction of
whatever it is handed: a 1 MB write was becoming 170 commits and
170 flushes for nothing. fs_ops carries max_write now, and SchFS
sets no limit.
Also here: a real mount table, so `mount -t schfs /dev/hdb /mnt` shows
up in /proc/mounts -- df looks a path up there before it calls statfs at
all, so a mount missing from that list was invisible to it -- several
volumes mounted at once, snapshots mounted by name, and /proc/fsstat
with an rdtsc-based counter, which is the only instrument fine enough to
see inside a single syscall.
Verified: `make schfs-check` passes in full, including the sweep that
crashes the volume at each of 119 points mid-transaction and requires
every one to mount with exactly its committed data, and the case where
both newest uberblocks are rubbish and the volume still comes back. The
headless boot check passes with 27 [ok] and no failures, on a SchFS root.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
243 lines
12 KiB
C
243 lines
12 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);
|
|
// Optional: make every write issued so far durable, and do not return until
|
|
// it is. NULL means the backing store has no volatile stage to worry about
|
|
// -- a memory-backed device, or a host file the tools fsync themselves.
|
|
//
|
|
// This is the one hook the format's crash-safety argument actually rests
|
|
// on. Copy-on-write guarantees that a tree is never half-modified, but only
|
|
// if the blocks a root points at reach the medium *before* the block that
|
|
// names that root. Nothing in the ordering of bwrite calls guarantees that:
|
|
// a drive with a write cache is free to complete them in any order it
|
|
// likes. Without a barrier between the two, a power cut lands on a newest
|
|
// uberblock whose tree was never written -- exactly the failure copy-on-
|
|
// write is supposed to make impossible.
|
|
int (*bflush)(void *ctx);
|
|
};
|
|
|
|
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
|
|
// Which block `blk` currently holds, as far as this cursor knows, or 0 for
|
|
// "nothing yet". See cur_load: re-reading a block that is already in the
|
|
// buffer costs a 4 KB copy and a CRC32c over the whole 4 KB, and the code
|
|
// was paying both on every single access to the current record.
|
|
//
|
|
// Cleared by schfs_cursor_init rather than trusted across cursors, because
|
|
// the buffer belongs to the caller and the *next* caller's contents are not
|
|
// this cursor's business. Cross-checked against the block header on every
|
|
// use, so a second cursor sharing the same page cannot fool it either.
|
|
uint64_t resident;
|
|
uint64_t epoch; // schfs_tree_epoch when it was loaded
|
|
struct schfs_path_ent path[SCHFS_MAX_LEVEL + 1];
|
|
};
|
|
|
|
// Every mutation bumps this, retiring every cursor's cached block. See
|
|
// cur_load in btree.c.
|
|
extern uint64_t schfs_tree_epoch;
|
|
|
|
// `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 */
|