diff --git a/Makefile b/Makefile index 599116d..d5b21a8 100644 --- a/Makefile +++ b/Makefile @@ -711,6 +711,27 @@ $(BUILD)/sysroot.stamp: sysroot-install @cmp -s $@.new $@ || mv -f $@.new $@ @rm -f $@.new +# A small block device for the USB test to read. +# +# bootcheck -Usb attaches this as a usb-storage device and has the driver read +# block 1 off it, which is the only check that separates "a device answered +# INQUIRY" from "the driver can actually transfer data". +# +# Its own image, not the root volume and not build/fs.img: +# - attaching the root volume twice (once as hdb, once over USB) broke the +# xHCI interrupt check as well as the storage ones; +# - build/fs.img is no longer built by anything, since SchFS became the root, +# so pointing at it meant QEMU refused to start at all; +# - at FSSIZE blocks it would now be ~600 MB, because BSIZE went from 1024 to +# 4096. 16 MB is plenty to read one block from. +USBSTICK = $(BUILD)/usbstick.img +$(USBSTICK): $(MKFS) $(BUILD)/sysroot.stamp + @mkdir -p $(BUILD) + @rm -f $@ + $(MKFS) $@ --size 4096 --sysroot $(SYSROOT) + +usbstick: $(USBSTICK) + $(BUILD)/fs.img: $(MKFS) $(BUILD)/sysroot.stamp @mkdir -p $(BUILD) @echo "Creating SchOS File System Image..." diff --git a/include/fs.h b/include/fs.h index 29bad86..56778a0 100644 --- a/include/fs.h +++ b/include/fs.h @@ -13,7 +13,18 @@ #define FS_H #define ROOTINO 1 // root i-number -#define BSIZE 1024 // block size +// 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 diff --git a/include/param.h b/include/param.h index 7412dfe..7e85e38 100644 --- a/include/param.h +++ b/include/param.h @@ -24,5 +24,16 @@ #define MAXPATH 128 // max path #define MAXOPBLOCKS 30 // max # of blocks any FS op writes #define LOGSIZE (MAXOPBLOCKS*3) // max data blocks in on-disk log -#define NBUF (MAXOPBLOCKS*3) // size of disk block cache +// Disk block cache, in blocks. 256 * BSIZE = 1 MB. +// +// Was MAXOPBLOCKS*3 = 90 blocks, and at the old 1 KB BSIZE that is a 90 KB +// cache -- smaller than the toybox binary that every single command execs. So +// nothing was ever still resident on the next command and each one paid the +// full read again. 1 MB holds toybox with room to spare, which is what makes +// the *second* command fast rather than just the first. +// +// Sized rather than maximised: these are statically allocated in bio.c, so +// this is memory the system always spends. 1 MB is affordable on a machine +// with tens of megabytes; 8 MB would not be. +#define NBUF 256 #define FSSIZE 150000 // size of file system in blocks diff --git a/kernel/dev/ide.c b/kernel/dev/ide.c index be33f9c..f9fdf91 100644 --- a/kernel/dev/ide.c +++ b/kernel/dev/ide.c @@ -23,6 +23,7 @@ #define IDE_CMD_READ 0x20 #define IDE_CMD_WRITE 0x30 #define IDE_CMD_RDMUL 0xc4 +#define IDE_CMD_SETMULT 0xc6 #define IDE_CMD_WRMUL 0xc5 // idequeue points to the buf now being read/written to the disk. @@ -69,6 +70,28 @@ ide_irq(void *arg) ideintr(); } +// SET MULTIPLE MODE: how many sectors the drive moves per interrupt during +// READ/WRITE MULTIPLE. Must equal BSIZE/SECTOR_SIZE or idestart()'s single +// insl/outsl of a whole block does not line up with the drive's bursts. +// +// A drive may refuse a count it cannot do (IDENTIFY word 47 carries its +// maximum; QEMU allows 16). If it does, say so rather than continue: the +// transfers would silently desynchronise, which reads as filesystem +// corruption rather than as a driver problem. +static void +ide_set_multiple(int dev) +{ + int spb = BSIZE / SECTOR_SIZE; + + outb(0x1f6, 0xe0 | ((dev & 1) << 4)); + if(idewait(0) < 0) + return; + outb(0x1f2, spb); + outb(0x1f7, IDE_CMD_SETMULT); + if(idewait(1) < 0) + cprintf("ide%d: SET MULTIPLE MODE %d rejected; expect trouble\n", dev, spb); +} + void ideinit(void) { @@ -95,6 +118,23 @@ ideinit(void) } } + // Tell each drive how many sectors to move per interrupt. + // + // READ/WRITE MULTIPLE transfer the drive's *multiple count* of sectors + // between interrupts, and that count is whatever SET MULTIPLE MODE last + // established -- it is not taken from the sector-count register. Nothing + // ever issued it here, so the count was undefined, which is the real reason + // idestart() carried a `sector_per_block > 7` panic: BSIZE was 1024, two + // sectors, and nobody had tested further. + // + // Establishing it explicitly is what lets BSIZE be a whole SchFS block. At + // 1024 every 4096-byte SchFS block cost four IDE requests, each with its own + // interrupt and sleep/wakeup, and exec() of the 800 KB toybox binary meant + // ~800 round trips -- about two seconds, on every command. + ide_set_multiple(0); + if(havedisk1) + ide_set_multiple(1); + // Switch back to disk 0. outb(0x1f6, 0xe0 | (0<<4)); } @@ -121,7 +161,10 @@ idestart(struct buf *b) int read_cmd = (sector_per_block == 1) ? IDE_CMD_READ : IDE_CMD_RDMUL; int write_cmd = (sector_per_block == 1) ? IDE_CMD_WRITE : IDE_CMD_WRMUL; - if (sector_per_block > 7) panic("idestart"); + // No `sector_per_block > 7` panic here any more. That limit existed because + // nothing had set the drive's multiple count, so anything past the tested + // two sectors was a guess; ideinit() issues SET MULTIPLE MODE now, and the + // count is whatever BSIZE says. idewait(0); outb(0x3f6, 0); // generate interrupt diff --git a/kernel/proc/proc.c b/kernel/proc/proc.c index ef2bc7d..8232dfa 100644 --- a/kernel/proc/proc.c +++ b/kernel/proc/proc.c @@ -551,6 +551,26 @@ wakeup(void *chan) // mksh depends on kill(getpid(), SIGCHLD) genuinely re-raising SIGCHLD // (its held_sigchld recovery in j_startjob); the previous sys_kill // silently discarded every non-fatal signal. +// PID 1 does not die from a signal it never asked to handle. +// +// POSIX, and Linux in sig_task_ignored(): a signal whose disposition is the +// default action is discarded for init rather than delivered. Without this the +// system has a single keystroke that kills it -- Ctrl-C sends SIGINT to the +// foreground process group, init is pgid 1, and any moment where tty_fg_pgrp +// is 1 (before a shell claims the terminal, after one exits, or in the window +// between the two while the key is held down) lands SIGINT on init. init has +// no handler, so killed is set, exit() runs, and exit() panics outright on +// initproc: "init exiting". Spamming Ctrl-C at a frozen program is exactly the +// input that finds that window. +// +// Applies to SIGKILL too, which is the point: init is not killable from +// userspace on any Unix. A handler it did install is still delivered. +static int +init_ignores(struct proc *p, int sig) +{ + return p == initproc && p->sig_handler[sig] == 0; +} + int sigsend(int pid, int sig) { @@ -562,6 +582,10 @@ sigsend(int pid, int sig) acquire(&ptable.lock); for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){ if(p->pid == pid && p->state != UNUSED && p->state != ZOMBIE){ + if(init_ignores(p, sig)){ + release(&ptable.lock); + return 0; // silently dropped, as POSIX requires + } p->pending_signals |= 1ULL << sig; if(p->sig_handler[sig] == 0 && (sig == SIGKILL || sig == SIGTERM || sig == SIGABRT || @@ -607,6 +631,8 @@ sigsend_pgrp(int pgid, int sig) found = 1; if(sig == 0) continue; // signal 0 is an existence probe only + if(init_ignores(p, sig)) + continue; // see init_ignores(): this is the Ctrl-C case p->pending_signals |= 1ULL << sig; if(p->sig_handler[sig] == 0 && (sig == SIGKILL || sig == SIGTERM || sig == SIGABRT || diff --git a/tools/windows/bootcheck.ps1 b/tools/windows/bootcheck.ps1 index 4dc2d42..fa80798 100644 --- a/tools/windows/bootcheck.ps1 +++ b/tools/windows/bootcheck.ps1 @@ -49,7 +49,7 @@ param( # else in this suite touches. [switch]$Ps, [int]$BootSeconds = 20, - [string]$Qemu = (Find-Qemu) + [string]$Qemu ) # QEMU, on whichever host this is. @@ -75,6 +75,8 @@ function Find-Qemu { throw "qemu-system-x86_64 not found on PATH. Install QEMU, or pass -Qemu ." } +if (-not $Qemu) { $Qemu = Find-Qemu } + $ErrorActionPreference = 'Stop' # Repo root is two levels up: this script lives in tools/windows/. $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent @@ -132,6 +134,22 @@ $commands += 'wc -l /etc/motd' # above and fail here. $commands += 'cat /proc/schfs' +# init must survive signals it has no handler for. +# +# This is a crash-the-whole-machine bug, not a nicety: Ctrl-C sends SIGINT to +# the foreground process group, init is pgid 1, and any window where +# tty_fg_pgrp is 1 lands it on init -- which had 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. +# +# SIGKILL is in here deliberately: init is not killable from userspace on any +# Unix, and a guard that covers SIGINT but not SIGKILL is half a fix. +$commands += 'kill -INT 1' +$commands += 'kill -KILL 1' +$commands += 'kill -TERM 1' +$commands += 'echo INIT-SURVIVED' +$commands += 'ps | head -3' + if ($Fb) { # /dev/fb0. The geometry has to come back from the mode the bootloader # actually programmed -- three places in the kernel used to assume @@ -213,14 +231,14 @@ $usbDevices = if ($Usb) { # the hub sits on port 2 and the mouse on port 2.1 of the same bus. '-device usb-hub,id=hub,bus=xhci.0,port=2' '-device usb-mouse,bus=xhci.0,port=2.1' - # Always the v1 image, never $hdb. The checks below read a v1 superblock + # Its own small image, never $hdb. The checks below read a v1 superblock # off this stick and assert on its sector count and size field, so the # stick has to be a v1 image whatever the root filesystem is -- under # the root $hdb is a SchFS volume and those fields are simply not # there, which failed three USB checks that had nothing wrong with USB. # It also stops the same file being attached twice, once as the root # disk and once as a USB device. - "-drive id=usbstick,file=`"$(Join-Path $root 'build/fs.img')`",format=raw,if=none" + "-drive id=usbstick,file=`"$(Join-Path $root 'build/usbstick.img')`",format=raw,if=none" '-device usb-storage,bus=xhci.0,drive=usbstick') } else { @() } @@ -419,6 +437,12 @@ Expect '00:01\.1 8086:7010 IDE' 'the IDE controller was identified by class' Expect '\[vga\]' 'a driver bound itself to the display controller' Expect 'vga: display controller' 'the display driver found its aperture' +# If init had died the machine would have panicked and nothing below this +# point would appear at all -- but check explicitly, so a failure names the +# cause instead of showing up as "the shell stopped answering". +Expect 'INIT-SURVIVED' 'init survives SIGINT, SIGKILL and SIGTERM' +Expect '(?m)^\s*1\s' 'pid 1 is still in the process table afterwards' + Expect 'schfs: mounted' 'the kernel mounted SchFS as the root filesystem' Expect 'init: ready' 'init came up on a SchFS root' Expect 'label:\s*schos' '/proc/schfs reports the volume label' @@ -457,5 +481,62 @@ if ($Fb) { Expect 'gradient drawn' 'mmap of /dev/fb0 maps the real framebuffer' } +if ($Tty) { + # Each of these is a bug that once looked like a broken *command*. + # `ls | wc -l` reported 48 rather than the real count because ioctl + # answered TIOCGWINSZ on a pipe with success, so ls formatted in columns. + # Anything in the hundreds means it printed one name per line. + Expect '(?m)^\s*[2-9][0-9]{2}\s*$' 'ls into a pipe prints one name per line' + # writev capped iovcnt at 16; toybox builds 256 of them, so `yes` failed + # outright. + Expect '(?ms)yes \| head -2\s*\n\s*y\s*\n\s*y' 'yes writes through a pipe' + # 0x7F E L F. Four bytes, not three -- the leading byte is invisible. + Expect 'ELF' 'a binary reads back through a pipe intact' + Expect 'caf.*-.*-' 'UTF-8 survives the terminal unmangled' + # SIG_IGN is handler value 1, and signal delivery once jumped to address 1, + # which killed the shell rather than the pipeline. + Expect 'TTY-SURVIVED' 'the shell is still alive after all of that' + Expect 'shmtest: all checks passed' 'shared memory and file-backed mmap work' +} + +if ($Usb) { + # A registered handler that never fires looks exactly like a working one + # from userspace. A non-zero count on the xhci line is the difference. + # Not anchored to the start of the line: xHCI shares IRQ 11 with the e1000 + # here, so the line reads "11: 28 e1000, 28 xhci level" and the xhci + # entry is the second one on it. + Expect '[1-9]\d*\s+xhci' 'the xHCI interrupt is actually delivered' + # build/usbstick.img is 16 MB = 32768 sectors of 512 bytes. READ CAPACITY + # returning the true geometry is what separates a driver that parsed the + # reply from one that assumed a size. + Expect 'usb-storage: 32768 sectors of 512 bytes' 'READ CAPACITY returned the true size' + # And then an actual transfer: block 1 of a v1 image is its superblock. + Expect 'block 1 magic 10203040' 'a real block came back off the USB disk' + Expect 'size 4096' 'the superblock read over USB is the right one' +} + +if ($Sock) { + Expect 'socktest: ALL CHECKS PASSED' 'every socket check passed' +} + +if ($Ps) { + # /proc is synthetic, so a per-process directory is the thing to prove. + # `ls /proc` prints in columns, so pid 1 is a token on a line rather than a + # line of its own. + Expect '(?m)^1\s+\d' '/proc has a directory for pid 1' + Expect '(?m)^1 \(' '/proc/1/stat starts with the pid and comm' + Expect 'sinit|init' 'ps names pid 1' + Expect '(?m)PID.*PPID.*COMM' 'ps honours -o with the requested columns' + Expect 'load average|Mem|CPU' 'top produces a report rather than an error' +} + +if ($Evdev) { + # A key has to be *pressed* for this to say anything: characters typed at + # the serial port never reach the keyboard controller, which is why this + # run drives the QEMU monitor's sendkey instead. + Expect 'inputtest: done|event limit reached' 'the input layer delivered events' + Expect 'value' 'events carry a value, not just a type' +} + if ($script:fail) { exit 1 } Write-Host 'boot check passed' -ForegroundColor Green diff --git a/tools/windows/crashshot.ps1 b/tools/windows/crashshot.ps1 index ce8b847..9e4cf0f 100644 --- a/tools/windows/crashshot.ps1 +++ b/tools/windows/crashshot.ps1 @@ -14,7 +14,7 @@ param( [int]$BootSeconds = 20, [string]$Out = "build/crashscreen.ppm", - [string]$Qemu = (Find-Qemu) + [string]$Qemu ) # QEMU, on whichever host this is. @@ -40,6 +40,8 @@ function Find-Qemu { throw "qemu-system-x86_64 not found on PATH. Install QEMU, or pass -Qemu ." } +if (-not $Qemu) { $Qemu = Find-Qemu } + $ErrorActionPreference = 'Stop' # Repo root is two levels up: this script lives in tools/windows/. $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent diff --git a/tools/windows/dispshot.ps1 b/tools/windows/dispshot.ps1 index e1884c3..9599737 100644 --- a/tools/windows/dispshot.ps1 +++ b/tools/windows/dispshot.ps1 @@ -19,7 +19,7 @@ param( [int]$BootSeconds = 20, [string]$Out = "build/dispshot.ppm", - [string]$Qemu = (Find-Qemu) + [string]$Qemu ) # QEMU, on whichever host this is. @@ -45,6 +45,8 @@ function Find-Qemu { throw "qemu-system-x86_64 not found on PATH. Install QEMU, or pass -Qemu ." } +if (-not $Qemu) { $Qemu = Find-Qemu } + $ErrorActionPreference = 'Stop' # Repo root is two levels up: this script lives in tools/windows/. $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent diff --git a/tools/windows/fbshot.ps1 b/tools/windows/fbshot.ps1 index 2999503..c996b5c 100644 --- a/tools/windows/fbshot.ps1 +++ b/tools/windows/fbshot.ps1 @@ -13,7 +13,7 @@ param( [ValidateSet('fill', 'bars')] [string]$Mode = 'fill', [int]$BootSeconds = 20, - [string]$Qemu = (Find-Qemu) + [string]$Qemu ) # QEMU, on whichever host this is. @@ -39,6 +39,8 @@ function Find-Qemu { throw "qemu-system-x86_64 not found on PATH. Install QEMU, or pass -Qemu ." } +if (-not $Qemu) { $Qemu = Find-Qemu } + $ErrorActionPreference = 'Stop' # Repo root is two levels up: this script lives in tools/windows/. $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent diff --git a/tools/windows/makeiso.ps1 b/tools/windows/makeiso.ps1 index 23423c6..9d920eb 100644 --- a/tools/windows/makeiso.ps1 +++ b/tools/windows/makeiso.ps1 @@ -14,7 +14,7 @@ param( [switch]$Run, [switch]$Clean, - [string]$Qemu = (Find-Qemu) + [string]$Qemu ) # QEMU, on whichever host this is. @@ -40,6 +40,8 @@ function Find-Qemu { throw "qemu-system-x86_64 not found on PATH. Install QEMU, or pass -Qemu ." } +if (-not $Qemu) { $Qemu = Find-Qemu } + $ErrorActionPreference = 'Stop' # Repo root is two levels up: this script lives in tools/windows/. $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent diff --git a/tools/windows/splashshot.ps1 b/tools/windows/splashshot.ps1 index c697ea7..99bf8dd 100644 --- a/tools/windows/splashshot.ps1 +++ b/tools/windows/splashshot.ps1 @@ -17,7 +17,7 @@ param( [int]$Every = 400, # milliseconds between frames [int]$For = 8, # seconds to keep capturing [string]$OutDir = "build/shots", - [string]$Qemu = (Find-Qemu) + [string]$Qemu ) # QEMU, on whichever host this is. @@ -43,6 +43,8 @@ function Find-Qemu { throw "qemu-system-x86_64 not found on PATH. Install QEMU, or pass -Qemu ." } +if (-not $Qemu) { $Qemu = Find-Qemu } + $ErrorActionPreference = 'Stop' # Repo root is two levels up: this script lives in tools/windows/. $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent diff --git a/tools/windows/textshot.ps1 b/tools/windows/textshot.ps1 index b178eba..b0ace22 100644 --- a/tools/windows/textshot.ps1 +++ b/tools/windows/textshot.ps1 @@ -19,7 +19,7 @@ param( [int]$BootSeconds = 20, [string]$Out = "build/textshot.ppm", - [string]$Qemu = (Find-Qemu) + [string]$Qemu ) # QEMU, on whichever host this is. @@ -45,6 +45,8 @@ function Find-Qemu { throw "qemu-system-x86_64 not found on PATH. Install QEMU, or pass -Qemu ." } +if (-not $Qemu) { $Qemu = Find-Qemu } + $ErrorActionPreference = 'Stop' # Repo root is two levels up: this script lives in tools/windows/. $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent diff --git a/tools/windows/ttyshot.ps1 b/tools/windows/ttyshot.ps1 index 155f564..584fbec 100644 --- a/tools/windows/ttyshot.ps1 +++ b/tools/windows/ttyshot.ps1 @@ -12,7 +12,7 @@ param( [int]$BootSeconds = 22, [string]$Out = "build/ttyshot.ppm", - [string]$Qemu = (Find-Qemu) + [string]$Qemu ) # QEMU, on whichever host this is. @@ -38,6 +38,8 @@ function Find-Qemu { throw "qemu-system-x86_64 not found on PATH. Install QEMU, or pass -Qemu ." } +if (-not $Qemu) { $Qemu = Find-Qemu } + $ErrorActionPreference = 'Stop' # Repo root is two levels up: this script lives in tools/windows/. $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent