SchOS/kernel/arch/x86_64/trap.c
Nexoniarz a5ff83de46 proc: real CPU time, a tty number, and an exe that is a symlink
Four Gate 0 rows, and a fifth found while checking one of them.

**getrusage() did not distinguish RUSAGE_CHILDREN from RUSAGE_SELF**, which
undersells it: it ignored `who` entirely and reported the machine's *uptime*
as the caller's user time. Every process of every age answered with the same
number. Per-process CPU time is sampled at the timer tick now and split on the
interrupted privilege level; children's time is rolled up in wait() rather than
exit(), because RUSAGE_CHILDREN means *reaped* children and a running one has
not contributed yet.

**And wait4()'s fourth argument is a `struct rusage *` that was read off the
stack and thrown away.** That is the one shells actually use -- wait3() is the
same syscall with pid -1 -- so zsh's `time` reported

    16777216.01s user 1073.78s system 927983249% cpu

for every command. 2^24 seconds is uninitialised memory, and it did not vary
with the workload because it was never written. Now:

    busy loop   2.66s user  97% cpu   2.720 total
    sleep 2     0.02s user   0% cpu   2.248 total
    ls -lR /    0.70s user  65% cpu   1.070 total

**ru_stime is a structural zero and says so.** MSR_SFMASK includes FL_IF, so
SYSCALL clears interrupts and no tick can land while a process is inside a
syscall. Tick sampling cannot see kernel time at all here. Charging it needs
TSC deltas closed and reopened around every sched(), which is scheduler
accounting rather than sampling; the limitation is written down in
docs/architecture/kernel.md rather than papered over.

**/proc/<pid>/exe is a symlink.** It also needed a real st_size, and every
other procfs file deliberately reports zero -- readlink(2) bounds its copy by
it, so a symlink of size 0 resolves to the empty string, which looks like it
worked and is worse than the regular file was. Generating that one path in
ilock() costs one small render per stat() of one file.

**OVMF memory reporting is closed**: 1 GB reports 1017 MB usable, 2 GB reports
2041 MB. The firmware keeps about 7 MB either way, which is the right shape.

**And `ps` printed a header and nothing else**, which is not in any list
because nobody had run it. `ps` with no arguments shows only processes on
*this* terminal; /proc/<pid>/stat hardcoded tty_nr to 0, so nothing could ever
match. `ps -e` always worked, which is exactly what hid it. p->ctty was already
tracked -- procfs simply was not asked. Fields 14..17 were hardcoded zero too,
so TIME read 00:00:00 however long a process had run; they carry the new
counters now, and a shell that has burned 653 ticks reports 00:00:06.

Verified: utime climbs 18 -> 653 across a loop in the shell itself, `ps` lists
its own terminal, `ps -e` still lists everything, `readlink /proc/1/exe` gives
/bin/schinit, and sigtest/$()/rename/dmesg are all still green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 02:45:00 +02:00

224 lines
8 KiB
C

#include "types.h"
#include "defs.h"
#include "irq.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "x86.h"
#include "traps.h"
#include "spinlock.h"
#include "posix.h"
uint *idt;
extern addr_t vectors[];
struct spinlock tickslock;
uint ticks;
static void
mkgate(uint *idt, uint n, addr_t kva, uint pl)
{
uint64 addr = (uint64) kva;
n *= 4;
idt[n+0] = (addr & 0xFFFF) | (KERNEL_CS << 16);
idt[n+1] = (addr & 0xFFFF0000) | 0x8E00 | ((pl & 3) << 13);
idt[n+2] = addr >> 32;
idt[n+3] = 0;
}
void idtinit(void)
{
struct {
ushort limit;
uint64 base;
} __attribute__((packed)) idtdesc;
idtdesc.limit = PGSIZE - 1;
idtdesc.base = (uint64)idt;
asm volatile("lidt %0" : : "m" (idtdesc));
}
void tvinit(void)
{
int n;
idt = (uint*) kalloc();
memset(idt, 0, PGSIZE);
for (n = 0; n < 256; n++)
mkgate(idt, n, vectors[n], 0);
mkgate(idt, T_SYSCALL, vectors[T_SYSCALL], 3);
}
uint
uptime(void)
{
uint xticks;
acquire(&tickslock);
xticks = ticks;
release(&tickslock);
return xticks;
}
// Which signal a CPU fault corresponds to, so a process killed by a fault is
// reported to wait() as died-by-signal with a sensible cause rather than a
// blanket SIGKILL.
static int
trapsig(uint64 trapno)
{
switch(trapno){
case T_DIVIDE: return SIGFPE;
case T_ILLOP: return SIGILL;
case T_GPFLT: return SIGSEGV;
case T_PGFLT: return SIGSEGV;
case T_BRKPT: return SIGTRAP;
case T_DEBUG: return SIGTRAP;
default: return SIGSEGV;
}
}
void
trap(struct trapframe *tf)
{
kstack_check_canary("trap entry");
switch(tf->trapno){
case T_IRQ0 + IRQ_TIMER:
if(cpunum() == 0){
acquire(&tickslock);
ticks++;
wakeup(&ticks);
release(&tickslock);
}
// Charge the tick to whoever it interrupted. Every CPU does this, not just
// CPU 0 -- the global clock advances once, but the *work* happened on
// whichever processor was running that process.
//
// The split is by the interrupted privilege level, because that is the
// only thing the tick knows about what it interrupted. It is sampling, not
// measurement, which is what Linux does too and why these are ticks rather
// than microseconds.
//
// **stime is therefore almost always zero here, and that is structural.**
// MSR_SFMASK includes FL_IF (see syscallinit), so SYSCALL clears interrupts
// and nothing re-enables them for the duration -- a timer tick cannot land
// while a process is executing a syscall. The only ticks that ever reach
// the `else` are ones that arrive in a nested trap. Charging real system
// time needs TSC deltas closed and reopened around every sched(), which is
// scheduler accounting rather than sampling, and is not written.
if(myproc()){
if((tf->cs & 3) == DPL_USER) myproc()->utime++;
else myproc()->stime++;
}
// Debug registers are per-CPU, and there is no general IPI here, so this
// is how a watchpoint armed on one processor reaches the others: each
// notices the table's generation has moved and reloads. One comparison
// per tick when nothing has changed. See kernel/arch/x86_64/watch.c.
watch_sync();
lapiceoi();
break;
case T_DEBUG:
// A hardware watchpoint fired. It is a *trap*, not a fault -- the store
// has already happened -- so reporting and returning carries on with the
// program none the wiser, which is what lets this be used on a machine
// that is still running.
if(watch_trap(tf))
break;
// Not ours: a single-step, or a #DB from somewhere else. Falls through to
// the unexpected-trap path rather than being swallowed.
goto unexpected;
case T_IRQ0 + IRQ_SPURIOUS:
cprintf("cpu%d: spurious interrupt at %p:%p\n",
cpunum(), tf->cs, tf->rip);
lapiceoi();
break;
default:
// Every other IOAPIC pin goes through the registration table. The device
// interrupts used to be listed here one case at a time, which cannot work
// for anything found on the PCI bus: a driver does not know its line
// until it reads the device's config space at probe time, so there is no
// constant to write a case label for. See include/irq.h.
if(tf->trapno >= T_IRQ0 && tf->trapno < T_IRQ0 + NIRQ){
irq_dispatch(tf->trapno - T_IRQ0);
lapiceoi();
break;
}
unexpected:
if(myproc() == 0 || (tf->cs&3) == 0){
// The rip is named where it can be. This is a kernel address, so
// ksym_lookup() has something to say about it -- and the whole reason
// docs/architecture/debugging.md puts D0 first is that "rip ffff8000001294c2"
// costs an objdump session and "rip in dma_alloc+0x8c" does not.
char where[72];
ksym_fmt(where, sizeof(where), tf->rip);
cprintf("unexpected trap %d from cpu %d rip %p <%s> (cr2=0x%p) err=0x%p\n",
tf->trapno, cpunum(), tf->rip, where, rcr2(), tf->err);
if (myproc())
cprintf("proc id: %d\n", myproc()->pid);
if (tf->trapno == T_GPFLT) {
uint64 *iretframe = (uint64*)tf->rsp;
cprintf("iretq frame @ %p: rip=%p cs=%p rflags=%p rsp=%p ss=%p\n",
iretframe, iretframe[0], iretframe[1], iretframe[2],
iretframe[3], iretframe[4]);
if (myproc())
cprintf("this tf=%p myproc()->tf=%p myproc()->kstack=%p (tf-kstack=%p)\n",
tf, myproc()->tf, myproc()->kstack, (uint64)tf - (uint64)myproc()->kstack);
}
panic("trap");
}
cprintf("pid %d %s: trap %d err %d on cpu %d "
"rip 0x%p addr 0x%p--kill proc\n",
myproc()->pid, myproc()->name, tf->trapno, tf->err, cpunum(), tf->rip,
rcr2());
// General-purpose register dump. This used to be followed by ~25 lines
// that walked musl's mallocng group/meta structures out of rax/rdi and
// dumped the user stack -- scaffolding for the boot-crash investigation
// recorded in docs/history/kernel.md section 1. That
// investigation is closed and the underlying kernel bugs are fixed, so
// the allocator-specific part is gone; the registers stay, since they are
// useful for any fault.
// The full picture, for the two faults that mean something went wrong
// *elsewhere* rather than here: a general protection fault, and a jump to
// an address no code is ever linked at.
//
// The second case earns its own dump because of what it catches. A process
// executing at rip 0 did not branch there; it was handed a zero and
// followed it, and the registers say which one. The same dump on a
// *faulting address* below a page caught `addr 0xffffffffffffffea` in
// schinitd -- which is -22, which is -EINVAL: a call returned an error and
// the caller used it as a pointer. Neither of those is visible from the
// one-line report.
if (tf->trapno == T_GPFLT || tf->rip < PGSIZE || rcr2() + PGSIZE < PGSIZE * 2) {
cprintf(" rax=%p rbx=%p rcx=%p rdx=%p rsi=%p rdi=%p rbp=%p rsp=%p\n",
tf->rax, tf->rbx, tf->rcx, tf->rdx, tf->rsi, tf->rdi, tf->rbp, tf->rsp);
cprintf(" cs=%p ss=%p rflags=%p r11=%p tf=%p kstack=%p sz=%p\n",
tf->cs, tf->ss, tf->rflags, tf->r11, tf,
myproc()->kstack, myproc()->sz);
// The top of the user stack -- whatever a `ret` was about to use.
cprintf(" ustack:");
for (int i = -2; i < 4; i++) {
addr_t v;
if (fetchaddr(tf->rsp + i * 8, &v) == 0) cprintf(" %p", v);
else cprintf(" ????");
}
cprintf("\n");
}
myproc()->killed = 1;
if(myproc()->killsig == 0)
myproc()->killsig = trapsig(tf->trapno);
}
if(myproc() && myproc()->killed && (tf->cs&3) == DPL_USER)
exit(W_EXITCODE(0, myproc()->killsig ? myproc()->killsig : SIGKILL));
if(myproc() && myproc()->state == RUNNING && tf->trapno == T_IRQ0+IRQ_TIMER)
yield();
if(myproc() && myproc()->killed && (tf->cs&3) == DPL_USER)
exit(W_EXITCODE(0, myproc()->killsig ? myproc()->killsig : SIGKILL));
kstack_check_canary("trap exit");
}