// SPDX-License-Identifier: Apache-2.0 // // SchFS internals: the volume (uberblock ring, block allocation) and the object // layer (inodes, directories, file data) that sit on top of the B-tree. // // Split three ways so the kernel links only what it needs: // // vol.c open / format / commit — the uberblock ring // obj.c read paths — kernel needs these in phase 2 // objw.c write paths — host tools only, until phase 3 // // Everything here is portable C compiled both hosted (mkfs, fsck) and freestanding // (kernel), so a bug is found on the host, where a test takes a second rather // than a boot. #ifndef SCHFS_INT_H #define SCHFS_INT_H #include "btree.h" // --------------------------------------------------------------------------- // space.c — the allocation bitmap // // One bit per block, held in RAM and derived from the committed tree at mount // rather than stored on disk. See space.c for why. // // The bitmap is a vector of pages because the kernel allocates a page at a time // and gives no promise that two are adjacent. That vector is fixed-size, and // its length is therefore the hard ceiling on how large a volume this build can // mount: SCHFS_SPACE_PAGES pages x SCHFS_SPACE_BITS_PER_PAGE blocks each. // --------------------------------------------------------------------------- #define SCHFS_SPACE_BITS_PER_PAGE ((uint64_t)SCHFS_BLKSIZE * 8) /* 32768 */ #define SCHFS_SPACE_PAGES 512 #define SCHFS_MAX_BLOCKS \ (SCHFS_SPACE_BITS_PER_PAGE * (uint64_t)SCHFS_SPACE_PAGES) /* 64 GiB */ struct schfs_space { uint64_t *inuse[SCHFS_SPACE_PAGES]; // 1 = allocated uint64_t *pend [SCHFS_SPACE_PAGES]; // 1 = freed, but not yet reusable int npages; uint64_t nblocks; uint64_t free_blocks; uint64_t pending; // bits set in pend[], counted uint64_t hint; // rotating search start // SCHFS_SPACE_RETAIN_ALL while a snapshot exists -- see schfs_space_free. unsigned flags; }; // A mounted (or under-construction) volume. // // NOT copyable: tree.dev points at this struct's own dev field, so a copy // leaves the tree talking to whichever struct it was built in. Open into the // storage it will live in. struct schfs_vol { struct schfs_bdev dev; struct schfs_btree tree; // The three block buffers every mutation borrows, handed in at open or // format and owned by the caller for the life of the volume. Nothing in // SchFS may put a SCHFS_BLKSIZE buffer on the stack: a block is 4096 bytes // and so is the whole kernel stack. // // The object layer also uses ctx.a as a cursor buffer, which is safe on the // one condition that no cursor is still live when a tree mutation runs. // Every write path here reads what it needs out of the cursor first. struct schfs_ctx ctx; struct schfs_space space; uint64_t total_blocks; uint64_t free_blocks; uint64_t next_objectid; uint64_t root_objectid; uint64_t generation; // last *committed* transaction group int uber_slot; // ring slot the last commit used int readonly; char label[SCHFS_LABEL_MAX]; uint8_t uuid[16]; // Staging for a directory's packed dirent list. Deliberately not one of the // ctx buffers: a split reuses ctx.b and ctx.c while the value being // inserted is still being read from, so the value needs storage of its own. char dirbuf[SCHFS_MAX_INLINE]; }; // --------------------------------------------------------------------------- // vol.c // --------------------------------------------------------------------------- // Scan the whole uberblock ring and adopt the highest generation that both // checksums and points at a readable root. This *is* the crash recovery story: // there is no journal to replay, because a torn update simply leaves an older // slot as the newest valid one. Returns 0, -1 if no valid uberblock exists // (not a SchFS volume), -2 on I/O error. // // `x` supplies the volume's three scratch blocks; they are kept for its whole // lifetime, so they must outlive it and must not alias one another. `palloc` // yields pages for the allocation bitmap and may be NULL for a read-only mount, // which then cannot allocate at all. int schfs_vol_open(struct schfs_vol *v, struct schfs_bdev *dev, struct schfs_ctx *x, void *(*palloc)(void)); // Lay down an empty volume: one empty tree and one uberblock. 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)); // Publish the current tree by writing the next slot in the ring. Until this // returns, everything written since the last commit is invisible and a crash // discards it — which is exactly what makes the format crash-safe. int schfs_vol_commit(struct schfs_vol *v); // Snapshots. Create records the *committed* root; the caller commits after. // Lookup fills `out` for a given id. Deleting forgets the record, and the next // derivation stops marking what only it reached. int schfs_snap_create(struct schfs_vol *v, uint64_t id, const char *name, unsigned namelen); int schfs_snap_delete(struct schfs_vol *v, uint64_t id); int schfs_snap_get(struct schfs_vol *v, struct schfs_cursor *c, uint64_t id, struct schfs_snap *out); // Block allocation. Returns 0 when the volume is full. // // Not themselves schfs_bdev callbacks: a bdev has one ctx shared by all of its // hooks, and the caller's block I/O needs that slot for its own state (a file // handle, a device number). Callers wrap these in a one-line adapter instead. uint64_t schfs_vol_alloc(struct schfs_vol *v); uint64_t schfs_vol_alloc_run(struct schfs_vol *v, uint64_t n); void schfs_vol_free(struct schfs_vol *v, uint64_t blk); // --------------------------------------------------------------------------- // space.c // --------------------------------------------------------------------------- // Build an empty map covering `nblocks`, with everything below // SCHFS_FIRST_BLOCK already reserved. `palloc` yields SCHFS_BLKSIZE-sized // pages; the map keeps them for the volume's lifetime. Returns 0, -3 if the // volume is too small to hold anything, -4 if it is larger than this build can // map (see SCHFS_MAX_BLOCKS), -5 if palloc ran out. int schfs_space_init(struct schfs_space *s, uint64_t nblocks, void *(*palloc)(void)); // Record a run as allocated. Idempotent, because an extent range may be shared // by several files. void schfs_space_mark(struct schfs_space *s, uint64_t blk, uint64_t n); uint64_t schfs_space_alloc(struct schfs_space *s); uint64_t schfs_space_alloc_run(struct schfs_space *s, uint64_t n); // Release a block — but only for the *next* transaction group. Until the // commit that supersedes it, the last committed tree still points here and a // crash would land on it. void schfs_space_free(struct schfs_space *s, uint64_t blk); // Fold this group's deferred frees back into the free map. Called by // schfs_vol_commit once the new uberblock is on the disk, never before. void schfs_space_commit(struct schfs_space *s); // Walk the committed tree and mark every block it reaches. This is what makes // the map correct without storing it. Returns 0 or -2 on an unreadable tree. int schfs_space_scan(struct schfs_vol *v); // --------------------------------------------------------------------------- // obj.c — read side // --------------------------------------------------------------------------- uint64_t schfs_namehash(const char *name, unsigned len); // Test-only knob; see obj.c. Narrowing this forces directory-hash collisions // so the packed-dirent path actually runs. Never changed outside tools/. extern uint64_t schfs_hash_mask; void schfs_mkkey(struct schfs_key *k, uint64_t objectid, uint8_t type, uint64_t offset); // Fetch an inode record. Returns 0, -1 if there is no such object, -2 corrupt. int schfs_inode_get(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino, struct schfs_inode *out); // Resolve one name inside a directory. Returns 0 and fills *out_ino/*out_type, // -1 if absent, -2 corrupt. int schfs_dir_lookup(struct schfs_vol *v, struct schfs_cursor *c, uint64_t dir, const char *name, unsigned namelen, uint64_t *out_ino, uint16_t *out_type); // Iterate a directory's real entries in hash order. "." and ".." are not // stored and are not reported; callers that need them synthesise them. // A non-zero return from fn stops the walk and is passed back. int schfs_dir_iterate(struct schfs_vol *v, struct schfs_cursor *c, uint64_t dir, int (*fn)(uint64_t ino, uint16_t type, const char *name, unsigned namelen, void *arg), void *arg); // Read from a file. Short reads at end-of-file are normal; returns the byte // count, or -1 on a missing object, -2 on corruption or a failed data checksum. // `dblk` is a caller-owned SCHFS_BLKSIZE scratch buffer for data blocks. /* Drops the one-entry inode and extent read caches in obj.c. Every write * entry point calls this before touching anything: the caches are only safe * because invalidation is blunt, and one that is merely usually right hands * back another file's data. */ void schfs_cache_invalidate(void); long schfs_file_read(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino, uint64_t off, void *buf, unsigned n, void *dblk); // Walk an absolute path. Returns the object id, or 0 if any component is // missing or is not a directory. uint64_t schfs_path_lookup(struct schfs_vol *v, struct schfs_cursor *c, const char *path); // --------------------------------------------------------------------------- // objw.c — write side // // Shared error codes throughout: 0 success, -1 absent or not permitted, // -2 corruption or I/O failure, -3 out of space, -4 a directory hash-collision // list that will not fit in one item, -5 directory not empty. // // None of it is visible to a reader until schfs_vol_commit publishes the tree, // which is what lets a failure part way through a multi-record operation be // abandoned rather than unwound. // --------------------------------------------------------------------------- uint64_t schfs_alloc_objectid(struct schfs_vol *v); int schfs_inode_put(struct schfs_vol *v, uint64_t ino, const struct schfs_inode *in); // Add one entry, appending to the packed list if another name already hashes // the same. -1 if the name is already present. int schfs_dir_add(struct schfs_vol *v, uint64_t dir, const char *name, unsigned namelen, uint64_t child, uint16_t type); // Remove one entry. -1 if there is no such name. int schfs_dir_del(struct schfs_vol *v, uint64_t dir, const char *name, unsigned namelen); // 1 if the directory holds no entries, 0 if it holds some, -2 on corruption. int schfs_dir_empty(struct schfs_vol *v, uint64_t dir); // Claim a contiguous run of data blocks for a file whose final size is already // known, and record the whole run as ONE extent. Worth calling before a bulk // write: without it, data and tree blocks interleave, and only the merging in // schfs_file_write keeps the extent count down. int schfs_file_reserve(struct schfs_vol *v, uint64_t ino, uint64_t nblocks); // Write at an arbitrary offset, allocating blocks, extent records and data // checksums, copying any block that a committed tree can still reach, and // splitting the extent record around it. Writing past the end extends the file; // a gap left behind stays a hole and reads back as zeros. Returns the byte // count written, or a negative error. long schfs_file_write(struct schfs_vol *v, uint64_t ino, uint64_t off, const void *buf, unsigned n, void *dblk); // Append at the current end of file. A convenience over schfs_file_write, kept // because image builders write nothing else. int schfs_file_append(struct schfs_vol *v, uint64_t ino, const void *buf, unsigned n, void *dblk); // Set a file's length, releasing whole blocks past the new end and zeroing what // is left of the block the new end falls inside — truncating down and growing // again has to read back zeros. Growing alone costs nothing: a range with no // extent records is a hole. `dblk` is a caller-owned SCHFS_BLKSIZE buffer. int schfs_file_truncate(struct schfs_vol *v, uint64_t ino, uint64_t size, void *dblk); // Release an object's data, checksums and inode record. The caller must already // have removed every name pointing at it. int schfs_object_free(struct schfs_vol *v, uint64_t ino); // Create an object and the entry naming it. `now` is the timestamp to stamp all // three times with — SchFS has no clock of its own. int schfs_create(struct schfs_vol *v, uint64_t dir, const char *name, unsigned namelen, uint16_t type, uint32_t mode, uint16_t rdev_major, uint16_t rdev_minor, uint64_t now, uint64_t *out_ino); // A second name for an existing object. -1 for a directory: hard-linking one // makes cycles possible, and a cycle turns every recursive walk infinite. int schfs_link(struct schfs_vol *v, uint64_t dir, const char *name, unsigned namelen, uint64_t ino); // Remove a name, and the object with it once the last name is gone. Handles // directories too, refusing with -5 if the directory still has entries. int schfs_unlink(struct schfs_vol *v, uint64_t dir, const char *name, unsigned namelen); // Move a name, replacing the target if one exists. Refuses to move a directory // into its own subtree, which would detach that subtree from the root. int schfs_rename(struct schfs_vol *v, uint64_t olddir, const char *oldname, unsigned oldlen, uint64_t newdir, const char *newname, unsigned newlen); #endif /* SCHFS_INT_H */