Initial commit: Schunix as it stands before the SchOS rename
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.
This commit is contained in:
commit
d0f7acf9eb
275 changed files with 51417 additions and 0 deletions
1181
kernel/fs/schfs/btree.c
Normal file
1181
kernel/fs/schfs/btree.c
Normal file
File diff suppressed because it is too large
Load diff
215
kernel/fs/schfs/btree.h
Normal file
215
kernel/fs/schfs/btree.h
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
// 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 */
|
||||
623
kernel/fs/schfs/kmount.c
Normal file
623
kernel/fs/schfs/kmount.c
Normal file
|
|
@ -0,0 +1,623 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// SchFS in the kernel: block device, mount, and the transaction bracket.
|
||||
//
|
||||
// The filesystem code itself (btree.c, space.c, vol.c, obj.c, objw.c) is the
|
||||
// same portable C the host tools compile, so nothing here re-implements the
|
||||
// format. This file is only the three things that cannot be shared: a block
|
||||
// device backed by the buffer cache, the boot-time mount, and the rule about
|
||||
// when a group of changes becomes durable.
|
||||
//
|
||||
// The running root filesystem is still the v1 code in kernel/fs/fs.c, so a
|
||||
// SchFS volume that fails to mount costs nothing — the system boots exactly as
|
||||
// before and says so on the console.
|
||||
//
|
||||
// ---------------------------------------------------------------------------
|
||||
// Transactions
|
||||
//
|
||||
// Every entry point below that changes anything is one transaction group: it
|
||||
// takes the volume lock, does its work, and commits before returning. That
|
||||
// makes an operation durable the moment it returns, and — because a commit is a
|
||||
// single uberblock write that either lands or does not — indivisible against a
|
||||
// crash. There is no journal to replay and nothing to undo, because none of the
|
||||
// work is reachable until that one block is written.
|
||||
//
|
||||
// The cost is one uberblock write per operation. Batching several operations
|
||||
// into a group would amortise it, and the structure here is deliberately shaped
|
||||
// so that becomes a change to where commit is called rather than a redesign;
|
||||
// but a filesystem that loses the last few operations on a crash is a much
|
||||
// worse default than a slow one.
|
||||
|
||||
#include "types.h"
|
||||
#include "defs.h"
|
||||
#include "param.h"
|
||||
#include "spinlock.h"
|
||||
#include "sleeplock.h"
|
||||
#include "fs.h"
|
||||
#include "buf.h"
|
||||
|
||||
#include "schfs_int.h"
|
||||
|
||||
// A SchFS block is 4096 bytes and a buffer-cache block is BSIZE (1024), so each
|
||||
// filesystem block spans SCHFS_BPB consecutive buffers. Keeping the buffer
|
||||
// cache at its own block size avoids disturbing the v1 filesystem, the log and
|
||||
// the IDE driver, all of which assume BSIZE.
|
||||
#define SCHFS_BPB (SCHFS_BLKSIZE / BSIZE)
|
||||
|
||||
struct schfs_kdev {
|
||||
uint dev;
|
||||
};
|
||||
|
||||
static struct schfs_kdev kdev;
|
||||
static struct schfs_vol kvol;
|
||||
static int kmounted;
|
||||
|
||||
// Block buffers, allocated once at mount.
|
||||
//
|
||||
// Nothing in SchFS may put a block on the stack: a cursor needs a whole
|
||||
// SCHFS_BLKSIZE buffer and the kernel stack is one 4096-byte page (KSTACKSIZE),
|
||||
// so a single such local overflows it. kalloc returns exactly one page, which
|
||||
// is exactly one SchFS block, so each of these fits with nothing to spare.
|
||||
//
|
||||
// kcurblk and kdatablk serve the read paths; kctx* are the three the volume
|
||||
// keeps for mutations. They are separate pages because a write path runs a
|
||||
// cursor of its own inside the volume's buffers while the caller's cursor is
|
||||
// still positioned in kcurblk.
|
||||
static char *kcurblk;
|
||||
static char *kdatablk;
|
||||
static char *kctxa, *kctxb, *kctxc;
|
||||
|
||||
// Those buffers are shared by every caller, so every entry point below holds
|
||||
// this lock for the whole operation. A sleeplock rather than a spinlock because
|
||||
// bread() sleeps waiting on the disk.
|
||||
//
|
||||
// It also serialises transactions, which is not incidental: a group being built
|
||||
// rewrites its own blocks in place, so two operations interleaving inside one
|
||||
// group would each see the other's half-finished tree.
|
||||
static struct sleeplock schfs_lock;
|
||||
|
||||
// Seconds, for inode timestamps. The kernel has no wall clock wired up, so this
|
||||
// matches what fs.c does for the v1 filesystem rather than inventing a second,
|
||||
// differently-wrong answer.
|
||||
static uint64_t
|
||||
know(void)
|
||||
{
|
||||
return (uint64_t)uptime();
|
||||
}
|
||||
|
||||
// Start and finish one transaction. `changed` says whether anything was
|
||||
// written, so a read does not pay for an uberblock write.
|
||||
static void
|
||||
ktx_begin(void)
|
||||
{
|
||||
acquiresleep(&schfs_lock);
|
||||
}
|
||||
|
||||
static int
|
||||
ktx_end(int changed, int r)
|
||||
{
|
||||
if (changed && r >= 0) {
|
||||
int c = schfs_vol_commit(&kvol);
|
||||
if (c != 0)
|
||||
r = c;
|
||||
}
|
||||
releasesleep(&schfs_lock);
|
||||
return r;
|
||||
}
|
||||
|
||||
static int
|
||||
kd_read(void *ctx, uint64_t blk, void *out)
|
||||
{
|
||||
struct schfs_kdev *d = ctx;
|
||||
|
||||
if (blk == 0)
|
||||
return -1;
|
||||
// The buffer cache addresses blocks with a uint, which caps the volume long
|
||||
// before the on-disk format does. Refusing out here beats truncating and
|
||||
// reading some unrelated sector, which would surface as corruption.
|
||||
if (blk > 0xffffffffULL / SCHFS_BPB)
|
||||
return -1;
|
||||
|
||||
for (int i = 0; i < SCHFS_BPB; i++) {
|
||||
struct buf *b = bread(d->dev, (uint)(blk * SCHFS_BPB + i));
|
||||
memmove((char *)out + i * BSIZE, b->data, BSIZE);
|
||||
brelse(b);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
kd_write(void *ctx, uint64_t blk, const void *in)
|
||||
{
|
||||
struct schfs_kdev *d = ctx;
|
||||
|
||||
if (blk == 0)
|
||||
return -1;
|
||||
if (blk > 0xffffffffULL / SCHFS_BPB)
|
||||
return -1;
|
||||
|
||||
// Straight through the buffer cache, with no log. The xv6 log exists to make
|
||||
// multi-block updates atomic; copy-on-write makes them atomic by never
|
||||
// modifying a block the committed tree can reach, so logging every write
|
||||
// would double the I/O to re-solve a problem the format has already solved.
|
||||
//
|
||||
// bread before the overwrite because the cache has no "give me this block
|
||||
// without reading it" entry point. It costs a read the first time a block is
|
||||
// touched and nothing afterwards.
|
||||
for (int i = 0; i < SCHFS_BPB; i++) {
|
||||
struct buf *b = bread(d->dev, (uint)(blk * SCHFS_BPB + i));
|
||||
memmove(b->data, (const char *)in + i * BSIZE, BSIZE);
|
||||
bwrite(b);
|
||||
brelse(b);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Allocation goes through the volume's space manager. These take no ctx of
|
||||
// their own because a schfs_bdev has one ctx shared by every hook and the block
|
||||
// I/O above needs it for the device number.
|
||||
static uint64_t
|
||||
kd_alloc(void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
return schfs_vol_alloc(&kvol);
|
||||
}
|
||||
|
||||
static uint64_t
|
||||
kd_alloc_run(void *ctx, uint64_t n)
|
||||
{
|
||||
(void)ctx;
|
||||
return schfs_vol_alloc_run(&kvol, n);
|
||||
}
|
||||
|
||||
static void
|
||||
kd_free(void *ctx, uint64_t b)
|
||||
{
|
||||
(void)ctx;
|
||||
schfs_vol_free(&kvol, b);
|
||||
}
|
||||
|
||||
// Pages for the allocation bitmap: one per 32768 blocks of volume, taken once
|
||||
// at mount and never given back.
|
||||
static void *
|
||||
kd_palloc(void)
|
||||
{
|
||||
return kalloc();
|
||||
}
|
||||
|
||||
// Try to mount a SchFS volume from `dev`. Never panics: a disk that is not
|
||||
// SchFS, or is an unreadable SchFS, simply leaves the volume unmounted.
|
||||
void
|
||||
schfs_kmount(uint dev)
|
||||
{
|
||||
if (kmounted)
|
||||
return;
|
||||
|
||||
initsleeplock(&schfs_lock, "schfs");
|
||||
|
||||
if (kcurblk == 0) kcurblk = kalloc();
|
||||
if (kdatablk == 0) kdatablk = kalloc();
|
||||
if (kctxa == 0) kctxa = kalloc();
|
||||
if (kctxb == 0) kctxb = kalloc();
|
||||
if (kctxc == 0) kctxc = kalloc();
|
||||
if (kcurblk == 0 || kdatablk == 0 ||
|
||||
kctxa == 0 || kctxb == 0 || kctxc == 0) {
|
||||
cprintf("schfs: no memory for block buffers\n");
|
||||
return;
|
||||
}
|
||||
|
||||
kdev.dev = dev;
|
||||
|
||||
struct schfs_bdev bd;
|
||||
bd.ctx = &kdev;
|
||||
bd.bread = kd_read;
|
||||
bd.bwrite = kd_write;
|
||||
bd.balloc = kd_alloc;
|
||||
bd.bfree = kd_free;
|
||||
bd.balloc_run = kd_alloc_run;
|
||||
|
||||
struct schfs_ctx cx;
|
||||
cx.a = kctxa;
|
||||
cx.b = kctxb;
|
||||
cx.c = kctxc;
|
||||
|
||||
// The uberblock scan doubles as the probe: no valid uberblock means this is
|
||||
// simply not a SchFS volume, which is not an error worth reporting loudly.
|
||||
// Opening with a page allocator also builds the allocation bitmap, by walking
|
||||
// the tree — the only mount-time cost SchFS has, and what buys it a mount
|
||||
// that cannot disagree with the filesystem it just adopted.
|
||||
int r = schfs_vol_open(&kvol, &bd, &cx, kd_palloc);
|
||||
if (r == -1)
|
||||
return;
|
||||
if (r == -4) {
|
||||
cprintf("schfs: device %d is larger than %d GB and cannot be mapped\n",
|
||||
dev, (int)(SCHFS_MAX_BLOCKS * SCHFS_BLKSIZE / (1024 * 1024 * 1024)));
|
||||
return;
|
||||
}
|
||||
if (r == -5) {
|
||||
cprintf("schfs: no memory for the allocation map of device %d\n", dev);
|
||||
return;
|
||||
}
|
||||
if (r != 0) {
|
||||
cprintf("schfs: device %d has a SchFS uberblock but an unreadable tree\n",
|
||||
dev);
|
||||
return;
|
||||
}
|
||||
|
||||
kmounted = 1;
|
||||
cprintf("schfs: mounted '%s' on device %d, %d MB, generation %d\n",
|
||||
kvol.label, dev,
|
||||
(int)(kvol.total_blocks * SCHFS_BLKSIZE / (1024 * 1024)),
|
||||
(int)kvol.generation);
|
||||
}
|
||||
|
||||
int
|
||||
schfs_kmounted(void)
|
||||
{
|
||||
return kmounted;
|
||||
}
|
||||
|
||||
// Resolve a path on the mounted volume. Returns the object id, or 0.
|
||||
uint64_t
|
||||
schfs_klookup(const char *path)
|
||||
{
|
||||
if (!kmounted)
|
||||
return 0;
|
||||
acquiresleep(&schfs_lock);
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &kvol.tree, kcurblk);
|
||||
uint64_t r = schfs_path_lookup(&kvol, &c, path);
|
||||
releasesleep(&schfs_lock);
|
||||
return r;
|
||||
}
|
||||
|
||||
int
|
||||
schfs_kstat(uint64_t ino, struct schfs_inode *out)
|
||||
{
|
||||
if (!kmounted)
|
||||
return -1;
|
||||
acquiresleep(&schfs_lock);
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &kvol.tree, kcurblk);
|
||||
int r = schfs_inode_get(&kvol, &c, ino, out);
|
||||
releasesleep(&schfs_lock);
|
||||
return r;
|
||||
}
|
||||
|
||||
long
|
||||
schfs_kread(uint64_t ino, uint64_t off, void *buf, unsigned n)
|
||||
{
|
||||
if (!kmounted)
|
||||
return -1;
|
||||
acquiresleep(&schfs_lock);
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &kvol.tree, kcurblk);
|
||||
long r = schfs_file_read(&kvol, &c, ino, off, buf, n, kdatablk);
|
||||
releasesleep(&schfs_lock);
|
||||
return r;
|
||||
}
|
||||
|
||||
int
|
||||
schfs_kreaddir(uint64_t dir,
|
||||
int (*fn)(uint64_t, uint16_t, const char *, unsigned, void *),
|
||||
void *arg)
|
||||
{
|
||||
if (!kmounted)
|
||||
return -1;
|
||||
// NOTE: fn must not call back into schfs_k* -- the lock is not recursive and
|
||||
// the cursor buffer is shared. Callers that need per-entry detail collect the
|
||||
// entries first, then look them up after the walk.
|
||||
acquiresleep(&schfs_lock);
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &kvol.tree, kcurblk);
|
||||
int r = schfs_dir_iterate(&kvol, &c, dir, fn, arg);
|
||||
releasesleep(&schfs_lock);
|
||||
return r;
|
||||
}
|
||||
|
||||
struct schfs_vol *
|
||||
schfs_kvol(void)
|
||||
{
|
||||
return kmounted ? &kvol : 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Write side */
|
||||
/* */
|
||||
/* Each of these is one transaction: everything it does is invisible */
|
||||
/* until it commits, and a crash part way through leaves the volume at */
|
||||
/* the state before the call. That is the entire failure story — there */
|
||||
/* is no partial state to detect and nothing to roll back. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
int
|
||||
schfs_kcreate(uint64_t dir, const char *name, unsigned namelen, uint16_t type,
|
||||
uint32_t mode, uint16_t major, uint16_t minor, uint64_t *out)
|
||||
{
|
||||
if (!kmounted || kvol.readonly)
|
||||
return -1;
|
||||
ktx_begin();
|
||||
int r = schfs_create(&kvol, dir, name, namelen, type, mode, major, minor,
|
||||
know(), out);
|
||||
return ktx_end(1, r);
|
||||
}
|
||||
|
||||
long
|
||||
schfs_kwrite(uint64_t ino, uint64_t off, const void *buf, unsigned n)
|
||||
{
|
||||
if (!kmounted || kvol.readonly)
|
||||
return -1;
|
||||
ktx_begin();
|
||||
long r = schfs_file_write(&kvol, ino, off, buf, n, kdatablk);
|
||||
// ktx_end speaks in error codes, and a write speaks in byte counts. Passing
|
||||
// the count straight through told it the transaction had failed with status
|
||||
// 17, which it duly reported -- and a short write is a panic one layer up.
|
||||
int e = ktx_end(1, r < 0 ? (int)r : 0);
|
||||
return e == 0 ? r : (long)e;
|
||||
}
|
||||
|
||||
int
|
||||
schfs_ktruncate(uint64_t ino, uint64_t size)
|
||||
{
|
||||
if (!kmounted || kvol.readonly)
|
||||
return -1;
|
||||
ktx_begin();
|
||||
int r = schfs_file_truncate(&kvol, ino, size, kdatablk);
|
||||
return ktx_end(1, r);
|
||||
}
|
||||
|
||||
int
|
||||
schfs_kunlink(uint64_t dir, const char *name, unsigned namelen)
|
||||
{
|
||||
if (!kmounted || kvol.readonly)
|
||||
return -1;
|
||||
ktx_begin();
|
||||
int r = schfs_unlink(&kvol, dir, name, namelen);
|
||||
return ktx_end(1, r);
|
||||
}
|
||||
|
||||
int
|
||||
schfs_klink(uint64_t dir, const char *name, unsigned namelen, uint64_t ino)
|
||||
{
|
||||
if (!kmounted || kvol.readonly)
|
||||
return -1;
|
||||
ktx_begin();
|
||||
int r = schfs_link(&kvol, dir, name, namelen, ino);
|
||||
return ktx_end(1, r);
|
||||
}
|
||||
|
||||
int
|
||||
schfs_krename(uint64_t olddir, const char *oldname, unsigned oldlen,
|
||||
uint64_t newdir, const char *newname, unsigned newlen)
|
||||
{
|
||||
if (!kmounted || kvol.readonly)
|
||||
return -1;
|
||||
ktx_begin();
|
||||
int r = schfs_rename(&kvol, olddir, oldname, oldlen,
|
||||
newdir, newname, newlen);
|
||||
return ktx_end(1, r);
|
||||
}
|
||||
|
||||
// Write back an inode record the caller has modified — the one way to change
|
||||
// mode, ownership or timestamps. The caller reads it with schfs_kstat, edits
|
||||
// the struct, and hands it back.
|
||||
int
|
||||
schfs_ksetattr(uint64_t ino, const struct schfs_inode *in)
|
||||
{
|
||||
if (!kmounted || kvol.readonly)
|
||||
return -1;
|
||||
ktx_begin();
|
||||
int r = schfs_inode_put(&kvol, ino, in);
|
||||
return ktx_end(1, r);
|
||||
}
|
||||
|
||||
// Flush without changing anything. Nothing needs this yet — every operation
|
||||
// above commits already — but sync(2) has to mean something.
|
||||
int
|
||||
schfs_ksync(void)
|
||||
{
|
||||
if (!kmounted || kvol.readonly)
|
||||
return 0;
|
||||
ktx_begin();
|
||||
return ktx_end(1, 0);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Pieces the VFS needs */
|
||||
/* */
|
||||
/* The VFS keeps its own link count and decides when an object dies, */
|
||||
/* so it needs the directory entry and the object as separate */
|
||||
/* operations rather than the combined create/unlink above. Two layers */
|
||||
/* counting links independently is exactly how a filesystem loses a */
|
||||
/* file that something still has open. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
// Resolve one name inside a directory. 0 if there is no such entry.
|
||||
uint64_t
|
||||
schfs_klookup_at(uint64_t dir, const char *name, unsigned namelen)
|
||||
{
|
||||
if (!kmounted)
|
||||
return 0;
|
||||
acquiresleep(&schfs_lock);
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &kvol.tree, kcurblk);
|
||||
uint64_t ino = 0;
|
||||
if (schfs_dir_lookup(&kvol, &c, dir, name, namelen, &ino, 0) != 0)
|
||||
ino = 0;
|
||||
releasesleep(&schfs_lock);
|
||||
return ino;
|
||||
}
|
||||
|
||||
// 1 if the directory holds no entries, 0 if it does, -1 on error.
|
||||
int
|
||||
schfs_kdirempty(uint64_t dir)
|
||||
{
|
||||
if (!kmounted)
|
||||
return -1;
|
||||
acquiresleep(&schfs_lock);
|
||||
int r = schfs_dir_empty(&kvol, dir);
|
||||
releasesleep(&schfs_lock);
|
||||
return r == -2 ? -1 : r;
|
||||
}
|
||||
|
||||
// An object with no name and no links yet. Nothing can reach it until the
|
||||
// caller links it, and nothing reaches the disk until the transaction commits,
|
||||
// so a caller that gives up in between leaves nothing behind.
|
||||
int
|
||||
schfs_kalloc_object(uint16_t type, uint32_t mode, uint64_t *out)
|
||||
{
|
||||
if (!kmounted || kvol.readonly)
|
||||
return -1;
|
||||
ktx_begin();
|
||||
|
||||
uint64_t ino = schfs_alloc_objectid(&kvol);
|
||||
struct schfs_inode in;
|
||||
memset(&in, 0, sizeof(in));
|
||||
in.type = type;
|
||||
in.mode = mode;
|
||||
in.nlink = 0;
|
||||
in.atime = in.mtime = in.ctime = know();
|
||||
in.gen = kvol.tree.generation;
|
||||
|
||||
int r = schfs_inode_put(&kvol, ino, &in);
|
||||
if (r == 0 && out)
|
||||
*out = ino;
|
||||
return ktx_end(1, r);
|
||||
}
|
||||
|
||||
int
|
||||
schfs_kdiradd(uint64_t dir, const char *name, unsigned namelen, uint64_t ino,
|
||||
uint16_t type)
|
||||
{
|
||||
if (!kmounted || kvol.readonly)
|
||||
return -1;
|
||||
ktx_begin();
|
||||
int r = schfs_dir_add(&kvol, dir, name, namelen, ino, type);
|
||||
return ktx_end(1, r);
|
||||
}
|
||||
|
||||
// Remove one directory entry, leaving the object alone. The VFS decrements the
|
||||
// link count itself and calls schfs_kfree when it reaches zero.
|
||||
int
|
||||
schfs_kdirdel(uint64_t dir, const char *name, unsigned namelen)
|
||||
{
|
||||
if (!kmounted || kvol.readonly)
|
||||
return -1;
|
||||
ktx_begin();
|
||||
int r = schfs_dir_del(&kvol, dir, name, namelen);
|
||||
return ktx_end(1, r);
|
||||
}
|
||||
|
||||
// Release an object's data, checksums and inode record. Called from iput once
|
||||
// the last name is gone and nothing has it open.
|
||||
int
|
||||
schfs_kfree(uint64_t ino)
|
||||
{
|
||||
if (!kmounted || kvol.readonly)
|
||||
return -1;
|
||||
ktx_begin();
|
||||
int r = schfs_object_free(&kvol, ino);
|
||||
return ktx_end(1, r);
|
||||
}
|
||||
|
||||
uint64_t
|
||||
schfs_kroot(void)
|
||||
{
|
||||
return kmounted ? kvol.root_objectid : 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* /proc/schfs */
|
||||
/* */
|
||||
/* Phase 2 has no VFS integration -- SchFS files are not reachable */
|
||||
/* through open(2) yet -- so this is how the mount is observable from */
|
||||
/* userland at all. It reports the volume and lists the root directory, */
|
||||
/* which is enough to confirm from inside a running Schunix that the */
|
||||
/* kernel really did parse an image mkschfs wrote. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void
|
||||
kp_puts(char *buf, int cap, int *len, const char *s)
|
||||
{
|
||||
while (*s && *len < cap - 1)
|
||||
buf[(*len)++] = *s++;
|
||||
}
|
||||
|
||||
static void
|
||||
kp_putu(char *buf, int cap, int *len, uint64 v)
|
||||
{
|
||||
char tmp[24];
|
||||
int i = 0;
|
||||
if (v == 0) { if (*len < cap - 1) buf[(*len)++] = '0'; return; }
|
||||
while (v && i < (int)sizeof(tmp)) { tmp[i++] = '0' + (v % 10); v /= 10; }
|
||||
while (i--) { if (*len < cap - 1) buf[(*len)++] = tmp[i]; }
|
||||
}
|
||||
|
||||
struct kp_ctx {
|
||||
char *buf;
|
||||
int cap;
|
||||
int *len;
|
||||
int count;
|
||||
};
|
||||
|
||||
static int
|
||||
kp_entry(uint64_t ino, uint16_t type, const char *name, unsigned namelen,
|
||||
void *arg)
|
||||
{
|
||||
struct kp_ctx *c = arg;
|
||||
c->count++;
|
||||
// Only the first handful, and no per-entry stat: schfs_kreaddir holds the
|
||||
// lock across the walk, so calling schfs_kstat from here would deadlock.
|
||||
if (c->count > 16)
|
||||
return 0;
|
||||
kp_puts(c->buf, c->cap, c->len, " ");
|
||||
kp_puts(c->buf, c->cap, c->len,
|
||||
type == SCHFS_T_DIR ? "dir " :
|
||||
type == SCHFS_T_SYMLINK ? "symlink " :
|
||||
type == SCHFS_T_DEV ? "dev " : "file ");
|
||||
for (unsigned i = 0; i < namelen && *c->len < c->cap - 1; i++)
|
||||
c->buf[(*c->len)++] = name[i];
|
||||
kp_puts(c->buf, c->cap, c->len, " #");
|
||||
kp_putu(c->buf, c->cap, c->len, ino);
|
||||
kp_puts(c->buf, c->cap, c->len, "\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
schfs_kinfo(char *buf, int cap)
|
||||
{
|
||||
int len = 0;
|
||||
|
||||
if (!kmounted) {
|
||||
kp_puts(buf, cap, &len, "no SchFS volume mounted\n");
|
||||
return len;
|
||||
}
|
||||
|
||||
kp_puts(buf, cap, &len, "label: ");
|
||||
kp_puts(buf, cap, &len, kvol.label);
|
||||
kp_puts(buf, cap, &len, "\nblocksize: ");
|
||||
kp_putu(buf, cap, &len, SCHFS_BLKSIZE);
|
||||
kp_puts(buf, cap, &len, "\ntotal_blocks: ");
|
||||
kp_putu(buf, cap, &len, kvol.total_blocks);
|
||||
kp_puts(buf, cap, &len, "\nfree_blocks: ");
|
||||
kp_putu(buf, cap, &len, kvol.free_blocks);
|
||||
kp_puts(buf, cap, &len, "\ngeneration: ");
|
||||
kp_putu(buf, cap, &len, kvol.generation);
|
||||
kp_puts(buf, cap, &len, "\ntree_root: ");
|
||||
kp_putu(buf, cap, &len, kvol.tree.root);
|
||||
kp_puts(buf, cap, &len, "\nroot_objectid: ");
|
||||
kp_putu(buf, cap, &len, kvol.root_objectid);
|
||||
kp_puts(buf, cap, &len, "\n\nroot directory:\n");
|
||||
|
||||
struct kp_ctx c = { buf, cap, &len, 0 };
|
||||
if (schfs_kreaddir(kvol.root_objectid, kp_entry, &c) != 0) {
|
||||
kp_puts(buf, cap, &len, " (directory read failed)\n");
|
||||
return len;
|
||||
}
|
||||
if (c.count > 16) {
|
||||
kp_puts(buf, cap, &len, " ... ");
|
||||
kp_putu(buf, cap, &len, (uint64)(c.count - 16));
|
||||
kp_puts(buf, cap, &len, " more\n");
|
||||
}
|
||||
return len;
|
||||
}
|
||||
441
kernel/fs/schfs/obj.c
Normal file
441
kernel/fs/schfs/obj.c
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// SchFS object layer, read side: inodes, directories and file data.
|
||||
//
|
||||
// Everything here is a record in the one B-tree, which is the point of the
|
||||
// btrfs-style design — there is no inode table, no directory block format and
|
||||
// no indirect-block scheme to get wrong, only key types:
|
||||
//
|
||||
// (ino, INODE, 0) struct schfs_inode
|
||||
// (dir, DIRENT, hash) packed list of schfs_dirent_rec + name
|
||||
// (ino, EXTENT, fileblk) struct schfs_extent
|
||||
// (CSUM, CSUM, diskblk) uint32 CRC32c of that data block
|
||||
//
|
||||
// Kernel code reaches all of this through a caller-supplied cursor, so no
|
||||
// function here puts a 4096-byte block on the stack.
|
||||
|
||||
#include "schfs_int.h"
|
||||
|
||||
#ifdef SCHFS_HOSTED
|
||||
#include <string.h>
|
||||
#else
|
||||
#include "types.h"
|
||||
#include "defs.h"
|
||||
#endif
|
||||
|
||||
// Point a cursor at the volume's current tree, keeping the caller's block
|
||||
// buffer. Called before every operation rather than once at open, because the
|
||||
// tree root moves on every commit under copy-on-write: a cursor that cached the
|
||||
// old root would keep reading a stale — though perfectly valid — filesystem.
|
||||
static void
|
||||
cur_bind(struct schfs_cursor *c, struct schfs_vol *v)
|
||||
{
|
||||
c->t = &v->tree;
|
||||
c->depth = 0;
|
||||
c->valid = 0;
|
||||
}
|
||||
|
||||
void
|
||||
schfs_mkkey(struct schfs_key *k, uint64_t objectid, uint8_t type,
|
||||
uint64_t offset)
|
||||
{
|
||||
memset(k, 0, sizeof(*k));
|
||||
k->objectid = objectid;
|
||||
k->type = type;
|
||||
k->offset = offset;
|
||||
}
|
||||
|
||||
// FNV-1a, 64-bit. Directory lookup is a tree descent on this value, so it wants
|
||||
// to spread short, highly similar names ("ls", "ln", "cp") across the keyspace.
|
||||
// FNV does that, is a dozen lines, and needs no table — worth more here than
|
||||
// the marginally better distribution of something larger.
|
||||
//
|
||||
// Collisions are handled by the packed dirent list rather than assumed away;
|
||||
// see struct schfs_dirent_rec.
|
||||
// Test-only: narrows the hash so collisions become common.
|
||||
//
|
||||
// With a full 64-bit hash, two names in one directory colliding is so unlikely
|
||||
// that the packed-list code which handles it would never run — and untested
|
||||
// code that only executes on rare inputs is exactly the code that loses a file
|
||||
// years later. Setting this to a small mask makes every directory a pile of
|
||||
// collisions, so the round-trip test exercises the path properly. Always ~0
|
||||
// in the kernel and in real images.
|
||||
uint64_t schfs_hash_mask = ~0ULL;
|
||||
|
||||
uint64_t
|
||||
schfs_namehash(const char *name, unsigned len)
|
||||
{
|
||||
uint64_t h = 1469598103934665603ULL; /* FNV offset basis */
|
||||
for (unsigned i = 0; i < len; i++) {
|
||||
h ^= (uint8_t)name[i];
|
||||
h *= 1099511628211ULL; /* FNV prime */
|
||||
}
|
||||
// Key ordering is by this value, so a directory's entries come back in an
|
||||
// order that has nothing to do with their names. That is fine for readdir
|
||||
// (POSIX promises no order) and is what makes lookup one descent.
|
||||
return h & schfs_hash_mask;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Inodes */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
int
|
||||
schfs_inode_get(struct schfs_vol *v, struct schfs_cursor *c,
|
||||
uint64_t ino, struct schfs_inode *out)
|
||||
{
|
||||
struct schfs_key k;
|
||||
schfs_mkkey(&k, ino, SCHFS_KEY_INODE, 0);
|
||||
|
||||
cur_bind(c, v);
|
||||
unsigned len = sizeof(*out);
|
||||
int r = schfs_btree_lookup_cur(c, &k, out, &len);
|
||||
if (r != 0)
|
||||
return r;
|
||||
// A short record means an image written by an older or broken tool. Reject
|
||||
// it rather than acting on a half-filled struct.
|
||||
if (len != sizeof(*out))
|
||||
return -2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Directories */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
// Walk a packed dirent list, calling fn per entry. Stops early if fn returns
|
||||
// non-zero. Returns fn's value, or -2 if the list is malformed.
|
||||
static int
|
||||
dirent_walk(const void *val, unsigned len,
|
||||
int (*fn)(const struct schfs_dirent_rec *, const char *, void *),
|
||||
void *arg)
|
||||
{
|
||||
unsigned off = 0;
|
||||
while (off < len) {
|
||||
if (len - off < sizeof(struct schfs_dirent_rec))
|
||||
return -2;
|
||||
const struct schfs_dirent_rec *d =
|
||||
(const struct schfs_dirent_rec *)((const char *)val + off);
|
||||
unsigned step = SCHFS_DIRENT_LEN(d->namelen);
|
||||
// Bounds-check before touching the name: a corrupt namelen would
|
||||
// otherwise read past the end of the block.
|
||||
if (d->namelen == 0 || d->namelen > SCHFS_NAME_MAX || off + step > len)
|
||||
return -2;
|
||||
int r = fn(d, (const char *)d + sizeof(*d), arg);
|
||||
if (r)
|
||||
return r;
|
||||
off += step;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct find_ctx {
|
||||
const char *name;
|
||||
unsigned namelen;
|
||||
uint64_t ino;
|
||||
uint16_t type;
|
||||
};
|
||||
|
||||
static int
|
||||
find_one(const struct schfs_dirent_rec *d, const char *nm, void *arg)
|
||||
{
|
||||
struct find_ctx *f = (struct find_ctx *)arg;
|
||||
if (d->namelen != f->namelen)
|
||||
return 0;
|
||||
if (memcmp(nm, f->name, f->namelen) != 0)
|
||||
return 0;
|
||||
f->ino = d->objectid;
|
||||
f->type = d->type;
|
||||
return 1; /* found; stop */
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
|
||||
return -1;
|
||||
|
||||
struct schfs_key k;
|
||||
schfs_mkkey(&k, dir, SCHFS_KEY_DIRENT, schfs_namehash(name, namelen));
|
||||
|
||||
cur_bind(c, v);
|
||||
int r = schfs_cursor_seek(c, &k);
|
||||
if (r == -2)
|
||||
return -2;
|
||||
if (r != 0)
|
||||
return -1;
|
||||
|
||||
const struct schfs_key *got = schfs_cursor_key(c);
|
||||
if (!got || schfs_key_cmp(got, &k) != 0)
|
||||
return -1;
|
||||
|
||||
unsigned len;
|
||||
const void *val = schfs_cursor_val(c, &len);
|
||||
if (!val)
|
||||
return -2;
|
||||
|
||||
struct find_ctx f;
|
||||
f.name = name; f.namelen = namelen; f.ino = 0; f.type = 0;
|
||||
r = dirent_walk(val, len, find_one, &f);
|
||||
if (r == -2)
|
||||
return -2;
|
||||
if (r != 1)
|
||||
return -1; /* hash matched, name did not */
|
||||
|
||||
if (out_ino) *out_ino = f.ino;
|
||||
if (out_type) *out_type = f.type;
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct iter_ctx {
|
||||
int (*fn)(uint64_t, uint16_t, const char *, unsigned, void *);
|
||||
void *arg;
|
||||
struct schfs_cursor *cur;
|
||||
uint64_t dir;
|
||||
int stop;
|
||||
};
|
||||
|
||||
// Hand each entry of one packed dirent list to the caller.
|
||||
//
|
||||
// The list lives in the cursor's block buffer, and almost every readdir caller
|
||||
// immediately looks each entry up — to stat it, to recurse into it — through a
|
||||
// cursor that may well share that buffer. Walking the list in place meant the
|
||||
// name and every record after it turned to garbage the moment the callback
|
||||
// touched the filesystem, which is how an extracted tree once ended up with
|
||||
// directories reparented under their own children.
|
||||
//
|
||||
// The obvious fix, copying the whole item aside, costs SCHFS_MAX_INLINE (just
|
||||
// over 1 KB) of stack — a quarter of the kernel's entire 4096-byte stack, on a
|
||||
// path that also runs under procread's 2 KB buffer. So only the current entry
|
||||
// is copied, and the item is re-fetched from the cursor each time round:
|
||||
// schfs_cursor_val re-reads the block, which restores whatever the callback
|
||||
// disturbed.
|
||||
static int
|
||||
iter_item(const struct schfs_key *k, const void *val, unsigned len, void *arg)
|
||||
{
|
||||
struct iter_ctx *ic = (struct iter_ctx *)arg;
|
||||
(void)val;
|
||||
|
||||
// The range scan runs until something stops it, so stopping at the first
|
||||
// key that is no longer this directory's is what bounds the walk.
|
||||
if (k->objectid != ic->dir || k->type != SCHFS_KEY_DIRENT)
|
||||
return 1;
|
||||
if (len > SCHFS_MAX_INLINE)
|
||||
return 1;
|
||||
|
||||
unsigned off = 0;
|
||||
while (off < len) {
|
||||
unsigned now;
|
||||
const char *v = (const char *)schfs_cursor_val(ic->cur, &now);
|
||||
if (!v || now != len) {
|
||||
ic->stop = -2;
|
||||
return 1;
|
||||
}
|
||||
if (len - off < sizeof(struct schfs_dirent_rec)) {
|
||||
ic->stop = -2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
const struct schfs_dirent_rec *d =
|
||||
(const struct schfs_dirent_rec *)(v + off);
|
||||
unsigned namelen = d->namelen;
|
||||
unsigned step = SCHFS_DIRENT_LEN(namelen);
|
||||
// Bounds-check before touching the name: a corrupt namelen would
|
||||
// otherwise read past the end of the block.
|
||||
if (namelen == 0 || namelen > SCHFS_NAME_MAX || off + step > len) {
|
||||
ic->stop = -2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
char name[SCHFS_NAME_MAX];
|
||||
uint64_t oid = d->objectid;
|
||||
uint16_t type = d->type;
|
||||
memmove(name, (const char *)d + sizeof(*d), namelen);
|
||||
|
||||
int r = ic->fn(oid, type, name, namelen, ic->arg);
|
||||
if (r) {
|
||||
ic->stop = r;
|
||||
return 1;
|
||||
}
|
||||
off += step;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
schfs_dir_iterate(struct schfs_vol *v, struct schfs_cursor *c, uint64_t dir,
|
||||
int (*fn)(uint64_t, uint16_t, const char *, unsigned, void *),
|
||||
void *arg)
|
||||
{
|
||||
struct schfs_key start;
|
||||
schfs_mkkey(&start, dir, SCHFS_KEY_DIRENT, 0);
|
||||
|
||||
struct iter_ctx ic;
|
||||
ic.fn = fn; ic.arg = arg; ic.cur = c; ic.dir = dir; ic.stop = 0;
|
||||
|
||||
cur_bind(c, v);
|
||||
int r = schfs_btree_range(c, &start, iter_item, &ic);
|
||||
if (r == -2)
|
||||
return -2;
|
||||
return ic.stop == 1 ? 0 : ic.stop;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* File data */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
// Map a file block to a disk block. Returns 1 if mapped, 0 for a hole, -2 on
|
||||
// corruption. Extents are keyed by the file block they start at, so the record
|
||||
// governing block B is the last one at or before (ino, EXTENT, B).
|
||||
static int
|
||||
extent_map(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino,
|
||||
uint64_t fblk, uint64_t *out)
|
||||
{
|
||||
struct schfs_key k;
|
||||
schfs_mkkey(&k, ino, SCHFS_KEY_EXTENT, fblk);
|
||||
|
||||
cur_bind(c, v);
|
||||
int r = schfs_btree_lookup_le(c, &k);
|
||||
if (r == -2)
|
||||
return -2;
|
||||
if (r != 0)
|
||||
return 0;
|
||||
|
||||
const struct schfs_key *got = schfs_cursor_key(c);
|
||||
if (!got)
|
||||
return 0;
|
||||
// lookup_le can land on some other object's records entirely; a sparse file
|
||||
// whose first extent starts past this block looks the same way.
|
||||
if (got->objectid != ino || got->type != SCHFS_KEY_EXTENT)
|
||||
return 0;
|
||||
|
||||
unsigned len;
|
||||
const struct schfs_extent *e =
|
||||
(const struct schfs_extent *)schfs_cursor_val(c, &len);
|
||||
if (!e || len != sizeof(*e))
|
||||
return -2;
|
||||
|
||||
if (fblk >= got->offset + e->nblocks)
|
||||
return 0; /* hole past the end of this extent */
|
||||
*out = e->disk_block + (fblk - got->offset);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Verify a data block against its checksum record. Tree nodes carry their
|
||||
// checksum in the parent pointer; data blocks have no parent, so the check must
|
||||
// be an explicit lookup. Returns 0 if good, -2 if it fails, and 0 as well when
|
||||
// no record exists — an image may legitimately predate checksums for a block,
|
||||
// and refusing to read data over a *missing* checksum would turn an upgrade
|
||||
// into a total loss.
|
||||
static int
|
||||
csum_check(struct schfs_vol *v, struct schfs_cursor *c, uint64_t dblk,
|
||||
const void *data)
|
||||
{
|
||||
struct schfs_key k;
|
||||
schfs_mkkey(&k, SCHFS_CSUM_OBJECTID, SCHFS_KEY_CSUM, dblk);
|
||||
|
||||
uint32_t want;
|
||||
unsigned len = sizeof(want);
|
||||
cur_bind(c, v);
|
||||
int r = schfs_btree_lookup_cur(c, &k, &want, &len);
|
||||
if (r == -1)
|
||||
return 0;
|
||||
if (r != 0 || len != sizeof(want))
|
||||
return -2;
|
||||
|
||||
if (schfs_crc32c(0, data, SCHFS_BLKSIZE) != want)
|
||||
return -2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
long
|
||||
schfs_file_read(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino,
|
||||
uint64_t off, void *buf, unsigned n, void *dblk)
|
||||
{
|
||||
struct schfs_inode in;
|
||||
int r = schfs_inode_get(v, c, ino, &in);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
if (off >= in.size)
|
||||
return 0;
|
||||
if (off + n > in.size)
|
||||
n = (unsigned)(in.size - off);
|
||||
|
||||
char *dst = (char *)buf;
|
||||
unsigned done = 0;
|
||||
while (done < n) {
|
||||
uint64_t fblk = (off + done) / SCHFS_BLKSIZE;
|
||||
unsigned inblk = (unsigned)((off + done) % SCHFS_BLKSIZE);
|
||||
unsigned want = SCHFS_BLKSIZE - inblk;
|
||||
if (want > n - done)
|
||||
want = n - done;
|
||||
|
||||
uint64_t disk = 0;
|
||||
int m = extent_map(v, c, ino, fblk, &disk);
|
||||
if (m == -2)
|
||||
return -2;
|
||||
|
||||
if (m == 0) {
|
||||
memset(dst + done, 0, want); /* hole reads as zeros */
|
||||
} else {
|
||||
if (v->dev.bread(v->dev.ctx, disk, dblk) != 0)
|
||||
return -2;
|
||||
if (csum_check(v, c, disk, dblk) != 0)
|
||||
return -2;
|
||||
memmove(dst + done, (char *)dblk + inblk, want);
|
||||
}
|
||||
done += want;
|
||||
}
|
||||
return (long)done;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Path resolution */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
uint64_t
|
||||
schfs_path_lookup(struct schfs_vol *v, struct schfs_cursor *c, const char *path)
|
||||
{
|
||||
uint64_t cur = v->root_objectid;
|
||||
|
||||
while (*path == '/')
|
||||
path++;
|
||||
|
||||
while (*path) {
|
||||
const char *s = path;
|
||||
while (*path && *path != '/')
|
||||
path++;
|
||||
unsigned len = (unsigned)(path - s);
|
||||
while (*path == '/')
|
||||
path++;
|
||||
|
||||
if (len == 1 && s[0] == '.')
|
||||
continue;
|
||||
if (len == 2 && s[0] == '.' && s[1] == '.') {
|
||||
// ".." is synthetic: directories store a parent id rather than an
|
||||
// entry, so walking up is an inode read, not a lookup.
|
||||
struct schfs_inode in;
|
||||
if (schfs_inode_get(v, c, cur, &in) != 0)
|
||||
return 0;
|
||||
cur = in.parent ? in.parent : v->root_objectid;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Every component but the last must be a directory. Checking here is
|
||||
// what makes "cat /etc/passwd/x" fail rather than silently succeed.
|
||||
struct schfs_inode in;
|
||||
if (schfs_inode_get(v, c, cur, &in) != 0)
|
||||
return 0;
|
||||
if (in.type != SCHFS_T_DIR)
|
||||
return 0;
|
||||
|
||||
uint64_t next;
|
||||
if (schfs_dir_lookup(v, c, cur, s, len, &next, 0) != 0)
|
||||
return 0;
|
||||
cur = next;
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
932
kernel/fs/schfs/objw.c
Normal file
932
kernel/fs/schfs/objw.c
Normal file
|
|
@ -0,0 +1,932 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// SchFS object layer, write side: create, link, unlink, rename, write,
|
||||
// truncate.
|
||||
//
|
||||
// The same portable C as the read side, compiled both hosted (mkschfs) and
|
||||
// freestanding (the kernel mount), so an image the tools produce and one the
|
||||
// kernel produces come from a single implementation rather than two that agree
|
||||
// by luck. Everything below is expressed as records in the one B-tree; there is
|
||||
// no inode table, no directory block format and no bitmap to keep in step.
|
||||
//
|
||||
// Two rules run through the whole file.
|
||||
//
|
||||
// Nothing here may put a block on the stack. A SchFS block is 4096 bytes and so
|
||||
// is the entire kernel stack, so every function borrows the volume's three
|
||||
// scratch buffers (v->ctx) instead. Where a cursor is needed, ctx.a doubles as
|
||||
// its buffer — which is safe only because no path below holds a cursor across a
|
||||
// tree mutation, and every one of them copies what it needs out first.
|
||||
//
|
||||
// Nothing reachable from a committed root is ever overwritten. For tree blocks
|
||||
// the B-tree enforces that; for *data* blocks it is enforced here, by
|
||||
// schfs_extent.gen. A run allocated during the group being built may be written
|
||||
// in place, because no published root can see it. Anything older is copied to a
|
||||
// fresh block and the extent record split around it.
|
||||
|
||||
#include "schfs_int.h"
|
||||
|
||||
#ifdef SCHFS_HOSTED
|
||||
#include <string.h>
|
||||
#else
|
||||
#include "types.h"
|
||||
#include "defs.h"
|
||||
#endif
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Inodes */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
uint64_t
|
||||
schfs_alloc_objectid(struct schfs_vol *v)
|
||||
{
|
||||
return v->next_objectid++;
|
||||
}
|
||||
|
||||
int
|
||||
schfs_inode_put(struct schfs_vol *v, uint64_t ino, const struct schfs_inode *in)
|
||||
{
|
||||
struct schfs_key k;
|
||||
schfs_mkkey(&k, ino, SCHFS_KEY_INODE, 0);
|
||||
return schfs_btree_insert(&v->tree, &v->ctx, &k, in, sizeof(*in));
|
||||
}
|
||||
|
||||
// Read an inode using the volume's own scratch block. Every mutation below is a
|
||||
// read-modify-write of one of these, and each one that took a cursor buffer
|
||||
// from its caller was one more place to get the aliasing rules wrong.
|
||||
static int
|
||||
inode_load(struct schfs_vol *v, uint64_t ino, struct schfs_inode *out)
|
||||
{
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &v->tree, v->ctx.a);
|
||||
return schfs_inode_get(v, &c, ino, out);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Directory entries */
|
||||
/* */
|
||||
/* One key per name hash, holding a packed list of records, because two */
|
||||
/* names in a directory can hash the same and the second insert would */
|
||||
/* otherwise silently replace the first. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
// Load the packed list for `name`'s hash into v->dirbuf. Returns its length, or
|
||||
// 0 if there is no such key, or a negative error.
|
||||
static int
|
||||
dirlist_load(struct schfs_vol *v, uint64_t dir, const char *name,
|
||||
unsigned namelen, struct schfs_key *k)
|
||||
{
|
||||
schfs_mkkey(k, dir, SCHFS_KEY_DIRENT, schfs_namehash(name, namelen));
|
||||
|
||||
unsigned len = sizeof(v->dirbuf);
|
||||
int r = schfs_btree_lookup(&v->tree, &v->ctx, k, v->dirbuf, &len);
|
||||
if (r == -2)
|
||||
return -2;
|
||||
if (r != 0)
|
||||
return 0;
|
||||
if (len > sizeof(v->dirbuf))
|
||||
return -2; /* stored value larger than can exist */
|
||||
return (int)len;
|
||||
}
|
||||
|
||||
// Offset of `name` within the packed list, or -1 if absent, -2 if malformed.
|
||||
static int
|
||||
dirlist_find(const char *list, unsigned len, const char *name, unsigned namelen)
|
||||
{
|
||||
for (unsigned off = 0; off + sizeof(struct schfs_dirent_rec) <= len; ) {
|
||||
const struct schfs_dirent_rec *d =
|
||||
(const struct schfs_dirent_rec *)(list + off);
|
||||
unsigned step = SCHFS_DIRENT_LEN(d->namelen);
|
||||
if (step == 0 || off + step > len)
|
||||
return -2;
|
||||
if (d->namelen == namelen &&
|
||||
memcmp(list + off + sizeof(*d), name, namelen) == 0)
|
||||
return (int)off;
|
||||
off += step;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int
|
||||
schfs_dir_add(struct schfs_vol *v, uint64_t dir, const char *name,
|
||||
unsigned namelen, uint64_t child, uint16_t type)
|
||||
{
|
||||
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
|
||||
return -1;
|
||||
|
||||
struct schfs_key k;
|
||||
int got = dirlist_load(v, dir, name, namelen, &k);
|
||||
if (got < 0)
|
||||
return got;
|
||||
unsigned len = (unsigned)got;
|
||||
|
||||
// Reject a duplicate outright. Without this, adding a name twice produces a
|
||||
// directory with two entries that lookup can never tell apart.
|
||||
int at = dirlist_find(v->dirbuf, len, name, namelen);
|
||||
if (at == -2)
|
||||
return -2;
|
||||
if (at >= 0)
|
||||
return -1;
|
||||
|
||||
unsigned add = SCHFS_DIRENT_LEN(namelen);
|
||||
if (len + add > sizeof(v->dirbuf))
|
||||
return -4; /* collision list will not fit in one item */
|
||||
|
||||
struct schfs_dirent_rec *d = (struct schfs_dirent_rec *)(v->dirbuf + len);
|
||||
memset(d, 0, add);
|
||||
d->objectid = child;
|
||||
d->type = type;
|
||||
d->namelen = (uint16_t)namelen;
|
||||
memmove(v->dirbuf + len + sizeof(*d), name, namelen);
|
||||
|
||||
return schfs_btree_insert(&v->tree, &v->ctx, &k, v->dirbuf, len + add);
|
||||
}
|
||||
|
||||
int
|
||||
schfs_dir_del(struct schfs_vol *v, uint64_t dir, const char *name,
|
||||
unsigned namelen)
|
||||
{
|
||||
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
|
||||
return -1;
|
||||
|
||||
struct schfs_key k;
|
||||
int got = dirlist_load(v, dir, name, namelen, &k);
|
||||
if (got < 0)
|
||||
return got;
|
||||
unsigned len = (unsigned)got;
|
||||
if (len == 0)
|
||||
return -1;
|
||||
|
||||
int at = dirlist_find(v->dirbuf, len, name, namelen);
|
||||
if (at == -2)
|
||||
return -2;
|
||||
if (at < 0)
|
||||
return -1;
|
||||
|
||||
const struct schfs_dirent_rec *d =
|
||||
(const struct schfs_dirent_rec *)(v->dirbuf + at);
|
||||
unsigned step = SCHFS_DIRENT_LEN(d->namelen);
|
||||
memmove(v->dirbuf + at, v->dirbuf + at + step, len - (unsigned)at - step);
|
||||
len -= step;
|
||||
|
||||
// An emptied list means the key itself goes, not a zero-length value: a key
|
||||
// that exists with nothing in it would make the directory scan stop on a
|
||||
// record readdir cannot describe.
|
||||
if (len == 0)
|
||||
return schfs_btree_delete(&v->tree, &v->ctx, &k);
|
||||
return schfs_btree_insert(&v->tree, &v->ctx, &k, v->dirbuf, len);
|
||||
}
|
||||
|
||||
int
|
||||
schfs_dir_empty(struct schfs_vol *v, uint64_t dir)
|
||||
{
|
||||
struct schfs_key start;
|
||||
schfs_mkkey(&start, dir, SCHFS_KEY_DIRENT, 0);
|
||||
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &v->tree, v->ctx.a);
|
||||
int r = schfs_cursor_seek(&c, &start);
|
||||
if (r == -2)
|
||||
return -2;
|
||||
if (r != 0)
|
||||
return 1; /* nothing at or after: empty */
|
||||
|
||||
const struct schfs_key *got = schfs_cursor_key(&c);
|
||||
if (!got)
|
||||
return 1;
|
||||
return (got->objectid == dir && got->type == SCHFS_KEY_DIRENT) ? 0 : 1;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Data-block checksums */
|
||||
/* */
|
||||
/* Tree nodes carry their checksum in their parent's pointer. Data */
|
||||
/* blocks have no parent, so theirs live under one reserved object id, */
|
||||
/* exactly as btrfs keeps a checksum tree. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static int
|
||||
csum_put(struct schfs_vol *v, uint64_t dblk, const void *data)
|
||||
{
|
||||
struct schfs_key k;
|
||||
schfs_mkkey(&k, SCHFS_CSUM_OBJECTID, SCHFS_KEY_CSUM, dblk);
|
||||
uint32_t c = schfs_crc32c(0, data, SCHFS_BLKSIZE);
|
||||
return schfs_btree_insert(&v->tree, &v->ctx, &k, &c, sizeof(c));
|
||||
}
|
||||
|
||||
static int
|
||||
csum_del(struct schfs_vol *v, uint64_t dblk)
|
||||
{
|
||||
struct schfs_key k;
|
||||
schfs_mkkey(&k, SCHFS_CSUM_OBJECTID, SCHFS_KEY_CSUM, dblk);
|
||||
int r = schfs_btree_delete(&v->tree, &v->ctx, &k);
|
||||
return r == -1 ? 0 : r; /* absent is not a failure */
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Extents */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static int
|
||||
extent_put(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
|
||||
const struct schfs_extent *e)
|
||||
{
|
||||
struct schfs_key k;
|
||||
schfs_mkkey(&k, ino, SCHFS_KEY_EXTENT, fblk);
|
||||
return schfs_btree_insert(&v->tree, &v->ctx, &k, e, sizeof(*e));
|
||||
}
|
||||
|
||||
static int
|
||||
extent_del(struct schfs_vol *v, uint64_t ino, uint64_t fblk)
|
||||
{
|
||||
struct schfs_key k;
|
||||
schfs_mkkey(&k, ino, SCHFS_KEY_EXTENT, fblk);
|
||||
int r = schfs_btree_delete(&v->tree, &v->ctx, &k);
|
||||
return r == -1 ? 0 : r;
|
||||
}
|
||||
|
||||
// The extent covering file block `fblk`, if any. Extents are keyed by the file
|
||||
// block they start at, so the record governing block B is the last one at or
|
||||
// before (ino, EXTENT, B). Returns 1 and fills *start and *e, 0 for a hole, -2
|
||||
// on corruption.
|
||||
static int
|
||||
extent_find(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
|
||||
uint64_t *start, struct schfs_extent *e)
|
||||
{
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &v->tree, v->ctx.a);
|
||||
|
||||
struct schfs_key probe;
|
||||
schfs_mkkey(&probe, ino, SCHFS_KEY_EXTENT, fblk);
|
||||
int r = schfs_btree_lookup_le(&c, &probe);
|
||||
if (r == -2)
|
||||
return -2;
|
||||
if (r != 0)
|
||||
return 0;
|
||||
|
||||
const struct schfs_key *got = schfs_cursor_key(&c);
|
||||
unsigned vlen;
|
||||
const struct schfs_extent *found =
|
||||
(const struct schfs_extent *)schfs_cursor_val(&c, &vlen);
|
||||
if (!got || !found)
|
||||
return 0;
|
||||
// lookup_le happily lands on some other object's records; so does a sparse
|
||||
// file whose first extent starts past this block.
|
||||
if (got->objectid != ino || got->type != SCHFS_KEY_EXTENT)
|
||||
return 0;
|
||||
if (vlen != sizeof(*e))
|
||||
return -2;
|
||||
if (fblk >= got->offset + found->nblocks)
|
||||
return 0;
|
||||
|
||||
*start = got->offset;
|
||||
*e = *found;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The last extent record belonging to `ino`, or 0 if it has none.
|
||||
static int
|
||||
extent_last(struct schfs_vol *v, uint64_t ino, uint64_t *start,
|
||||
struct schfs_extent *e)
|
||||
{
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &v->tree, v->ctx.a);
|
||||
|
||||
struct schfs_key probe;
|
||||
schfs_mkkey(&probe, ino, SCHFS_KEY_EXTENT, ~0ULL);
|
||||
int r = schfs_btree_lookup_le(&c, &probe);
|
||||
if (r == -2)
|
||||
return -2;
|
||||
if (r != 0)
|
||||
return 0;
|
||||
|
||||
const struct schfs_key *got = schfs_cursor_key(&c);
|
||||
unsigned vlen;
|
||||
const struct schfs_extent *found =
|
||||
(const struct schfs_extent *)schfs_cursor_val(&c, &vlen);
|
||||
if (!got || !found)
|
||||
return 0;
|
||||
if (got->objectid != ino || got->type != SCHFS_KEY_EXTENT)
|
||||
return 0;
|
||||
if (vlen != sizeof(*e))
|
||||
return -2;
|
||||
|
||||
*start = got->offset;
|
||||
*e = *found;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Record a newly allocated single block, merging it into the preceding extent
|
||||
// when the two are adjacent both in the file and on the disk.
|
||||
//
|
||||
// This is what keeps extents worth having. Sequential writes get consecutive
|
||||
// blocks from the allocator, so without merging a one-megabyte file carries 256
|
||||
// extent records instead of one, and the tree ends up mostly describing where
|
||||
// the data is rather than holding it.
|
||||
static int
|
||||
extent_grow(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
|
||||
const struct schfs_extent *e)
|
||||
{
|
||||
if (fblk > 0) {
|
||||
uint64_t pstart;
|
||||
struct schfs_extent p;
|
||||
int m = extent_find(v, ino, fblk - 1, &pstart, &p);
|
||||
if (m == -2)
|
||||
return -2;
|
||||
if (m == 1 &&
|
||||
pstart + p.nblocks == fblk &&
|
||||
p.disk_block + p.nblocks == e->disk_block &&
|
||||
p.refs == e->refs && p.gen == e->gen) {
|
||||
p.nblocks++;
|
||||
return extent_put(v, ino, pstart, &p);
|
||||
}
|
||||
}
|
||||
return extent_put(v, ino, fblk, e);
|
||||
}
|
||||
|
||||
// Release a run of data blocks and the checksum records that describe them.
|
||||
// Shared runs are left alone: another file still points at them, and proper
|
||||
// refcounting needs an extent tree that nothing creates yet.
|
||||
static int
|
||||
extent_release(struct schfs_vol *v, const struct schfs_extent *e,
|
||||
uint64_t first, uint64_t n)
|
||||
{
|
||||
if (e->refs != 1)
|
||||
return 0;
|
||||
for (uint64_t i = 0; i < n; i++) {
|
||||
int r = csum_del(v, first + i);
|
||||
if (r != 0)
|
||||
return r;
|
||||
v->dev.bfree(v->dev.ctx, first + i);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Drop everything from file block `from` onward. Used by truncate and by the
|
||||
// deletion of an object.
|
||||
static int
|
||||
extents_drop_from(struct schfs_vol *v, uint64_t ino, struct schfs_inode *in,
|
||||
uint64_t from)
|
||||
{
|
||||
for (;;) {
|
||||
uint64_t start;
|
||||
struct schfs_extent e;
|
||||
int m = extent_last(v, ino, &start, &e);
|
||||
if (m == -2)
|
||||
return -2;
|
||||
if (m == 0)
|
||||
return 0; /* no extents left */
|
||||
if (start + e.nblocks <= from)
|
||||
return 0; /* nothing beyond the cut */
|
||||
|
||||
if (start >= from) {
|
||||
int r = extent_release(v, &e, e.disk_block, e.nblocks);
|
||||
if (r != 0)
|
||||
return r;
|
||||
r = extent_del(v, ino, start);
|
||||
if (r != 0)
|
||||
return r;
|
||||
if (in->blocks >= e.nblocks)
|
||||
in->blocks -= e.nblocks;
|
||||
continue; /* there may be earlier ones too */
|
||||
}
|
||||
|
||||
// The cut falls inside this run: keep its head, release its tail.
|
||||
uint64_t keep = from - start;
|
||||
uint64_t tail = e.nblocks - keep;
|
||||
int r = extent_release(v, &e, e.disk_block + keep, tail);
|
||||
if (r != 0)
|
||||
return r;
|
||||
e.nblocks = keep;
|
||||
r = extent_put(v, ino, start, &e);
|
||||
if (r != 0)
|
||||
return r;
|
||||
if (in->blocks >= tail)
|
||||
in->blocks -= tail;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* File data */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
int
|
||||
schfs_file_reserve(struct schfs_vol *v, uint64_t ino, uint64_t nblocks)
|
||||
{
|
||||
if (nblocks == 0)
|
||||
return 0;
|
||||
|
||||
// Take the whole run before touching the tree.
|
||||
//
|
||||
// This is what makes extents worth having when the final size is already
|
||||
// known. Allocating a data block, then inserting its extent record -- which
|
||||
// itself allocates blocks for the copy-on-write path -- then allocating the
|
||||
// next data block interleaves metadata with data, so consecutive file
|
||||
// blocks are never physically adjacent. Grabbing the run up front keeps it
|
||||
// contiguous and a whole file becomes one record.
|
||||
uint64_t first;
|
||||
if (v->dev.balloc_run) {
|
||||
first = v->dev.balloc_run(v->dev.ctx, nblocks);
|
||||
if (first == 0)
|
||||
return -3;
|
||||
} else {
|
||||
first = v->dev.balloc(v->dev.ctx);
|
||||
if (first == 0)
|
||||
return -3;
|
||||
uint64_t prev = first;
|
||||
for (uint64_t i = 1; i < nblocks; i++) {
|
||||
uint64_t b = v->dev.balloc(v->dev.ctx);
|
||||
if (b == 0)
|
||||
return -3;
|
||||
if (b != prev + 1)
|
||||
return -2; /* allocator is not handing out a run */
|
||||
prev = b;
|
||||
}
|
||||
}
|
||||
|
||||
struct schfs_extent e;
|
||||
memset(&e, 0, sizeof(e));
|
||||
e.disk_block = first;
|
||||
e.nblocks = nblocks;
|
||||
e.refs = 1;
|
||||
e.gen = v->tree.generation;
|
||||
|
||||
// Deliberately does not touch the inode. Re-reading and rewriting it here
|
||||
// costs two extra tree inserts per file, and every insert copies its whole
|
||||
// path. The caller knows the block count before it writes the inode the
|
||||
// first time, so it sets in.blocks there instead.
|
||||
return extent_put(v, ino, 0, &e);
|
||||
}
|
||||
|
||||
// Where the next write to file block `fblk` should land.
|
||||
struct wblk {
|
||||
uint64_t disk; // block to write
|
||||
uint64_t src; // block holding the current contents, 0 for a hole
|
||||
int fresh; // a block that did not exist before
|
||||
};
|
||||
|
||||
static int
|
||||
block_for_write(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
|
||||
struct wblk *w)
|
||||
{
|
||||
uint64_t start;
|
||||
struct schfs_extent e;
|
||||
int m = extent_find(v, ino, fblk, &start, &e);
|
||||
if (m == -2)
|
||||
return -2;
|
||||
|
||||
if (m == 0) {
|
||||
uint64_t d = v->dev.balloc(v->dev.ctx);
|
||||
if (d == 0)
|
||||
return -3;
|
||||
|
||||
struct schfs_extent ne;
|
||||
memset(&ne, 0, sizeof(ne));
|
||||
ne.disk_block = d;
|
||||
ne.nblocks = 1;
|
||||
ne.refs = 1;
|
||||
ne.gen = v->tree.generation;
|
||||
|
||||
int r = extent_grow(v, ino, fblk, &ne);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
w->disk = d;
|
||||
w->src = 0;
|
||||
w->fresh = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint64_t old = e.disk_block + (fblk - start);
|
||||
|
||||
// Allocated by the group being built, and owned outright: no published root
|
||||
// can reach it, so it may simply be overwritten.
|
||||
if (e.gen == v->tree.generation && e.refs == 1) {
|
||||
w->disk = old;
|
||||
w->src = old;
|
||||
w->fresh = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Otherwise copy this one block out of the run, and split the record around
|
||||
// it: a head covering what came before, the new block, and a tail covering
|
||||
// what came after. No deletion is needed — when the block being replaced is
|
||||
// the run's first, the middle record simply overwrites the original key.
|
||||
uint64_t nd = v->dev.balloc(v->dev.ctx);
|
||||
if (nd == 0)
|
||||
return -3;
|
||||
|
||||
if (fblk > start) {
|
||||
struct schfs_extent head = e;
|
||||
head.nblocks = fblk - start;
|
||||
int r = extent_put(v, ino, start, &head);
|
||||
if (r != 0)
|
||||
return r;
|
||||
}
|
||||
{
|
||||
struct schfs_extent mid;
|
||||
memset(&mid, 0, sizeof(mid));
|
||||
mid.disk_block = nd;
|
||||
mid.nblocks = 1;
|
||||
mid.refs = 1;
|
||||
mid.gen = v->tree.generation;
|
||||
int r = extent_put(v, ino, fblk, &mid);
|
||||
if (r != 0)
|
||||
return r;
|
||||
}
|
||||
if (fblk + 1 < start + e.nblocks) {
|
||||
struct schfs_extent tail = e;
|
||||
tail.disk_block = e.disk_block + (fblk + 1 - start);
|
||||
tail.nblocks = start + e.nblocks - (fblk + 1);
|
||||
int r = extent_put(v, ino, fblk + 1, &tail);
|
||||
if (r != 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
// The block just replaced rejoins the free map at the next commit, not now:
|
||||
// until then the committed tree still points at it and a crash lands there.
|
||||
int r = extent_release(v, &e, old, 1);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
w->disk = nd;
|
||||
w->src = old;
|
||||
w->fresh = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
long
|
||||
schfs_file_write(struct schfs_vol *v, uint64_t ino, uint64_t off,
|
||||
const void *buf, unsigned n, void *dblk)
|
||||
{
|
||||
struct schfs_inode in;
|
||||
int r = inode_load(v, ino, &in);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
const char *src = (const char *)buf;
|
||||
unsigned done = 0;
|
||||
|
||||
while (done < n) {
|
||||
uint64_t at = off + done;
|
||||
uint64_t fblk = at / SCHFS_BLKSIZE;
|
||||
unsigned inblk = (unsigned)(at % SCHFS_BLKSIZE);
|
||||
unsigned want = SCHFS_BLKSIZE - inblk;
|
||||
if (want > n - done)
|
||||
want = n - done;
|
||||
|
||||
struct wblk w;
|
||||
r = block_for_write(v, ino, fblk, &w);
|
||||
if (r != 0)
|
||||
return r;
|
||||
if (w.fresh)
|
||||
in.blocks++;
|
||||
|
||||
// A whole-block write needs no read: every byte is about to be
|
||||
// replaced. A partial one has to start from whatever is there, or from
|
||||
// zeros where the file had a hole.
|
||||
if (inblk != 0 || want != SCHFS_BLKSIZE) {
|
||||
if (w.src) {
|
||||
if (v->dev.bread(v->dev.ctx, w.src, dblk) != 0)
|
||||
return -2;
|
||||
} else {
|
||||
memset(dblk, 0, SCHFS_BLKSIZE);
|
||||
}
|
||||
}
|
||||
memmove((char *)dblk + inblk, src + done, want);
|
||||
|
||||
if (v->dev.bwrite(v->dev.ctx, w.disk, dblk) != 0)
|
||||
return -2;
|
||||
// The checksum is rewritten every time the block changes, not just when
|
||||
// it is allocated, or a topped-up tail block fails its own check on the
|
||||
// very next read.
|
||||
r = csum_put(v, w.disk, dblk);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
done += want;
|
||||
}
|
||||
|
||||
if (off + n > in.size)
|
||||
in.size = off + n;
|
||||
in.gen = v->tree.generation;
|
||||
r = schfs_inode_put(v, ino, &in);
|
||||
if (r != 0)
|
||||
return r;
|
||||
return (long)done;
|
||||
}
|
||||
|
||||
int
|
||||
schfs_file_append(struct schfs_vol *v, uint64_t ino, const void *buf,
|
||||
unsigned n, void *dblk)
|
||||
{
|
||||
struct schfs_inode in;
|
||||
int r = inode_load(v, ino, &in);
|
||||
if (r != 0)
|
||||
return r == -1 ? -2 : r;
|
||||
|
||||
long got = schfs_file_write(v, ino, in.size, buf, n, dblk);
|
||||
if (got < 0)
|
||||
return (int)got;
|
||||
return got == (long)n ? 0 : -2;
|
||||
}
|
||||
|
||||
int
|
||||
schfs_file_truncate(struct schfs_vol *v, uint64_t ino, uint64_t size,
|
||||
void *dblk)
|
||||
{
|
||||
struct schfs_inode in;
|
||||
int r = inode_load(v, ino, &in);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
if (size < in.size) {
|
||||
uint64_t keep = (size + SCHFS_BLKSIZE - 1) / SCHFS_BLKSIZE;
|
||||
r = extents_drop_from(v, ino, &in, keep);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
// Whole blocks past the new end are gone; the block the new size falls
|
||||
// *inside* has to have its tail zeroed. It is tempting not to bother,
|
||||
// since a read stops at in.size and can never see those bytes — but
|
||||
// truncating down and then growing again is required to read back
|
||||
// zeros, and without this it reads back whatever the file used to hold.
|
||||
unsigned tail = (unsigned)(size % SCHFS_BLKSIZE);
|
||||
if (tail != 0) {
|
||||
uint64_t fblk = size / SCHFS_BLKSIZE;
|
||||
uint64_t start;
|
||||
struct schfs_extent e;
|
||||
int m = extent_find(v, ino, fblk, &start, &e);
|
||||
if (m == -2)
|
||||
return -2;
|
||||
if (m == 1) { /* a hole is already zeros */
|
||||
struct wblk w;
|
||||
r = block_for_write(v, ino, fblk, &w);
|
||||
if (r != 0)
|
||||
return r;
|
||||
if (w.src) {
|
||||
if (v->dev.bread(v->dev.ctx, w.src, dblk) != 0)
|
||||
return -2;
|
||||
} else {
|
||||
memset(dblk, 0, SCHFS_BLKSIZE);
|
||||
}
|
||||
memset((char *)dblk + tail, 0, SCHFS_BLKSIZE - tail);
|
||||
if (v->dev.bwrite(v->dev.ctx, w.disk, dblk) != 0)
|
||||
return -2;
|
||||
r = csum_put(v, w.disk, dblk);
|
||||
if (r != 0)
|
||||
return r;
|
||||
if (w.fresh)
|
||||
in.blocks++;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Growing needs nothing at all: the range with no extent records reads back
|
||||
// as zeros, which is a sparse file for free.
|
||||
|
||||
in.size = size;
|
||||
in.gen = v->tree.generation;
|
||||
return schfs_inode_put(v, ino, &in);
|
||||
}
|
||||
|
||||
// Release an object entirely: its data, its checksums and its inode record.
|
||||
int
|
||||
schfs_object_free(struct schfs_vol *v, uint64_t ino)
|
||||
{
|
||||
struct schfs_inode in;
|
||||
int r = inode_load(v, ino, &in);
|
||||
if (r == -1)
|
||||
return 0; /* already gone */
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
r = extents_drop_from(v, ino, &in, 0);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
struct schfs_key k;
|
||||
schfs_mkkey(&k, ino, SCHFS_KEY_INODE, 0);
|
||||
r = schfs_btree_delete(&v->tree, &v->ctx, &k);
|
||||
return r == -1 ? 0 : r;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Namespace */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
// A directory's link count is 2 (itself and its own ".") plus one per
|
||||
// subdirectory, because each subdirectory's ".." points back at it. Neither
|
||||
// entry is stored, but the count still has to match what POSIX tools expect or
|
||||
// "find -type d" prunes real directories and du walks the tree wrongly.
|
||||
static int
|
||||
parent_nlink_adjust(struct schfs_vol *v, uint64_t dir, int delta)
|
||||
{
|
||||
struct schfs_inode in;
|
||||
int r = inode_load(v, dir, &in);
|
||||
if (r != 0)
|
||||
return r;
|
||||
if (delta < 0 && in.nlink < (uint32_t)(-delta))
|
||||
return -2;
|
||||
in.nlink = (uint32_t)((int64_t)in.nlink + delta);
|
||||
return schfs_inode_put(v, dir, &in);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
|
||||
return -1;
|
||||
|
||||
struct schfs_inode parent;
|
||||
int r = inode_load(v, dir, &parent);
|
||||
if (r != 0)
|
||||
return r;
|
||||
if (parent.type != SCHFS_T_DIR)
|
||||
return -1;
|
||||
|
||||
uint64_t ino = schfs_alloc_objectid(v);
|
||||
|
||||
struct schfs_inode in;
|
||||
memset(&in, 0, sizeof(in));
|
||||
in.type = type;
|
||||
in.parent = dir;
|
||||
in.mode = mode;
|
||||
in.nlink = type == SCHFS_T_DIR ? 2 : 1;
|
||||
in.rdev_major = rdev_major;
|
||||
in.rdev_minor = rdev_minor;
|
||||
in.atime = in.mtime = in.ctime = now;
|
||||
in.gen = v->tree.generation;
|
||||
|
||||
r = schfs_inode_put(v, ino, &in);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
// The entry goes in last, so a failure leaves an unreferenced object rather
|
||||
// than a name pointing at nothing. Neither survives a crash — none of this
|
||||
// is visible until the transaction commits — but an unreachable inode is
|
||||
// the harmless one of the two if a later step in the same group fails.
|
||||
r = schfs_dir_add(v, dir, name, namelen, ino, type);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
if (type == SCHFS_T_DIR) {
|
||||
r = parent_nlink_adjust(v, dir, +1);
|
||||
if (r != 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
if (out_ino)
|
||||
*out_ino = ino;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
schfs_link(struct schfs_vol *v, uint64_t dir, const char *name,
|
||||
unsigned namelen, uint64_t ino)
|
||||
{
|
||||
struct schfs_inode in;
|
||||
int r = inode_load(v, ino, &in);
|
||||
if (r != 0)
|
||||
return r;
|
||||
// Directories are not hard-linkable. Allowing it makes cycles possible, and
|
||||
// a cycle turns every recursive walk into an infinite one.
|
||||
if (in.type == SCHFS_T_DIR)
|
||||
return -1;
|
||||
|
||||
r = schfs_dir_add(v, dir, name, namelen, ino, in.type);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
in.nlink++;
|
||||
in.ctime = in.mtime;
|
||||
return schfs_inode_put(v, ino, &in);
|
||||
}
|
||||
|
||||
int
|
||||
schfs_unlink(struct schfs_vol *v, uint64_t dir, const char *name,
|
||||
unsigned namelen)
|
||||
{
|
||||
uint64_t ino;
|
||||
uint16_t type;
|
||||
|
||||
{
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &v->tree, v->ctx.a);
|
||||
int r = schfs_dir_lookup(v, &c, dir, name, namelen, &ino, &type);
|
||||
if (r != 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
struct schfs_inode in;
|
||||
int r = inode_load(v, ino, &in);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
if (in.type == SCHFS_T_DIR) {
|
||||
int e = schfs_dir_empty(v, ino);
|
||||
if (e == -2)
|
||||
return -2;
|
||||
if (e == 0)
|
||||
return -5; /* directory not empty */
|
||||
}
|
||||
|
||||
r = schfs_dir_del(v, dir, name, namelen);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
if (in.type == SCHFS_T_DIR) {
|
||||
r = parent_nlink_adjust(v, dir, -1);
|
||||
if (r != 0)
|
||||
return r;
|
||||
return schfs_object_free(v, ino);
|
||||
}
|
||||
|
||||
if (in.nlink > 1) {
|
||||
in.nlink--;
|
||||
return schfs_inode_put(v, ino, &in);
|
||||
}
|
||||
return schfs_object_free(v, ino);
|
||||
}
|
||||
|
||||
int
|
||||
schfs_rename(struct schfs_vol *v, uint64_t olddir, const char *oldname,
|
||||
unsigned oldlen, uint64_t newdir, const char *newname,
|
||||
unsigned newlen)
|
||||
{
|
||||
uint64_t ino;
|
||||
uint16_t type;
|
||||
|
||||
{
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &v->tree, v->ctx.a);
|
||||
int r = schfs_dir_lookup(v, &c, olddir, oldname, oldlen, &ino, &type);
|
||||
if (r != 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
// Renaming a directory into its own subtree would detach that subtree from
|
||||
// the root and leave it reachable only from itself. Walking up from the new
|
||||
// parent is the cheap way to catch it, since every directory records its
|
||||
// own parent.
|
||||
if (type == SCHFS_T_DIR && newdir != olddir) {
|
||||
uint64_t up = newdir;
|
||||
for (int i = 0; i <= SCHFS_MAX_LEVEL * 8 && up != 0; i++) {
|
||||
if (up == ino)
|
||||
return -1;
|
||||
struct schfs_inode t;
|
||||
if (inode_load(v, up, &t) != 0)
|
||||
break;
|
||||
if (up == v->root_objectid)
|
||||
break;
|
||||
up = t.parent;
|
||||
}
|
||||
}
|
||||
|
||||
// A target that already exists is replaced, which means removing it first —
|
||||
// and refusing when that would silently discard a non-empty directory.
|
||||
{
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &v->tree, v->ctx.a);
|
||||
uint64_t victim;
|
||||
if (schfs_dir_lookup(v, &c, newdir, newname, newlen, &victim, 0) == 0) {
|
||||
if (victim == ino)
|
||||
return 0; /* renaming a name onto itself */
|
||||
int r = schfs_unlink(v, newdir, newname, newlen);
|
||||
if (r != 0)
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
int r = schfs_dir_add(v, newdir, newname, newlen, ino, type);
|
||||
if (r != 0)
|
||||
return r;
|
||||
r = schfs_dir_del(v, olddir, oldname, oldlen);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
if (newdir != olddir) {
|
||||
if (type == SCHFS_T_DIR) {
|
||||
// ".." is synthesised from the inode's parent field, so moving a
|
||||
// directory means rewriting that field and moving the link its ".."
|
||||
// contributes to each parent's count.
|
||||
struct schfs_inode in;
|
||||
r = inode_load(v, ino, &in);
|
||||
if (r != 0)
|
||||
return r;
|
||||
in.parent = newdir;
|
||||
r = schfs_inode_put(v, ino, &in);
|
||||
if (r != 0)
|
||||
return r;
|
||||
r = parent_nlink_adjust(v, olddir, -1);
|
||||
if (r != 0)
|
||||
return r;
|
||||
r = parent_nlink_adjust(v, newdir, +1);
|
||||
if (r != 0)
|
||||
return r;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
277
kernel/fs/schfs/schfs_int.h
Normal file
277
kernel/fs/schfs/schfs_int.h
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
// 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 */
|
||||
314
kernel/fs/schfs/space.c
Normal file
314
kernel/fs/schfs/space.c
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// SchFS space manager: which blocks are in use.
|
||||
//
|
||||
// There is no free-space map on disk, and that is the interesting decision
|
||||
// here. The committed tree already records exactly which blocks are live —
|
||||
// every tree node is named by its parent's pointer, every data block by an
|
||||
// extent record — so the map is *derived* at mount by walking the tree once,
|
||||
// held in RAM, and never written back.
|
||||
//
|
||||
// The alternative, persisting it, buys a fast mount and costs the one problem
|
||||
// copy-on-write is otherwise free of: the map has to be updated atomically with
|
||||
// the tree it describes, and writing the map needs blocks, which changes the
|
||||
// map. ZFS spends real complexity on that recursion and btrfs resolves it with
|
||||
// a fixpoint loop. Deriving the map instead cannot disagree with the filesystem
|
||||
// no matter what a crash interrupts, because it is computed from it — there is
|
||||
// no second source of truth to get out of step.
|
||||
//
|
||||
// What it costs is a metadata scan at mount, proportional to the number of tree
|
||||
// blocks rather than to the size of the volume. Nothing is written, so it is
|
||||
// also safe on a read-only mount. If mount time ever becomes the thing that
|
||||
// hurts, the map can be cached on disk later purely as a hint, validated
|
||||
// against the uberblock's generation and discarded when it does not match — an
|
||||
// addition, not a format change.
|
||||
//
|
||||
// ---------------------------------------------------------------------------
|
||||
// Deferred frees
|
||||
//
|
||||
// A block released during the transaction group being built is still reachable
|
||||
// from the last *committed* root, which is where a crash lands. Handing it
|
||||
// straight back would let the filesystem overwrite the very copy it is supposed
|
||||
// to fall back to. So frees go into a second bitmap and only rejoin the free
|
||||
// map when the commit that supersedes them lands. One transaction of delay, no
|
||||
// more: once group N is published, group N-1's tree is no longer anybody's
|
||||
// fallback.
|
||||
|
||||
#include "schfs_int.h"
|
||||
|
||||
#ifdef SCHFS_HOSTED
|
||||
#include <string.h>
|
||||
#else
|
||||
#include "types.h"
|
||||
#include "defs.h"
|
||||
#endif
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Bit vectors, paged */
|
||||
/* */
|
||||
/* The bitmap has to be one flat array to be indexable, and the kernel */
|
||||
/* hands out memory a page at a time with no promise that two pages */
|
||||
/* are adjacent. So it is a vector of page pointers instead: one page */
|
||||
/* covers SCHFS_SPACE_BITS_PER_PAGE blocks, and the page vector itself */
|
||||
/* is a fixed-size array, which is what puts a hard ceiling on volume */
|
||||
/* size. See docs/architecture/schfs.md for the resulting limits. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static int
|
||||
bit_test(uint64_t *const *pages, uint64_t b)
|
||||
{
|
||||
uint64_t pg = b / SCHFS_SPACE_BITS_PER_PAGE;
|
||||
uint64_t off = b % SCHFS_SPACE_BITS_PER_PAGE;
|
||||
return (int)((pages[pg][off / 64] >> (off % 64)) & 1);
|
||||
}
|
||||
|
||||
static void
|
||||
bit_set(uint64_t *const *pages, uint64_t b)
|
||||
{
|
||||
uint64_t pg = b / SCHFS_SPACE_BITS_PER_PAGE;
|
||||
uint64_t off = b % SCHFS_SPACE_BITS_PER_PAGE;
|
||||
pages[pg][off / 64] |= 1ULL << (off % 64);
|
||||
}
|
||||
|
||||
// The whole 64-block word containing b, so a scan can skip a fully used region
|
||||
// sixty-four blocks at a time instead of one. On a nearly full volume that is
|
||||
// the difference between an allocation costing microseconds and milliseconds.
|
||||
static uint64_t
|
||||
bit_word(uint64_t *const *pages, uint64_t b)
|
||||
{
|
||||
uint64_t pg = b / SCHFS_SPACE_BITS_PER_PAGE;
|
||||
uint64_t off = b % SCHFS_SPACE_BITS_PER_PAGE;
|
||||
return pages[pg][off / 64];
|
||||
}
|
||||
|
||||
static int
|
||||
popcount64(uint64_t v)
|
||||
{
|
||||
int n = 0;
|
||||
while (v) { v &= v - 1; n++; }
|
||||
return n;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Setup */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
int
|
||||
schfs_space_init(struct schfs_space *s, uint64_t nblocks,
|
||||
void *(*palloc)(void))
|
||||
{
|
||||
memset(s, 0, sizeof(*s));
|
||||
|
||||
if (nblocks <= SCHFS_FIRST_BLOCK)
|
||||
return -3;
|
||||
if (nblocks > SCHFS_MAX_BLOCKS)
|
||||
return -4; /* larger than this build can map */
|
||||
|
||||
uint64_t npages = (nblocks + SCHFS_SPACE_BITS_PER_PAGE - 1)
|
||||
/ SCHFS_SPACE_BITS_PER_PAGE;
|
||||
|
||||
for (uint64_t i = 0; i < npages; i++) {
|
||||
void *p = palloc();
|
||||
if (p == 0)
|
||||
return -5; /* caller frees what was taken */
|
||||
memset(p, 0, SCHFS_BLKSIZE);
|
||||
s->inuse[i] = (uint64_t *)p;
|
||||
|
||||
p = palloc();
|
||||
if (p == 0)
|
||||
return -5;
|
||||
memset(p, 0, SCHFS_BLKSIZE);
|
||||
s->pend[i] = (uint64_t *)p;
|
||||
s->npages = (int)(i + 1);
|
||||
}
|
||||
|
||||
s->nblocks = nblocks;
|
||||
s->free_blocks = nblocks - SCHFS_FIRST_BLOCK;
|
||||
s->hint = SCHFS_FIRST_BLOCK;
|
||||
|
||||
// The boot block and the uberblock ring are never allocatable, and neither
|
||||
// is anything past the end of the volume: the last bitmap page covers more
|
||||
// blocks than exist, and a stray allocation out there would be written to a
|
||||
// sector the device does not have.
|
||||
for (uint64_t b = 0; b < SCHFS_FIRST_BLOCK; b++)
|
||||
bit_set(s->inuse, b);
|
||||
for (uint64_t b = nblocks; b < (uint64_t)npages * SCHFS_SPACE_BITS_PER_PAGE; b++)
|
||||
bit_set(s->inuse, b);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
schfs_space_mark(struct schfs_space *s, uint64_t blk, uint64_t n)
|
||||
{
|
||||
for (uint64_t i = 0; i < n; i++) {
|
||||
uint64_t b = blk + i;
|
||||
if (b < SCHFS_FIRST_BLOCK || b >= s->nblocks)
|
||||
continue;
|
||||
if (bit_test(s->inuse, b))
|
||||
continue; /* already counted; extents may be shared */
|
||||
bit_set(s->inuse, b);
|
||||
s->free_blocks--;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Allocation */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
uint64_t
|
||||
schfs_space_alloc_run(struct schfs_space *s, uint64_t n)
|
||||
{
|
||||
if (n == 0 || n > s->free_blocks)
|
||||
return 0;
|
||||
|
||||
// Two sweeps: from the rotating hint to the end of the volume, then from
|
||||
// the start back to it. Starting every search at block 0 would make the
|
||||
// front of the volume the only place anything is ever written and turn
|
||||
// allocation into a linear scan of everything already used.
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
uint64_t lo = pass == 0 ? s->hint : SCHFS_FIRST_BLOCK;
|
||||
uint64_t hi = pass == 0 ? s->nblocks : s->hint;
|
||||
uint64_t run = 0, first = 0;
|
||||
|
||||
for (uint64_t b = lo; b < hi; ) {
|
||||
if (b % 64 == 0 && bit_word(s->inuse, b) == ~0ULL) {
|
||||
run = 0;
|
||||
b += 64;
|
||||
continue;
|
||||
}
|
||||
if (bit_test(s->inuse, b)) {
|
||||
run = 0;
|
||||
b++;
|
||||
continue;
|
||||
}
|
||||
if (run == 0)
|
||||
first = b;
|
||||
run++;
|
||||
b++;
|
||||
if (run == n) {
|
||||
for (uint64_t i = 0; i < n; i++)
|
||||
bit_set(s->inuse, first + i);
|
||||
s->free_blocks -= n;
|
||||
s->hint = first + n < s->nblocks ? first + n : SCHFS_FIRST_BLOCK;
|
||||
return first;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint64_t
|
||||
schfs_space_alloc(struct schfs_space *s)
|
||||
{
|
||||
return schfs_space_alloc_run(s, 1);
|
||||
}
|
||||
|
||||
void
|
||||
schfs_space_free(struct schfs_space *s, uint64_t blk)
|
||||
{
|
||||
if (blk < SCHFS_FIRST_BLOCK || blk >= s->nblocks)
|
||||
return;
|
||||
if (!bit_test(s->inuse, blk))
|
||||
return; /* never allocated, or freed twice */
|
||||
if (bit_test(s->pend, blk))
|
||||
return; /* already queued this group */
|
||||
bit_set(s->pend, blk);
|
||||
s->pending++;
|
||||
}
|
||||
|
||||
void
|
||||
schfs_space_commit(struct schfs_space *s)
|
||||
{
|
||||
uint64_t words = SCHFS_SPACE_BITS_PER_PAGE / 64;
|
||||
for (int p = 0; p < s->npages; p++) {
|
||||
uint64_t *pend = s->pend[p];
|
||||
uint64_t *inuse = s->inuse[p];
|
||||
for (uint64_t w = 0; w < words; w++) {
|
||||
if (pend[w] == 0)
|
||||
continue;
|
||||
s->free_blocks += popcount64(pend[w] & inuse[w]);
|
||||
inuse[w] &= ~pend[w];
|
||||
pend[w] = 0;
|
||||
}
|
||||
}
|
||||
s->pending = 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Deriving the map from the committed tree */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
// Every extent record in a leaf names a run of data blocks. A range shared by
|
||||
// several files (a reflink, a snapshot) appears more than once, which is why
|
||||
// schfs_space_mark is idempotent rather than counting.
|
||||
static void
|
||||
mark_leaf_extents(struct schfs_space *s, const void *blk)
|
||||
{
|
||||
const struct schfs_item *it = SCHFS_ITEMS(blk);
|
||||
int n = SCHFS_HDR(blk)->nritems;
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (it[i].key.type != SCHFS_KEY_EXTENT)
|
||||
continue;
|
||||
if (it[i].size != sizeof(struct schfs_extent))
|
||||
continue;
|
||||
if ((int)it[i].offset + (int)it[i].size > SCHFS_BLKSIZE)
|
||||
continue;
|
||||
const struct schfs_extent *e =
|
||||
(const struct schfs_extent *)((const char *)blk + it[i].offset);
|
||||
schfs_space_mark(s, e->disk_block, e->nblocks);
|
||||
}
|
||||
}
|
||||
|
||||
// Walk the whole tree, marking every block it reaches.
|
||||
//
|
||||
// Iterative, with one block buffer and a path of block numbers, for the same
|
||||
// reason everything else here is: a recursive walk keeps a block per level and
|
||||
// the kernel stack is one block in total. Climbing re-reads the parent, so an
|
||||
// internal node is read once per child — mount-time cost that the buffer cache
|
||||
// largely absorbs, in exchange for bounded memory.
|
||||
int
|
||||
schfs_space_scan(struct schfs_vol *v)
|
||||
{
|
||||
struct schfs_space *s = &v->space;
|
||||
void *blk = v->ctx.a;
|
||||
|
||||
struct {
|
||||
uint64_t nr;
|
||||
uint32_t crc;
|
||||
int idx; /* next child to descend into */
|
||||
} path[SCHFS_MAX_LEVEL + 1];
|
||||
|
||||
int d = 0;
|
||||
path[0].nr = v->tree.root;
|
||||
path[0].crc = v->tree.root_crc;
|
||||
path[0].idx = 0;
|
||||
schfs_space_mark(s, v->tree.root, 1);
|
||||
|
||||
for (;;) {
|
||||
if (schfs_node_read(&v->dev, path[d].nr, path[d].crc, blk) != 0)
|
||||
return -2;
|
||||
|
||||
if (SCHFS_HDR(blk)->level == 0) {
|
||||
mark_leaf_extents(s, blk);
|
||||
d--;
|
||||
} else if (path[d].idx < SCHFS_HDR(blk)->nritems) {
|
||||
struct schfs_kptr *kp = &SCHFS_KPTRS(blk)[path[d].idx];
|
||||
path[d].idx++;
|
||||
if (d + 1 > SCHFS_MAX_LEVEL)
|
||||
return -2;
|
||||
d++;
|
||||
path[d].nr = kp->blocknr;
|
||||
path[d].crc = kp->crc;
|
||||
path[d].idx = 0;
|
||||
schfs_space_mark(s, path[d].nr, 1);
|
||||
continue;
|
||||
} else {
|
||||
d--; /* every child visited */
|
||||
}
|
||||
|
||||
if (d < 0)
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
414
kernel/fs/schfs/vfs.c
Normal file
414
kernel/fs/schfs/vfs.c
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// SchFS behind the VFS: the fs_ops table that makes SchFS files reachable
|
||||
// through open(2), read(2), write(2) and the rest.
|
||||
//
|
||||
// Everything below is translation, not filesystem logic. The filesystem lives
|
||||
// in btree.c/space.c/vol.c/obj.c/objw.c and is driven through the schfs_k*
|
||||
// entry points in kmount.c, each of which is one transaction. This file's whole
|
||||
// job is turning a `struct inode` into an object id and back.
|
||||
//
|
||||
// ---------------------------------------------------------------------------
|
||||
// The two models, and where they disagree
|
||||
//
|
||||
// The VFS inherited xv6's shape: allocate an inode, fill it in, then link a
|
||||
// name to it. SchFS has no free-standing inode — an object exists because a
|
||||
// record with its id exists — so schfs_ialloc writes an inode record with
|
||||
// nlink 0 and no name at all. Nothing can reach it until dirlink adds an entry.
|
||||
// If the caller dies in between, the record is unreferenced but harmless, and
|
||||
// it never reaches the disk anyway: nothing is durable until the transaction
|
||||
// that created it commits.
|
||||
//
|
||||
// The other disagreement is `nlink`. The VFS adjusts it itself (create sets it
|
||||
// to 1, link bumps it, unlink drops it) and then calls iupdate. So the object
|
||||
// layer's own link accounting is deliberately bypassed here: schfs_dir_add and
|
||||
// schfs_dir_del are used rather than schfs_link and schfs_unlink, and iput's
|
||||
// ifree hook is what releases an object once the VFS has decided it has no
|
||||
// names left. Two pieces of code counting links independently is precisely how
|
||||
// a filesystem loses a file that is still referenced.
|
||||
|
||||
#include "types.h"
|
||||
#include "x86.h"
|
||||
#include "defs.h"
|
||||
#include "param.h"
|
||||
#include "stat.h"
|
||||
#include "spinlock.h"
|
||||
#include "proc.h" /* `proc` — copyin/copyout need the current pgdir */
|
||||
#include "sleeplock.h"
|
||||
#include "fs.h"
|
||||
#include "buf.h"
|
||||
#include "file.h"
|
||||
|
||||
#include "schfs_int.h"
|
||||
|
||||
// The VFS talks in T_*; the disk format uses SCHFS_T_*. Neither is going to
|
||||
// change to suit the other, so the mapping lives here, once, in both
|
||||
// directions rather than as an assumption that the numbers happen to line up.
|
||||
static short
|
||||
type_to_vfs(uint16_t t)
|
||||
{
|
||||
switch (t) {
|
||||
case SCHFS_T_DIR: return T_DIR;
|
||||
case SCHFS_T_DEV: return T_DEV;
|
||||
case SCHFS_T_SYMLINK: return T_SYMLINK;
|
||||
default: return T_FILE;
|
||||
}
|
||||
}
|
||||
|
||||
static uint16_t
|
||||
type_from_vfs(short t)
|
||||
{
|
||||
switch (t) {
|
||||
case T_DIR: return SCHFS_T_DIR;
|
||||
case T_DEV: return SCHFS_T_DEV;
|
||||
case T_SYMLINK: return SCHFS_T_SYMLINK;
|
||||
default: return SCHFS_T_FILE;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Inode load / store / free */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void
|
||||
schfs_vfs_iload(struct inode *ip)
|
||||
{
|
||||
struct schfs_inode in;
|
||||
|
||||
if (schfs_kstat(ip->inum, &in) != 0) {
|
||||
// An unreadable inode on the root filesystem is a panic in v1 because the
|
||||
// system cannot continue without it. Here it is a mounted volume that may
|
||||
// simply be damaged, so report an empty file and let the caller fail
|
||||
// normally rather than taking the machine down with it.
|
||||
ip->type = T_FILE;
|
||||
ip->nlink = 0;
|
||||
ip->size = 0;
|
||||
ip->valid = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
ip->type = type_to_vfs(in.type);
|
||||
ip->major = (short)in.rdev_major;
|
||||
ip->minor = (short)in.rdev_minor;
|
||||
ip->nlink = (short)in.nlink;
|
||||
ip->size = in.size;
|
||||
ip->rwmode = RW;
|
||||
ip->supermode = 0;
|
||||
ip->showmode = 1;
|
||||
ip->atime = (uint)in.atime;
|
||||
ip->mtime = (uint)in.mtime;
|
||||
ip->ctime = (uint)in.ctime;
|
||||
memset(ip->addrs, 0, sizeof(ip->addrs));
|
||||
ip->valid = 1;
|
||||
}
|
||||
|
||||
static void
|
||||
schfs_vfs_istore(struct inode *ip)
|
||||
{
|
||||
struct schfs_inode in;
|
||||
|
||||
// Read-modify-write rather than build-from-scratch: the on-disk record holds
|
||||
// fields the VFS has no slot for (uid, gid, the real mode bits, the
|
||||
// generation), and rebuilding it here would quietly zero every one of them.
|
||||
if (schfs_kstat(ip->inum, &in) != 0) {
|
||||
memset(&in, 0, sizeof(in));
|
||||
in.mode = ip->type == T_DIR ? 0755 : 0644;
|
||||
}
|
||||
|
||||
in.type = type_from_vfs(ip->type);
|
||||
in.rdev_major = (uint16_t)ip->major;
|
||||
in.rdev_minor = (uint16_t)ip->minor;
|
||||
in.nlink = (uint32_t)ip->nlink;
|
||||
in.size = ip->size;
|
||||
in.atime = ip->atime;
|
||||
in.mtime = ip->mtime;
|
||||
in.ctime = ip->ctime;
|
||||
|
||||
schfs_ksetattr(ip->inum, &in);
|
||||
}
|
||||
|
||||
static void
|
||||
schfs_vfs_ifree(struct inode *ip)
|
||||
{
|
||||
schfs_kfree(ip->inum);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Data */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
// Reads and writes are staged through a bounce buffer rather than going
|
||||
// straight to user memory, because the filesystem takes a sleeplock and walks
|
||||
// the tree while a copyout could fault. One page at a time keeps that buffer
|
||||
// off the 4096-byte kernel stack -- it is kalloc'd -- and bounds how long the
|
||||
// volume lock is held.
|
||||
#define BOUNCE 2048
|
||||
|
||||
static int
|
||||
schfs_vfs_read(struct inode *ip, int user_dst, uint64 dst, uint64 off, uint n)
|
||||
{
|
||||
char *tmp = kalloc();
|
||||
if (tmp == 0)
|
||||
return -1;
|
||||
|
||||
uint done = 0;
|
||||
while (done < n) {
|
||||
uint want = n - done;
|
||||
if (want > BOUNCE)
|
||||
want = BOUNCE;
|
||||
|
||||
long got = schfs_kread(ip->inum, off + done, tmp, want);
|
||||
if (got < 0) { kfree(tmp); return done ? (int)done : -1; }
|
||||
if (got == 0) break; /* end of file */
|
||||
|
||||
if (user_dst) {
|
||||
if (copyout(myproc()->pgdir, dst + done, tmp, (uint)got) < 0)
|
||||
break;
|
||||
} else {
|
||||
memmove((char *)(dst + done), tmp, (uint)got);
|
||||
}
|
||||
done += (uint)got;
|
||||
if ((uint)got < want)
|
||||
break; /* short read: end of file */
|
||||
}
|
||||
|
||||
kfree(tmp);
|
||||
return (int)done;
|
||||
}
|
||||
|
||||
static int
|
||||
schfs_vfs_write(struct inode *ip, int user_src, uint64 src, uint64 off, uint n)
|
||||
{
|
||||
char *tmp = kalloc();
|
||||
if (tmp == 0)
|
||||
return -1;
|
||||
|
||||
uint done = 0;
|
||||
while (done < n) {
|
||||
uint want = n - done;
|
||||
if (want > BOUNCE)
|
||||
want = BOUNCE;
|
||||
|
||||
if (user_src) {
|
||||
if (copyin(myproc()->pgdir, (addr_t)(src + done), tmp, (uint64)want) < 0)
|
||||
break;
|
||||
} else {
|
||||
memmove(tmp, (char *)(src + done), want);
|
||||
}
|
||||
|
||||
long put = schfs_kwrite(ip->inum, off + done, tmp, want);
|
||||
if (put <= 0) break;
|
||||
done += (uint)put;
|
||||
if ((uint)put < want) break;
|
||||
}
|
||||
|
||||
kfree(tmp);
|
||||
if (off + done > ip->size)
|
||||
ip->size = off + done;
|
||||
return (int)done;
|
||||
}
|
||||
|
||||
static void
|
||||
schfs_vfs_truncate(struct inode *ip, uint64 newsize)
|
||||
{
|
||||
if (schfs_ktruncate(ip->inum, newsize) == 0)
|
||||
ip->size = newsize;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Directories */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static struct inode *
|
||||
schfs_vfs_dirlookup(struct inode *dp, char *name, uint *poff)
|
||||
{
|
||||
// "." and ".." are not stored: a directory costs nothing until it has real
|
||||
// entries. Both are answered from what the VFS already knows, except ".."
|
||||
// at the mount root, which the mount layer redirects before we get here.
|
||||
if (name[0] == '.' && name[1] == '\0') {
|
||||
if (poff) *poff = 0;
|
||||
return iget(dp->dev, dp->inum);
|
||||
}
|
||||
|
||||
unsigned namelen = 0;
|
||||
while (name[namelen]) namelen++;
|
||||
|
||||
if (namelen == 2 && name[0] == '.' && name[1] == '.') {
|
||||
struct schfs_inode in;
|
||||
if (schfs_kstat(dp->inum, &in) != 0)
|
||||
return 0;
|
||||
if (poff) *poff = 0;
|
||||
return iget(dp->dev, in.parent ? in.parent : dp->inum);
|
||||
}
|
||||
|
||||
uint64 ino = schfs_klookup_at(dp->inum, name, namelen);
|
||||
if (ino == 0)
|
||||
return 0;
|
||||
// poff is v1's byte offset into the directory file. SchFS finds an entry by
|
||||
// name, so there is nothing meaningful to hand back; dirunlink below takes
|
||||
// the name and ignores it.
|
||||
if (poff) *poff = 0;
|
||||
return iget(dp->dev, ino);
|
||||
}
|
||||
|
||||
static int
|
||||
schfs_vfs_dirlink(struct inode *dp, char *name, uint64 inum)
|
||||
{
|
||||
unsigned namelen = 0;
|
||||
while (name[namelen]) namelen++;
|
||||
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
|
||||
return -1;
|
||||
|
||||
struct schfs_inode in;
|
||||
if (schfs_kstat(inum, &in) != 0)
|
||||
return -1;
|
||||
|
||||
int r = schfs_kdiradd(dp->inum, name, namelen, inum, in.type);
|
||||
if (r != 0)
|
||||
return -1;
|
||||
|
||||
// A subdirectory's ".." is synthesised from its parent field, so moving or
|
||||
// creating one has to set that field or the new directory reports the wrong
|
||||
// parent -- and every ".." walk out of it lands somewhere else entirely.
|
||||
if (in.type == SCHFS_T_DIR && in.parent != dp->inum) {
|
||||
in.parent = dp->inum;
|
||||
schfs_ksetattr(inum, &in);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
schfs_vfs_dirunlink(struct inode *dp, char *name, uint off)
|
||||
{
|
||||
(void)off;
|
||||
unsigned namelen = 0;
|
||||
while (name[namelen]) namelen++;
|
||||
return schfs_kunlink(dp->inum, name, namelen) == 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
static int
|
||||
schfs_vfs_dirempty(struct inode *dp)
|
||||
{
|
||||
return schfs_kdirempty(dp->inum);
|
||||
}
|
||||
|
||||
static struct inode *
|
||||
schfs_vfs_ialloc(uint dev, short type)
|
||||
{
|
||||
uint64 ino = 0;
|
||||
if (schfs_kalloc_object(type_from_vfs(type), type == T_DIR ? 0755 : 0644,
|
||||
&ino) != 0)
|
||||
return 0;
|
||||
return iget(dev, ino);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* getdents64 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
// One record's worth of state for the callback below.
|
||||
//
|
||||
// Entries come back in name-hash order, which has nothing to do with their
|
||||
// names -- POSIX promises no order, and hash order is what makes a lookup one
|
||||
// tree descent instead of a scan. The position handed back to userland is
|
||||
// therefore an *index*: entry N of the walk. Resuming means walking to N again,
|
||||
// which readdir already does linearly, so a full listing stays linear overall
|
||||
// only because the common case is resuming exactly where the last call stopped.
|
||||
struct gd_ctx {
|
||||
char *out;
|
||||
int cap;
|
||||
int written;
|
||||
uint64 skip; // entries to step over before emitting
|
||||
uint64 index; // entries seen so far
|
||||
int full;
|
||||
};
|
||||
|
||||
static int
|
||||
gd_entry(uint64 ino, unsigned short type, const char *name, unsigned namelen,
|
||||
void *arg)
|
||||
{
|
||||
struct gd_ctx *g = arg;
|
||||
|
||||
if (g->index++ < g->skip)
|
||||
return 0;
|
||||
if (g->full)
|
||||
return 1;
|
||||
|
||||
if (namelen > 255)
|
||||
namelen = 255;
|
||||
int reclen = (8 + 8 + 2 + 1 + (int)namelen + 1 + 7) & ~7;
|
||||
if (g->written + reclen > g->cap) {
|
||||
g->full = 1;
|
||||
return 1; /* stop; the caller will come back */
|
||||
}
|
||||
|
||||
char *k = g->out + g->written;
|
||||
uint64 *p64 = (uint64 *)k;
|
||||
p64[0] = ino;
|
||||
p64[1] = g->index; /* resume position: entries consumed */
|
||||
*(uint16 *)(k + 16) = (uint16)reclen;
|
||||
*(uint8 *)(k + 18) = type == SCHFS_T_DIR ? 4 /* DT_DIR */
|
||||
: type == SCHFS_T_DEV ? 2 /* DT_CHR */
|
||||
: type == SCHFS_T_SYMLINK ? 10 /* DT_LNK */
|
||||
: 8; /* DT_REG */
|
||||
memmove(k + 19, name, namelen);
|
||||
memset(k + 19 + namelen, 0, reclen - 19 - namelen);
|
||||
g->written += reclen;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
schfs_vfs_getdents(struct inode *dp, char *out, int cap, uint64 *poff)
|
||||
{
|
||||
struct gd_ctx g;
|
||||
g.out = out;
|
||||
g.cap = cap;
|
||||
g.written = 0;
|
||||
g.skip = *poff;
|
||||
g.index = 0;
|
||||
g.full = 0;
|
||||
|
||||
// "." and ".." are synthesised: they are not records, so a walk would never
|
||||
// produce them, and every readdir caller expects both.
|
||||
static const char *dots[2] = { ".", ".." };
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (g.index++ < g.skip)
|
||||
continue;
|
||||
int reclen = (8 + 8 + 2 + 1 + (i + 1) + 1 + 7) & ~7;
|
||||
if (g.written + reclen > cap)
|
||||
return g.written;
|
||||
char *k = out + g.written;
|
||||
uint64 *p64 = (uint64 *)k;
|
||||
p64[0] = dp->inum; /* close enough for ".."; nothing reads it */
|
||||
p64[1] = g.index;
|
||||
*(uint16 *)(k + 16) = (uint16)reclen;
|
||||
*(uint8 *)(k + 18) = 4; /* DT_DIR */
|
||||
memmove(k + 19, dots[i], i + 1);
|
||||
memset(k + 19 + i + 1, 0, reclen - 19 - i - 1);
|
||||
g.written += reclen;
|
||||
}
|
||||
|
||||
if (schfs_kreaddir(dp->inum, gd_entry, &g) != 0 && g.written == 0)
|
||||
return -1;
|
||||
|
||||
*poff = g.index;
|
||||
return g.written;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const struct fs_ops schfs_ops = {
|
||||
.name = "schfs",
|
||||
.iload = schfs_vfs_iload,
|
||||
.istore = schfs_vfs_istore,
|
||||
.ifree = schfs_vfs_ifree,
|
||||
.read = schfs_vfs_read,
|
||||
.write = schfs_vfs_write,
|
||||
.truncate = schfs_vfs_truncate,
|
||||
.dirlookup = schfs_vfs_dirlookup,
|
||||
.dirlink = schfs_vfs_dirlink,
|
||||
.dirunlink = schfs_vfs_dirunlink,
|
||||
.dirempty = schfs_vfs_dirempty,
|
||||
.ialloc = schfs_vfs_ialloc,
|
||||
.getdents = schfs_vfs_getdents,
|
||||
};
|
||||
299
kernel/fs/schfs/vol.c
Normal file
299
kernel/fs/schfs/vol.c
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// SchFS volume layer: the uberblock ring.
|
||||
//
|
||||
// A ring of SCHFS_UBER_COUNT slots is written round-robin, each stamped with a
|
||||
// transaction number and a checksum. Mounting reads every slot and adopts the
|
||||
// highest-numbered one that validates.
|
||||
//
|
||||
// That single rule replaces journalling entirely. A commit writes one block; it
|
||||
// either lands whole or it does not. If it does not, its checksum fails, the
|
||||
// previous slot is still the highest valid one, and the volume comes up at the
|
||||
// previous transaction with no replay and no fsck. Rotating through many slots
|
||||
// rather than overwriting one means a run of bad sectors under the current slot
|
||||
// cannot take the volume with it.
|
||||
|
||||
#include "schfs_int.h"
|
||||
|
||||
#ifdef SCHFS_HOSTED
|
||||
#include <string.h>
|
||||
#else
|
||||
#include "types.h"
|
||||
#include "defs.h"
|
||||
#endif
|
||||
|
||||
// Checksum an uberblock with its own crc field zeroed, the same convention the
|
||||
// tree nodes use.
|
||||
static uint32_t
|
||||
uber_crc(struct schfs_uberblock *u)
|
||||
{
|
||||
uint32_t saved = u->crc;
|
||||
u->crc = 0;
|
||||
uint32_t c = schfs_crc32c(0, u, sizeof(*u));
|
||||
u->crc = saved;
|
||||
return c;
|
||||
}
|
||||
|
||||
static int
|
||||
uber_valid(struct schfs_uberblock *u)
|
||||
{
|
||||
if (u->magic != SCHFS_MAGIC)
|
||||
return 0;
|
||||
if (u->version != SCHFS_VERSION)
|
||||
return 0;
|
||||
// A volume made with a different block size would otherwise be read with
|
||||
// every offset wrong, which looks exactly like corruption.
|
||||
if (u->blocksize != SCHFS_BLKSIZE)
|
||||
return 0;
|
||||
if (uber_crc(u) != u->crc)
|
||||
return 0;
|
||||
// Self-consistency: a root inside the ring or past the end of the device is
|
||||
// a corrupt uberblock that happens to checksum, not a usable one.
|
||||
if (u->root < SCHFS_FIRST_BLOCK || u->root >= u->total_blocks)
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void
|
||||
vol_from_uber(struct schfs_vol *v, struct schfs_uberblock *u, int slot)
|
||||
{
|
||||
v->tree.dev = &v->dev;
|
||||
v->tree.root = u->root;
|
||||
v->tree.root_crc = u->root_crc;
|
||||
v->tree.root_gen = u->root_generation;
|
||||
// The tree's `generation` is the transaction group being *built*, one past
|
||||
// the newest committed one. Setting it to the committed root's generation
|
||||
// instead would tell the tree that every block of the mounted filesystem
|
||||
// belongs to the open transaction, and it would then rewrite the live tree
|
||||
// in place — losing the very thing copy-on-write exists to preserve.
|
||||
v->tree.generation = u->generation + 1;
|
||||
|
||||
v->total_blocks = u->total_blocks;
|
||||
v->free_blocks = u->free_blocks;
|
||||
v->next_objectid = u->next_objectid;
|
||||
v->root_objectid = u->root_objectid;
|
||||
v->generation = u->generation;
|
||||
v->uber_slot = slot;
|
||||
|
||||
memmove(v->uuid, u->uuid, sizeof(v->uuid));
|
||||
memmove(v->label, u->label, sizeof(v->label));
|
||||
v->label[SCHFS_LABEL_MAX - 1] = '\0';
|
||||
}
|
||||
|
||||
int
|
||||
schfs_vol_open(struct schfs_vol *v, struct schfs_bdev *dev,
|
||||
struct schfs_ctx *x, void *(*palloc)(void))
|
||||
{
|
||||
struct schfs_uberblock *u = (struct schfs_uberblock *)x->a;
|
||||
int best_slot = -1;
|
||||
uint64_t best_gen = 0;
|
||||
int io_errors = 0;
|
||||
|
||||
v->dev = *dev;
|
||||
v->ctx = *x;
|
||||
v->tree.dev = &v->dev;
|
||||
|
||||
// Remember which slot won, not a copy of it.
|
||||
//
|
||||
// An uberblock is a whole SCHFS_BLKSIZE block, and keeping one as a local
|
||||
// put 4096 bytes on a kernel stack that is 4096 bytes in total — the canary
|
||||
// in kalloc'd stacks caught it as "kstack overflow" on the very first boot.
|
||||
// Re-reading the winner costs one block read, once, at mount.
|
||||
for (int i = 0; i < SCHFS_UBER_COUNT; i++) {
|
||||
if (v->dev.bread(v->dev.ctx, SCHFS_UBER_START + i, u) != 0) {
|
||||
io_errors++;
|
||||
continue; // an unreadable slot is not fatal: try the rest
|
||||
}
|
||||
if (!uber_valid(u))
|
||||
continue;
|
||||
if (best_slot < 0 || u->generation > best_gen) {
|
||||
best_gen = u->generation;
|
||||
best_slot = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (best_slot < 0)
|
||||
return io_errors == SCHFS_UBER_COUNT ? -2 : -1;
|
||||
|
||||
if (v->dev.bread(v->dev.ctx, SCHFS_UBER_START + best_slot, u) != 0)
|
||||
return -2;
|
||||
if (!uber_valid(u))
|
||||
return -2; /* changed under us, or a flaky device */
|
||||
|
||||
vol_from_uber(v, u, best_slot);
|
||||
v->readonly = 1; // until an allocation map exists, below
|
||||
|
||||
// The newest uberblock passing its own checksum does not prove the tree it
|
||||
// names is readable — a misdirected write could have landed on the root.
|
||||
// Check now, while there is still the option of falling back, rather than
|
||||
// discovering it on the first lookup.
|
||||
if (v->dev.bread(v->dev.ctx, v->tree.root, x->a) != 0)
|
||||
return -2;
|
||||
{
|
||||
struct schfs_node_hdr *h = (struct schfs_node_hdr *)x->a;
|
||||
if (h->magic != SCHFS_NODE_MAGIC || h->blocknr != v->tree.root)
|
||||
return -2;
|
||||
}
|
||||
|
||||
// Rebuild the allocation map from the tree just adopted. Without a palloc
|
||||
// the volume stays read-only, which is exactly right for a tool that only
|
||||
// inspects an image: it can then never allocate, rather than allocating
|
||||
// from a map that says everything is free.
|
||||
if (palloc) {
|
||||
int sr = schfs_space_init(&v->space, v->total_blocks, palloc);
|
||||
if (sr != 0)
|
||||
return sr;
|
||||
if (schfs_space_scan(v) != 0)
|
||||
return -2;
|
||||
v->free_blocks = v->space.free_blocks;
|
||||
v->readonly = 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
schfs_vol_commit(struct schfs_vol *v)
|
||||
{
|
||||
struct schfs_uberblock *u = (struct schfs_uberblock *)v->ctx.a;
|
||||
|
||||
v->generation++;
|
||||
v->uber_slot = (v->uber_slot + 1) % SCHFS_UBER_COUNT;
|
||||
|
||||
memset(u, 0, sizeof(*u));
|
||||
u->magic = SCHFS_MAGIC;
|
||||
u->version = SCHFS_VERSION;
|
||||
u->blocksize = SCHFS_BLKSIZE;
|
||||
u->generation = v->generation;
|
||||
u->root = v->tree.root;
|
||||
u->root_crc = v->tree.root_crc;
|
||||
u->root_generation = v->tree.root_gen;
|
||||
u->total_blocks = v->total_blocks;
|
||||
// Blocks this group released count as free *as of this uberblock*: nothing
|
||||
// the root below reaches touches them. They are only still marked in use in
|
||||
// memory because the group being superseded is what a crash would land on
|
||||
// until this very block is written.
|
||||
u->free_blocks = v->space.nblocks
|
||||
? v->space.free_blocks + v->space.pending
|
||||
: v->free_blocks;
|
||||
u->next_objectid = v->next_objectid;
|
||||
u->root_objectid = v->root_objectid;
|
||||
memmove(u->uuid, v->uuid, sizeof(u->uuid));
|
||||
memmove(u->label, v->label, sizeof(u->label));
|
||||
u->crc = uber_crc(u);
|
||||
|
||||
if (v->dev.bwrite(v->dev.ctx, SCHFS_UBER_START + v->uber_slot, u) != 0)
|
||||
return -2;
|
||||
|
||||
// Order matters here, and it is the whole crash-safety argument in three
|
||||
// lines. Only now that the new uberblock is on the disk is the group this
|
||||
// freed blocks in superseded, so only now may those blocks be handed out
|
||||
// again; releasing them before the write would let a crash come back to a
|
||||
// tree whose blocks had already been reused.
|
||||
if (v->space.nblocks) {
|
||||
schfs_space_commit(&v->space);
|
||||
v->free_blocks = v->space.free_blocks;
|
||||
}
|
||||
|
||||
// Everything written from here on belongs to the next transaction group, so
|
||||
// the blocks just published become read-only to the tree again and the
|
||||
// copy-on-write rule resumes. Advancing this is the whole reason a commit
|
||||
// is a barrier rather than just another block write.
|
||||
v->tree.generation = v->generation + 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
if (total_blocks <= SCHFS_FIRST_BLOCK)
|
||||
return -3;
|
||||
|
||||
memset(v, 0, sizeof(*v));
|
||||
v->dev = *dev;
|
||||
v->ctx = *x;
|
||||
v->tree.dev = &v->dev;
|
||||
v->total_blocks = total_blocks;
|
||||
v->root_objectid = SCHFS_ROOT_OBJECTID;
|
||||
|
||||
// Nothing is allocated on a fresh volume beyond the boot block and the
|
||||
// ring, so the map starts empty rather than being scanned into existence.
|
||||
{
|
||||
int sr = schfs_space_init(&v->space, total_blocks, palloc);
|
||||
if (sr != 0)
|
||||
return sr;
|
||||
}
|
||||
v->free_blocks = v->space.free_blocks;
|
||||
// The root's object id is consumed by the root itself, so allocation must
|
||||
// start *past* it. Starting at SCHFS_FIRST_OBJECTID instead handed the
|
||||
// first directory created the same id as the root: that directory then
|
||||
// silently became the root, so it contained itself and any tree walk
|
||||
// recursed until the stack ran out.
|
||||
v->next_objectid = SCHFS_ROOT_OBJECTID + 1;
|
||||
v->generation = 0; /* nothing committed yet */
|
||||
v->tree.generation = 1; /* ...so this is the group being built */
|
||||
v->uber_slot = -1;
|
||||
v->readonly = 0;
|
||||
|
||||
if (label) {
|
||||
unsigned i = 0;
|
||||
for (; i < SCHFS_LABEL_MAX - 1 && label[i]; i++)
|
||||
v->label[i] = label[i];
|
||||
v->label[i] = '\0';
|
||||
}
|
||||
|
||||
// A UUID from the block count and a fixed salt. Not random — mkfs is
|
||||
// deliberately reproducible, so two runs over the same tree produce
|
||||
// byte-identical images and a diff means a real change.
|
||||
{
|
||||
uint32_t a = schfs_crc32c(0, &total_blocks, sizeof(total_blocks));
|
||||
uint32_t b = schfs_crc32c(a, v->label, SCHFS_LABEL_MAX);
|
||||
for (int i = 0; i < 16; i++)
|
||||
v->uuid[i] = (uint8_t)((i & 1 ? a : b) >> ((i % 4) * 8));
|
||||
}
|
||||
|
||||
// Zero the whole ring so a stale uberblock from a previous filesystem on
|
||||
// this device cannot outrank ours and be mounted instead.
|
||||
memset(x->a, 0, SCHFS_BLKSIZE);
|
||||
for (int i = 0; i < SCHFS_UBER_COUNT; i++)
|
||||
if (v->dev.bwrite(v->dev.ctx, SCHFS_UBER_START + i, x->a) != 0)
|
||||
return -2;
|
||||
|
||||
if (schfs_btree_create(&v->tree, &v->dev, x) != 0)
|
||||
return -2;
|
||||
|
||||
return schfs_vol_commit(v);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Allocation */
|
||||
/* */
|
||||
/* Thin adapters over the space manager, so callers wiring up a */
|
||||
/* schfs_bdev have something with the right shape to point at. The */
|
||||
/* readonly check lives here rather than in space.c because "this */
|
||||
/* volume may not be written" is a property of the mount, not of the */
|
||||
/* bitmap. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
uint64_t
|
||||
schfs_vol_alloc(struct schfs_vol *v)
|
||||
{
|
||||
if (v->readonly)
|
||||
return 0;
|
||||
return schfs_space_alloc(&v->space);
|
||||
}
|
||||
|
||||
uint64_t
|
||||
schfs_vol_alloc_run(struct schfs_vol *v, uint64_t n)
|
||||
{
|
||||
if (v->readonly)
|
||||
return 0;
|
||||
return schfs_space_alloc_run(&v->space, n);
|
||||
}
|
||||
|
||||
void
|
||||
schfs_vol_free(struct schfs_vol *v, uint64_t blk)
|
||||
{
|
||||
schfs_space_free(&v->space, blk);
|
||||
}
|
||||
Loading…
Reference in a new issue