// 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 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); } 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; }