SchOS/.claude/memory/schos-performance-and-init.md
Nexoniarz a1c965da1e 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
2026-10-23 15:05:12 +02:00

3.1 KiB

name description metadata
schos-performance-and-init BSIZE must equal SCHFS_BLKSIZE or everything is slow; init must ignore default-action signals or Ctrl-C kills the machine
node_type type originSessionId modified
memory project 9d8a2aef-bcf0-47a0-96c6-f706efab4a95 2026-08-09T13:54:32.858Z

BSIZE must equal SCHFS_BLKSIZE

BSIZE was 1024 (xv6's) 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 and sleep/wakeup. exec() of the 800 KB toybox binary meant ~800 round trips — about two seconds, on every command.

The measurement that found it: 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 are dominated by loading the same binary. When everything is slow by the same constant, measure something that does nothing — the floor is the bug.

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

Three coupled changes, all required together:

  1. BSIZE 1024 → 4096 (include/fs/fs.h). Keep it equal to SCHFS_BLKSIZE.
  2. NBUF 90 → 256 blocks (1 MB). At 90 KB the cache was smaller than the binary every command execs, so nothing was ever resident on the next command. This is what makes the second command fast.
  3. ideinit() issues SET MULTIPLE MODE (0xC6). READ/WRITE MULTIPLE move the drive's multiple count of sectors per interrupt, and that count comes from this command — not from the sector-count register. Nothing had ever issued it, which is the real reason idestart() carried if (sector_per_block > 7) panic("idestart"): BSIZE was two sectors and nobody had tested further. Without it, a 4096-byte transfer desynchronises and reads as filesystem corruption.

Still open: the first command after boot pays a cold ~2.4s. Per-request cost under QEMU PIO dominates; DMA would be the next step.

FSSIZE is in blocks, so at BSIZE 4096 a full v1 image is ~600 MB. That is why the USB test gets its own 16 MB build/usbstick.img (make usbstick) rather than build/fs.img.

init must ignore default-action signals

Ctrl-C could kill pid 1 and panic the machine. 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 down — lands SIGINT on init. No handler, so killed was set, exit() ran, and exit() panics outright on initproc. Spamming Ctrl-C at a frozen program is what finds that window.

init_ignores() in kernel/proc/proc.c drops a signal for init when sig_handler[sig] == 0, in both sigsend() and sigsend_pgrp(). SIGKILL included — init is not killable from userspace on any Unix, and a guard that covers SIGINT but not SIGKILL is half a fix. A handler init did install is still delivered.

Regression test lives in the base bootcheck run: kill -INT 1, -KILL 1, -TERM 1, then INIT-SURVIVED.