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.
277 lines
13 KiB
C
277 lines
13 KiB
C
// 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
|
|
};
|
|
|
|
// 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);
|
|
|
|
// 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.
|
|
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 */
|