`mount -t schfs @golden /mnt` mounts a snapshot. The source argument
finally means something: sys_mount ignored it outright, because there was
one volume and nothing to choose between. `@name` rather than a path
because a snapshot is not a device -- there is no node in /dev for it and
there should not be, since it lives inside the volume already mounted.
A second volume rather than a parameter threaded through all 23 schfs_k*
entry points. Only the read side can ever address it, so the write path
keeps talking to the live volume and needed no change; the four read
entry points grew _dev variants and pick the volume from the device the
inode already carries. One fs_ops table serves both, because every entry
takes an inode and dispatches on it -- a second table would be the same
pointers under a different name.
It needs its own device number: a snapshot's object ids are the *same
numbers* as the live tree's, so one device would make the two views alias
in the inode cache and hand a reader whichever was cached first.
Three things this turned up, none of them new but all of them reachable
for the first time:
- sys_mount refused with `dev == SCHFS_DEV`, meaning "do not mount SchFS
onto SchFS". A snapshot's mount point is necessarily on the live root,
so that refused the entire feature. The real condition is mounting a
device onto *itself*, which is `dev == fsdev`.
- createat allocates an inode and then panics if dirlink fails --
reasonable about a writable filesystem, a crash on a read-only one.
The refusal now happens before anything is allocated. Unlink and
rename already handled a refusal properly.
- a refused create reported ENOENT, so writing to a snapshot said "No
such file or directory" -- the sentence a shell prints for every
create failure. It reports EROFS now.
Verified in the running system against an image built with
--snapshot golden: the mount succeeds, ls lists the snapshot's tree,
/mnt2/etc/motd reads back through it, and a write is refused with
"Read-only file system".
One at a time, and no umount path for it yet: mount_del_by_root removes
the mount table entry, but ksnap_unmount is only called on the failure
paths. schfs-check passes; bootcheck 108 [ok].
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
808 lines
24 KiB
C
808 lines
24 KiB
C
// 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 "file.h" /* SCHFS_DEV, SCHFS_SNAP_DEV */
|
|
#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;
|
|
|
|
// A mounted snapshot: the same block device with an older tree root, read-only.
|
|
//
|
|
// A second volume rather than a parameter threaded through all 23 entry points,
|
|
// because only the read side can ever address it -- a snapshot cannot be
|
|
// written to, so every mutation keeps talking to kvol and needs no change.
|
|
static struct schfs_vol ksnapvol;
|
|
static int ksnap_mounted;
|
|
static char *ksnapcurblk;
|
|
static char *ksnapdatablk;
|
|
|
|
// 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 takes 64-bit block numbers now, so this no longer caps the
|
|
// volume; the check remains as an overflow guard on the multiply below.
|
|
if (blk > (~0ULL) / 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 > (~0ULL) / 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);
|
|
}
|
|
|
|
// Which volume a device number names. The VFS hands an inode to every fs_ops
|
|
// call and an inode carries its device, so this is all the dispatch a
|
|
// read-only second view needs.
|
|
static struct schfs_vol *
|
|
vol_for(uint dev)
|
|
{
|
|
return dev == SCHFS_SNAP_DEV ? &ksnapvol : &kvol;
|
|
}
|
|
|
|
static char *
|
|
curblk_for(uint dev)
|
|
{
|
|
return dev == SCHFS_SNAP_DEV ? ksnapcurblk : kcurblk;
|
|
}
|
|
|
|
static char *
|
|
datablk_for(uint dev)
|
|
{
|
|
return dev == SCHFS_SNAP_DEV ? ksnapdatablk : kdatablk;
|
|
}
|
|
|
|
static int
|
|
mounted_for(uint dev)
|
|
{
|
|
return dev == SCHFS_SNAP_DEV ? ksnap_mounted : kmounted;
|
|
}
|
|
|
|
// Mount a snapshot by name, read-only.
|
|
//
|
|
// The snapshot's records live in the *live* tree, so the name is resolved
|
|
// there; what comes back is a root, and the second volume is a copy of the
|
|
// first with its tree pointed at it. Nothing else differs -- same device, same
|
|
// node format, same checksums.
|
|
int
|
|
schfs_ksnap_mount(const char *name, unsigned namelen)
|
|
{
|
|
if (!kmounted)
|
|
return -1;
|
|
if (ksnap_mounted)
|
|
return -2; /* one at a time, for now */
|
|
|
|
if (ksnapcurblk == 0) ksnapcurblk = kalloc();
|
|
if (ksnapdatablk == 0) ksnapdatablk = kalloc();
|
|
if (ksnapcurblk == 0 || ksnapdatablk == 0)
|
|
return -3;
|
|
|
|
acquiresleep(&schfs_lock);
|
|
|
|
struct schfs_cursor c;
|
|
schfs_cursor_init(&c, &kvol.tree, kcurblk);
|
|
|
|
struct schfs_key k;
|
|
schfs_mkkey(&k, SCHFS_SNAP_OBJECTID, SCHFS_KEY_SNAP, 0);
|
|
|
|
int found = 0;
|
|
struct schfs_snap sn;
|
|
int r = schfs_cursor_seek(&c, &k);
|
|
while (r == 0) {
|
|
const struct schfs_key *got = schfs_cursor_key(&c);
|
|
if (!got || got->objectid != SCHFS_SNAP_OBJECTID ||
|
|
got->type != SCHFS_KEY_SNAP)
|
|
break;
|
|
|
|
unsigned len;
|
|
const struct schfs_snap *p = (const struct schfs_snap *)
|
|
schfs_cursor_val(&c, &len);
|
|
if (p && len == sizeof(*p)) {
|
|
unsigned i = 0;
|
|
while (i < namelen && i < SCHFS_SNAP_NAME_MAX && p->name[i] == name[i])
|
|
i++;
|
|
if (i == namelen &&
|
|
(namelen == SCHFS_SNAP_NAME_MAX || p->name[namelen] == 0)) {
|
|
sn = *p;
|
|
found = 1;
|
|
break;
|
|
}
|
|
}
|
|
r = schfs_cursor_next(&c);
|
|
}
|
|
|
|
if (!found) {
|
|
releasesleep(&schfs_lock);
|
|
return -4;
|
|
}
|
|
|
|
// Same volume, older root. The device, the space map and the block callbacks
|
|
// are all the live volume's -- a snapshot is not a separate filesystem, it is
|
|
// a second way into this one.
|
|
ksnapvol = kvol;
|
|
ksnapvol.tree.root = sn.root;
|
|
ksnapvol.tree.root_crc = sn.root_crc;
|
|
ksnapvol.tree.root_gen = sn.root_generation;
|
|
// Its own scratch, or a read through the snapshot would tread on a cursor
|
|
// the live volume still has positioned.
|
|
ksnapvol.ctx.a = ksnapcurblk;
|
|
|
|
ksnap_mounted = 1;
|
|
releasesleep(&schfs_lock);
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
schfs_ksnap_unmount(void)
|
|
{
|
|
acquiresleep(&schfs_lock);
|
|
ksnap_mounted = 0;
|
|
releasesleep(&schfs_lock);
|
|
}
|
|
|
|
uint64
|
|
schfs_ksnap_root(void)
|
|
{
|
|
return ksnap_mounted ? ksnapvol.root_objectid : 0;
|
|
}
|
|
|
|
// The read path, addressable per device so a snapshot can be read through the
|
|
// same VFS. Each is the existing call with the volume chosen rather than
|
|
// assumed; the originals below stay as the live-volume spelling, because most
|
|
// callers have no business knowing there is a choice.
|
|
long
|
|
schfs_kread_dev(uint dev, uint64_t ino, uint64_t off, void *buf, unsigned n)
|
|
{
|
|
if (!mounted_for(dev))
|
|
return -1;
|
|
acquiresleep(&schfs_lock);
|
|
struct schfs_cursor c;
|
|
schfs_cursor_init(&c, &vol_for(dev)->tree, curblk_for(dev));
|
|
long r = schfs_file_read(vol_for(dev), &c, ino, off, buf, n, datablk_for(dev));
|
|
releasesleep(&schfs_lock);
|
|
return r;
|
|
}
|
|
|
|
int
|
|
schfs_kstat_dev(uint dev, uint64_t ino, struct schfs_inode *out)
|
|
{
|
|
if (!mounted_for(dev))
|
|
return -1;
|
|
acquiresleep(&schfs_lock);
|
|
struct schfs_cursor c;
|
|
schfs_cursor_init(&c, &vol_for(dev)->tree, curblk_for(dev));
|
|
int r = schfs_inode_get(vol_for(dev), &c, ino, out);
|
|
releasesleep(&schfs_lock);
|
|
return r;
|
|
}
|
|
|
|
uint64
|
|
schfs_klookup_at_dev(uint dev, uint64_t dir, const char *name, unsigned len)
|
|
{
|
|
if (!mounted_for(dev))
|
|
return 0;
|
|
acquiresleep(&schfs_lock);
|
|
struct schfs_cursor c;
|
|
schfs_cursor_init(&c, &vol_for(dev)->tree, curblk_for(dev));
|
|
uint64_t ino = 0;
|
|
uint16_t type = 0;
|
|
int r = schfs_dir_lookup(vol_for(dev), &c, dir, name, len, &ino, &type);
|
|
releasesleep(&schfs_lock);
|
|
return r == 0 ? ino : 0;
|
|
}
|
|
|
|
int
|
|
schfs_kreaddir_dev(uint dev, uint64_t dir,
|
|
int (*fn)(uint64_t, uint16_t, const char *, unsigned, void *),
|
|
void *arg)
|
|
{
|
|
if (!mounted_for(dev))
|
|
return -1;
|
|
acquiresleep(&schfs_lock);
|
|
struct schfs_cursor c;
|
|
schfs_cursor_init(&c, &vol_for(dev)->tree, curblk_for(dev));
|
|
int r = schfs_dir_iterate(vol_for(dev), &c, dir, fn, arg);
|
|
releasesleep(&schfs_lock);
|
|
return r;
|
|
}
|
|
|
|
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 SchOS 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;
|
|
}
|