Painting every kernel stack at allocation and scanning it on each trap puts the
high-water mark at **3736 of 4096 bytes** -- 91%, in netd, inside lwIP's input
path. 360 bytes of headroom on a kernel that takes interrupts on the current
stack is not headroom: a timer landing at that depth pushes a trapframe and
runs trap() straight off the bottom of the page and into whatever kalloc handed
out next, which is very often another process's kernel stack.
The canary at kstack[0] did not catch this and could not have. It is checked on
trap entry and exit, and a frame allocated in one `sub $N, %rsp` steps clean
over the bottom eight bytes and writes below them without ever touching it. It
catches a slow walk past the end, not one deep call.
8192 now, and the stacks are static -- one per ptable slot, NPROC * KSTACKSIZE
= 1 MB of BSS. Static rather than a bigger kalloc because kalloc() hands out
exactly one page and there is no contiguous multi-page allocator to ask. A
process's slot is fixed for its lifetime, so the stack belonging to that slot is
the natural answer: no allocation to fail, no free to forget, and the kalloc
failure path in allocproc() goes away with it. The page was being paid for out
of kalloc before, one at a time, and undercounted.
**And the AP scheduler stack was already wrong.** startothers() did
`stack = kalloc()` for one page and then handed the AP `stack + KSTACKSIZE` as
its stack top. Exactly right for as long as KSTACKSIZE happened to equal
PGSIZE, and a whole page past the allocation the moment it did not -- which
this commit would have made true. That is not a throwaway boot stack either:
the AP runs mpmain() and then scheduler() on it for the rest of the machine's
life, and takes interrupts on it whenever no process is current. Static too,
NCPU of them.
**This does not fix the intermittent zsh crash**, and the measurement above was
taken while looking for it. It is worth saying plainly: 1-3 shells per boot
still die, so the hypothesis was wrong and the stack was a separate latent bug
that the search turned up.
What the search did establish is worth more than the guess was:
- **It is SMP-only.** Three boots at -smp 1: zero faults. Three at -smp 2:
one, three, one.
- **It is not only zsh.** schinitd took one too.
- **The faults name their cause if you print enough.** `rip 0x0` is a process
handed a zero and following it, not a process that branched anywhere. And
schinitd's `addr 0xffffffffffffffea` is -22, which is -EINVAL: a call
returned an error and the caller used it as a pointer.
So trap.c dumps registers, segments, and the top of the user stack for a fault
at rip below a page or a faulting address near zero, the way it already did for
a GP fault. Neither of the two facts above was visible from the one-line
report, and both came out of the first dump that included them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
68 lines
3.5 KiB
C
68 lines
3.5 KiB
C
#pragma once
|
|
|
|
// The timer interrupt rate the kernel is built around. Everything that turns
|
|
// ticks into a duration divides by this. It used to be the literal 100 written
|
|
// out at each site, which made it look like a fact about the hardware; it is
|
|
// not. It is a target, and kernel/arch/x86_64/lapic.c calibrates the LAPIC
|
|
// timer against the PIT to actually hit it.
|
|
#define HZ 100 // timer interrupts per second
|
|
|
|
// The tick rate the *user-space ABI* is defined in, which is a different thing
|
|
// from HZ and must not follow it. glibc and musl both answer sysconf(_SC_CLK_TCK)
|
|
// with a hardcoded 100, so times() and /proc/stat are read as hundredths of a
|
|
// second no matter what the kernel's internal rate is. Linux calls this USER_HZ
|
|
// and scales at the syscall boundary; so does SchOS, in the two places below.
|
|
#define USER_HZ 100
|
|
|
|
// Converting a POSIX timespec/timeval into ticks. These used to appear as the
|
|
// bare literals 10000000 and 10000 at each call site, where they looked like
|
|
// magic numbers rather than what they are: the length of one tick, expressed
|
|
// in the units the caller happens to be using.
|
|
#define NSEC_PER_TICK (1000000000 / HZ)
|
|
#define USEC_PER_TICK (1000000 / HZ)
|
|
#define MSEC_PER_TICK (1000 / HZ)
|
|
|
|
#define NPROC 128 // maximum number of processes
|
|
#define KSTACKSIZE 8192 // per-process kernel stack; see kstacks[] in proc.c
|
|
#define KSTACK_CANARY 0xDEADC0DEDEADC0DEULL // sentinel at kstack[0], see kstack_check_canary()
|
|
#define NCPU 16 // maximum number of CPUs
|
|
#define NOFILE 128 // open files per process
|
|
#define NFILE 512 // open files per system
|
|
#define NINODE 200 // maximum number of active i-nodes
|
|
#define NDEV 12 // maximum major device number
|
|
#define RAMDISK_DEV 2 // memory-backed block device (kernel/dev/ramdisk.c)
|
|
#define USBDISK_DEV 3 // USB mass storage (kernel/drivers/usb_storage.c)
|
|
|
|
// Device the root filesystem is mounted from. Building with RAMROOT=1 defines
|
|
// RAMDISK_ROOT and boots from the SCHFS image linked into the kernel instead
|
|
// of the IDE disk, leaving the physical disk free to be reformatted from a
|
|
// running system. See kernel/dev/ramdisk.c.
|
|
#ifdef RAMDISK_ROOT
|
|
#define ROOTDEV RAMDISK_DEV
|
|
#else
|
|
#define ROOTDEV 1 // device number of file system root disk
|
|
#endif
|
|
#define MAXARG 32 // max exec arguments
|
|
#define MAXENV 64 // max environment variables passed through exec
|
|
#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
|
|
// 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
|
|
|
|
// Network interfaces the stack will attach. Two is already an ordinary machine
|
|
// -- a laptop with Ethernet plus a virtual adapter -- and each one costs a
|
|
// struct netif, so the limit is about keeping a fixed array honest rather than
|
|
// about memory.
|
|
#define NETIF_MAX 4
|