include: one folder per subsystem, and includes that name it

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
This commit is contained in:
Nexoniarz 2026-10-23 15:05:12 +02:00
commit a1c965da1e
267 changed files with 1560 additions and 1440 deletions

76
include/fs/blkdev.h Normal file
View file

@ -0,0 +1,76 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
// Block devices that are found rather than assumed.
//
// The first four device numbers are fixed and always were: IDE 0 and 1, the
// ramdisk, the first USB stick. Everything since -- a SATA disk behind AHCI,
// an NVMe namespace, and every partition on any of them -- registers here at
// probe time and is handed the next free number from BLKDEV_FIRST.
//
// A partition is a block device in its own right, with block 0 at its first
// sector. That is the whole of partition support from a filesystem's side:
// SchFS on /dev/sata0p2 sees a volume that starts at zero, exactly as it would
// on a whole disk, and needs to know nothing about GPT -- which is also what
// a Linux driver for SchFS will see when it is handed /dev/sda2.
#define BLKDEV_FIRST 4
// Disks and partitions together. A laptop's NVMe drive alone often carries
// eight partitions, and a stick or an SD card more, so 16 ran out.
#define NBLKDEV 32 // device numbers 4..35, below SCHFS_DEV (64)
// The GPT partition type of a SchFS volume, as it is written and printed:
// 47138FA9-B79C-47A6-9F7D-AC1F7F572D5E. sgdisk and fdisk accept it as is.
// Generated for SchFS on 2026-10-22; nothing else uses it.
#define SCHFS_GPT_TYPE "47138FA9-B79C-47A6-9F7D-AC1F7F572D5E"
// What a driver supplies. `rw` moves `count` of the device's own sectors at
// `lba` to or from `buf`, a kernel address; it is never asked for more than
// BSIZE bytes at once. Returns 0, or -1 if the device refused.
struct blkdev_ops {
int (*rw)(void *priv, uint64 lba, uint32 count, void *buf, int write);
int (*flush)(void *priv); // 0 when there is nothing to flush
};
// Register a whole disk. Its partition table, if it has one, is read here and
// every partition registered after it. Returns the disk's device number, or
// -1 if the table is full.
int blkdev_register(const char *name, const struct blkdev_ops *ops,
void *priv, uint64 nsectors, uint32 secsize);
// The disk has gone -- a USB stick pulled out. Removes it and every partition
// on it, and drops whatever the buffer cache still holds for them, so that a
// different disk given the same number later is not served the old one's
// blocks. A filesystem still mounted from it fails on its next access.
void blkdev_unregister(int dev);
void binval(uint dev); // kernel/dev/bio.c
struct buf *bgetblk(uint dev, uint64 blockno); // bread() without the read
int blkdev_present(uint dev);
void blkdev_rw(struct buf *b);
int blkdev_flush(uint dev);
// For the root probe: the registered devices in order, partitions after the
// disk they are on. Returns the device number of the i'th, or -1 past the end.
int blkdev_nth(int i);
const char *blkdev_name(uint dev);
// 1 for a partition typed SCHFS_GPT_TYPE, 0 for anything that might be SchFS,
// and -1 for one whose type says it cannot be -- an EFI system partition, an
// MBR FAT type. The root probe takes 1 and then 0, so -1 is never tried: a
// FAT partition is mounted as FAT (kernel/fs/fat), not offered as a root.
int blkdev_is_schfs(uint dev);
int blkdev_info(char *buf, int cap); // /proc/partitions
// Sectors straight to and from a filesystem's own buffer, for one that does
// not go through the buffer cache. kernel/fs/fat is: FAT addresses 512-byte
// sectors, a partition need not be a whole number of 4 KB blocks, and a
// cache block hanging over the end of one is a write into the next.
//
// `lba` counts the device's own sectors from the start of the partition, and
// the transfer is at most BSIZE bytes. Returns 0, or -1 for a transfer out of
// range or one the device refused -- it does not panic, because a USB stick
// can leave in the middle of one.
int blkdev_xfer(uint dev, uint64 lba, uint32 count, void *buf, int write);
// Size in sectors and sector size; 0 if there is no such device.
uint64 blkdev_sectors(uint dev, uint32 *secsize);

26
include/fs/buf.h Normal file
View file

@ -0,0 +1,26 @@
struct buf {
int flags;
uint dev;
// 64-bit, because the filesystem's block numbers are. This was a uint, which
// capped any device at 2^32 blocks (16 TB at 4 KB) no matter what the format
// above it could address -- SchFS is a 64-bit format and was being served by
// a 32-bit cache.
uint64 blockno;
struct sleeplock lock;
uint refcnt;
struct buf *prev; // LRU cache list
struct buf *next;
struct buf *qnext; // disk queue
struct buf *hnext; // hash chain, by (dev, blockno)
struct buf *anext; // every buffer, in the order made; never reordered
// BSIZE bytes, a page of its own. A pointer rather than an array since the
// cache grows at run time (see bio.c); drivers copy through their own DMA
// buffers, so where the page sits does not matter to them.
uchar *data;
};
#define B_VALID 0x2 // buffer has been read from disk
#define B_DIRTY 0x4 // buffer needs to be written to disk
// How many buffers the cache holds now, and the most it will grow to. bio.c.
void bcache_size(uint *n, uint *max);

30
include/fs/fat.h Normal file
View file

@ -0,0 +1,30 @@
#pragma once
// FAT12, FAT16 and FAT32, with VFAT long names: the filesystem every other
// operating system can read, which is the whole reason to have it. A USB
// stick, an EFI system partition, an SD card. See kernel/fs/fat/fat.c.
//
// Mounted the way Linux mounts it -- `mount -t vfat /dev/usb0p1 /mnt` -- and
// "msdos" and "fat" are accepted as the same thing.
// Filesystem device numbers for mounted FAT volumes. Like SCHFS_DEV, runtime
// identities for the inode cache that nothing on disk records.
#define FAT_VOL_SLOTS 8 // 104..111
#define FAT_DEV 104
#define FAT_IS_DEV(d) ((d) >= FAT_DEV && (d) < FAT_DEV + FAT_VOL_SLOTS)
// The root directory's inode number on every FAT volume.
#define FAT_ROOT_INO 1
// Mount the FAT volume on block device `bdev`. Returns its filesystem device
// number, or a negative errno: -ENODEV for no FAT there, -EBUSY for no free
// slot or already mounted.
int fat_kmount(uint bdev, int readonly);
// Flush it, mark it cleanly unmounted, and let it go.
int fat_kumount(uint dev);
int fat_readonly(uint dev);
// Write out everything dirty on every mounted volume. `final` also marks each
// one clean, for a shutdown that will not get to unmount them.
void fat_ksync_all(int final);
// Cluster size, clusters and free clusters, for statfs. -1 if not FAT.
int fat_kstatfs(uint dev, uint64 *bsize, uint64 *total, uint64 *freec);

240
include/fs/file.h Normal file
View file

@ -0,0 +1,240 @@
struct file {
// FD_MEMFD: an anonymous shared memory object, the thing memfd_create(2)
// returns. It is a descriptor with no name and no inode behind it, which is
// the entire reason memfd exists -- two processes share memory by passing
// the descriptor, not by agreeing on a path or a System V key.
enum { FD_NONE, FD_PIPE, FD_INODE, FD_SOCKET, FD_MEMFD } type;
int ref; // reference count
char readable;
char writable;
// O_NONBLOCK. A property of the open file description, not of the
// descriptor, which is why it lives here and not in myproc()->ofile_flags --
// dup() shares it and F_SETFL on either fd changes it for both, exactly as
// POSIX says.
char nonblock;
struct pipe *pipe;
struct inode *ip;
struct socket *sock;
// The shared segment behind an FD_MEMFD, as an index into the table in
// kernel/sys/sys_mem.c. Not a pointer, because the segment table is where
// the reference counting lives and handing out pointers into it would
// invite somebody to keep one.
int memfd_seg;
uint64 off;
struct file *fnext; // file.c's free list, while ref is 0
};
// ---------------------------------------------------------------------------
// Filesystem operations
//
// One table per filesystem, chosen by iget() from the device number and hung
// off the inode. Everything above this line -- the syscall layer, exec, the
// page cache -- calls the wrappers in fs.c and never learns which filesystem it
// is talking to.
//
// A vtable rather than a chain of `if (dev == SCHFS_DEV)` because there are two
// filesystems today and there will be more: the whole point of the exercise is
// that adding one is a new table, not another branch in fifteen functions.
//
// Every hook takes an inode that is already locked, except ialloc (which has no
// inode yet) and iload (which is what makes one valid in the first place).
// ---------------------------------------------------------------------------
struct stat;
struct fs_ops {
const char *name;
// Fill in the type/size/link fields of a freshly iget()'d inode.
void (*iload)(struct inode *);
// Write those fields back to disk.
void (*istore)(struct inode *);
// The last link is gone and nobody has it open: release the object.
void (*ifree)(struct inode *);
int (*read)(struct inode *, int user_dst, uint64 dst, uint64 off, uint n);
int (*write)(struct inode *, int user_src, uint64 src, uint64 off, uint n);
// Set the length. Growing is allowed only where the filesystem can represent
// the gap; v1 cannot and ignores it.
void (*truncate)(struct inode *, uint64 newsize);
// Directory operations. `poff` is an opaque cookie identifying where the
// entry lives, meaningful only to the same filesystem's dirunlink.
struct inode *(*dirlookup)(struct inode *dp, char *name, uint *poff);
int (*dirlink)(struct inode *dp, char *name, uint64 inum);
int (*dirunlink)(struct inode *dp, char *name, uint off);
int (*dirempty)(struct inode *dp);
// Create an object with no name yet. The caller sets nlink and calls
// dirlink; until it does, the object is unreachable.
struct inode *(*ialloc)(uint dev, short type);
// Read directory entries into a getdents64-shaped stream. `off` is an opaque
// position that starts at 0 and is echoed back; returns bytes written, 0 at
// the end of the directory, or negative on error.
int (*getdents)(struct inode *dp, char *out, int cap, uint64 *off);
// The largest write this filesystem wants in one call, or 0 for "no limit".
//
// v1 needs one: a write has to fit in the on-disk log, so filewrite splits
// anything larger and each piece is its own transaction. That limit is a
// property of the journal, not of writing, and applying it to SchFS was
// actively harmful -- SchFS makes one transaction out of whatever it is
// handed, so chopping a 1 MB write into 6 KB pieces bought 170 commits and
// the flushes that go with them for nothing.
uint max_write;
// Object types this filesystem has no way to store, as a mask of 1 << T_*.
// Creating one fails with EPERM, which is what Linux answers for mknod or a
// symlink on vfat. 0 for "anything".
uint refuse_types;
// One name per file: link() fails with EPERM. Rename still works -- it is
// a second name for as long as it takes to remove the first.
int no_hardlinks;
};
// In-memory inode.
//
// `inum` and `size` are 64-bit because the filesystem underneath may be: SchFS
// object ids and file sizes are both uint64 on disk, and a 32-bit copy here
// would silently truncate them into a different file. The v1 filesystem uses a
// small fraction of the range and is unaffected.
struct inode {
uint dev;
uint64 inum;
int ref;
struct sleeplock lock;
int valid;
const struct fs_ops *ops; // chosen by iget from dev; never null once held
struct inode *hnext; // fs.c: hash chain while held, free list after
struct inode *anext; // fs.c: every inode there is, never reordered
short type;
short major;
short minor;
// int, not short. SchFS stores a uint32 link count and this truncated it, so
// a file that passed 32767 names came back with a negative one -- and iput
// frees an object whose count reaches zero. Signed rather than unsigned on
// purpose: `nlink--` on a count that is somehow already zero should land
// somewhere a check can see it, not wrap to four billion.
int nlink;
uint64 size;
uint rwmode;
uint supermode;
uint showmode;
uint atime;
uint mtime;
uint ctime;
uint addrs[NDIRECT+3];
};
#define I_VALID 0x2
// Device numbers for filesystems that are not a raw disk. Real disks are 0..3,
// and these are deliberately far from them so a stray value never looks like a
// SchFS inode. Nothing on any disk records them -- they are runtime identities
// for the inode cache, so the numbering can be rearranged freely.
//
// Mounted SchFS volumes: SCHFS_DEV through SCHFS_DEV + SCHFS_VOL_SLOTS - 1.
//
// This was a single number, because there was a single volume: kmount.c held
// one `kvol` and every write entry point named it directly, so `mount -t schfs
// /dev/hdb /mnt` ignored its source argument entirely and handed back whichever
// disk the kernel had found at boot. Slot 0 is that boot volume, which is why
// SCHFS_DEV alone still means "the root" everywhere that only ever wanted it.
// Sixteen, which fills 64..79 up to PROCFS_DEV.
#define SCHFS_VOL_SLOTS 16
#define SCHFS_DEV 64
#define SCHFS_IS_VOL_DEV(d) \
((d) >= SCHFS_DEV && (d) < SCHFS_DEV + SCHFS_VOL_SLOTS)
// Mounted snapshots. Same block device, an older tree root, read-only.
//
// Each needs its own device number because an inode is identified by
// (dev, inum) and a snapshot's object ids are the *same numbers* as the live
// tree's -- one number would make the views alias in the inode cache and hand
// a reader whichever was cached first. So a slot is a device: SCHFS_SNAP_DEV
// through SCHFS_SNAP_DEV + SCHFS_SNAP_SLOTS - 1.
//
// More than one because "one at a time" is an odd thing to explain and
// the cost is two scratch pages per slot, allocated on first use. Not many
// because each is a device number and they are a fixed, small space.
#define SCHFS_SNAP_SLOTS 8 // 96..103, up to FAT_DEV
#define SCHFS_SNAP_DEV 96
#define SCHFS_IS_SNAP_DEV(d) \
((d) >= SCHFS_SNAP_DEV && (d) < SCHFS_SNAP_DEV + SCHFS_SNAP_SLOTS)
// Anything SchFS answers for, live volume or snapshot.
#define SCHFS_IS_ANY_DEV(d) (SCHFS_IS_VOL_DEV(d) || SCHFS_IS_SNAP_DEV(d))
// The synthetic /proc.
#define PROCFS_DEV 80
// The synthetic /dev. See kernel/fs/devfs.c.
#define DEVFS_DEV 81
// The synthetic /sys. See kernel/fs/sysfs.c.
#define SYSFS_DEV 82
// Graft it over `target`; 0 or a negative errno. In sysfs.c.
int sysfs_mount(const char *target);
// The buffer /proc renders a file into, and how big it is; one at a time, so
// every get is followed by a put. In kernel/dev/procfs.c.
char *proc_genbuf_get(int *cap);
void proc_genbuf_put(void);
// Mounted FAT volumes, FAT_DEV onward. See include/fs/fat.h.
// Linux's limits, for every filesystem behind the VFS. A path component can be
// NAME_MAX bytes and a whole path PATH_MAX including its NUL. Each filesystem
// may refuse less: SchFS and FAT take 255, the v1 format DIRSIZ.
//
// The VFS used to copy every component into a DIRSIZ buffer and every path
// into a MAXPATH one, which cut names at 29 bytes and paths at 127 without a
// word -- so a longer name opened whichever file shared its first 29 bytes.
#define VFS_NAME_MAX 255
#define VFS_PATH_MAX 4096 // one page: namex_from() kalloc()s it
// 1 if some inode on dev with this number is referenced. In fs.c.
int iheld(uint dev, uint64 inum);
// 1 if any inode on dev but `except` is referenced (0: no exception). In fs.c.
int ibusy(uint dev, uint64 except);
// How many in-memory inodes exist; the table grows past NINODE. In fs.c.
uint inode_slots(void);
// How many open file descriptions exist; the table grows past NFILE. file.c.
uint file_slots(void);
// Path lookup that follows a final symlink too; 0 with *err set on failure.
// `start` 0 means the cwd. In fs.c.
struct inode *nameifollow(struct inode *start, char *path, int *err);
// execve(2) with the argv and envp arrays still in the caller's memory. In
// exec.c; exec() is the same for arrays the kernel holds.
int exec_user(char *path, addr_t uargv, addr_t uenvp);
// table mapping major device number to
// device functions
struct devsw {
int (*read)(struct inode*, uint64, char*, int);
int (*write)(struct inode*, uint64, char*, int);
// Optional. Returns 0 when a read would block, which is the only thing
// fileread needs to know to honour O_NONBLOCK without every device driver
// growing a flags argument. A device that omits this is treated as always
// ready, which is the right answer for a disk and for /dev/zero.
int (*ready)(struct inode*);
// Optional. The last descriptor for this open file is going away. A device
// that hands a program exclusive-feeling control of the hardware needs this
// to put the hardware back -- /dev/fb0 uses it to give the text console its
// screen back when whatever was drawing on it exits.
void (*close)(struct inode*);
// Optional. A descriptor has just been opened on this device. /dev/input
// uses it to discard events that arrived while nobody was listening -- see
// inputopen().
void (*open)(struct inode*);
};
extern struct devsw devsw[];
#define CONSOLE 1
// Major number of /dev/input/eventN. The minor is the device index.
#define INPUT_MAJOR 7
// Major number of /dev/dsp. See include/dev/audio.h.
#define AUDIO_MAJOR 12

214
include/fs/fs.h Normal file
View file

@ -0,0 +1,214 @@
// SCHFS - On-disk file system format.
// Both the kernel and user programs use this header file.
//
// Changes from original:
// - struct dinode gains atime, mtime, ctime (timestamps) and checksum
// (CRC32c). Padding reduced from 44 to 28 bytes; struct stays 128 bytes.
// - struct superblock gains checksum field.
// - struct fsv1_extent added for T_EXTENT inode block mapping.
// - OVERFLOW_DIR_START / OVERFLOW_DIR_END replace the bare "113 * BSIZE"
// magic numbers used in fs.c and mkfs.c.
#ifndef FS_H
#define FS_H
#define ROOTINO 1 // root i-number
// Buffer-cache block size. Matches SCHFS_BLKSIZE deliberately.
//
// This was 1024, inherited from xv6, while a SchFS block is 4096 -- so every
// SchFS block cost *four* bread() calls, and each bread() is one IDE request
// with its own interrupt, sleep and wakeup. Loading the 800 KB toybox binary
// meant ~800 round-trips, which is why every command took about two seconds:
// `seq 1 200`, which touches no file and prints nothing, cost the same 2.0s as
// `cat /bin/toybox`, because both are dominated by exec() reading the same
// binary. `ls | wc -l` cost exactly double, loading it twice.
//
// At 4096 a SchFS block is one request. Keep the two equal.
#define BSIZE 4096 // block size
// Permission mode bits (rwmode field)
#define RW 3 // 11 read+write
#define W_ 1 // 01 write-only
#define R_ 2 // 10 read-only
#define NONE 0 // 00 no access
// ---------------------------------------------------------------------------
// Superblock
// ---------------------------------------------------------------------------
// Disk layout:
// [ boot | super | log | inode-bitmap | inode-blocks | block-bitmap | data ]
struct superblock {
uint magic; // Must be FSMAGIC
uint size; // Size of file system image (blocks)
uint nblocks; // Number of data blocks
uint ninodes; // Number of inodes
uint nlog; // Number of log blocks
uint logstart; // Block number of first log block
uint inodestart; // Block number of first inode block
uint bmapstart; // Block number of first free-block bitmap block
uint ibmapstart; // Block number of first inode-bitmap block
uint freeinodes; // Count of free inodes
uint freeblocks; // Count of free blocks
uint checksum; // CRC32c of this struct (field zeroed during computation)
};
extern struct superblock sb;
#define FSMAGIC 0x10203040
// ---------------------------------------------------------------------------
// Block / inode geometry
// ---------------------------------------------------------------------------
#define NDIRECT 12
#define NINDIRECT (BSIZE / sizeof(uint))
// Maximum file size in blocks: direct + 1x + 2x + 3x indirect
#define MAXFILE (NDIRECT \
+ NINDIRECT \
+ (NINDIRECT * NINDIRECT) \
+ (NINDIRECT * NINDIRECT * NINDIRECT))
// ---------------------------------------------------------------------------
// Extent descriptor (T_EXTENT inodes only)
//
// Up to MAX_INLINE_EXTENTS are packed into ip->addrs[0..NDIRECT-1].
// addrs[] = NDIRECT uints = 12*4 = 48 bytes
// each fsv1_extent = 12 bytes → 48/12 = 4 inline extents
// (addrs[NDIRECT], [NDIRECT+1], [NDIRECT+2] remain as indirect pointers)
//
// For files needing more extents, addrs[NDIRECT] points to an indirect block
// of BSIZE/sizeof(struct fsv1_extent) = 85 additional extents.
// ---------------------------------------------------------------------------
// Renamed from "schfs_extent". The new format in include/fs/schfs.h has its own,
// completely different, struct of that name -- 64-bit fields and a refcount --
// and any file including both headers hit a redefinition error. Two structs
// with one name and two layouts is a trap worth removing rather than working
// around; this one belongs to the v1 format and dies with it.
struct fsv1_extent {
uint start_block; // logical block number in the file (first of run)
uint start_blkaddr; // physical disk block address
uint len; // number of contiguous blocks in this run
};
// Inline extents packed into addrs[0 .. NDIRECT-1] only.
#define MAX_INLINE_EXTENTS (NDIRECT * sizeof(uint) / sizeof(struct fsv1_extent))
// ---------------------------------------------------------------------------
// On-disk inode (exactly 128 bytes; IPB = 8 inodes per 1024-byte block)
//
// Byte layout:
// 0 short type 2
// 2 short major 2
// 4 short minor 2
// 6 short nlink 2
// 8 uint size 4
// 12 uint rwmode 4
// 16 uint supermode 4
// 20 uint addrs[15] 60 T_EXTENT: packed fsv1_extent[4] in [0..11], rest normal
// 80 uint showmode 4
// 84 uint atime 4 ← last access time (Unix seconds)
// 88 uint mtime 4 ← last modification time
// 92 uint ctime 4 ← last metadata-change time
// 96 uint checksum 4 ← CRC32c (zeroed during computation)
// 100 uchar pad[28] 28 ← was 44; reduced by 16 for the 4 new fields
// 128 bytes total
// ---------------------------------------------------------------------------
struct dinode {
short type; // file type
short major; // major device number (T_DEVICE only)
short minor; // minor device number (T_DEVICE only)
short nlink; // hard-link count
uint size; // file size in bytes
uint rwmode; // read/write permission flags
uint supermode; // superuser-only flag
uint addrs[NDIRECT + 3]; // block addresses / packed extent entries
uint showmode; // visibility flag
uint atime; // last access time (seconds since epoch)
uint mtime; // last modification time
uint ctime; // last metadata-change time
uint checksum; // CRC32c of this dinode (field = 0 when hashing)
uchar pad[28]; // reserved padding (struct = 128 bytes)
};
// Inodes per block
#define IPB (BSIZE / sizeof(struct dinode))
// Block containing inode i
#define IBLOCK(i, sb) ((i) / IPB + (sb).inodestart)
// Bitmap bits per block
#define BPB (BSIZE * 8)
// Block of free-block bitmap containing bit for block b
#define BBLOCK(b, sb) ((b) / BPB + (sb).bmapstart)
// Block of inode bitmap containing bit for inode i
#define IBBLOCK(i, sb) ((i) / BPB + (sb).ibmapstart)
// ---------------------------------------------------------------------------
// Directory entry
// ---------------------------------------------------------------------------
// 30, not 14.
//
// 14 was xv6's, and the v1 filesystem truncated to it *silently*: a file called
// JetBrainsMono-Regular.ttf was stored as "JetBrainsMono-", could not be opened
// by its real name, and would have collided outright with
// JetBrainsMono-Bold.ttf. Any two files sharing a 14-character prefix mapped to
// one entry. That is data loss, not a display limit.
//
// The value is chosen so that `struct dirent` is exactly 32 bytes, because the
// directory HTree below is laid out in *bytes* and a bucket must stay 256 bytes
// (see DIRENT_BUCKET_BYTES). Going from 16-byte to 32-byte entries therefore
// halves the slots per bucket from 16 to 8 and changes no offset anywhere else
// -- HASHRANGE, the overflow region and MAXFSIZE all keep their values. The
// cost is more overflow use in heavily collided directories, which is a far
// better trade than silently mangling names.
//
// It is still not POSIX's 255. Getting there means reshaping the directory
// format, which wants the on-disk version policy SchFS is getting first.
#define DIRSIZ 30
struct dirent {
ushort inum;
char name[DIRSIZ];
};
// ---------------------------------------------------------------------------
// Directory HTree constants
//
// BKDRHash(name) returns a value in [16, 16 + HASHRANGE).
// Each bucket is 256 bytes: 8 dirent slots of 32 bytes. It was 16 slots of 16
// until DIRSIZ grew; the byte size is what the layout depends on, so it is the
// thing held constant.
//
// With HASHRANGE=443:
// max primary end byte = (16 + 443) * 256 = 117 504 (~114.75 blocks)
// OVERFLOW_DIR_START = 113 * BSIZE = 115 712
// OVERFLOW_DIR_END = 115 * BSIZE = 117 760
// MAXFSIZE = 119 * BSIZE = 121 856
// ---------------------------------------------------------------------------
#define DIRENT_NUM 7168
#define FIRST_LAYER_NODE_NUM 112
#define OVERFLOW_BLOCK (FIRST_LAYER_NODE_NUM + 1)
#define NUM_DIRENT_PER_BLOCK (BSIZE / sizeof(struct dirent))
#define SECOND_LAYER_NODE_NUM NUM_DIRENT_PER_BLOCK
#define HASHRANGE 443
// One hash bucket, in bytes. Every offset in the HTree is derived from this, so
// it must not change when DIRSIZ does -- see the note on DIRSIZ.
#define DIRENT_BUCKET_BYTES 256
#define DIRENT_PER_BUCKET (DIRENT_BUCKET_BYTES / sizeof(struct dirent))
#define MAXFSIZE (119 * BSIZE)
// Overflow bucket: shared between fs.c and mkfs.c.
// These replace the bare "113 * BSIZE" / "115 * BSIZE" literals.
#define OVERFLOW_DIR_START (113 * BSIZE)
#define OVERFLOW_DIR_END (115 * BSIZE)
#endif // FS_H

71
include/fs/fsstat.h Normal file
View file

@ -0,0 +1,71 @@
// SPDX-License-Identifier: GPL-2.0-or-later
//
// Counters for the filesystem and block paths, read through /proc/fsstat.
//
// These exist because "the system feels slow" is not a bug report anyone can
// act on, and every guess about where the time goes in a filesystem is wrong
// until it is measured. `ls /bin` taking three seconds could be the directory
// walk, the tree descents, the copy path, or the disk -- and those four call
// for completely different fixes.
//
// Deliberately plain `uint64` with no locking. A counter that is off by one
// because two CPUs raced tells you the same thing as one that is exact, and a
// lock here would change the very timings it is meant to report.
#pragma once
struct fsstat {
// Buffer cache. `bread_miss` is the one that matters: a miss is a real
// transfer, a hit is a memcpy.
uint64 bread_calls;
uint64 bread_miss;
uint64 bget_evict_dirty; // had to write a buffer out to make room
// Actual device transfers, and the cycles spent inside them.
uint64 disk_reads;
uint64 disk_writes;
uint64 disk_cycles;
// The VFS read path: one call is one read(2) reaching a SchFS file.
uint64 read_calls;
uint64 read_bytes;
uint64 read_cycles;
// Directory listing.
uint64 getdents_calls;
uint64 getdents_cycles;
uint64 getdents_walked; // entries stepped over, including ones re-walked
// B-tree descents from the root, and the block reads they cost.
uint64 descents;
// exec(2), which on this system means reading an 800 KB multicall binary.
uint64 exec_calls;
uint64 exec_cycles;
// ---------------------------------------------------------------------
// Table occupancy, for finding leaks.
//
// `_used` is the live count; `_peak` is the high-water mark, and the peak is
// the number that answers the question. A live count only tells you about the
// moment you looked, and a reference leak is invisible in it right up until
// the table is full -- at which point iget() panics and filealloc() returns
// 0, which is where the trail goes cold.
//
// Maintained at the 0->1 and 1->0 transitions rather than by scanning, so
// they cost one increment on a path that already holds the relevant lock.
//
// What to watch: run a long ls-heavy session and read /proc/fsstat between
// commands. A peak that climbs and never comes back down after the commands
// finish is the leak, and which of the three it is says where to look.
uint64 inode_used, inode_peak;
uint64 file_used, file_peak;
uint64 buf_pinned, buf_pinned_peak;
};
// Bump a (used, peak) pair. Callers hold the table's own lock already.
#define FSSTAT_TAKE(used, peak) \
do { if (++(used) > (peak)) (peak) = (used); } while (0)
#define FSSTAT_DROP(used) \
do { if ((used) > 0) (used)--; } while (0)
extern struct fsstat fsstat;

485
include/fs/schfs.h Normal file
View file

@ -0,0 +1,485 @@
// SchFS — on-disk format.
//
// Design rationale lives in docs/architecture/schfs.md. In short: copy-on-write
// with the checksum of a block stored in the pointer that leads to it (ZFS),
// one B-tree reused for every kind of record (btrfs), and attribute indexes as
// just another record type (BeFS).
//
// This header is shared by the kernel and by host tools (tools/mkfs,
// tools/schfs_test), so it must stay free of kernel-only types.
#ifndef SCHFS_H
#define SCHFS_H
#include <stdint.h>
// ---------------------------------------------------------------------------
// Records that live at an arbitrary offset inside a block.
//
// A leaf's value area holds variable-length records packed end to end, so a
// record begins wherever the one before it ended -- which is a multiple of 8
// only by luck. The code reads them by casting a pointer into the block, and
// that is a lie to the compiler unless it is told: the type says "8-byte
// aligned" and the pointer is not.
//
// It works on x86, which is why it never showed as a bug, and it is undefined
// in C. A compiler entitled to assume alignment may emit an instruction that
// requires it, and the failure would arrive at a different optimisation level
// or on a different architecture looking like filesystem corruption rather than
// like codegen. UndefinedBehaviorSanitizer reported 31 of these across obj.c,
// objw.c, attr.c and space.c.
//
// This annotation changes NO on-disk layout. Every struct it is applied to
// already carries explicit `pad` fields and therefore has no implicit padding;
// sizes and member offsets are identical with and without it, which was
// measured rather than assumed before it was added. What it changes is the
// code generated to read them: byte-wise where the compiler cannot prove
// alignment, which is exactly the truth here.
//
// It is deliberately NOT applied to schfs_node_hdr, schfs_item, schfs_kptr or
// schfs_uberblock. Those sit at fixed offsets in a block -- the header is 32
// bytes, so everything after it is 8-aligned -- and annotating them would cost
// byte-wise access on the hottest paths in the tree to fix a problem they do
// not have.
#define SCHFS_PACKED __attribute__((packed))
#define SCHFS_MAGIC 0x53636846u // "SchF"
#define SCHFS_NODE_MAGIC 0x536e6f64u // "Snod"
// 4096 to match modern media and make extents worth having. The old format
// used 1024, which is also why its directories were so wasteful.
#define SCHFS_BLKSIZE 4096
// Deepest tree we will walk without recursing. 8 levels of a ~84-way internal
// node is far more than any plausible volume needs.
#define SCHFS_MAX_LEVEL 8
// ---------------------------------------------------------------------------
// Keys
//
// Every record in the filesystem is addressed by one 24-byte key, sorted by
// (objectid, type, offset). This is the whole reason there is only one tree:
// inodes, directory entries, extent maps and indexes are just different `type`
// values in the same keyspace.
// ---------------------------------------------------------------------------
#define SCHFS_KEY_INODE 1 // (ino, INODE, 0) -> struct schfs_inode
#define SCHFS_KEY_DIRENT 2 // (dirino,DIRENT, namehash) -> packed dirent list
#define SCHFS_KEY_EXTENT 3 // (ino, EXTENT, fileblk) -> struct schfs_extent
#define SCHFS_KEY_ATTR 4 // (ino, ATTR, namehash) -> attribute value
#define SCHFS_KEY_INDEX 5 // (idxid, INDEX, valuehash) -> objectid
#define SCHFS_KEY_CSUM 6 // (CSUM, CSUM, diskblk) -> uint32 CRC32c
#define SCHFS_KEY_SNAP 7 // (SNAP, SNAP, id) -> struct schfs_snap
#define SCHFS_KEY_EXTREF 8 // (EXTREF,EXTREF, diskblk) -> struct schfs_extref
// Data-block checksums live under one reserved object id, exactly as btrfs
// keeps a checksum tree. Tree nodes carry their checksum in their parent
// pointer, but file *data* blocks have no parent to hold it — without these
// records "end-to-end checksums" would only cover metadata, which is the part
// least likely to rot.
#define SCHFS_CSUM_OBJECTID 1ULL
// Snapshots live under their own reserved objectid, as ordinary records in the
// live tree. A snapshot is nothing more than a tree root that is kept: the
// B-tree is copy-on-write, so the blocks a root reaches are already immutable
// as long as nothing frees them. Keeping one costs a record.
#define SCHFS_SNAP_OBJECTID 2ULL
// Shared extents, keyed by the disk block a run starts at. btrfs calls this the
// extent tree and so, in effect, is this -- one reserved object id whose records
// say how many files point at a run.
//
// Only *shared* runs have a record. A run nothing has cloned has no entry at
// all, and absence means a reference count of one. That is not a space
// optimisation so much as a correctness one: the overwhelmingly common case
// stays exactly as it was, so the write path pays nothing for a feature it is
// not using, and there is no second structure to keep in step on every write.
#define SCHFS_EXTREF_OBJECTID 3ULL
// Object ids below this are reserved; the root directory is the first real one.
#define SCHFS_FIRST_OBJECTID 16ULL
#define SCHFS_ROOT_OBJECTID 16ULL
struct schfs_key {
uint64_t objectid;
uint64_t offset;
uint8_t type;
uint8_t pad[7];
}; /* 24 bytes */
// ---------------------------------------------------------------------------
// Node header — identical for internal nodes and leaves.
//
// `blocknr` is a self-pointer: a block that was written to the wrong place
// still checksums correctly, so the only way to catch a misdirected write is
// for the block to say where it thinks it lives.
// ---------------------------------------------------------------------------
struct schfs_node_hdr {
uint32_t magic; // SCHFS_NODE_MAGIC
uint32_t crc; // CRC32c of the whole block, this field zeroed
uint64_t blocknr; // where this block believes it lives
uint64_t generation; // transaction that wrote it
uint16_t level; // 0 = leaf, >0 = internal
uint16_t nritems;
uint32_t pad;
}; /* 32 bytes */
// Internal node entry: a key and the child it leads to, plus that child's
// checksum and generation. Storing the checksum *here* rather than in the
// child is what makes the tree self-validating.
struct schfs_kptr {
struct schfs_key key;
uint64_t blocknr;
uint64_t generation;
uint32_t crc;
uint32_t pad;
}; /* 48 bytes */
// Leaf entry. Items grow forward from the end of the header; their data grows
// backward from the end of the block, exactly like btrfs. `offset` is measured
// from the start of the block.
struct schfs_item {
struct schfs_key key;
uint32_t offset;
uint32_t size;
}; /* 32 bytes */
#define SCHFS_NODE_SPACE (SCHFS_BLKSIZE - (int)sizeof(struct schfs_node_hdr))
#define SCHFS_MAX_KPTR (SCHFS_NODE_SPACE / (int)sizeof(struct schfs_kptr))
#define SCHFS_MAX_ITEMS (SCHFS_NODE_SPACE / (int)sizeof(struct schfs_item))
// Largest value that can live inline in a leaf. Anything bigger becomes an
// extent record pointing at data blocks.
#define SCHFS_MAX_INLINE (SCHFS_NODE_SPACE / 4)
// ---------------------------------------------------------------------------
// Uberblock
//
// A ring of these is written round-robin. Mounting means reading them all and
// taking the highest generation that checksums — that is the entire crash
// recovery story, in place of a journal replay.
// ---------------------------------------------------------------------------
#define SCHFS_UBER_COUNT 128
#define SCHFS_UBER_START 1 // block 0 stays free for a boot sector
// First block available for the tree and for file data: everything below is
// the boot block and the uberblock ring.
#define SCHFS_FIRST_BLOCK (SCHFS_UBER_START + SCHFS_UBER_COUNT)
// ---------------------------------------------------------------------------
// The on-disk format version, and what a bump means
//
// The rule, so that "can this driver read this volume" has an answer that does
// not depend on reading the release notes:
//
// * SCHFS_VERSION changes only when the *frame* changes -- the uberblock, the
// node header, the key, the things every reader must understand before it
// can parse anything at all. A driver refuses a version above its own,
// loudly, and says so; it does not guess and it does not fall back to an
// older uberblock, because an older uberblock is a stale filesystem.
//
// * Everything else is a FEATURE FLAG, and this is what keeps the version
// number from being a blunt instrument. Adding a record type, an index
// kind or an optional field does not break a reader that ignores it, so it
// must not cost every older reader the volume.
//
// Three classes, the arrangement ext4 and btrfs both arrived at because it is
// the one that works:
//
// compat Unknown bits may be ignored. Mount read-write regardless.
// ro_compat Unknown bits mean the volume may be MOUNTED READ-ONLY. The
// writer would maintain a structure this driver does not know
// about, so writing would silently invalidate it.
// incompat Unknown bits mean REFUSE. The volume cannot be parsed correctly
// without understanding them.
//
// Choosing the class is the whole design decision, and the test is simple:
// what happens if a driver that does not know this feature writes to the
// volume? Nothing -> compat. Something on disk goes stale -> ro_compat.
// Something is unreadable -> incompat.
//
// All three words are zero on every volume written so far: schfs_vol_commit
// memsets the uberblock before filling it in, so v1 images already say "no
// features" without having been written by code that knew the field existed.
// ---------------------------------------------------------------------------
#define SCHFS_VERSION 1
// No features are defined yet. The masks exist so that the *checks* exist:
// adding a flag later is one bit here, and every driver already refuses
// correctly for the bits it does not have.
#define SCHFS_FEAT_COMPAT_SUPP 0u
#define SCHFS_FEAT_RO_COMPAT_SUPP 0u
#define SCHFS_FEAT_INCOMPAT_SUPP 0u
#define SCHFS_LABEL_MAX 32
// A retained tree root.
//
// The root a snapshot names is a *previous* version of the live tree, and it is
// reached exactly as the live one is -- same node format, same checksums. What
// makes it survive is schfs_space_scan walking it too, so its blocks are never
// handed out again.
//
// The record is stored in the live tree, which means creating one rewrites the
// path to it under copy-on-write and produces a new live root. That is correct
// and is why the snapshot names the root as it was *before* the record was
// added: a snapshot that included its own creation would have to refer to a
// block that does not exist yet.
#define SCHFS_SNAP_NAME_MAX 64
// Set on a volume that has at least one snapshot. See schfs_space_free.
#define SCHFS_SPACE_RETAIN_ALL 1
struct schfs_snap {
uint64_t root; // block number of the retained tree root
uint64_t root_generation;
uint32_t root_crc; // checksum of that root block
uint32_t pad0;
uint64_t created; // generation the snapshot was taken at
char name[SCHFS_SNAP_NAME_MAX];
} SCHFS_PACKED;
struct schfs_uberblock {
uint32_t magic; // SCHFS_MAGIC
uint32_t crc; // CRC32c of this struct, this field zeroed
uint32_t version; // SCHFS_VERSION
uint32_t blocksize; // SCHFS_BLKSIZE; a reader must refuse a mismatch
uint64_t generation; // transaction number; highest valid one wins
uint64_t root; // block number of the tree root
uint32_t root_crc; // checksum of that root block
uint32_t pad0;
uint64_t root_generation;
uint64_t total_blocks;
// Informational only. Which blocks are actually free is derived from the
// tree at mount rather than stored, so this is what the volume believed at
// commit time — useful for df before anything is mounted, never trusted.
uint64_t free_blocks;
// Feature flags. See the block comment above SCHFS_VERSION for what the
// three classes mean and how to choose between them.
//
// These occupy what was one `uint64_t reserved0`, so the offset of every
// field after them is unchanged and a v1 image -- which has zeros here --
// reads identically. That is the only reason this could be added without a
// version bump, and it is the last time that trick is available.
uint32_t feature_incompat;
uint32_t feature_ro_compat;
uint64_t next_objectid;
uint64_t root_objectid; // normally SCHFS_ROOT_OBJECTID
uint8_t uuid[16];
char label[SCHFS_LABEL_MAX];
uint32_t feature_compat;
uint8_t pad[SCHFS_BLKSIZE - 140];
};
// The uberblock is one block, and every field above is at a fixed offset in it.
// Both are on-disk facts rather than C conveniences, so the compiler checks
// them: a padding change or a reordered field is a format change that would
// otherwise be found by a volume that no longer mounts.
_Static_assert(sizeof(struct schfs_uberblock) == SCHFS_BLKSIZE,
"uberblock must be exactly one block");
_Static_assert(__builtin_offsetof(struct schfs_uberblock, version) == 8,
"uberblock version moved -- older drivers read this offset");
_Static_assert(__builtin_offsetof(struct schfs_uberblock, feature_incompat) == 64,
"feature flags moved -- they must stay where reserved0 was");
_Static_assert(__builtin_offsetof(struct schfs_uberblock, next_objectid) == 72,
"fields after the feature flags must not shift");
// ---------------------------------------------------------------------------
// Records
// ---------------------------------------------------------------------------
#define SCHFS_T_FILE 1
#define SCHFS_T_DIR 2
#define SCHFS_T_DEV 3
#define SCHFS_T_SYMLINK 4
// A bound AF_UNIX socket. A *compat* addition by the rule above: a driver that
// does not know this value maps it to SCHFS_T_FILE and sees a zero-length
// regular file, which is harmless -- nothing goes stale and nothing becomes
// unreadable, so it needs no feature flag.
#define SCHFS_T_SOCK 5
struct schfs_inode {
uint64_t size;
uint64_t blocks;
uint64_t atime, mtime, ctime;
// Containing directory. Meaningful for directories only, where it is what
// ".." resolves to — neither "." nor ".." is stored on disk, so a directory
// costs nothing until it has real entries. For files with several hard
// links "parent" is simply the first directory that linked them and must
// not be trusted.
uint64_t parent;
uint32_t mode; // real POSIX mode bits, at last
uint32_t uid, gid;
uint32_t nlink;
uint16_t type; // SCHFS_T_*
uint16_t rdev_major, rdev_minor;
uint16_t pad;
uint32_t flags; // SCHFS_I_* — none defined yet
uint32_t gen; // transaction that last changed this inode;
// what an incremental send/snapshot diff needs
} SCHFS_PACKED;
// An extent maps a run of file blocks onto a run of disk blocks. refs is what
// makes snapshots and reflink copies nearly free: sharing a range is a refcount
// bump rather than a data copy.
struct schfs_extent {
uint64_t disk_block;
uint64_t nblocks;
// Transaction group that allocated this run. A run stamped with the group
// currently being built cannot be reached from any committed root, so its
// blocks may be overwritten in place — which is what stops a program that
// writes a hundred bytes at a time from copying a whole 4 KB block, and
// then a whole tree path, on every call.
//
// A full 64 bits, matching the tree's generation counter, because a 32-bit
// one would wrap after four billion commits and a wrapped comparison says
// "same group" about a block belonging to a committed tree — the one
// mistake this field exists to prevent. Only ever trusted together with
// refs == 1; a shared run belongs to somebody else as well.
uint64_t gen;
// Whether this run is shared, as far as *this* file knows. A hint, not the
// truth: the count that decides whether blocks may be released lives in the
// extent-reference tree (SCHFS_KEY_EXTREF), because a per-file copy cannot
// be decremented by the other file that shares the run.
//
// Kept because it makes the common case free: refs == 1 here means nothing
// has ever cloned this run, so the write path can rewrite it in place
// without consulting the tree at all.
uint32_t refs;
uint32_t pad;
} SCHFS_PACKED;
// How many files point at a run of blocks. Present only for runs with more than
// one reference; see SCHFS_EXTREF_OBJECTID.
struct schfs_extref {
uint64_t nblocks;
uint64_t refs;
} SCHFS_PACKED;
// Directory entry payload. The key's offset field holds the name hash, so a
// lookup is one tree descent rather than a directory scan.
//
// Two names in one directory can hash the same, and the key would then be
// identical — the second insert would silently replace the first. So the value
// is a *packed list*: one schfs_dirent_rec plus its name bytes, repeated. A
// 64-bit hash makes lists longer than one vanishingly rare, but "vanishingly
// rare" is not "impossible", and a filesystem that loses a file under a hash
// collision is not one to build on.
struct schfs_dirent_rec {
uint64_t objectid;
uint16_t type;
uint16_t namelen;
uint32_t pad;
/* name bytes follow, namelen of them, not NUL-terminated */
} SCHFS_PACKED;
#define SCHFS_NAME_MAX 255
// ---------------------------------------------------------------------------
// Attributes, and the index over them
//
// An attribute is a named value on an object -- the same thing Linux calls an
// extended attribute, and the thing BeFS built its query interface on. Stored
// under (ino, ATTR, namehash), packed exactly as directory entries are, and for
// the same reason: two names can hash alike, and a filesystem that loses one of
// them under a collision is not one to build on.
//
// The point of having them is the *index*. An attribute may be indexed, and an
// indexed attribute gets a second set of records keyed by its value rather than
// by the object -- so "every object whose `type` is `photo`" is a range scan of
// a few keys rather than a walk of every object in the filesystem. That is the
// whole of what "a filesystem you can query" means here, and the reason the
// design is one B-tree with many key types: the feature is a few hundred lines
// on machinery that already exists, rather than a second engine.
struct schfs_attr_rec {
uint32_t namelen;
uint32_t valuelen;
uint16_t type; // SCHFS_ATTR_* below; what a query compares as
uint16_t flags;
uint32_t pad;
/* name bytes, then value bytes; the pair padded to an 8-byte boundary */
} SCHFS_PACKED;
// What a value *is*, so that a query can compare two of them meaningfully. A
// string and an integer that happen to share bytes are not the same value, and
// ordering differs between them.
#define SCHFS_ATTR_RAW 0 // opaque bytes; only equality is meaningful
#define SCHFS_ATTR_STRING 1
#define SCHFS_ATTR_INT 2 // int64, little-endian
#define SCHFS_ATTR_TIME 3 // seconds since the epoch, int64
#define SCHFS_ATTR_NAME_MAX 63
#define SCHFS_ATTR_VALUE_MAX 255
// Step from one packed attribute record to the next. 8-byte aligned so the
// uint32s of the next one are naturally aligned.
#define SCHFS_ATTR_LEN(namelen, valuelen) \
((unsigned)((sizeof(struct schfs_attr_rec) + (namelen) + (valuelen) + 7) & ~7u))
// An index entry: one object that carries a given value for a given attribute.
//
// Keyed (indexid, INDEX, valuehash), where indexid is the hash of the attribute
// *name*. Several objects can share a value, so the payload is a packed list of
// these, exactly as dirents and attributes are.
struct schfs_index_rec {
uint64_t objectid;
uint32_t valuelen;
uint32_t pad;
/* value bytes follow, so a query can reject a hash collision */
} SCHFS_PACKED;
#define SCHFS_INDEX_LEN(valuelen) \
((unsigned)((sizeof(struct schfs_index_rec) + (valuelen) + 7) & ~7u))
// Which attributes are indexed. One record, under the index object id, whose
// value is a packed list of attribute names.
//
// A list rather than a flag on each attribute, because the question asked at
// every setxattr is "is *this name* indexed", and answering it from one small
// record is a single cached lookup rather than a search.
#define SCHFS_INDEX_OBJECTID 4ULL
#define SCHFS_INDEX_LIST_OFF 0ULL /* the one record's offset in the key */
// Step from one packed dirent to the next. Records are 8-byte aligned so the
// uint64 objectid of the next one is naturally aligned.
#define SCHFS_DIRENT_LEN(namelen) \
((unsigned)((sizeof(struct schfs_dirent_rec) + (namelen) + 7) & ~7u))
// ---------------------------------------------------------------------------
// Compile-time layout checks.
//
// Every one of these is a number that ends up baked into images on disk. A
// silent change to any of them — adding a field, changing a type, a compiler
// padding a struct differently — makes every existing image unreadable in a way
// that looks like corruption rather than like a mistake. Failing the build is
// enormously cheaper than debugging that.
// ---------------------------------------------------------------------------
#define SCHFS_ASSERT_SIZE(t, n) \
typedef char schfs_sizecheck_##t[(sizeof(struct t) == (n)) ? 1 : -1]
SCHFS_ASSERT_SIZE(schfs_key, 24);
SCHFS_ASSERT_SIZE(schfs_node_hdr, 32);
SCHFS_ASSERT_SIZE(schfs_kptr, 48);
SCHFS_ASSERT_SIZE(schfs_item, 32);
SCHFS_ASSERT_SIZE(schfs_uberblock, SCHFS_BLKSIZE);
SCHFS_ASSERT_SIZE(schfs_inode, 80);
SCHFS_ASSERT_SIZE(schfs_extent, 32);
SCHFS_ASSERT_SIZE(schfs_dirent_rec, 16);
SCHFS_ASSERT_SIZE(schfs_extref, 16);
SCHFS_ASSERT_SIZE(schfs_attr_rec, 16);
SCHFS_ASSERT_SIZE(schfs_index_rec, 16);
#endif /* SCHFS_H */
// The annotation above must not have moved anything. These are on-disk sizes:
// if one changes, every existing volume is misread from that record onward, and
// the failure looks like corruption rather than like a struct that grew.
_Static_assert(sizeof(struct schfs_dirent_rec) == 16, "dirent record size changed");
_Static_assert(sizeof(struct schfs_extent) == 32, "extent record size changed");
_Static_assert(sizeof(struct schfs_extref) == 16, "extref record size changed");
_Static_assert(sizeof(struct schfs_index_rec) == 16, "index record size changed");
_Static_assert(sizeof(struct schfs_attr_rec) == 16, "attr record size changed");
_Static_assert(sizeof(struct schfs_snap) == 96, "snapshot record size changed");
_Static_assert(sizeof(struct schfs_inode) == 80, "inode record size changed");