Overview
Process scheduling algorithms control how an operating system allocates CPU time among competing processes or threads. The scheduler makes decisions about which process runs next, for how long, and when to preempt running processes. These algorithms directly impact system responsiveness, throughput, fairness, and resource utilization.
Operating systems face the fundamental problem of managing multiple processes competing for limited CPU resources. A single-core system can execute only one process at a time, while multi-core systems can execute one process per core. The scheduler creates the illusion of simultaneous execution through rapid context switching between processes.
The scheduling decision occurs during several events: when a process transitions from running to waiting state, when a process transitions from running to ready state, when a process transitions from waiting to ready state, or when a process terminates. Non-preemptive schedulers make decisions only when processes voluntarily yield the CPU, while preemptive schedulers can forcibly interrupt running processes.
Process scheduling differs from thread scheduling in scope and context. Process scheduling involves heavier context switches that include memory space changes, while thread scheduling within the same process involves lighter context switches. Modern operating systems implement multi-level scheduling with separate algorithms for process and thread management.
# Basic process representation
class Process
attr_accessor :pid, :arrival_time, :burst_time, :priority, :state
attr_reader :waiting_time, :turnaround_time, :remaining_time
def initialize(pid, arrival_time, burst_time, priority = 0)
@pid = pid
@arrival_time = arrival_time
@burst_time = burst_time
@remaining_time = burst_time
@priority = priority
@state = :new
@waiting_time = 0
@turnaround_time = 0
end
def execute(time_slice)
executed = [time_slice, @remaining_time].min
@remaining_time -= executed
@state = @remaining_time.zero? ? :terminated : :running
executed
end
def complete?
@remaining_time.zero?
end
end
Key Principles
Process scheduling algorithms optimize various metrics that measure scheduler effectiveness. Turnaround time measures the total time from process arrival to completion, including waiting time and execution time. Waiting time tracks how long a process spends in the ready queue before execution. Response time measures the interval from process arrival to first CPU allocation, critical for interactive systems.
CPU utilization represents the percentage of time the CPU performs useful work rather than remaining idle. Throughput measures the number of processes completed per time unit. Fairness ensures all processes receive reasonable CPU time without indefinite postponement. These metrics often conflict, requiring schedulers to balance competing objectives based on system goals.
The convoy effect occurs when short processes wait behind long-running processes, degrading average waiting time. Starvation happens when low-priority processes never receive CPU time due to continuous arrival of higher-priority processes. Aging techniques increment process priority over time to prevent starvation.
Preemption allows the scheduler to interrupt running processes to allocate CPU time to other processes. Preemptive algorithms improve response time and prevent monopolization but introduce context switching overhead. The time quantum or time slice defines how long a process runs before preemption in round-robin systems.
Context switching involves saving the state of the current process and loading the state of the next process. This includes program counters, registers, memory mappings, and file descriptors. Context switch overhead directly impacts scheduler efficiency, particularly with small time quanta.
The ready queue holds processes waiting for CPU allocation. Different data structures serve different scheduling algorithms: FIFO queues for first-come-first-served, priority queues for priority-based scheduling, and multiple queues for multi-level feedback systems.
Process states transition through new, ready, running, waiting, and terminated states. The scheduler selects processes from the ready state for execution. Processes enter the waiting state during I/O operations or synchronization events, returning to ready when the event completes.
# Process state machine
class ProcessStateMachine
STATES = [:new, :ready, :running, :waiting, :terminated]
TRANSITIONS = {
new: [:ready],
ready: [:running],
running: [:ready, :waiting, :terminated],
waiting: [:ready],
terminated: []
}
def self.valid_transition?(from, to)
TRANSITIONS[from]&.include?(to) || false
end
def self.transition(process, new_state)
unless valid_transition?(process.state, new_state)
raise "Invalid transition from #{process.state} to #{new_state}"
end
process.state = new_state
end
end
Implementation Approaches
First-Come-First-Served (FCFS) scheduling executes processes in arrival order using a FIFO queue. The scheduler selects the process at the queue head and runs it to completion. FCFS provides simplicity and fairness in arrival order but suffers from the convoy effect when short processes queue behind long processes. FCFS is non-preemptive, making it unsuitable for interactive systems requiring quick response times.
Shortest Job First (SJF) scheduling selects the process with the shortest burst time. SJF minimizes average waiting time when all processes arrive simultaneously. The algorithm requires predicting future CPU burst times, typically through exponential averaging of previous bursts. Preemptive SJF, called Shortest Remaining Time First (SRTF), preempts running processes when shorter processes arrive. SJF can cause starvation of long processes.
Priority scheduling assigns each process a priority value and executes highest-priority processes first. Priorities derive from internal factors like memory requirements or time limits, or external factors like importance or payment. Preemptive priority scheduling interrupts lower-priority running processes when higher-priority processes arrive. Priority scheduling risks indefinite blocking of low-priority processes, mitigated through aging mechanisms that gradually increase priority.
Round-Robin (RR) scheduling allocates fixed time slices to processes in circular order. After a process exhausts its quantum, the scheduler preempts it and moves it to the ready queue's tail. Round-robin provides fairness and good response time for time-sharing systems. Performance depends critically on quantum size: small quanta increase context switch overhead, while large quanta degrade response time approaching FCFS behavior.
Multi-Level Queue scheduling partitions the ready queue into separate queues with different priorities and algorithms. Each queue has its own scheduling algorithm, and processes belong to one queue based on properties like process type or priority. Foreground interactive processes might use round-robin while background batch processes use FCFS. The system scheduler allocates CPU time between queues using fixed priority or time slicing.
Multi-Level Feedback Queue (MLFQ) scheduling allows processes to move between queues based on behavior. A process consuming its entire quantum moves to a lower-priority queue, while processes yielding before quantum expiration move to higher-priority queues. This approach favors I/O-bound and interactive processes without requiring burst time prediction. MLFQ parameters include number of queues, scheduling algorithm per queue, quantum per level, and queue promotion/demotion criteria.
Lottery scheduling assigns lottery tickets to processes, conducting a lottery to determine the next process. Processes with more tickets have higher winning probability. Ticket distribution implements relative priorities while maintaining randomness. Processes can transfer tickets to other processes during client-server interactions. Lottery scheduling provides probabilistic fairness without complex priority calculations.
Completely Fair Scheduler (CFS) maintains a red-black tree of processes ordered by virtual runtime. Virtual runtime tracks how long a process has executed, normalized by process weight. The scheduler always selects the process with minimum virtual runtime, ensuring fairness over time. CFS achieves O(log n) scheduling decisions and handles varying workloads without manual tuning.
# Scheduler decision framework
module SchedulingStrategy
def self.select_next(ready_queue)
raise NotImplementedError
end
end
class FCFSStrategy
include SchedulingStrategy
def self.select_next(ready_queue)
ready_queue.min_by(&:arrival_time)
end
end
class SJFStrategy
include SchedulingStrategy
def self.select_next(ready_queue)
ready_queue.min_by(&:remaining_time)
end
end
class PriorityStrategy
include SchedulingStrategy
def self.select_next(ready_queue)
ready_queue.max_by(&:priority)
end
end
Ruby Implementation
Ruby provides threading primitives that demonstrate scheduling concepts, though the actual thread scheduling depends on the Ruby implementation. MRI Ruby uses a Global Interpreter Lock (GIL) that prevents true parallel thread execution, while JRuby and TruffleRuby support native thread parallelism. The following implementations simulate process scheduling algorithms for educational purposes.
class Scheduler
attr_reader :processes, :current_time, :completed_processes
def initialize(strategy)
@strategy = strategy
@processes = []
@ready_queue = []
@current_time = 0
@completed_processes = []
@running_process = nil
end
def add_process(process)
@processes << process
end
def run
until all_complete?
# Move arriving processes to ready queue
arriving = @processes.select { |p| p.arrival_time == @current_time && p.state == :new }
arriving.each do |p|
p.state = :ready
@ready_queue << p
end
# Select next process if none running
if @running_process.nil? && !@ready_queue.empty?
@running_process = @strategy.select_next(@ready_queue)
@ready_queue.delete(@running_process)
@running_process.state = :running
end
# Execute current process
if @running_process
@running_process.execute(1)
if @running_process.complete?
@running_process.turnaround_time = @current_time + 1 - @running_process.arrival_time
@running_process.waiting_time = @running_process.turnaround_time - @running_process.burst_time
@completed_processes << @running_process
@running_process = nil
end
end
# Update waiting time for ready processes
@ready_queue.each { |p| p.instance_variable_set(:@waiting_time, p.waiting_time + 1) }
@current_time += 1
end
calculate_statistics
end
private
def all_complete?
@processes.all?(&:complete?)
end
def calculate_statistics
{
avg_waiting_time: @completed_processes.sum(&:waiting_time).to_f / @completed_processes.size,
avg_turnaround_time: @completed_processes.sum(&:turnaround_time).to_f / @completed_processes.size,
total_time: @current_time
}
end
end
Round-robin scheduling requires quantum-based preemption and queue rotation:
class RoundRobinScheduler
attr_reader :quantum
def initialize(quantum = 2)
@quantum = quantum
@ready_queue = []
@current_time = 0
@completed = []
@current_process = nil
@time_slice_remaining = 0
end
def add_process(process)
@ready_queue << process
end
def run
until @ready_queue.empty? && @current_process.nil?
# Select process if none running
if @current_process.nil? && !@ready_queue.empty?
@current_process = @ready_queue.shift
@time_slice_remaining = @quantum
end
if @current_process
executed = @current_process.execute(1)
@time_slice_remaining -= executed
@current_time += executed
# Process completed
if @current_process.complete?
@current_process.turnaround_time = @current_time - @current_process.arrival_time
@completed << @current_process
@current_process = nil
@time_slice_remaining = 0
# Quantum expired
elsif @time_slice_remaining.zero?
@ready_queue << @current_process
@current_process = nil
end
else
@current_time += 1
end
end
statistics
end
def statistics
{
avg_waiting_time: @completed.sum(&:waiting_time).to_f / @completed.size,
avg_turnaround_time: @completed.sum(&:turnaround_time).to_f / @completed.size
}
end
end
Priority scheduling with aging prevents starvation:
class PrioritySchedulerWithAging
AGING_RATE = 1
AGING_INTERVAL = 5
def initialize
@ready_queue = []
@current_time = 0
@completed = []
@last_aging = 0
end
def add_process(process)
process.instance_variable_set(:@original_priority, process.priority)
process.instance_variable_set(:@boosted_priority, process.priority)
@ready_queue << process
end
def run
until @ready_queue.empty?
# Apply aging
if @current_time - @last_aging >= AGING_INTERVAL
age_processes
@last_aging = @current_time
end
# Select highest priority process
current = @ready_queue.max_by { |p| p.instance_variable_get(:@boosted_priority) }
@ready_queue.delete(current)
# Execute to completion
while !current.complete?
current.execute(1)
@current_time += 1
end
current.turnaround_time = @current_time - current.arrival_time
current.waiting_time = current.turnaround_time - current.burst_time
@completed << current
end
statistics
end
private
def age_processes
@ready_queue.each do |process|
boosted = process.instance_variable_get(:@boosted_priority)
process.instance_variable_set(:@boosted_priority, boosted + AGING_RATE)
end
end
def statistics
{
avg_waiting_time: @completed.sum(&:waiting_time).to_f / @completed.size,
avg_turnaround_time: @completed.sum(&:turnaround_time).to_f / @completed.size
}
end
end
Multi-level feedback queue implementation:
class MLFQScheduler
def initialize(num_queues: 3, base_quantum: 2)
@queues = Array.new(num_queues) { [] }
@quantums = Array.new(num_queues) { |i| base_quantum * (2 ** i) }
@current_time = 0
@completed = []
end
def add_process(process)
process.instance_variable_set(:@queue_level, 0)
@queues[0] << process
end
def run
until all_queues_empty?
# Find highest priority non-empty queue
queue_index = @queues.index { |q| !q.empty? }
next (@current_time += 1) unless queue_index
process = @queues[queue_index].shift
quantum = @quantums[queue_index]
time_executed = 0
# Execute for quantum or until completion
while time_executed < quantum && !process.complete?
process.execute(1)
@current_time += 1
time_executed += 1
end
if process.complete?
process.turnaround_time = @current_time - process.arrival_time
process.waiting_time = process.turnaround_time - process.burst_time
@completed << process
else
# Demote to lower priority queue
new_level = [queue_index + 1, @queues.size - 1].min
process.instance_variable_set(:@queue_level, new_level)
@queues[new_level] << process
end
end
statistics
end
private
def all_queues_empty?
@queues.all?(&:empty?)
end
def statistics
{
avg_waiting_time: @completed.sum(&:waiting_time).to_f / @completed.size,
avg_turnaround_time: @completed.sum(&:turnaround_time).to_f / @completed.size,
queue_distribution: @completed.group_by { |p| p.instance_variable_get(:@queue_level) }
.transform_values(&:count)
}
end
end
Performance Considerations
Scheduling algorithm performance varies significantly based on workload characteristics and system requirements. FCFS performs well with uniform burst times but suffers when long processes precede short ones. Average waiting time depends entirely on arrival order, making worst-case performance unbounded.
SJF achieves optimal average waiting time for batch systems where all processes arrive simultaneously. The algorithm requires accurate burst time prediction, typically using exponential moving averages. Prediction accuracy directly impacts performance. Inaccurate predictions can cause the algorithm to perform worse than simpler strategies.
# Burst time prediction using exponential moving average
class BurstPredictor
def initialize(alpha: 0.5)
@alpha = alpha
@predictions = {}
@history = Hash.new { |h, k| h[k] = [] }
end
def predict(process_id)
@predictions[process_id] || 10 # Default estimate
end
def update(process_id, actual_burst)
@history[process_id] << actual_burst
predicted = predict(process_id)
@predictions[process_id] = (@alpha * actual_burst) + ((1 - @alpha) * predicted)
end
def accuracy(process_id)
history = @history[process_id]
return 0 if history.size < 2
errors = history.each_cons(2).map { |actual, _| (predict(process_id) - actual).abs }
1 - (errors.sum / history.sum.to_f)
end
end
Round-robin performance depends critically on quantum size. Small quanta provide better response time but increase context switch overhead. Large quanta reduce overhead but degrade response time. The optimal quantum balances these factors based on context switch cost and desired responsiveness. A quantum of 10-100 milliseconds works well for most interactive systems.
Context switch overhead includes direct costs like register saving and loading, and indirect costs like cache invalidation and TLB flushing. Modern processors experience 1-10 microsecond context switches. When quantum approaches context switch time, the system spends more time switching than executing useful work.
# Performance measurement with context switch overhead
class PerformanceAnalyzer
CONTEXT_SWITCH_COST = 0.01 # milliseconds
def initialize(scheduler)
@scheduler = scheduler
@context_switches = 0
end
def measure(&block)
start_time = Time.now
result = block.call
end_time = Time.now
{
execution_time: (end_time - start_time) * 1000,
context_switches: @context_switches,
overhead: @context_switches * CONTEXT_SWITCH_COST,
efficiency: 1 - ((@context_switches * CONTEXT_SWITCH_COST) / ((end_time - start_time) * 1000))
}
end
def record_context_switch
@context_switches += 1
end
end
Priority scheduling performance depends on priority assignment accuracy. Static priorities work well for systems with known workload characteristics. Dynamic priorities adapt to changing conditions but require overhead for priority recalculation. Aging mechanisms prevent starvation but add computational complexity.
Multi-level feedback queues adapt to process behavior without explicit burst time knowledge. Short processes complete quickly in high-priority queues. Long processes settle into lower-priority queues with larger quanta, reducing context switch overhead. MLFQ performs well across diverse workloads but requires tuning of queue count, quantum progression, and promotion/demotion rules.
CPU utilization measures scheduler effectiveness. High utilization indicates efficient resource use, but 100% utilization leaves no capacity for bursts. Target utilization typically ranges from 70-90% for interactive systems to allow responsiveness. Batch systems can sustain higher utilization.
Throughput measures completed processes per time unit. Throughput increases with efficient scheduling but varies by workload. Batch-oriented schedulers maximize throughput by minimizing context switches. Interactive schedulers sacrifice some throughput for better response time.
Real-time scheduling requires meeting deadlines with predictable worst-case performance. Rate-monotonic scheduling assigns static priorities based on period. Earliest Deadline First dynamically prioritizes based on absolute deadlines. These algorithms provide schedulability guarantees when workload meets specific conditions.
Design Considerations
Algorithm selection depends on system requirements and workload characteristics. Batch systems prioritize throughput and CPU utilization, favoring non-preemptive algorithms like FCFS or SJF. Interactive systems require quick response time, favoring preemptive algorithms like round-robin or MLFQ. Real-time systems require deadline guarantees, necessitating specialized real-time scheduling algorithms.
FCFS suits simple systems with predictable, uniform workloads. Implementation requires only a FIFO queue with minimal overhead. FCFS provides fairness in arrival order and avoids starvation. However, convoy effects make FCFS unsuitable for interactive systems or mixed workloads with varying burst times.
SJF minimizes average waiting time but requires burst time prediction. Historical data provides reasonable predictions for repeating workloads. SJF suits batch systems where processes have predictable execution patterns. The risk of starvation requires careful monitoring and possible timeout mechanisms.
Round-robin balances fairness and responsiveness, making it suitable for time-sharing systems. Quantum selection trades response time against overhead. Small quanta improve interactivity but increase context switches. Large quanta approach FCFS behavior. Round-robin provides bounded waiting time regardless of burst time.
Priority scheduling supports systems with varying importance levels. External priorities reflect business requirements or user preferences. Internal priorities reflect resource requirements or deadlines. Priority scheduling requires starvation prevention through aging or periodic priority resets.
MLFQ adapts to diverse workloads without manual configuration. Short interactive processes stay in high-priority queues with quick response. Long batch processes migrate to low-priority queues with efficient execution. MLFQ requires tuning for specific workloads but provides good default behavior.
Workload analysis informs algorithm selection. I/O-bound processes benefit from quick response time to maximize I/O utilization. CPU-bound processes require fair CPU allocation and efficient throughput. Mixed workloads challenge schedulers to balance competing requirements.
System monitoring reveals scheduling effectiveness. High waiting times indicate poor scheduling or overload. Low CPU utilization suggests insufficient workload or scheduling inefficiency. Response time distributions show interactive performance. Throughput measurements quantify batch processing efficiency.
Hybrid approaches combine multiple algorithms. Separate queues for interactive and batch processes allow different scheduling policies. Class-based scheduling assigns algorithm and priority by process type. Adaptive scheduling adjusts algorithms based on observed system behavior.
The scheduler's relationship with other system components affects design decisions. Memory management influences process selection when physical memory limits which processes remain in memory. I/O scheduling coordinates with CPU scheduling to maximize resource utilization. Energy management may prefer scheduling decisions that enable power-saving states.
# Decision framework for algorithm selection
class SchedulerSelector
def self.recommend(requirements)
if requirements[:real_time]
return :rate_monotonic if requirements[:static_priorities]
return :earliest_deadline_first
end
if requirements[:interactive]
return :round_robin if requirements[:simple]
return :mlfq
end
if requirements[:batch]
return :fcfs if requirements[:uniform_workload]
return :sjf if requirements[:burst_predictable]
return :priority if requirements[:importance_varies]
end
:mlfq # Default for mixed workloads
end
end
Practical Examples
An example workload demonstrates algorithm differences. Five processes arrive with varying burst times:
processes = [
Process.new(1, 0, 8, 2),
Process.new(2, 1, 4, 1),
Process.new(3, 2, 2, 3),
Process.new(4, 3, 1, 4),
Process.new(5, 4, 3, 2)
]
# FCFS scheduling
fcfs_scheduler = Scheduler.new(FCFSStrategy)
processes.each { |p| fcfs_scheduler.add_process(p.dup) }
fcfs_results = fcfs_scheduler.run
# => { avg_waiting_time: 6.4, avg_turnaround_time: 10.0, total_time: 18 }
# SJF scheduling
sjf_scheduler = Scheduler.new(SJFStrategy)
processes.each { |p| sjf_scheduler.add_process(p.dup) }
sjf_results = sjf_scheduler.run
# => { avg_waiting_time: 4.2, avg_turnaround_time: 7.8, total_time: 18 }
# Round-robin with quantum 2
rr_scheduler = RoundRobinScheduler.new(2)
processes.each { |p| rr_scheduler.add_process(p.dup) }
rr_results = rr_scheduler.run
# => { avg_waiting_time: 5.8, avg_turnaround_time: 9.4 }
A web server scenario illustrates interactive scheduling requirements. Short request handlers require quick response time while long background tasks need throughput:
# Web server request simulation
class WebRequest < Process
attr_accessor :request_type
def initialize(pid, arrival, burst, type)
super(pid, arrival, burst)
@request_type = type
end
end
requests = [
WebRequest.new(1, 0, 1, :api), # Fast API call
WebRequest.new(2, 0, 10, :report), # Long report generation
WebRequest.new(3, 1, 1, :api), # Fast API call
WebRequest.new(4, 2, 2, :query), # Database query
WebRequest.new(5, 2, 15, :batch), # Batch processing
WebRequest.new(6, 3, 1, :api) # Fast API call
]
# MLFQ handles mixed workload effectively
mlfq = MLFQScheduler.new(num_queues: 3, base_quantum: 2)
requests.each { |r| mlfq.add_process(r) }
results = mlfq.run
# API requests complete quickly in top queue
api_requests = results[:completed].select { |r| r.request_type == :api }
# => avg_turnaround_time: 2.3
# Long tasks migrate to lower queues
long_tasks = results[:completed].select { |r| [:report, :batch].include?(r.request_type) }
# => avg_turnaround_time: 20.5
A real-time scheduling scenario demonstrates deadline-driven priorities:
class RTProcess < Process
attr_accessor :deadline, :period
def initialize(pid, arrival, burst, deadline)
super(pid, arrival, burst)
@deadline = deadline
@period = deadline
end
def time_until_deadline(current_time)
@deadline - current_time
end
end
# Earliest Deadline First scheduling
class EDFScheduler
def initialize
@ready_queue = []
@current_time = 0
@completed = []
@missed_deadlines = []
end
def add_process(process)
@ready_queue << process
end
def run(max_time = 100)
while @current_time < max_time
break if @ready_queue.empty?
# Select process with earliest deadline
current = @ready_queue.min_by { |p| p.deadline }
@ready_queue.delete(current)
# Execute one time unit
current.execute(1)
@current_time += 1
if current.complete?
if @current_time <= current.deadline
@completed << current
else
@missed_deadlines << current
end
else
@ready_queue << current
end
end
{
completed: @completed.size,
missed: @missed_deadlines.size,
success_rate: @completed.size.to_f / (@completed.size + @missed_deadlines.size)
}
end
end
# Periodic tasks
tasks = [
RTProcess.new(1, 0, 3, 10), # Period 10, burst 3
RTProcess.new(2, 0, 2, 5), # Period 5, burst 2
RTProcess.new(3, 0, 1, 4) # Period 4, burst 1
]
scheduler = EDFScheduler.new
tasks.each { |t| scheduler.add_process(t) }
results = scheduler.run(20)
# => { completed: 8, missed: 0, success_rate: 1.0 }
A database query scheduler balances short transactions with long analytical queries:
class QueryProcess < Process
attr_accessor :query_type, :estimated_cost
def initialize(pid, arrival, burst, type, cost)
super(pid, arrival, burst)
@query_type = type
@estimated_cost = cost
@priority = calculate_priority
end
def calculate_priority
case @query_type
when :transaction then 10
when :oltp then 7
when :analytical then 3
when :report then 1
end
end
end
queries = [
QueryProcess.new(1, 0, 2, :transaction, 100),
QueryProcess.new(2, 1, 15, :analytical, 50000),
QueryProcess.new(3, 2, 1, :oltp, 200),
QueryProcess.new(4, 3, 20, :report, 80000),
QueryProcess.new(5, 4, 1, :transaction, 150)
]
# Priority with aging prevents analytical query starvation
scheduler = PrioritySchedulerWithAging.new
queries.each { |q| scheduler.add_process(q) }
results = scheduler.run
# Transactions complete quickly
transactions = results[:completed].select { |q| q.query_type == :transaction }
# => avg_turnaround_time: 3.5
# Analytical queries eventually execute despite low priority
analytical = results[:completed].select { |q| q.query_type == :analytical }
# => all queries complete within reasonable time due to aging
Reference
Algorithm Comparison
| Algorithm | Preemptive | Starvation Risk | Avg Waiting Time | Implementation Complexity | Best Use Case |
|---|---|---|---|---|---|
| FCFS | No | No | Poor | Very Low | Batch, uniform workload |
| SJF | No | Yes | Optimal | Low | Batch, predictable bursts |
| SRTF | Yes | Yes | Optimal | Medium | Varied burst times |
| Priority | Yes/No | Yes | Varies | Low | Importance-based systems |
| Round-Robin | Yes | No | Good | Low | Time-sharing, interactive |
| MLFQ | Yes | No | Good | High | General purpose, adaptive |
| Lottery | Yes | No | Fair | Medium | Proportional sharing |
| CFS | Yes | No | Good | High | Modern Linux systems |
Performance Metrics
| Metric | Definition | Formula | Optimization Goal |
|---|---|---|---|
| Turnaround Time | Process arrival to completion | completion_time - arrival_time | Minimize |
| Waiting Time | Time in ready queue | turnaround_time - burst_time | Minimize |
| Response Time | Arrival to first execution | first_run_time - arrival_time | Minimize |
| Throughput | Processes per time unit | completed_processes / total_time | Maximize |
| CPU Utilization | Percentage CPU busy | busy_time / total_time | Maximize |
| Fairness | Equal opportunity measure | variance of waiting times | Minimize |
Context Switch Components
| Component | Description | Typical Cost |
|---|---|---|
| Register Save | Save CPU registers | 100-500 cycles |
| Register Restore | Load new process registers | 100-500 cycles |
| Memory Context | Switch page tables | 500-1000 cycles |
| Cache Invalidation | Clear TLB entries | 1000-5000 cycles |
| Pipeline Flush | Clear instruction pipeline | 50-200 cycles |
| Total | Complete context switch | 1-10 microseconds |
Queue Data Structures
| Scheduler | Data Structure | Operations | Time Complexity |
|---|---|---|---|
| FCFS | FIFO Queue | enqueue, dequeue | O(1) |
| SJF | Min Heap | insert, extract-min | O(log n) |
| Priority | Priority Queue | insert, extract-max | O(log n) |
| Round-Robin | Circular Queue | enqueue, dequeue | O(1) |
| MLFQ | Multiple Queues | per-queue operations | O(1) per queue |
| CFS | Red-Black Tree | insert, find-min | O(log n) |
Common Configuration Parameters
| Parameter | Description | Typical Range | Impact |
|---|---|---|---|
| Time Quantum | RR time slice | 10-100 ms | Response time vs overhead |
| Priority Levels | Number of priorities | 4-256 | Granularity vs complexity |
| Aging Rate | Priority boost per interval | 1-5 | Starvation prevention |
| Queue Count | MLFQ queue levels | 3-8 | Workload classification |
| Quantum Growth | MLFQ quantum scaling | 2x per level | Long process efficiency |
Process State Transitions
| From State | To State | Trigger | Scheduler Action |
|---|---|---|---|
| New | Ready | Admission | Add to ready queue |
| Ready | Running | Dispatch | Select next process |
| Running | Ready | Preemption | Move to ready queue |
| Running | Waiting | I/O request | Remove from scheduling |
| Waiting | Ready | I/O completion | Add to ready queue |
| Running | Terminated | Exit | Remove from system |
Ruby Implementation Patterns
| Pattern | Code Example | Use Case |
|---|---|---|
| Strategy Pattern | Scheduler.new(FCFSStrategy) | Swappable algorithms |
| Template Method | class CustomScheduler < BaseScheduler | Algorithm framework |
| Observer Pattern | scheduler.on(:process_complete) | Event notification |
| Factory Pattern | SchedulerFactory.create(:round_robin) | Algorithm instantiation |
| Decorator Pattern | TimedScheduler.new(scheduler) | Add instrumentation |
Algorithm Selection Decision Tree
| Requirement | Recommended Algorithm | Alternative |
|---|---|---|
| Minimize avg waiting time | SJF | SRTF |
| Interactive system | Round-Robin | MLFQ |
| Real-time deadlines | EDF | Rate-Monotonic |
| Mixed workload | MLFQ | CFS |
| Simple implementation | FCFS | Round-Robin |
| Priority-based | Priority with Aging | Lottery |
| Fairness critical | CFS | Round-Robin |
| Batch processing | SJF | FCFS |