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
194 lines
9.9 KiB
Markdown
194 lines
9.9 KiB
Markdown
---
|
|
name: schfs-phase3-writes
|
|
description: "SchFS phase 3 is done — full write API, in-RAM allocation map derived from the tree; the only gap left is VFS integration"
|
|
metadata:
|
|
node_type: memory
|
|
type: project
|
|
originSessionId: a665e14b-86bd-4b41-a228-51377f3e9225
|
|
modified: 2026-08-03T13:59:08.496Z
|
|
---
|
|
|
|
**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:
|
|
|
|
1. `fsinit`/`namei` reach the root through `fs_ops` instead of
|
|
`iget(ROOTDEV, ROOTINO)`.
|
|
2. A `ramroot`-equivalent SchFS image, so root can be swapped without a disk.
|
|
3. Point `ROOTDEV` at the SchFS volume; keep v1 mountable until it is proven.
|
|
4. 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:
|
|
|
|
1. `fsinit()` stops requiring a v1 superblock on ROOTDEV; call `schfs_kmount()`.
|
|
2. `ROOTDEV` becomes `SCHFS_DEV` so `ops_for` returns `schfs_ops` for `/`.
|
|
3. `namei`'s `iget(ROOTDEV, ROOTINO)` uses `schfs_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:
|
|
|
|
1. `kernel/proc/proc.c` forkret: `schfs_kmount(ROOTDEV)` + `schfs_kmounted()`
|
|
check, replacing `fsinit(ROOTDEV)`. Mounted there and not in main() because
|
|
mounting reads blocks, bread() sleeps, and sleeping needs a process context.
|
|
`schfs_kmount` returns **void** -- it reports failure by leaving the volume
|
|
unmounted, because elsewhere it is a probe.
|
|
2. `kernel/fs/fs.c` namei: root is `iget(SCHFS_DEV, schfs_kroot())`, so
|
|
`ops_for()` routes `/` through `schfs_ops`. SCHFS_DEV is 64 -- a synthetic
|
|
number for ops dispatch, not the disk the volume lives on.
|
|
3. `kernel/fs/log.c`: `begin_op`/`end_op` return 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 -l` overran 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 read `cat /proc/pcicat /proc/schfs` and 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 to `build/fs.img` now 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.
|