CrackedRuby CrackedRuby

Process Scheduling Algorithms

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