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.
623 lines
19 KiB
C
623 lines
19 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 "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;
|
|
}
|