Sixty-five headers sat side by side in include/, so `#include "irq.h"` said nothing about where irq.h was or what it belonged to, and errno.h, stat.h and fcntl.h at the top shadowed the system's for any host tool built with -I./include. They are in folders now -- arch/ core/ fs/ dev/ gfx/ net/ uapi/ (and ldls/ for the Linux driver layer) -- and every include names the folder, as Linux's do: "core/proc.h". Done mechanically and checked the hard way: the kernel built from this tree disassembles identically to the one built from the tree before it. The full image boots, and userland is the same set of programs. Paths in docs and comments follow; docs/guides/layout.md has a table of what is where, and tools/fix-includes.py rewrites old-style includes from the folders in the tree, for code written before this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
9.9 KiB
| name | description | metadata | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| schfs-phase3-writes | SchFS phase 3 is done — full write API, in-RAM allocation map derived from the tree; the only gap left is VFS integration |
|
Phase 3 done (2026-08-03), including VFS integration. SchFS is a complete, mountable, read-write filesystem. Follows schfs-phase2-mount.
mkdir /mnt && mount -t schfs /dev/hdb /mnt
then ordinary ls/cat/>/>>/mkdir/cp/rm/rmdir all work on it.
The VFS now has a real dispatch layer: struct fs_ops in include/fs/file.h,
one table per filesystem, chosen by iget() from the device number and hung off
the inode. v1's implementations kept their bodies and are named v1_*; SchFS's
live in kernel/fs/schfs/vfs.c. Adding a filesystem is a new table, not
another branch in fifteen functions. getdents64 is an fs_op for the same
reason — v1's struct dirent (16-bit inum, 14-byte name) cannot represent a
SchFS entry, so each filesystem emits the record stream itself.
Mounting is a substitution during path resolution (mount_add/mount_cross in
fs.c), recorded nowhere on disk. .. at a mounted root has to be redirected
out explicitly, or the mount becomes a subtree you can enter and never leave.
Two design decisions that contradict the older design doc — the doc is now right, older notes are not:
- Free space is NOT tracked as tree keys. Allocating from a tree-resident map
mutates the tree, which allocates, which changes the map. Instead the map is
derived at mount by walking the tree (
kernel/fs/schfs/space.c) and lives only in RAM. It cannot disagree with the filesystem because it is computed from it. This is what caps a mounted volume at 64 GiB (SCHFS_SPACE_PAGES). - Data blocks carry a generation too (
schfs_extent.gen, 64-bit). Same rule as tree nodes: a run stamped with the group being built may be overwritten in place. Without it every 100-byte write copied a 4 KB block and split an extent.
Testing. make schfs-check now runs three suites — add schfs-objtest
(tools/schfs_objtest.c, memory-backed, covers files/dirs/space accounting) to
the two that existed. sh tools/mutate.sh injects nine bugs; all nine must be
caught.
The two tests that actually earn their keep, both found by mutation testing after everything else passed:
- Filling and emptying the volume three times on one mount and checking each round stores the same amount. A leaked bitmap heals on remount, so a remount-based check cannot see it; only spending the space again can.
- Overwriting a block after a commit. Within one transaction group both the tree and the data rewrite in place, so a test that never commits never runs the copy-on-write path at all.
struct schfs_vol is not copyable — tree.dev points at its own dev
field. Open into the storage it will live in.
Migrating root off the xv6 legacy FS (2026-08-09)
Nexoniarz: kill the legacy FS, keep SchFS ("SchFS is goat"). Agreed — and it is
a migration, not a rewrite: SchFS is 4,204 lines across 7 files against
kernel/fs/fs.c's 1,830, and it already builds images, mounts, and dispatches
through struct fs_ops. Every xv6 legacy problem (DIRSIZ 14, the 16-byte
dirent, the HTree byte layout, ushort inum) lives below a filesystem that has
already been replaced but never promoted.
The one line holding xv6 in charge:
kernel/fs/fs.c:1642: ip = (*spath == '/') ? iget(ROOTDEV, ROOTINO) : idup(start);
Blocker 1, now cleared: mkschfs would not build on Windows. It needed
sys/sysmacros.h (major/minor) and lstat/readlink/S_ISLNK, none of which
mingw has. Stubbed under __MINGW32__ — honest here because Windows has no
mknod and find sysroot -type l returns nothing, so neither device nodes nor
symlinks can appear in the tree being imaged. If symlinks are ever staged
(procfs wants /proc/<pid>/exe to be one) this host can no longer build a
faithful image and the stub must become a real reparse-point read.
make build/schfs.img now works: 586 of 8000 blocks, 2344 KB, generation 2.
Blocker 2, confirmed and not started: booting with schfs.img as the root
disk panics fsinit: invalid file system — v1 fsinit() probes ROOTDEV for a
v1 superblock and halts. So the order is:
fsinit/nameireach the root throughfs_opsinstead ofiget(ROOTDEV, ROOTINO).- A
ramroot-equivalent SchFS image, so root can be swapped without a disk. - Point
ROOTDEVat the SchFS volume; keep v1 mountable until it is proven. - Delete
fs.c.
Do this with a fresh session. Swapping the root filesystem is the change most able to leave an unbootable image, and step 1 wants room to boot, fail and retry.
Sizing step 1 (2026-08-09) -- it is small
ops_for(dev) in kernel/fs/fs.c:278 already dispatches the whole VFS:
if (dev == SCHFS_DEV) return &schfs_ops;
if (dev == PROCFS_DEV) return &procfs_ops;
return &v1_ops; // legacy, only because it is the default
struct fs_ops (include/fs/file.h:35) is a complete vtable -- iload, istore,
ifree, read, write, truncate, dirops -- and SchFS implements all of it. So
fs.c is no longer "the filesystem"; it is the fallback branch. Killing it is
three edits, not a rewrite:
fsinit()stops requiring a v1 superblock on ROOTDEV; callschfs_kmount().ROOTDEVbecomesSCHFS_DEVsoops_forreturnsschfs_opsfor/.namei'siget(ROOTDEV, ROOTINO)usesschfs_kroot()for the root inode.
Then v1_ops has no caller and fs.c can go. Keep a build flag or a second
disk to boot v1 until SchFS has carried / through a full bootcheck run.
Nexoniarz calls the legacy one "ShitFS v1" -- worth knowing that "SchFS v1" in older notes means the xv6-derived filesystem, not early SchFS.
The recycle bin, removed 2026-08-09
sys_delete (syscall 51, NO_LINUX) set an on-disk inode flag showmode = 0,
and fileread/filewrite then refused every operation with a printf. A
"deleted" file still existed, still had its name, still held its blocks, and
answered read() with -1 -- and since nothing was freed, deleting filled the disk
faster. Both gates removed; the syscall entry and the on-disk field stay only
because pulling them touches the inode layout, and both die with fs.c.
SchFS is the root filesystem -- default since 2026-08-09
make builds it, make qemu boots it, tools/bootcheck.ps1 tests it. There is
no opt-in switch any more: schfsroot, -DSCHFS_ROOT and the -SchFS /
-SchFSRoot bootcheck switches are all gone. schunix.img depends on
build/schfs.img, and a kernel handed anything else on device 1 panics in
forkret().
Full suite green -- base, -Tty, -Ps, -Sock, -Fb, -Usb, -Evdev.
Three edits carried / over:
kernel/proc/proc.cforkret:schfs_kmount(ROOTDEV)+schfs_kmounted()check, replacingfsinit(ROOTDEV). Mounted there and not in main() because mounting reads blocks, bread() sleeps, and sleeping needs a process context.schfs_kmountreturns void -- it reports failure by leaving the volume unmounted, because elsewhere it is a probe.kernel/fs/fs.cnamei: root isiget(SCHFS_DEV, schfs_kroot()), soops_for()routes/throughschfs_ops. SCHFS_DEV is 64 -- a synthetic number for ops dispatch, not the disk the volume lives on.kernel/fs/log.c:begin_op/end_opreturn immediately, the rest is#if 0. The v1 log made multi-block v1 updates atomic; SchFS gets that from CoW. They stay as no-ops because sys_fs.c calls them in dozens of places.
The bug that cost the most, and would cost it again: sinit() sets pid 1's
cwd with namei("/") in main(), before forkret mounts anything. The old v1
root survived that because iget(ROOTDEV, ROOTINO) is two compile-time
constants that touch no disk -- the inode fills in lazily at the first ilock.
SchFS reads its root object id off the uberblock, so pid 1 got a cwd built from
an unmounted volume. Absolute paths never consult cwd, so exec("/bin/init")
worked and everything looked healthy; the first relative lookup is init's
open("console"), which failed, so init had no fd 0/1/2, so every shell it
spawned exited at once and init respawned it forever. Silent boot, no
init: ready, indistinguishable from a hang. Fixed by re-pointing
myproc()->cwd after the mount.
Also: the kernel execs /bin/init (lib/initcode.S), not /init. Probing the
wrong path made lookup look broken when it was not.
Still to do -- the v1 format code is compiled but unreachable from /.
fs.c cannot simply be deleted: iget/ilock/iput/namei/readi, the whole VFS
layer, live in it too. Only the v1 on-disk format goes -- fsinit, the
superblock, bmap/balloc, v1_ops, and the third-party HTree/extent code -- plus
log.c, tools/mkfs.c and build/fs.img. The MIT attribution in NOTICE stays
regardless (schunix-licensing).
fs.img is still built: the -Usb suite uses it as the USB stick.
Two harness bugs found doing this, both of which faked OS failures
- bootcheck slept a flat 4s between commands.
ls /bin | wc -loverran it, so the next command was typed while the shell was busy -- the tty echoed it, no reader took it, and it landed concatenated onto the following one. The transcript readcat /proc/pcicat /proc/schfsand three PCI checks failed, looking exactly like a broken PCI driver. Now it waits for the prompt by polling the logfile QEMU is writing. (It still must not read QEMU's stdout -- that deadlocks, see the script header.) - The USB stick was
$hdb, so the root volume was attached twice, once as a disk and once over USB. That failed the storage checks and the xHCI interrupt check. The stick is pinned tobuild/fs.imgnow and -Usb is green.
The general lesson, twice over: a red check is not evidence until the harness is ruled out. Both of these looked like driver bugs.