sched: fair share by virtual runtime, with nice

The scheduler was xv6's round-robin: run everything RUNNABLE in slot
order and yield on every timer tick. Slot order is not fairness, a
process alone on a CPU paid a full context switch a hundred times a
second to be handed straight back to itself, and there was no way to
say that one thing mattered more than another.

Every process now carries vruntime, the CPU time it has used divided
by its weight, and the scheduler runs the runnable process with the
smallest. Fairness and interactivity both fall out of that one rule;
nice is a weight, 1.25x per level as UNIX has always had it. A slice
is 20 ms shared among the runnable processes, never below 2 ms, and
the timer tick preempts only once it is spent. A process waking from
a long sleep is brought up to min_vruntime less one period, so it is
scheduled promptly without being owed everything it slept through;
fork inherits vruntime, so forking does not reset a debt.

getpriority/setpriority are real, with Linux's offset of 20 on the
return; /proc/<pid>/schedstat and the ctxt_switches lines of status
report what the scheduler did. docs/architecture/scheduler.md has the
reasoning, including why the run queue is still a scan of ptable.

This was written in an earlier session and left uncommitted. Finished
here: a signal waking a sleeper goes through the same vruntime floor
as wakeup() does, and sched_yield() yields -- it returned 0 doing
nothing, harmless while every tick was a yield and a spin of a whole
slice once ticks stopped being one.

Checked under QEMU on 1 and 4 CPUs: a nice 10 loop beside a nice 0
one gets about a tenth of a shared CPU, and a command typed meanwhile
answers in 26 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
Nexoniarz 2026-10-23 15:01:27 +02:00
commit a2d987db5f
11 changed files with 666 additions and 35 deletions

View file

@ -62,6 +62,7 @@
#define PF_CMDLINE 3
#define PF_STATM 4
#define PF_EXE 5
#define PF_SCHEDSTAT 6
#define PF_MAX 6
#define INO_PID(ino) ((int)((ino) >> 8))
@ -112,6 +113,14 @@ static const struct global_file pidfiles[] = {
{ "cmdline", PF_CMDLINE },
{ "statm", PF_STATM },
{ "exe", PF_EXE },
// Three numbers about how the scheduler has treated this process, in the
// order Linux writes them: weighted CPU nanoseconds consumed, then -- where
// Linux reports run-queue wait -- this kernel's nice level, then the number
// of slices it has been given. The middle field differs on purpose and is
// documented in docs/architecture/scheduler.md; nothing here reads it, and
// inventing a wait time that is not measured would be worse than saying
// something true in its place.
{ "schedstat", PF_SCHEDSTAT },
};
#define NPIDFILES ((int)(sizeof(pidfiles) / sizeof(pidfiles[0])))
@ -219,6 +228,16 @@ gen_pid_status(struct sb *s, const struct procinfo *pi)
sbputc(s, '\t'); sbputi(s, pi->uid);
sbputs(s, "\nVmSize:\t"); sbputi(s, (int64)(pi->sz / 1024));
sbputs(s, " kB\nThreads:\t1\n");
// Where Linux keeps them, under the names Linux uses. The two are worth
// telling apart: a process with a high voluntary count is waiting on
// something, and one with a high involuntary count is being preempted --
// which is the difference between "this is slow" and "this is competing".
sbputs(s, "voluntary_ctxt_switches:\t");
sbputi(s, (int64)pi->nvcsw);
sbputs(s, "\nnonvoluntary_ctxt_switches:\t");
sbputi(s, (int64)pi->nivcsw);
sbputc(s, '\n');
}
static void
@ -258,6 +277,12 @@ gen_file(uint64 ino, char *buf, int cap)
case PF_CMDLINE: gen_pid_cmdline(&s, &pi); break;
case PF_STATM: gen_pid_statm(&s, &pi); break;
case PF_EXE: sbputs(&s, pi.exe[0] ? pi.exe : pi.name); break;
case PF_SCHEDSTAT:
sbputi(&s, (int64)pi.vruntime); sbputc(&s, ' ');
sbputi(&s, (int64)pi.nice); sbputc(&s, ' ');
sbputi(&s, (int64)(pi.nvcsw + pi.nivcsw));
sbputc(&s, '\n');
break;
default: return 0;
}
return s.n;