Files
zzg 661faba845
ci / test (push) Canceled after 0s
initial commit
2026-09-25 22:31:34 -07:00

415 lines
9.5 KiB
Go

package scheduler
import (
"sort"
"github.com/zg/os/process"
)
// FCFS runs processes to completion in arrival order.
//
// Simple and free of starvation, but subject to the convoy effect: one long
// job at the front delays every short job behind it, inflating average
// wait time badly.
type FCFS struct{}
func (FCFS) Name() string { return "first-come, first-served" }
func (f FCFS) Schedule(procs []*process.Process) *Result {
r := &Result{Processes: prepare(procs)}
now := 0
for _, p := range r.Processes {
// The CPU idles if the next arrival is in the future.
if now < p.ArrivalTime {
now = p.ArrivalTime
}
p.MarkStarted(now)
start := now
now += p.Remaining
p.Remaining = 0
p.CompletionTime = now
record(r, p, start, now)
}
r.TotalTime = now
finish(r)
return r
}
// SJF runs the shortest available job next, without preemption.
//
// Provably optimal for average wait time, but it needs burst times in
// advance — which a real kernel does not have — and a steady stream of
// short jobs starves long ones.
type SJF struct{}
func (SJF) Name() string { return "shortest job first" }
func (s SJF) Schedule(procs []*process.Process) *Result {
r := &Result{Processes: prepare(procs)}
now, done := 0, 0
for done < len(r.Processes) {
// Pick the shortest job that has arrived.
var next *process.Process
for _, p := range r.Processes {
if p.Remaining == 0 || p.ArrivalTime > now {
continue
}
if next == nil || p.Remaining < next.Remaining ||
(p.Remaining == next.Remaining && p.PID < next.PID) {
next = p
}
}
if next == nil {
// Nothing has arrived yet; idle until the next arrival rather
// than spinning.
earliest := -1
for _, p := range r.Processes {
if p.Remaining > 0 && (earliest < 0 || p.ArrivalTime < earliest) {
earliest = p.ArrivalTime
}
}
if earliest < 0 {
break
}
now = earliest
continue
}
next.MarkStarted(now)
start := now
now += next.Remaining
next.Remaining = 0
next.CompletionTime = now
record(r, next, start, now)
done++
}
r.TotalTime = now
finish(r)
return r
}
// SRTF is preemptive shortest-job-first: a newly arrived shorter job
// preempts the running one.
type SRTF struct{}
func (SRTF) Name() string { return "shortest remaining time first" }
func (s SRTF) Schedule(procs []*process.Process) *Result {
r := &Result{Processes: prepare(procs)}
now, done := 0, 0
for done < len(r.Processes) {
var next *process.Process
for _, p := range r.Processes {
if p.Remaining == 0 || p.ArrivalTime > now {
continue
}
if next == nil || p.Remaining < next.Remaining ||
(p.Remaining == next.Remaining && p.PID < next.PID) {
next = p
}
}
if next == nil {
earliest := -1
for _, p := range r.Processes {
if p.Remaining > 0 && (earliest < 0 || p.ArrivalTime < earliest) {
earliest = p.ArrivalTime
}
}
if earliest < 0 {
break
}
now = earliest
continue
}
// Run one tick at a time: preemption can only be decided by
// re-checking after each unit of time.
next.MarkStarted(now)
record(r, next, now, now+1)
next.Remaining--
now++
if next.Remaining == 0 {
next.CompletionTime = now
done++
}
}
r.TotalTime = now
finish(r)
return r
}
// RoundRobin gives each process a fixed quantum in turn.
//
// This is what bounds response time: with n processes and quantum q, no
// process waits longer than (n-1)q to run. A smaller quantum improves
// responsiveness but costs more context switches, which is the trade-off
// the metrics make visible.
type RoundRobin struct{ Quantum int }
func (rr RoundRobin) Name() string { return "round robin" }
func (rr RoundRobin) Schedule(procs []*process.Process) *Result {
quantum := rr.Quantum
if quantum <= 0 {
quantum = 1
}
r := &Result{Processes: prepare(procs)}
now, done := 0, 0
var queue []*process.Process
arrived := 0
// Admit everything that has arrived by `now`.
admit := func() {
for arrived < len(r.Processes) && r.Processes[arrived].ArrivalTime <= now {
queue = append(queue, r.Processes[arrived])
arrived++
}
}
admit()
for done < len(r.Processes) {
if len(queue) == 0 {
if arrived >= len(r.Processes) {
break
}
now = r.Processes[arrived].ArrivalTime
admit()
continue
}
p := queue[0]
queue = queue[1:]
p.MarkStarted(now)
slice := min(quantum, p.Remaining)
start := now
now += slice
p.Remaining -= slice
record(r, p, start, now)
// Admit arrivals *before* requeueing the preempted process, so a
// process that arrived during this quantum runs before the one
// that just used its turn. Requeueing first would give the running
// process an unfair extra turn.
admit()
if p.Remaining > 0 {
queue = append(queue, p)
} else {
p.CompletionTime = now
done++
}
}
r.TotalTime = now
finish(r)
return r
}
// Priority runs the highest-priority available process, optionally with
// ageing to prevent starvation.
type Priority struct {
Preemptive bool
// AgeInterval, when positive, promotes a waiting process's effective
// priority every this many ticks. Without it, a stream of high-priority
// work starves low-priority processes indefinitely.
AgeInterval int
}
func (p Priority) Name() string {
if p.Preemptive {
return "priority (preemptive)"
}
return "priority"
}
func (pr Priority) Schedule(procs []*process.Process) *Result {
r := &Result{Processes: prepare(procs)}
now, done := 0, 0
// effective holds the aged priority, so the original is preserved for
// reporting.
effective := map[process.PID]int{}
waiting := map[process.PID]int{}
for _, p := range r.Processes {
effective[p.PID] = p.Priority
}
for done < len(r.Processes) {
var next *process.Process
for _, p := range r.Processes {
if p.Remaining == 0 || p.ArrivalTime > now {
continue
}
if next == nil || effective[p.PID] < effective[next.PID] ||
(effective[p.PID] == effective[next.PID] && p.PID < next.PID) {
next = p
}
}
if next == nil {
earliest := -1
for _, p := range r.Processes {
if p.Remaining > 0 && (earliest < 0 || p.ArrivalTime < earliest) {
earliest = p.ArrivalTime
}
}
if earliest < 0 {
break
}
now = earliest
continue
}
next.MarkStarted(now)
run := next.Remaining
if pr.Preemptive {
run = 1 // re-decide every tick
}
record(r, next, now, now+run)
next.Remaining -= run
now += run
waiting[next.PID] = 0
if pr.AgeInterval > 0 {
for _, p := range r.Processes {
if p == next || p.Remaining == 0 || p.ArrivalTime > now {
continue
}
waiting[p.PID] += run
for waiting[p.PID] >= pr.AgeInterval && effective[p.PID] > 0 {
effective[p.PID]--
waiting[p.PID] -= pr.AgeInterval
}
}
}
if next.Remaining == 0 {
next.CompletionTime = now
done++
}
}
r.TotalTime = now
finish(r)
return r
}
// MLFQ is a multi-level feedback queue.
//
// It approximates SJF without knowing burst times in advance: everything
// starts in the highest-priority queue, and a process that uses its whole
// quantum is demoted. Short and interactive jobs therefore stay near the
// top, while CPU-bound jobs sink. Periodic boosting returns everything to
// the top queue, which is what stops the sunken jobs starving.
type MLFQ struct {
// Quanta gives each level's time slice, longest at the bottom.
Quanta []int
// BoostInterval, when positive, promotes everything to the top queue
// this often.
BoostInterval int
}
func (MLFQ) Name() string { return "multi-level feedback queue" }
func (m MLFQ) Schedule(procs []*process.Process) *Result {
quanta := m.Quanta
if len(quanta) == 0 {
quanta = []int{1, 2, 4, 8}
}
r := &Result{Processes: prepare(procs)}
now, done := 0, 0
lastBoost := 0
queues := make([][]*process.Process, len(quanta))
arrived := 0
admit := func() {
for arrived < len(r.Processes) && r.Processes[arrived].ArrivalTime <= now {
// New arrivals enter at the top, which is what gives an
// interactive process immediate responsiveness.
queues[0] = append(queues[0], r.Processes[arrived])
arrived++
}
}
admit()
boost := func() {
if m.BoostInterval <= 0 || now-lastBoost < m.BoostInterval {
return
}
lastBoost = now
for level := 1; level < len(queues); level++ {
queues[0] = append(queues[0], queues[level]...)
queues[level] = nil
}
}
for done < len(r.Processes) {
boost()
level := -1
for i, q := range queues {
if len(q) > 0 {
level = i
break
}
}
if level < 0 {
if arrived >= len(r.Processes) {
break
}
now = r.Processes[arrived].ArrivalTime
admit()
continue
}
p := queues[level][0]
queues[level] = queues[level][1:]
p.MarkStarted(now)
slice := min(quanta[level], p.Remaining)
start := now
now += slice
p.Remaining -= slice
record(r, p, start, now)
admit()
switch {
case p.Remaining == 0:
p.CompletionTime = now
done++
case slice == quanta[level] && level < len(queues)-1:
// Used the whole quantum: CPU-bound, so demote it.
queues[level+1] = append(queues[level+1], p)
default:
queues[level] = append(queues[level], p)
}
}
r.TotalTime = now
finish(r)
return r
}
// All returns one of each scheduler, for comparison.
func All(quantum int) []Scheduler {
return []Scheduler{
FCFS{},
SJF{},
SRTF{},
RoundRobin{Quantum: quantum},
Priority{},
Priority{Preemptive: true, AgeInterval: 5},
MLFQ{Quanta: []int{1, 2, 4, 8}, BoostInterval: 20},
}
}
// SortByPID orders processes by identifier, for stable reporting.
func SortByPID(procs []*process.Process) {
sort.SliceStable(procs, func(i, j int) bool { return procs[i].PID < procs[j].PID })
}