SchOS/kernel/fs/schfs/kmount.c
Nexoniarz d0941780f5 ramdisk: the live root grows into half of memory
The live root was a fixed 96 MB image in the kernel: 60 MB of zeros on
the ISO, twice, and room for 60 MB of packages whatever the machine had.

Now the image is only what the root holds, plus slack, measured by
building it once large. Past its end the ramdisk goes on to half of the
memory free at mount, as pages taken the first time each block is
written; an unwritten block reads as zeros and costs nothing. SchFS
keeps no free-space map on disk, so growing a volume is a larger total
before open builds the map: grow_to, set for the ramdisk root.

8 GB machine: a 4 GB root, Xorg installed into it in 77 s with 7.6 GB
still free. The ISO is 172 MB instead of 344.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-24 17:20:43 +02:00

1472 lines
49 KiB
C

// SPDX-License-Identifier: GPL-2.0-or-later
//
// 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.
//
// SchFS is the root filesystem: proc.c mounts it before pid 1 gets a working
// directory and panics if it is not there. That was not true when this file was
// written -- v1 in kernel/fs/fs.c was still the root, and a SchFS volume that
// failed to mount cost nothing -- and it is worth knowing that a mount failure
// here now stops the machine.
//
// ---------------------------------------------------------------------------
// Transactions
//
// Every entry point below that changes anything joins the open transaction
// group, and the group commits later: when it is SCHFS_COMMIT_TICKS old, on
// sync(2) or fsync(2), on unmount and shutdown, or early when the blocks it
// freed start to crowd out the free ones. That is how ext4 (5 s by default),
// btrfs (30 s) and ZFS (5 s) all do it, and it is the same contract: a write
// that returned is durable once it has been synced, and a crash loses at most
// the last few seconds and never leaves a half-done operation, because a
// commit is a single uberblock write that either lands or does not. There is
// still no journal to replay and nothing to undo.
//
// It used to commit before every operation returned. Durable at once, and two
// cache flushes plus an uberblock per write(2): 4 KB writes ran at 2.8 MB/s on
// a disk that takes 5.6 in 64 KB ones, and every block of data cost about five
// block writes, the same tree path rewritten once per call. Inside one group a
// block the group already wrote is changed in place, so a path rewritten a
// hundred times between commits goes to the disk once.
//
// Callers doing many related changes can still bracket them -- see
// schfs_ktx_open below -- which keeps them in one group whatever the clock
// does. Measured over 1 MB of file data: 1410 block writes
// committing every 4 KB against 267 for the same megabyte as one group. Not a
// tuning difference. A commit contains a flush, and a flush is a barrier, so
// committing often is exactly the same thing as forbidding the buffer cache to
// coalesce anything.
//
// Note that this is the opposite of what the numbers said before kd_write
// started deferring: batching used to buy about 8%, because every write went
// straight to the disk whether it was committed or not, so there was nothing
// for a bracket to save. Deferral is what makes batching worth anything, and
// batching is what makes deferral worth anything. Neither pays alone.
#include "core/types.h"
#include "core/defs.h"
#include "core/param.h"
#include "core/spinlock.h"
#include "core/sleeplock.h"
#include "core/proc.h" /* myproc() — transaction brackets are per-process */
#include "fs/fs.h"
#include "fs/buf.h"
#include "fs/file.h" /* SCHFS_DEV, SCHFS_SNAP_DEV */
#include "uapi/errno.h" /* mounting reports why it failed, not just that it did */
#include "arch/x86.h"
#include "fs/fsstat.h"
#include "fs/blkdev.h" /* bgetblk */
#include "schfs_int.h"
// How many buffer-cache blocks one filesystem block spans. BSIZE was 1024 when
// this was written, so it was four; BSIZE is a whole SchFS block now and it is
// one. The loops below stay written for the general case because the constant
// is the sort of thing that moves back.
#define SCHFS_BPB (SCHFS_BLKSIZE / BSIZE)
struct schfs_kdev {
uint dev; // the *block* device this volume lives on
};
// One mounted volume.
//
// This used to be a handful of file-scope singletons -- one kvol, one kmounted,
// one set of block buffers -- because there was one volume and no way to ask for
// another. Everything that mutated named kvol directly, so sys_mount's source
// argument had nothing it could mean and was ignored: `mount -t schfs /dev/hdb
// /mnt` mounted whatever the kernel had probed at boot, whatever you typed.
//
// Slot 0 is the root volume, so SCHFS_DEV on its own still names it.
struct schfs_mount {
struct schfs_kdev kdev;
struct schfs_vol vol;
int mounted;
// Block buffers, allocated once at mount and kept for its lifetime.
//
// 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.
//
// curblk and datablk serve the read paths; ctx* 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 curblk.
char *curblk, *datablk;
char *ctxa, *ctxb, *ctxc;
// Those buffers are shared by every caller of this volume, 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.
//
// Per volume rather than one for all of them: two mounted disks share no
// state at all, and a single lock would have made a write to one wait behind
// a write to the other for no reason.
struct sleeplock lock;
// The explicit transaction bracket, per volume for the same reason.
//
// The pid is carried alongside the pointer, and both must match, because a
// `struct proc *` alone does not identify a process: ptable slots are
// recycled, so an exited owner's pointer compares equal to whichever
// unrelated process next lands in that slot. tx_mine() would then be true for
// a process that holds nothing, klock() would skip the lock entirely, and two
// callers would share one volume's scratch pages. A recycled slot always
// carries a new pid, so the pair is unambiguous.
struct proc *tx_owner;
int tx_owner_pid;
int tx_depth;
int tx_dirty;
// Changes not yet committed, and since when. Written under `lock`; the
// flusher peeks at them without it, and a stale look only delays a commit
// by one of its rounds.
int dirty;
uint dirty_since;
};
// How long a change may wait for its commit, in 10 ms ticks: 5 s, ext4's and
// ZFS's default. The flusher looks every second.
// How much of a file one step of deleting it frees: 4 MB, 1024 blocks.
#define SCHFS_FREE_STEP (1024ull * SCHFS_BLKSIZE)
#define SCHFS_COMMIT_TICKS 500
#define SCHFS_FLUSH_TICKS 100
static struct schfs_mount kmnt[SCHFS_VOL_SLOTS];
// A mounted snapshot: the same block device with an older tree root, read-only.
//
// A snapshot borrows its live volume's lock and block device; only its tree
// root and scratch pages are its own. It is not a separate filesystem, it is a
// second way into one that is already mounted.
static struct schfs_vol ksnapvol[SCHFS_SNAP_SLOTS];
static int ksnap_mounted[SCHFS_SNAP_SLOTS];
static int ksnap_home[SCHFS_SNAP_SLOTS]; // which volume slot
static char *ksnapcurblk[SCHFS_SNAP_SLOTS];
static char *ksnapdatablk[SCHFS_SNAP_SLOTS];
// Seconds since the epoch, for inode timestamps.
//
// This used to return uptime(), with a comment saying the kernel had no wall
// clock. It has had one since time_init() started reading the RTC at boot, and
// the stale comment outlived the condition it described -- which is how every
// file created after boot ended up stamped 1970.
//
// Two errors, not one, and the second is the less obvious: uptime() counts
// 10 ms *ticks*, and they were being used as *seconds*. A node created 60 ticks
// (0.6 s) into boot therefore read back as 00:01:00, so the timestamps were not
// merely offset, they ran a hundred times too fast.
//
// wallclock() is the one source (kernel/core/time.c), so this cannot disagree
// with gettimeofday about what time it is -- which was the whole reason that
// function exists.
static uint64_t
know(void)
{
uint64 sec;
wallclock(&sec, 0);
return (uint64_t)sec;
}
// Explicit transaction brackets.
//
// One schfs_k* call is one transaction group, which is the right default and
// the wrong granularity for a caller making many related changes. A single
// write(2) of 1 MB arrives here as 256 separate 4 KB writes, and committing
// each one costs two cache flushes and an uberblock — measured at 1410 block
// writes to store 256 blocks of data, against 267 when the lot is one group.
// A flush is a barrier: nothing before it can be deferred past it, so the more
// often you commit the less the buffer cache can coalesce.
//
// So a caller may bracket a run of calls into one group. It is not only
// faster: the bracket is indivisible against a crash as a whole, which is
// closer to what a single write(2) is supposed to mean than 256 separately
// durable pieces ever were.
//
// `tx_owner` is read without the lock, which is safe in the one direction it
// is used: only the process inside a bracket ever writes its own proc pointer
// there, and it holds the lock the whole time it is set. Nobody else can
// observe their own pointer, so the test cannot be wrong for the caller asking.
// The null check matters — myproc() is 0 in some early contexts, and 0 == 0
// would otherwise read as "I hold the lock" for a process that does not exist.
/* ------------------------------------------------------------------ */
/* 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 either a second disk or a */
/* read-only snapshot needs. */
/* ------------------------------------------------------------------ */
// The mount a device number belongs to. A snapshot belongs to the live volume
// it was taken from: they share a block device, a lock and a space map.
static struct schfs_mount *
mnt_for(uint dev)
{
if (SCHFS_IS_SNAP_DEV(dev))
return &kmnt[ksnap_home[dev - SCHFS_SNAP_DEV]];
if (SCHFS_IS_VOL_DEV(dev))
return &kmnt[dev - SCHFS_DEV];
return &kmnt[0]; // not ours; callers check mounted_for first
}
// The volume a device number reads through. For a snapshot this is the slot's
// own copy, holding an older tree root.
static struct schfs_vol *
vol_for(uint dev)
{
return SCHFS_IS_SNAP_DEV(dev) ? &ksnapvol[dev - SCHFS_SNAP_DEV]
: &mnt_for(dev)->vol;
}
static char *
curblk_for(uint dev)
{
return SCHFS_IS_SNAP_DEV(dev) ? ksnapcurblk[dev - SCHFS_SNAP_DEV]
: mnt_for(dev)->curblk;
}
static char *
datablk_for(uint dev)
{
return SCHFS_IS_SNAP_DEV(dev) ? ksnapdatablk[dev - SCHFS_SNAP_DEV]
: mnt_for(dev)->datablk;
}
static int
mounted_for(uint dev)
{
if (SCHFS_IS_SNAP_DEV(dev))
return ksnap_mounted[dev - SCHFS_SNAP_DEV];
if (SCHFS_IS_VOL_DEV(dev))
return kmnt[dev - SCHFS_DEV].mounted;
return 0;
}
// Is this volume writable? A snapshot never is: it is a view of a tree that has
// already been committed, and there is nowhere for a change to go.
static int
writable_for(uint dev)
{
return SCHFS_IS_VOL_DEV(dev) && mounted_for(dev) &&
!kmnt[dev - SCHFS_DEV].vol.readonly;
}
static int
tx_mine(struct schfs_mount *m)
{
struct proc *p = myproc();
return m->tx_owner != 0 && p != 0 &&
m->tx_owner == p && m->tx_owner_pid == p->pid;
}
// Take a volume's lock unless this process already holds it through a bracket.
// Every entry point in this file goes through these two rather than touching a
// sleeplock directly: it is not recursive, so a bracketed caller reaching a
// bare acquiresleep would wait on itself forever.
static void
klock(uint dev)
{
struct schfs_mount *m = mnt_for(dev);
if (!tx_mine(m))
acquiresleep(&m->lock);
}
static void
kunlock(uint dev)
{
struct schfs_mount *m = mnt_for(dev);
if (!tx_mine(m))
releasesleep(&m->lock);
}
// The bracket's state, for /proc/fsstat. A wedged filesystem looks exactly like
// a slow one from userland -- every path hangs in acquiresleep with no way to
// tell whether anything owns the lock. This says so.
//
// Read without the lock on purpose: the one time it matters is when the lock
// cannot be taken, and a reporting path that blocks on the thing it is
// reporting on is useless.
void
schfs_lockinfo(int slot, int *locked, int *owner_pid, int *depth)
{
if (slot < 0 || slot >= SCHFS_VOL_SLOTS) {
*locked = *owner_pid = *depth = -1;
return;
}
struct schfs_mount *m = &kmnt[slot];
*locked = m->mounted ? m->lock.locked : -1;
*owner_pid = m->tx_owner_pid;
*depth = m->tx_depth;
}
static int kdirty_locked(struct schfs_mount *m);
void
schfs_ktx_open(uint dev)
{
struct schfs_mount *m = mnt_for(dev);
if (tx_mine(m)) {
m->tx_depth++; // already inside one: nest, do not relock
return;
}
acquiresleep(&m->lock);
m->tx_owner = myproc();
m->tx_owner_pid = myproc() ? myproc()->pid : 0;
m->tx_depth = 1;
m->tx_dirty = 0;
}
int
schfs_ktx_close(uint dev)
{
struct schfs_mount *m = mnt_for(dev);
int r = 0;
if (!tx_mine(m))
return -1; // not ours to close
if (--m->tx_depth > 0)
return 0;
if (m->tx_dirty)
r = kdirty_locked(m);
m->tx_dirty = 0;
m->tx_owner = 0;
m->tx_owner_pid = 0;
releasesleep(&m->lock);
return r;
}
// Commit the open group, if it has anything in it. Caller holds m->lock and
// is not inside someone's bracket. A failed commit leaves the group dirty, so
// the next one tries again.
static int
kcommit_locked(struct schfs_mount *m)
{
if (!m->dirty || m->vol.readonly)
return 0;
int r = schfs_vol_commit(&m->vol);
if (r == 0)
m->dirty = 0;
return r;
}
static void
ksleep_ticks(uint n)
{
acquire(&tickslock);
uint start = ticks;
while (ticks - start < n)
sleep(&ticks, &tickslock);
release(&tickslock);
}
// The thread that commits what nobody synced. Started by the first change.
static void
schfs_flusher(void)
{
for (;;) {
ksleep_ticks(SCHFS_FLUSH_TICKS);
for (int i = 0; i < SCHFS_VOL_SLOTS; i++) {
struct schfs_mount *m = &kmnt[i];
if (!m->mounted || !m->dirty ||
ticks - m->dirty_since < SCHFS_COMMIT_TICKS)
continue;
acquiresleep(&m->lock);
if (m->mounted) {
int r = kcommit_locked(m);
if (r != 0)
cprintf("schfs: commit of volume %d failed (%d); retrying\n", i, r);
}
releasesleep(&m->lock);
}
}
}
// Note that the open group has changes, and commit it now if waiting would
// hurt. Caller holds m->lock.
//
// Blocks a group frees stay unusable until it commits -- the committed tree
// still points at them -- so a group rewriting a large file can tie up free
// space that is really free. Once that is a quarter of what is left, the
// group commits early rather than report ENOSPC on a disk with room on it.
static int
kdirty_locked(struct schfs_mount *m)
{
static int flusher;
if (!m->dirty) {
m->dirty = 1;
m->dirty_since = ticks;
}
if (!flusher && kthread_create(schfs_flusher, "schfsd") >= 0)
flusher = 1;
struct schfs_space *sp = &m->vol.space;
if (sp->nblocks && sp->pending > sp->free_blocks / 4)
return kcommit_locked(m);
return 0;
}
// Is the volume down to the part of its reserve that new names may not use?
// See schfs_space_low(). Caller holds the lock.
static int
klow(uint dev)
{
return schfs_space_low(&mnt_for(dev)->vol.space, 2, 0);
}
// Shorten a file towards `size` in steps, from the end, each one a whole
// operation the group may commit after -- as ext4 restarts its transaction
// through a long truncate. The caller does the last step. Lock held.
//
// Freeing 80 MB at once deleted 20000 checksum records in one operation, and
// every block that freed stays unusable until a commit, so `rm` on a full disk
// ran out of room halfway through deleting and left the file half there.
// Returns 0, or -1 if there is no such object.
static int
kshrink_locked(uint dev, uint64_t ino, uint64_t size)
{
struct schfs_vol *v = vol_for(dev);
struct schfs_mount *m = mnt_for(dev);
struct schfs_cursor c;
struct schfs_inode in;
schfs_cursor_init(&c, &v->tree, curblk_for(dev));
int r = schfs_inode_get(v, &c, ino, &in);
if (r != 0 || in.type != SCHFS_T_FILE)
return r;
uint64_t at = in.size;
while (r == 0 && at > size + SCHFS_FREE_STEP) {
at -= SCHFS_FREE_STEP;
r = schfs_file_truncate(v, ino, at, datablk_for(dev));
if (r == 0 && !tx_mine(m))
r = kdirty_locked(m);
}
return r;
}
// Start and finish one transaction. `changed` says whether anything was
// written, so a read does not dirty the group.
static void
ktx_begin(uint dev)
{
klock(dev);
}
static int
ktx_end(uint dev, int changed, int r)
{
struct schfs_mount *m = mnt_for(dev);
if (tx_mine(m)) {
// Inside an explicit bracket: the commit belongs to whoever opened it.
if (changed && r >= 0)
m->tx_dirty = 1;
return r;
}
if (changed && r >= 0) {
int c = kdirty_locked(m);
if (c != 0)
r = c;
}
kunlock(dev);
return r;
}
// Every hook below takes the mount as its ctx. It used to be a bare device
// number, which was enough while the allocator could reach the one volume
// there was by name; with slots, the block I/O and the space manager have to
// agree about *which* volume they are serving, and the mount is the one thing
// that carries both.
static int
kd_read(void *ctx, uint64_t blk, void *out)
{
struct schfs_kdev *d = &((struct schfs_mount *)ctx)->kdev;
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, 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 = &((struct schfs_mount *)ctx)->kdev;
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.
//
// bgetblk, not bread: the block is overwritten whole, so what the disk held
// there does not matter. Reading it first cost a disk read for every block
// a write allocated, which is most of them.
//
// bdirty, not bwrite. The B-tree seals and writes a node on every record it
// inserts, so a leaf that is collecting one file's checksums was written to
// the disk once per 4 KB of that file -- 1288 block writes to store 256
// blocks of data. Leaving them dirty in the cache collapses every repeat
// into the last one, and costs nothing in safety because kd_flush below
// gives the format the one ordering guarantee it actually asks for.
for (int i = 0; i < SCHFS_BPB; i++) {
struct buf *b = bgetblk(d->dev, blk * SCHFS_BPB + i);
memmove(b->data, (const char *)in + i * BSIZE, BSIZE);
bdirty(b);
brelse(b);
}
return 0;
}
// The barrier. schfs_vol_commit calls this either side of the uberblock write,
// and those two calls are the entire reason deferring above is safe.
static int
kd_flush(void *ctx)
{
struct schfs_kdev *d = &((struct schfs_mount *)ctx)->kdev;
return bflush(d->dev);
}
// Allocation goes through this volume's own space manager.
static uint64_t
kd_alloc(void *ctx)
{
return schfs_vol_alloc(&((struct schfs_mount *)ctx)->vol);
}
static uint64_t
kd_alloc_run(void *ctx, uint64_t n)
{
return schfs_vol_alloc_run(&((struct schfs_mount *)ctx)->vol, n);
}
static void
kd_free(void *ctx, uint64_t b, uint64_t gen)
{
schfs_vol_free(&((struct schfs_mount *)ctx)->vol, b, gen);
}
// 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();
}
// The other half of kd_palloc, and the reason unmount stopped leaking: the
// allocation map is up to SCHFS_SPACE_PAGES pages of kalloc'd memory, and
// nothing ever gave it back. A mount/unmount cycle leaked the lot.
static void
kd_pfree(void *p)
{
kfree((char *)p);
}
// Mount the SchFS volume on block device `bdev` into slot `slot`.
//
// Never panics: a disk that is not SchFS, or is an unreadable SchFS, simply
// leaves the slot unmounted. Returns the filesystem device number on success,
// or a negative error -- distinct codes, because "it did not work" is useless
// to a caller deciding between "wrong disk" and "out of memory".
static int
kmount_slot(int slot, uint bdev)
{
struct schfs_mount *m = &kmnt[slot];
if (m->mounted)
return -EBUSY;
initsleeplock(&m->lock, "schfs");
m->dirty = 0;
if (m->curblk == 0) m->curblk = kalloc();
if (m->datablk == 0) m->datablk = kalloc();
if (m->ctxa == 0) m->ctxa = kalloc();
if (m->ctxb == 0) m->ctxb = kalloc();
if (m->ctxc == 0) m->ctxc = kalloc();
if (m->curblk == 0 || m->datablk == 0 ||
m->ctxa == 0 || m->ctxb == 0 || m->ctxc == 0) {
cprintf("schfs: no memory for block buffers\n");
return -ENOMEM;
}
m->kdev.dev = bdev;
struct schfs_bdev bd;
bd.ctx = m;
bd.bread = kd_read;
bd.bwrite = kd_write;
bd.balloc = kd_alloc;
bd.bfree = kd_free;
bd.balloc_run = kd_alloc_run;
bd.bflush = kd_flush;
struct schfs_ctx cx;
cx.a = m->ctxa;
cx.b = m->ctxb;
cx.c = m->ctxc;
// 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.
// The live root fills its ramdisk, however much memory that turned out to
// be. See kernel/dev/ramdisk.c.
m->vol.grow_to = (bdev == RAMDISK_DEV) ? ramdisk_blocks() : 0;
int r = schfs_vol_open(&m->vol, &bd, &cx, kd_palloc, kd_pfree);
if (r == -1)
return -EINVAL; /* not a SchFS volume at all */
if (r == -4) {
cprintf("schfs: device %d is larger than %d GB and cannot be mapped\n",
bdev,
(int)(SCHFS_MAX_BLOCKS * SCHFS_BLKSIZE / (1024 * 1024 * 1024)));
return -EFBIG;
}
if (r == -5) {
cprintf("schfs: no memory for the allocation map of device %d\n", bdev);
return -ENOMEM;
}
if (r == -3) {
// Refused on purpose, and the wording matters: the volume is fine, this
// driver is the thing that is out of date. Older uberblocks may well be
// readable, and mounting one would quietly roll the filesystem back to
// before the upgrade -- so it is not offered.
cprintf("schfs: device %d was written by a newer SchFS than this one "
"understands; refusing rather than mounting a stale root\n", bdev);
return -ENOTSUP;
}
if (r != 0) {
cprintf("schfs: device %d has a SchFS uberblock but an unreadable tree\n",
bdev);
return -EIO;
}
m->mounted = 1;
// Free what was unlinked while open and never closed: the space scan above
// found them with no names. Nothing can reach them, so this is the last
// chance to give their blocks back.
if (m->vol.norphans && !m->vol.readonly) {
int n = m->vol.norphans;
for (int i = 0; i < n; i++)
schfs_kfree(SCHFS_DEV + slot, m->vol.orphans[i]);
schfs_ksync(SCHFS_DEV + slot);
cprintf("schfs: freed %d object%s unlinked while open%s\n", n,
n == 1 ? "" : "s", m->vol.orphans_more ? "; more next mount" : "");
m->vol.norphans = 0;
}
kinfo("schfs: mounted '%s' on device %d, %d MB, generation %d\n",
m->vol.label, bdev,
(int)(m->vol.total_blocks * SCHFS_BLKSIZE / (1024 * 1024)),
(int)m->vol.generation);
// Say so, loudly, when the mount had to reach past newer uberblocks to find
// a root it could read. The volume is consistent -- that is what the ring is
// for -- but transactions were lost, and a filesystem that rolls back
// silently is one that teaches you to trust it when you should not.
if (m->vol.recovered)
cprintf("schfs: WARNING: %d newer transaction group%s unreadable; "
"rolled back to generation %d\n",
m->vol.recovered, m->vol.recovered == 1 ? " was" : "s were",
(int)m->vol.generation);
return SCHFS_DEV + slot;
}
// The boot mount: slot 0, which is the root filesystem. Kept as its own entry
// point because proc.c calls it before there is anything to ask.
void
schfs_kmount(uint bdev)
{
kmount_slot(0, bdev);
}
// Mount another SchFS disk into a free slot. Returns its filesystem device
// number, or a negative errno.
int
schfs_kmount_dev(uint bdev)
{
// Already mounted? Hand back the slot it is in rather than mounting a second
// view of the same disk: two slots over one device would each derive their
// own allocation map from the same blocks, and both would then hand out the
// same free block to different files.
for (int i = 0; i < SCHFS_VOL_SLOTS; i++)
if (kmnt[i].mounted && kmnt[i].kdev.dev == bdev)
return -EBUSY;
for (int i = 1; i < SCHFS_VOL_SLOTS; i++)
if (!kmnt[i].mounted)
return kmount_slot(i, bdev);
return -EBUSY; /* every slot in use */
}
// Release a mounted volume. The blocks it holds in the buffer cache are pushed
// out first: a slot that is reused, or a disk that is unplugged, must not leave
// a transaction half on the medium.
int
schfs_kumount(uint dev)
{
if (!SCHFS_IS_VOL_DEV(dev))
return -EINVAL;
int slot = dev - SCHFS_DEV;
if (slot == 0)
return -EBUSY; /* the root filesystem; nothing to fall back to */
struct schfs_mount *m = &kmnt[slot];
if (!m->mounted)
return -EINVAL;
// A snapshot of this volume outliving it would read through a mount that no
// longer exists.
for (int i = 0; i < SCHFS_SNAP_SLOTS; i++)
if (ksnap_mounted[i] && ksnap_home[i] == slot)
return -EBUSY;
acquiresleep(&m->lock);
if (kcommit_locked(m) != 0)
cprintf("schfs: unmount: the last commit failed; changes since the "
"previous one are lost\n");
bflush(m->kdev.dev);
// Give the allocation map back. It is rebuilt from the tree on every mount,
// so there is nothing in it worth keeping -- and at 8 KiB per GiB of volume,
// keeping it was the largest thing this kernel leaked.
schfs_space_release(&m->vol.space);
m->mounted = 0;
releasesleep(&m->lock);
return 0;
}
// Mount a snapshot of the volume on `dev` by name, read-only. Returns the
// device number of the slot it landed in, or a negative error.
//
// The snapshot's records live in the *live* tree, so the name is resolved
// there; what comes back is a root, and the slot's volume is a copy of the live
// one with its tree pointed at it. Nothing else differs -- same device, same
// node format, same checksums.
int
schfs_ksnap_mount(uint dev, const char *name, unsigned namelen)
{
if (!SCHFS_IS_VOL_DEV(dev) || !mounted_for(dev))
return -1;
int home = dev - SCHFS_DEV;
int slot = -1;
for (int i = 0; i < SCHFS_SNAP_SLOTS; i++)
if (!ksnap_mounted[i]) { slot = i; break; }
if (slot < 0)
return -2; /* every slot in use */
if (ksnapcurblk[slot] == 0) ksnapcurblk[slot] = kalloc();
if (ksnapdatablk[slot] == 0) ksnapdatablk[slot] = kalloc();
if (ksnapcurblk[slot] == 0 || ksnapdatablk[slot] == 0)
return -3;
klock(dev);
struct schfs_cursor c;
fsstat.descents++;
schfs_cursor_init(&c, &vol_for(dev)->tree, curblk_for(dev));
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) {
kunlock(dev);
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. That is also why the slot records which mount
// it came from: mnt_for() sends a snapshot's device number to its live
// volume's lock, and unmounting that volume has to refuse while it is there.
ksnapvol[slot] = *vol_for(dev);
ksnapvol[slot].tree.root = sn.root;
ksnapvol[slot].tree.root_crc = sn.root_crc;
ksnapvol[slot].tree.root_gen = sn.root_generation;
// Belt and braces. Nothing can reach a snapshot through a write path --
// writable_for() requires a live volume device number -- but the copy above
// inherits the live volume's writability, and a flag that says the wrong
// thing is a trap for whoever adds the next entry point.
ksnapvol[slot].readonly = 1;
// Its own scratch, or a read through the snapshot would tread on a cursor
// the live volume still has positioned.
ksnapvol[slot].ctx.a = ksnapcurblk[slot];
ksnap_home[slot] = home;
ksnap_mounted[slot] = 1;
kunlock(dev);
return SCHFS_SNAP_DEV + slot;
}
void
schfs_ksnap_unmount(uint dev)
{
if (!SCHFS_IS_SNAP_DEV(dev))
return;
klock(dev);
ksnap_mounted[dev - SCHFS_SNAP_DEV] = 0;
kunlock(dev);
}
uint64
schfs_ksnap_root(uint dev)
{
if (!SCHFS_IS_SNAP_DEV(dev) || !ksnap_mounted[dev - SCHFS_SNAP_DEV])
return 0;
return ksnapvol[dev - SCHFS_SNAP_DEV].root_objectid;
}
// 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;
klock(dev);
struct schfs_cursor c;
fsstat.descents++;
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));
kunlock(dev);
return r;
}
int
schfs_kstat_dev(uint dev, uint64_t ino, struct schfs_inode *out)
{
if (!mounted_for(dev))
return -1;
klock(dev);
struct schfs_cursor c;
fsstat.descents++;
schfs_cursor_init(&c, &vol_for(dev)->tree, curblk_for(dev));
int r = schfs_inode_get(vol_for(dev), &c, ino, out);
kunlock(dev);
return r;
}
uint64
schfs_klookup_at_dev(uint dev, uint64_t dir, const char *name, unsigned len)
{
if (!mounted_for(dev))
return 0;
klock(dev);
struct schfs_cursor c;
fsstat.descents++;
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);
kunlock(dev);
return r == 0 ? ino : 0;
}
// Walk a directory from a given hash. What getdents resumes with: seeking
// straight to where the last call stopped is the difference between a listing
// that costs one descent per call and one that re-walks everything before it.
int
schfs_kreaddir_from_dev(uint dev, uint64_t dir, uint64_t start_hash,
schfs_dir_fn fn, void *arg)
{
if (!mounted_for(dev))
return -1;
klock(dev);
struct schfs_cursor c;
fsstat.descents++;
schfs_cursor_init(&c, &vol_for(dev)->tree, curblk_for(dev));
int r = schfs_dir_iterate_from(vol_for(dev), &c, dir, start_hash, fn, arg);
kunlock(dev);
return r;
}
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;
klock(dev);
struct schfs_cursor c;
fsstat.descents++;
schfs_cursor_init(&c, &vol_for(dev)->tree, curblk_for(dev));
int r = schfs_dir_iterate(vol_for(dev), &c, dir, fn, arg);
kunlock(dev);
return r;
}
// Is any SchFS volume mounted at all? Asked at boot, before there is a device
// number to name.
int
schfs_kmounted(void)
{
return kmnt[0].mounted;
}
// Resolve a path within one volume. Returns the object id, or 0.
uint64_t
schfs_klookup(uint dev, const char *path)
{
if (!mounted_for(dev))
return 0;
klock(dev);
struct schfs_cursor c;
fsstat.descents++;
schfs_cursor_init(&c, &vol_for(dev)->tree, curblk_for(dev));
uint64_t r = schfs_path_lookup(vol_for(dev), &c, path);
kunlock(dev);
return r;
}
struct schfs_vol *
schfs_kvol(uint dev)
{
return mounted_for(dev) ? vol_for(dev) : 0;
}
/* ------------------------------------------------------------------ */
/* Write side */
/* */
/* Each of these is one transaction unless the caller has a bracket */
/* open: 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. */
/* */
/* Every one takes the filesystem device number first. They used to */
/* name the single volume directly, which is why sys_mount had nothing */
/* it could do with its source argument. */
/* ------------------------------------------------------------------ */
int
schfs_kcreate(uint dev, uint64_t dir, const char *name, unsigned namelen,
uint16_t type, uint32_t mode, uint16_t major, uint16_t minor,
uint64_t *out)
{
if (!writable_for(dev))
return -1;
ktx_begin(dev);
if (klow(dev))
return ktx_end(dev, 0, -3);
int r = schfs_create(vol_for(dev), dir, name, namelen, type, mode, major,
minor, know(), out);
return ktx_end(dev, 1, r);
}
long
schfs_kwrite(uint dev, uint64_t ino, uint64_t off, const void *buf, unsigned n)
{
if (!writable_for(dev))
return -1;
ktx_begin(dev);
long r = schfs_file_write(vol_for(dev), ino, off, buf, n, datablk_for(dev));
// 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(dev, 1, r < 0 ? (int)r : 0);
return e == 0 ? r : (long)e;
}
int
schfs_ktruncate(uint dev, uint64_t ino, uint64_t size)
{
if (!writable_for(dev))
return -1;
ktx_begin(dev);
int r = kshrink_locked(dev, ino, size);
if (r == 0)
r = schfs_file_truncate(vol_for(dev), ino, size, datablk_for(dev));
return ktx_end(dev, 1, r);
}
int
schfs_kunlink(uint dev, uint64_t dir, const char *name, unsigned namelen)
{
if (!writable_for(dev))
return -1;
ktx_begin(dev);
int r = schfs_unlink(vol_for(dev), dir, name, namelen);
return ktx_end(dev, 1, r);
}
int
schfs_klink(uint dev, uint64_t dir, const char *name, unsigned namelen,
uint64_t ino)
{
if (!writable_for(dev))
return -1;
ktx_begin(dev);
if (klow(dev))
return ktx_end(dev, 0, -3);
int r = schfs_link(vol_for(dev), dir, name, namelen, ino);
return ktx_end(dev, 1, r);
}
int
schfs_krename(uint dev, uint64_t olddir, const char *oldname, unsigned oldlen,
uint64_t newdir, const char *newname, unsigned newlen)
{
if (!writable_for(dev))
return -1;
ktx_begin(dev);
int r = schfs_rename(vol_for(dev), olddir, oldname, oldlen,
newdir, newname, newlen);
return ktx_end(dev, 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_dev,
// edits the struct, and hands it back.
int
schfs_ksetattr(uint dev, uint64_t ino, const struct schfs_inode *in)
{
if (!writable_for(dev))
return -1;
ktx_begin(dev);
int r = schfs_inode_put(vol_for(dev), ino, in);
return ktx_end(dev, 1, r);
}
// Commit the open group now. What sync(2) and fsync(2) come down to: the
// group is the unit that commits, so making one file durable makes everything
// written before it durable too, as on ext4's data=ordered.
//
// Inside a bracket the commit belongs to whoever opened it, so this only
// marks the group, and the bracket's close commits it.
int
schfs_ksync(uint dev)
{
if (!writable_for(dev))
return 0;
struct schfs_mount *m = mnt_for(dev);
if (tx_mine(m)) {
m->tx_dirty = 1;
return 0;
}
acquiresleep(&m->lock);
int r = kcommit_locked(m);
releasesleep(&m->lock);
return r;
}
// Sync every mounted volume. What sys_sync wants: it names no filesystem.
int
schfs_ksync_all(void)
{
int r = 0;
for (int i = 0; i < SCHFS_VOL_SLOTS; i++)
if (kmnt[i].mounted) {
int e = schfs_ksync(SCHFS_DEV + i);
if (e != 0)
r = e;
}
return r;
}
/* ------------------------------------------------------------------ */
/* 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. */
/* ------------------------------------------------------------------ */
// 1 if the directory holds no entries, 0 if it does, -1 on error.
int
schfs_kdirempty(uint dev, uint64_t dir)
{
if (!mounted_for(dev))
return -1;
klock(dev);
int r = schfs_dir_empty(vol_for(dev), dir);
kunlock(dev);
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(uint dev, uint16_t type, uint32_t mode, uint64_t *out)
{
if (!writable_for(dev))
return -1;
ktx_begin(dev);
if (klow(dev))
return ktx_end(dev, 0, -3);
struct schfs_vol *v = vol_for(dev);
uint64_t ino = schfs_alloc_objectid(v);
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 = v->tree.generation;
int r = schfs_inode_put(v, ino, &in);
if (r == 0 && out)
*out = ino;
return ktx_end(dev, 1, r);
}
int
schfs_kdiradd(uint dev, uint64_t dir, const char *name, unsigned namelen,
uint64_t ino, uint16_t type)
{
if (!writable_for(dev))
return -1;
ktx_begin(dev);
if (klow(dev))
return ktx_end(dev, 0, -3);
int r = schfs_dir_add(vol_for(dev), dir, name, namelen, ino, type);
return ktx_end(dev, 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(uint dev, uint64_t dir, const char *name, unsigned namelen)
{
if (!writable_for(dev))
return -1;
ktx_begin(dev);
int r = schfs_dir_del(vol_for(dev), dir, name, namelen);
return ktx_end(dev, 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(uint dev, uint64_t ino)
{
if (!writable_for(dev))
return -1;
ktx_begin(dev);
int r = kshrink_locked(dev, ino, 0);
if (r == 0 || r == -1)
r = schfs_object_free(vol_for(dev), ino);
return ktx_end(dev, 1, r);
}
// The root object of one volume. SCHFS_DEV on its own still means the root
// filesystem, which is slot 0, so callers that only ever wanted that are
// unchanged.
uint64_t
schfs_kroot(uint dev)
{
return mounted_for(dev) ? vol_for(dev)->root_objectid : 0;
}
// Share one file's data blocks with another without copying them.
//
// One transaction, because a half-finished clone would be a file holding some
// of another's blocks and some of its own -- and no caller could tell.
int
schfs_kreflink(uint dev, uint64_t src, uint64_t dst)
{
if (!writable_for(dev))
return -1;
ktx_begin(dev);
if (klow(dev))
return ktx_end(dev, 0, -3);
int r = schfs_file_reflink(vol_for(dev), src, dst);
return ktx_end(dev, 1, r);
}
/* ------------------------------------------------------------------ */
/* Attributes, and the index over them */
/* ------------------------------------------------------------------ */
int
schfs_kattr_set(uint dev, uint64_t ino, const char *name, unsigned namelen,
const void *val, unsigned vallen, uint16_t type)
{
if (!writable_for(dev))
return -1;
ktx_begin(dev);
if (klow(dev))
return ktx_end(dev, 0, -3);
int r = schfs_attr_set(vol_for(dev), ino, name, namelen, val, vallen, type);
return ktx_end(dev, 1, r);
}
int
schfs_kattr_get(uint dev, uint64_t ino, const char *name, unsigned namelen,
void *out, unsigned cap, unsigned *vallen)
{
if (!mounted_for(dev))
return -1;
klock(dev);
int r = schfs_attr_get(vol_for(dev), ino, name, namelen, out, cap, vallen, 0);
kunlock(dev);
return r;
}
int
schfs_kattr_del(uint dev, uint64_t ino, const char *name, unsigned namelen)
{
if (!writable_for(dev))
return -1;
ktx_begin(dev);
int r = schfs_attr_del(vol_for(dev), ino, name, namelen);
return ktx_end(dev, 1, r);
}
// Start indexing an attribute. Covers values written after this only -- there
// is no reindexing pass, and the caller is told so rather than left to assume.
int
schfs_kindex(uint dev, const char *name, unsigned namelen)
{
if (!writable_for(dev))
return -1;
ktx_begin(dev);
if (klow(dev))
return ktx_end(dev, 0, -3);
int r = schfs_index_create(vol_for(dev), name, namelen);
return ktx_end(dev, 1, r);
}
int
schfs_kquery(uint dev, const char *name, unsigned namelen,
const void *val, unsigned vallen, uint64 *out, int cap)
{
if (!mounted_for(dev))
return -1;
klock(dev);
int r = schfs_query_equal(vol_for(dev), name, namelen, val, vallen, out, cap);
kunlock(dev);
return r;
}
// Volume-level numbers for statfs. Returns 0 if `dev` names a mounted SchFS
// volume and fills in the three counts, or -1 if it does not.
//
// The free count is the live one the space map maintains, not the figure in the
// uberblock: they agree at every commit and differ in between, and "in between"
// is exactly when someone runs df to find out whether their last write fitted.
//
// A snapshot reports its own total but the live volume's free space, which is
// the truth -- there is one pool of blocks and the snapshot is a second view of
// it, not a second filesystem with room of its own.
int
schfs_kstatfs(uint dev, uint64 *total, uint64 *freeblocks, uint64 *objects)
{
if (!mounted_for(dev))
return -1;
klock(dev);
struct schfs_vol *v = vol_for(dev);
// Free space comes from the *live* volume even when `dev` is a snapshot:
// there is one pool of blocks and a snapshot is a second view of it, not a
// filesystem with room of its own.
struct schfs_vol *live = &mnt_for(dev)->vol;
if (total) *total = v->total_blocks;
if (freeblocks) *freeblocks = live->space.nblocks ? live->space.free_blocks
: live->free_blocks;
// Object ids are handed out and never reused, so this is "ids consumed",
// which is the closest honest answer to "how many files". There is no inode
// table to be full of, and no number of free ones to report.
if (objects) *objects = v->next_objectid > SCHFS_ROOT_OBJECTID
? v->next_objectid - SCHFS_ROOT_OBJECTID : 0;
kunlock(dev);
return 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_SOCK ? "socket " :
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;
}
// One volume's section of /proc/schfs.
static void
kinfo_one(char *buf, int cap, int *len, uint dev)
{
struct schfs_mount *m = &kmnt[dev - SCHFS_DEV];
kp_puts(buf, cap, len, "\nfsdev: ");
kp_putu(buf, cap, len, dev);
kp_puts(buf, cap, len, "\nblockdev: ");
kp_putu(buf, cap, len, m->kdev.dev);
kp_puts(buf, cap, len, "\nlabel: ");
kp_puts(buf, cap, len, m->vol.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, m->vol.total_blocks);
kp_puts(buf, cap, len, "\nfree_blocks: ");
kp_putu(buf, cap, len, m->vol.space.nblocks ? m->vol.space.free_blocks
: m->vol.free_blocks);
kp_puts(buf, cap, len, "\ngeneration: ");
kp_putu(buf, cap, len, m->vol.generation);
kp_puts(buf, cap, len, "\ntree_root: ");
kp_putu(buf, cap, len, m->vol.tree.root);
kp_puts(buf, cap, len, "\nroot_objectid: ");
kp_putu(buf, cap, len, m->vol.root_objectid);
if (m->vol.recovered) {
// Reported here as well as on the console, because the console message
// scrolled past hours ago and this is the thing anyone looks at afterwards.
kp_puts(buf, cap, len, "\nrecovered: ");
kp_putu(buf, cap, len, (uint64)m->vol.recovered);
kp_puts(buf, cap, len, " newer transaction group(s) were unreadable");
}
kp_puts(buf, cap, len, "\n\nroot directory:\n");
struct kp_ctx c = { buf, cap, len, 0 };
if (schfs_kreaddir_dev(dev, m->vol.root_objectid, kp_entry, &c) != 0) {
kp_puts(buf, cap, len, " (directory read failed)\n");
return;
}
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");
}
}
int
schfs_kinfo(char *buf, int cap)
{
int len = 0;
int any = 0;
for (int i = 0; i < SCHFS_VOL_SLOTS; i++)
if (kmnt[i].mounted) {
kinfo_one(buf, cap, &len, SCHFS_DEV + i);
any = 1;
}
for (int i = 0; i < SCHFS_SNAP_SLOTS; i++)
if (ksnap_mounted[i]) {
kp_puts(buf, cap, &len, "\nsnapshot fsdev ");
kp_putu(buf, cap, &len, SCHFS_SNAP_DEV + i);
kp_puts(buf, cap, &len, " of fsdev ");
kp_putu(buf, cap, &len, SCHFS_DEV + ksnap_home[i]);
kp_puts(buf, cap, &len, ", generation ");
kp_putu(buf, cap, &len, ksnapvol[i].tree.root_gen);
kp_puts(buf, cap, &len, "\n");
}
if (!any)
kp_puts(buf, cap, &len, "no SchFS volume mounted\n");
return len;
}