// 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 */