schfs: the commit barrier, reflink, and attributes with an index over them

The last three phases of the filesystem, which were written together and
touch the same files throughout -- kmount.c, objw.c and vfs.c are each
rewritten by all three -- so they land as one commit rather than three
that could not be built separately.

Durability. A transaction that reaches the platter out of order is worse
than one that never reached it: the uberblock can name a tree whose
blocks are still in the drive's write cache. bdirty/bflush give the
buffer layer an explicit barrier, ideflush issues ATA FLUSH CACHE and
usb_storage_flush the SCSI SYNCHRONIZE CACHE that means the same thing,
and a commit now waits for the data before it writes the root that
points at it. The uberblock groups are read newest-first with fallback,
so a volume whose newest roots are torn mounts at an older one and says
which generations were lost.

Reflink. Extents carry a refcount, so two objects can name the same
blocks and neither frees them while the other holds them. Exposed as
FICLONE -- Linux's ioctl value, so `cp --reflink` works unmodified --
and as cmd/reflink for the case where there is no cp to hand.

Attributes, and the one genuinely new idea here. attr.c stores named
values on an object, reachable through setxattr/getxattr/removexattr at
Linux's syscall numbers. On top of that, schfs_index declares an index
over an attribute name and schfs_query answers "which objects carry this
value" from the B-tree rather than by walking. A query against an
unindexed attribute is refused with an error, never silently answered by
a full scan: a query that quietly becomes a scan is worse than one that
admits it cannot help. cmd/schattr drives both.

Three bugs fixed on the way, each found by the numbers being wrong
rather than by inspection:

  - statfs's f_fsid was declared int64[2]. Linux's fsid_t is two *ints*,
    so every field after it landed one slot late: userland read f_frsize
    out of f_namelen, got 255, and df reported a 32 MB root filesystem
    as 1992 KB. It hid for as long as it did because the fields used to
    be invented constants, so wrong numbers looked like the usual ones.
  - struct inode's nlink was a short against SchFS's uint32, so a file
    past 32767 names came back negative -- and iput frees an object
    whose count reaches zero. It is an int now, signed on purpose, so an
    underflow lands somewhere a check can see rather than wrapping.
  - filewrite split every write to fit v1's log. That limit belongs to
    the journal, not to writing, and SchFS makes one transaction of
    whatever it is handed: a 1 MB write was becoming 170 commits and
    170 flushes for nothing. fs_ops carries max_write now, and SchFS
    sets no limit.

Also here: a real mount table, so `mount -t schfs /dev/hdb /mnt` shows
up in /proc/mounts -- df looks a path up there before it calls statfs at
all, so a mount missing from that list was invisible to it -- several
volumes mounted at once, snapshots mounted by name, and /proc/fsstat
with an rdtsc-based counter, which is the only instrument fine enough to
see inside a single syscall.

Verified: `make schfs-check` passes in full, including the sweep that
crashes the volume at each of 119 points mid-transaction and requires
every one to mount with exactly its committed data, and the case where
both newest uberblocks are rubbish and the volume still comes back. The
headless boot check passes with 27 [ok] and no failures, on a SchFS root.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-08-15 16:01:22 +02:00
commit 03cc316001
39 changed files with 3942 additions and 550 deletions

View file

@ -23,6 +23,13 @@ void binit(void);
struct buf* bread(uint, uint64);
void brelse(struct buf*);
void bwrite(struct buf*);
// Modify without writing. The buffer stays dirty in the cache until an
// eviction or a bflush() takes it out; the caller is promising to ask for the
// ordering it needs explicitly rather than relying on write order.
void bdirty(struct buf*);
// Write out every dirty buffer for `dev`, then make the device commit them.
// Returns 0, or -1 if the device could not confirm durability.
int bflush(uint dev);
// console.c
struct termios;
@ -69,7 +76,12 @@ void mountinit(void);
// 1 if the filesystem on this device refuses writes -- a mounted snapshot.
// Ask before mutating, not after: the create path panics on a failed dirlink.
int fs_readonly(uint dev);
int mount_add(uint dev, uint64 inum, uint fsdev, uint64 root);
int mount_add(uint dev, uint64 inum, uint fsdev, uint64 root,
const char *path, const char *source, const char *type);
// Walk the mount table for /proc/mounts. `path` and `source` need MAXPATH
// bytes, `type` 16. Returns 0 for a live entry, -1 for an empty slot.
int mount_get(int i, char *path, char *source, char *type,
uint *fsdev);
int mount_del_by_root(uint fsdev, uint64 root);
struct inode* ialloc(uint, short);
struct inode* iget(uint, uint64);
@ -130,6 +142,8 @@ void iderw(struct buf*);
// For probes, not for reads: iderw() panics on an absent drive, so anything
// that is guessing whether a disk exists has to ask here first.
int ide_disk_present(int dev);
// ATA FLUSH CACHE: block until the drive has committed its write cache.
int ideflush(int dev);
// fs/schfs/kmount.c -- SchFS.
//
@ -138,52 +152,18 @@ int ide_disk_present(int dev);
// Every schfs_k* that changes anything is one transaction and is durable by the
// time it returns.
struct schfs_inode;
void schfs_kmount(uint dev);
// Mounting. Slot 0 is the root filesystem, mounted by proc.c before pid 1 has a
// working directory; schfs_kmount_dev takes any other SchFS disk and returns
// the filesystem device number it landed on, or a negative errno.
void schfs_kmount(uint bdev);
int schfs_kmount_dev(uint bdev);
int schfs_kumount(uint dev);
int schfs_kmounted(void);
uint64 schfs_klookup(const char *path);
int schfs_kstat(uint64 ino, struct schfs_inode *out);
long schfs_kread(uint64 ino, uint64 off, void *buf, unsigned n);
int schfs_kreaddir(uint64 dir,
int (*fn)(uint64, unsigned short, const char *,
unsigned, void *),
void *arg);
int schfs_kinfo(char *buf, int cap);
int schfs_kcreate(uint64 dir, const char *name, unsigned namelen,
unsigned short type, uint mode,
unsigned short major, unsigned short minor, uint64 *out);
long schfs_kwrite(uint64 ino, uint64 off, const void *buf,
unsigned n);
int schfs_ktruncate(uint64 ino, uint64 size);
int schfs_kunlink(uint64 dir, const char *name, unsigned namelen);
int schfs_klink(uint64 dir, const char *name, unsigned namelen,
uint64 ino);
int schfs_krename(uint64 olddir, const char *oldname,
unsigned oldlen, uint64 newdir, const char *newname,
unsigned newlen);
int schfs_ksetattr(uint64 ino, const struct schfs_inode *in);
int schfs_ksync(void);
// Pieces the VFS layer (fs/schfs/vfs.c) needs, where a directory entry and the
// object it names are separate operations. The VFS keeps the link count.
uint64 schfs_klookup_at(uint64 dir, const char *name, unsigned len);
int schfs_kdirempty(uint64 dir);
int schfs_kalloc_object(unsigned short type, uint mode,
uint64 *out);
int schfs_kdiradd(uint64 dir, const char *name, unsigned len,
uint64 ino, unsigned short type);
int schfs_kdirdel(uint64 dir, const char *name, unsigned len);
int schfs_kfree(uint64 ino);
uint64 schfs_kroot(void);
// A snapshot mounted read-only: the same block device with an older tree root.
// The read path takes a device number so the VFS can address either view; there
// is no write path, because a snapshot that could be written to would not be
// one. See kernel/fs/schfs/kmount.c.
// Returns the device number of the slot the snapshot landed in, or negative.
int schfs_ksnap_mount(const char *name, unsigned namelen);
void schfs_ksnap_unmount(uint dev);
uint64 schfs_ksnap_root(uint dev);
// Read side. `dev` is a filesystem device number -- a live volume or a mounted
// snapshot -- not the block device underneath it.
uint64 schfs_klookup(uint dev, const char *path);
long schfs_kread_dev(uint dev, uint64 ino, uint64 off, void *buf,
unsigned n);
int schfs_kstat_dev(uint dev, uint64 ino, struct schfs_inode *out);
@ -193,6 +173,85 @@ int schfs_kreaddir_dev(uint dev, uint64 dir,
int (*fn)(uint64, unsigned short, const char *,
unsigned, void *),
void *arg);
// Resume a directory walk at a tree key. The callback gets each entry's hash,
// which is what getdents hands back to userland as its position.
int schfs_kreaddir_from_dev(uint dev, uint64 dir, uint64 start_hash,
int (*fn)(uint64, unsigned short, const char *,
unsigned, uint64, void *),
void *arg);
uint64 schfs_kroot(uint dev);
int schfs_kinfo(char *buf, int cap);
// Write side. Each is one transaction and is durable by the time it returns,
// unless the caller has a bracket open -- see schfs_ktx_open.
int schfs_kcreate(uint dev, uint64 dir, const char *name,
unsigned namelen, unsigned short type, uint mode,
unsigned short major, unsigned short minor, uint64 *out);
long schfs_kwrite(uint dev, uint64 ino, uint64 off, const void *buf,
unsigned n);
int schfs_ktruncate(uint dev, uint64 ino, uint64 size);
int schfs_kunlink(uint dev, uint64 dir, const char *name,
unsigned namelen);
int schfs_klink(uint dev, uint64 dir, const char *name,
unsigned namelen, uint64 ino);
int schfs_krename(uint dev, uint64 olddir, const char *oldname,
unsigned oldlen, uint64 newdir, const char *newname,
unsigned newlen);
int schfs_ksetattr(uint dev, uint64 ino,
const struct schfs_inode *in);
int schfs_ksync(uint dev);
int schfs_ksync_all(void);
// Share src's data blocks with dst without copying. Both stay writable; the
// first write to either copies only the run it touches. Behind FICLONE.
int schfs_kreflink(uint dev, uint64 src, uint64 dst);
// Attributes, and the index over them. See kernel/fs/schfs/attr.c.
int schfs_kattr_set(uint dev, uint64 ino, const char *name,
unsigned namelen, const void *val, unsigned vallen,
unsigned short type);
int schfs_kattr_get(uint dev, uint64 ino, const char *name,
unsigned namelen, void *out, unsigned cap,
unsigned *vallen);
int schfs_kattr_del(uint dev, uint64 ino, const char *name,
unsigned namelen);
int schfs_kindex(uint dev, const char *name, unsigned namelen);
int schfs_kquery(uint dev, const char *name, unsigned namelen,
const void *val, unsigned vallen, uint64 *out, int cap);
// Bracket a run of changes into one transaction group. Every schfs_k* call made
// between open and close joins it and commits once, at the close; the bracket
// nests, and holds the volume lock for its whole extent. Costly to leave open —
// nothing else touches that volume meanwhile — so bracket a bounded piece of
// work such as one write(2), not a whole syscall's worth of unrelated steps.
// schfs_ktx_close returns the commit's status, or -1 if the caller had no
// bracket open.
void schfs_ktx_open(uint dev);
int schfs_ktx_close(uint dev);
// Volume counts for statfs. 0 if `dev` is a mounted SchFS volume (live or
// snapshot) and the counts are filled in, -1 if it is not.
int schfs_kstatfs(uint dev, uint64 *total, uint64 *freeblocks,
uint64 *objects);
// Pieces the VFS layer (fs/schfs/vfs.c) needs, where a directory entry and the
// object it names are separate operations. The VFS keeps the link count.
int schfs_kdirempty(uint dev, uint64 dir);
int schfs_kalloc_object(uint dev, unsigned short type, uint mode,
uint64 *out);
int schfs_kdiradd(uint dev, uint64 dir, const char *name,
unsigned len, uint64 ino, unsigned short type);
int schfs_kdirdel(uint dev, uint64 dir, const char *name,
unsigned len);
int schfs_kfree(uint dev, uint64 ino);
// A snapshot mounted read-only: the same block device with an older tree root.
// The read path takes a device number so the VFS can address either view; there
// is no write path, because a snapshot that could be written to would not be
// one. See kernel/fs/schfs/kmount.c.
// Returns the device number of the slot the snapshot landed in, or negative.
int schfs_ksnap_mount(uint dev, const char *name, unsigned namelen);
void schfs_ksnap_unmount(uint dev);
uint64 schfs_ksnap_root(uint dev);
// ramdisk.c
uint uptime(void);
@ -236,6 +295,8 @@ int ramdisk_present(void);
// usb_storage.c
void usb_storage_rw(struct buf*);
int usb_storage_present(void);
// SCSI SYNCHRONIZE CACHE: the same barrier as ideflush, over bulk transport.
int usb_storage_flush(void);
void usb_storage_dump(void); // `echo s > /proc/sysrq`
// ioapic.c