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

@ -842,6 +842,38 @@ addr_t sys_mremap(void) { return -ENOMEM; }
// Resource limits: always report unlimited.
addr_t sys_setrlimit(void) { return 0; }
// Process priority: always report 0.
addr_t sys_getpriority(void) { return 0; }
addr_t sys_setpriority(void) { return 0; }
// Process priority. These returned a flat 0 for as long as the scheduler had
// no notion of priority to report; it has one now (kernel/proc/proc.c), and a
// stub that says "everything is nice 0" is a lie the moment `renice` works.
//
// PRIO_PROCESS is 0, and `who` of 0 means the caller -- both from POSIX.
#define PRIO_PROCESS 0
addr_t
sys_getpriority(void)
{
int which, who, nice = 0, r;
argint(0, &which); argint(1, &who);
if (which != PRIO_PROCESS)
return -EINVAL;
if ((r = proc_getnice(who, &nice)) < 0)
return r;
// getpriority(2) returns the nice value *offset by 20*, because -1 is a
// legal nice level and also how a syscall says "error". libc subtracts the
// 20 back off. Returning the raw nice value here makes `renice -n -1` look
// like a failure to every caller that checks.
return (addr_t)(20 - nice);
}
addr_t
sys_setpriority(void)
{
int which, who, nice;
argint(0, &which); argint(1, &who); argint(2, &nice);
if (which != PRIO_PROCESS)
return -EINVAL;
return (addr_t)proc_setnice(who, nice);
}