SchOS/kernel/sys/sys_time.c
Nexoniarz 9d4cdb44fc kernel: the clock ran 7x slow because the GPU never acknowledged its interrupt
fastfetch reported 3 seconds of uptime after 15 real ones, `time` claimed
3.12s for a 20-second command, and ping's one-second interval was closer to
six. One cause, not three: `ticks` was advancing at ~14.5 Hz instead of 100.

It was not the timer. virtio-gpu-pci shares IRQ 11 with the e1000 and
registered no interrupt handler at all. PCI INTx is level-triggered, so the
device held the line asserted, the kernel signalled EOI, and it fired again
immediately -- 72,000 interrupts a second, 83% of CPU0, starving the tick.
The e1000's handler read ICR as zero half a million times, correctly
answering "not mine", which is what made it look like a network-free problem.

Reading virtio's ISR byte clears it and drops the line. That is the fix.

irq_dispatch now measures each line's rate against the TSC -- not ticks,
which is the first casualty of a storm -- and names the attached handlers
once above 20000/s. It reproduces the original fault as a single line.

Found along the way, and fixed because they were real:

  - lapicinit loaded TICR with xv6's uncalibrated 10000000, whose own comment
    admitted it was never measured. calibrate_timers() now measures the LAPIC
    and the TSC against the 8254 PIT. (It came back at 999.9 MHz, so this was
    not the bug -- but the next machine is not obliged to be 1 GHz.)
  - microdelay() was an empty function body. Every microdelay(1000) in the
    xHCI reset path and the USB hub power-on sequence returned instantly.
  - HZ is a constant in param.h now, not the literal 100 written out at a
    dozen ticks-to-seconds conversions. USER_HZ is separate and deliberately
    does not follow it: musl answers sysconf(_SC_CLK_TCK) with 100 always, so
    times() and /proc/stat scale at the syscall boundary the way Linux does.
  - TIOCGWINSZ answered a hardcoded 24x80. gfx_cols()/gfx_rows() have known
    the real geometry all along; at 1024x768 the console is 128x48, so every
    program that lays out a screen was formatting for the wrong one.
  - TCFLSH fell through to the catch-all `return 0`, claiming to discard
    input without discarding any.

uptime now advances 5.01 seconds per 5 real seconds. schfs-check passes.

Full investigation, including the two wrong hypotheses and the measurement
flaw that protected them, in docs/history/kernel.md section 13.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
2026-09-12 21:34:36 +02:00

139 lines
3.3 KiB
C

#include "types.h"
#include "defs.h"
#include "param.h"
#include "date.h"
#include "spinlock.h"
#include "proc.h"
#include "x86.h"
#include "syscall.h"
#include "posix.h"
#include "uaccess.h"
#include "errno.h"
addr_t
sys_alarm(void)
{
int seconds;
if (argint(0, &seconds) < 0)
return -1;
myproc()->alarm_ticks = seconds * HZ;
return 0;
}
addr_t
sys_gettimeofday(void)
{
struct timeval *tv;
if (argptr(0, (char**)&tv, sizeof(*tv)) < 0)
return -1;
if (tv) {
// Real time, from the hardware clock read at boot plus uptime -- not
// uptime alone, which is what this used to return and why every machine
// thought it was 1970. See kernel/core/time.c.
uint64 sec, usec;
wallclock(&sec, &usec);
tv->tv_sec = (long)sec;
tv->tv_usec = (long)usec;
}
return 0;
}
addr_t
sys_nanosleep(void)
{
addr_t tsaddr, raw;
if (argaddr(0, &tsaddr) < 0 || fetchaddr(tsaddr, &raw) < 0)
return -1;
int64 sec = (int64)raw;
if (sec > 0) {
acquire(&tickslock);
uint t0 = ticks;
while (ticks - t0 < (uint)(sec * HZ)) {
if (myproc()->killed) { release(&tickslock); return -EINTR; }
sleep(&ticks, &tickslock);
}
release(&tickslock);
}
return 0;
}
addr_t
sys_clock_gettime(void)
{
int clkid;
struct timespec *tp;
argint(0, &clkid);
if (argptr(1, (char**)&tp, sizeof(*tp)) < 0)
return -1;
// CLOCK_MONOTONIC (1) counts from boot; everything else is treated as a
// realtime clock and counts from the epoch. Returning boot-relative values
// for CLOCK_REALTIME is what made timestamps land in 1970.
uint64 sec, usec;
wallclock(&sec, &usec);
if (clkid == 1)
sec -= boot_epoch;
tp->tv_sec = (long)sec;
tp->tv_nsec = (long)(usec * 1000);
return 0;
}
addr_t sys_getitimer(void) { return 0; } // stub
addr_t
sys_setitimer(void)
{
int which;
addr_t newp;
argint(0, &which); argaddr(1, &newp);
// We don't implement interval timers; just track the alarm if it's ITIMER_REAL.
if (which == 0 && newp) {
struct itimerval iv;
if (copy_from_user(&iv, newp, sizeof(iv)) == 0)
myproc()->alarm_ticks = (int)(iv.it_value.tv_sec * HZ +
iv.it_value.tv_usec / USEC_PER_TICK);
}
return 0;
}
addr_t
sys_time(void)
{
addr_t tloc;
argaddr(0, &tloc);
uint64 sec;
wallclock(&sec, 0);
long t = (long)sec;
if (tloc)
put_user(t, tloc);
return (addr_t)t;
}
addr_t
sys_clock_getres(void)
{
struct timespec *tp;
if (argptr(1, (char**)&tp, sizeof(*tp)) < 0) return 0;
if (tp) { tp->tv_sec = 0; tp->tv_nsec = 10000000L; } // 10ms resolution
return 0;
}
addr_t
sys_clock_nanosleep(void)
{
// Treat the same as nanosleep: arg2 is the timespec, arg1 is flags.
// Linux: clock_nanosleep(clockid, flags, rqtp, rmtp)
addr_t tsaddr, raw;
if (argaddr(2, &tsaddr) < 0 || fetchaddr(tsaddr, &raw) < 0)
return -1;
int64 sec = (int64)raw;
if (sec > 0) {
acquire(&tickslock);
uint t0 = ticks;
while (ticks - t0 < (uint)(sec * HZ)) {
if (myproc()->killed) { release(&tickslock); return -EINTR; }
sleep(&ticks, &tickslock);
}
release(&tickslock);
}
return 0;
}