SchOS/include/fs.h
Nexoniarz 686d5cf006 init survives stray signals; commands are 4-34x faster
Two bugs reported from actual use, plus a test harness that had stopped
testing.

init could be killed by Ctrl-C, taking the machine with it. SIGINT goes
to the foreground process group, init is pgid 1, and any window where
tty_fg_pgrp is 1 -- before a shell claims the terminal, after one exits,
or between the two while the key is held -- lands it on init. init has
no handler, so killed was set, exit() ran, and exit() panics outright on
initproc. Spamming Ctrl-C at a frozen program is exactly the input that
finds that window. POSIX and Linux both discard a default-action signal
for pid 1; init_ignores() now does the same, SIGKILL included, because
init is not killable from userspace on any Unix.

Everything was slow because BSIZE was 1024 while a SchFS block is 4096,
so every block cost four IDE requests, each with its own interrupt and
sleep/wakeup. exec() of the 800 KB toybox binary meant ~800 round trips.
The giveaway: `seq 1 200`, which touches no file and prints nothing,
cost the same 2.0s as `cat /bin/toybox`, and `ls | wc -l` cost exactly
double -- both dominated by loading the same binary twice.

BSIZE is 4096 now, one request per SchFS block, and NBUF is 256 blocks
(1 MB) rather than 90 KB -- smaller than the binary every command execs,
so nothing was ever still resident. ideinit() issues SET MULTIPLE MODE,
which is what makes a 4096-byte transfer legal: READ/WRITE MULTIPLE move
the drive's multiple count per interrupt and nothing had ever set it,
which is the real reason idestart() carried a sector_per_block > 7 panic.

  seq 1 200          2.02s -> 0.06s
  ls /bin            2.76s -> 0.68s
  ls /bin | wc -l    4.68s -> 0.73s

And the harness. bootcheck called Find-Qemu from a param() default,
which is bound before any function in the body exists -- so it died on
its first line and printed an error containing neither "FAIL" nor
"Exception", the two things being grepped for. It had not run since it
was made host-portable. Worse, an earlier index-based edit had deleted
the Expect blocks for -Tty, -Usb, -Sock, -Ps and -Evdev, so five of the
seven modes asserted nothing at all. Restored, and the USB run gets its
own 16 MB build/usbstick.img: it had been pointed at build/fs.img, which
nothing builds since SchFS became root, so QEMU refused to start.

242 checks across seven modes, all passing, and make schfs-check green.
2026-08-09 15:54:05 +02:00

214 lines
9 KiB
C

// 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/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