Loading content...
Consider this scenario: A process with an 8ms CPU burst has just started executing. One millisecond later, a new process arrives with only a 2ms burst. Under non-preemptive SJF, the new process must wait the remaining 7ms for the current process to complete—even though the new process is shorter.
This seems wasteful. The short process is forced to wait behind a longer one, precisely the situation SJF was designed to avoid. The problem is that non-preemptive SJF only considers job lengths at scheduling decision points—when the CPU becomes free. It ignores new arrivals during execution.
Shortest Remaining Time First (SRTF), also called Preemptive SJF or Shortest Remaining Time (SRT), solves this by re-evaluating the schedule whenever a new process arrives. If a newly arrived process has a shorter remaining time than the currently running process, the current process is preempted—suspended mid-execution—and the new process takes over.
This simple enhancement reduces average waiting time even further, at the cost of additional context switches.
By completing this page, you will understand SRTF's algorithm mechanics, its optimality among all scheduling algorithms, implementation with remaining time tracking, context switch overhead analysis, and the tradeoffs between preemptive and non-preemptive approaches.
Shortest Remaining Time First (SRTF) extends SJF by considering the remaining burst time rather than the original burst time, and by making scheduling decisions at every process arrival.
Let R = {p₁, p₂, ..., pₖ} be the set of processes in the ready queue plus the currently running process at time t. For each process pᵢ, let rᵢ(t) represent its remaining CPU burst time at time t. SRTF selects:
p = argmin{rᵢ(t) | pᵢ ∈ R}*
If p* differs from the currently running process, preemption occurs.
Unlike non-preemptive SJF, SRTF makes scheduling decisions:
At each arrival, SRTF compares the new process's burst time with the running process's remaining time. If the new process is shorter, preemption occurs.
| Aspect | SJF (Non-Preemptive) | SRTF (Preemptive) |
|---|---|---|
| Decision criterion | Original burst time | Remaining burst time |
| Decision points | Process completion only | Completion + each arrival |
| Preemption | Never | When shorter process arrives |
| Context switches | Minimal (n switches for n processes) | Potentially many more |
| Average wait time | Optimal among non-preemptive | Optimal among ALL algorithms |
| Implementation complexity | Moderate | Higher (track remaining time) |
SRTF requires tracking how much CPU time each process has already consumed. When preemption occurs, the scheduler records the time used so far. The remaining time = original burst - time already used. This adds bookkeeping overhead not present in non-preemptive SJF.
The SRTF algorithm requires an event-driven approach, processing events (arrivals and completions) in chronological order.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
#include <stdio.h>#include <stdlib.h>#include <limits.h> #define MAX_PROCESSES 100 typedef struct { int pid; int arrival_time; int burst_time; int remaining_time; // Key for SRTF: tracks remaining burst int start_time; // First time process gets CPU int completion_time; int waiting_time; int turnaround_time; int last_scheduled; // Last time this process was scheduled (for wait calc) int started; // Has this process ever run?} Process; /** * Find process with shortest REMAINING time among arrived processes */int find_srtf(Process procs[], int n, int current_time, int completed[]) { int shortest_idx = -1; int min_remaining = INT_MAX; for (int i = 0; i < n; i++) { if (procs[i].arrival_time <= current_time && !completed[i] && procs[i].remaining_time > 0) { // Tie-breaker: earlier arrival, then lower PID if (procs[i].remaining_time < min_remaining || (procs[i].remaining_time == min_remaining && (shortest_idx == -1 || procs[i].arrival_time < procs[shortest_idx].arrival_time))) { min_remaining = procs[i].remaining_time; shortest_idx = i; } } } return shortest_idx;} /** * Get next event time (arrival or completion) * Returns next significant time point for scheduling decision */int next_event_time(Process procs[], int n, int current_time, int completed[], int running_idx) { int next_time = INT_MAX; // Check for next arrival for (int i = 0; i < n; i++) { if (!completed[i] && procs[i].arrival_time > current_time) { if (procs[i].arrival_time < next_time) { next_time = procs[i].arrival_time; } } } // Check for completion of running process if (running_idx >= 0) { int completion = current_time + procs[running_idx].remaining_time; if (completion < next_time) { next_time = completion; } } return next_time;} /** * SRTF Scheduler Simulation (Event-Driven) */void srtf_schedule(Process procs[], int n) { // Initialize remaining times for (int i = 0; i < n; i++) { procs[i].remaining_time = procs[i].burst_time; procs[i].started = 0; procs[i].waiting_time = 0; } int completed[MAX_PROCESSES] = {0}; int completed_count = 0; int current_time = 0; int running = -1; // Currently running process index int prev_running = -1; printf("\nSRTF Scheduling Simulation:\n"); printf("═══════════════════════════════════════════════════════════\n"); while (completed_count < n) { // Select process with shortest remaining time int selected = find_srtf(procs, n, current_time, completed); if (selected == -1) { // CPU idle: advance to next arrival int next_arrival = INT_MAX; for (int i = 0; i < n; i++) { if (!completed[i] && procs[i].arrival_time > current_time) { if (procs[i].arrival_time < next_arrival) { next_arrival = procs[i].arrival_time; } } } printf("Time %3d: CPU idle\n", current_time); current_time = next_arrival; continue; } // Context switch detection if (running != selected) { if (running >= 0 && procs[running].remaining_time > 0) { printf("Time %3d: P%d preempted (remaining=%d)\n", current_time, procs[running].pid, procs[running].remaining_time); } if (!procs[selected].started) { procs[selected].start_time = current_time; procs[selected].started = 1; } printf("Time %3d: P%d starts/resumes (remaining=%d)\n", current_time, procs[selected].pid, procs[selected].remaining_time); } running = selected; // Find next event (arrival that could preempt, or completion) int next_event = next_event_time(procs, n, current_time, completed, running); int run_time = next_event - current_time; // Check if running process completes before next event if (procs[running].remaining_time <= run_time) { run_time = procs[running].remaining_time; current_time += run_time; procs[running].remaining_time = 0; procs[running].completion_time = current_time; procs[running].turnaround_time = current_time - procs[running].arrival_time; procs[running].waiting_time = procs[running].turnaround_time - procs[running].burst_time; printf("Time %3d: P%d completes (turnaround=%d, wait=%d)\n", current_time, procs[running].pid, procs[running].turnaround_time, procs[running].waiting_time); completed[running] = 1; completed_count++; running = -1; } else { // Process runs until next event but doesn't complete procs[running].remaining_time -= run_time; current_time = next_event; } } printf("═══════════════════════════════════════════════════════════\n");}The code above uses event-driven simulation, jumping between significant events (arrivals, completions). An alternative is time-driven simulation, advancing one time unit at a time. Event-driven is more efficient (O(n²) vs O(T×n) where T is total time), but time-driven is simpler to implement.
Let's trace SRTF on the same process set we used for SJF, highlighting where preemption occurs:
| Process | Arrival Time | CPU Burst |
|---|---|---|
| P1 | 0 | 8 |
| P2 | 1 | 4 |
| P3 | 2 | 9 |
| P4 | 3 | 5 |
Time 0:
Time 1:
Time 2:
Time 3:
Time 5:
Time 10:
Time 17:
Time 26:
Completion Times:
Turnaround Time = Completion - Arrival:
Waiting Time = Turnaround - Burst:
Average Waiting Time = (9 + 0 + 15 + 2) / 4 = 6.5
| Metric | SJF | SRTF | Improvement |
|---|---|---|---|
| P1 Wait | 0 | 9 | Worse (preempted) |
| P2 Wait | 7 | 0 | Much better |
| P3 Wait | 15 | 15 | Same |
| P4 Wait | 9 | 2 | Better |
| Average | 7.75 | 6.5 | 16% better |
SRTF reduced average waiting time from 7.75 (SJF) to 6.5—a 16% improvement. P2 especially benefits: instead of waiting 7 time units behind P1, it preempts P1 and completes with zero wait. The cost: P1's wait increased from 0 to 9 due to being preempted.
SRTF has a remarkable property: it minimizes average waiting time among ALL scheduling algorithms—preemptive or non-preemptive. This is a stronger result than SJF's non-preemptive optimality.
SRTF achieves the minimum possible average waiting time for any set of processes with known arrival times and burst lengths.
The proof extends the SJF exchange argument:
Since every non-SRTF schedule can be transformed to SRTF with decreased total waiting time, SRTF is optimal.
Non-preemptive SJF can make suboptimal decisions because:
SRTF avoids this by continuously asking: "Is there a process that would complete sooner than the current one?" Preemption ensures the answer is always "no"—the running process always has the shortest remaining time.
Like SJF, SRTF's optimality assumes known burst times. With predictions instead of exact values, SRTF is an approximation. But it's the best approximation possible—if our predictions are perfect, SRTF is optimal; if predictions have errors, no other algorithm can do systematically better.
| Algorithm | Optimality Claim | Conditions |
|---|---|---|
| FCFS | None (often worst) | — |
| SJF (non-preemptive) | Optimal among non-preemptive | Known burst times |
| SRTF (preemptive SJF) | Optimal among ALL algorithms | Known burst times + arrival times |
| Round Robin | Good for response time, not wait time | — |
| Priority | No wait time guarantee | — |
SRTF's improved average waiting time comes at a cost: more context switches. Each preemption requires saving the current process's state and loading the new process's state—a non-trivial overhead.
A typical context switch involves:
| Operation | Typical Cost |
|---|---|
| Save registers | ~100-500 cycles |
| Save FPU/SIMD state | ~500-2000 cycles |
| Update PCB | ~100-200 cycles |
| TLB flush (partial) | 1000-10000 cycles |
| Cache disruption | Variable (huge) |
| Total direct cost | ~5,000-50,000 cycles (2-20µs) |
The direct cost underestimates the true impact. After a context switch:
Consider: P1 has 10ms remaining, P2 arrives with 9ms burst.
SRTF behavior: Preempt P1, run P2 Context switches: 1 preemption + 1 return to P1 = 2 extra switches
If each switch costs 20µs:
But if P1 has 50µs remaining and P2 has 40µs burst:
The overhead becomes significant when differences are small.
If many processes have similar burst times and arrivals are frequent, SRTF can thrash—constantly preempting before making progress. The scheduler spends more time context-switching than running processes. This is why pure SRTF is rarely used in practice.
Real systems use several techniques:
1. Minimum Granularity
Only preempt if the difference exceeds a threshold:
if (new_remaining < current_remaining - THRESHOLD)
preempt();
Typical threshold: 1-5ms
2. Quantum-Based Preemption
Check for preemption only at quantum boundaries, not every arrival. Reduces switch frequency while maintaining approximate SRTF behavior.
3. Hysteresis
Require the improvement to be sustained:
if (new_remaining < current_remaining for 3 consecutive checks)
preempt();
Prevents thrashing on noisy burst estimates.
Implementing SRTF requires additional bookkeeping beyond SJF.
For each process, maintain:
remaining_time: Updated whenever process stops runninglast_scheduled: When process last got the CPUWhen preemption occurs at time t:
running->remaining_time -= (t - running->last_scheduled);
new_process->last_scheduled = t;
The ready queue must support:
A min-heap with decrease-key is ideal:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
/** * SRTF Scheduler - Key Implementation Components */ typedef struct { int pid; int remaining_time; int arrival_time; // ... other fields} Process; // Min-heap keyed by remaining_timetypedef struct { Process* processes[MAX_PROCESSES]; int heap_position[MAX_PROCESSES]; // PID -> heap index (for decrease-key) int size;} SRTFReadyQueue; /** * Called when a new process arrives * May trigger preemption if new process is shorter */void on_process_arrival(SRTFReadyQueue* queue, Process* new_proc, Process** running, int current_time) { // Update remaining time of currently running process if (*running != NULL) { (*running)->remaining_time -= (current_time - (*running)->last_scheduled); } // Check if preemption needed if (*running == NULL || new_proc->remaining_time < (*running)->remaining_time) { // Preempt current process if (*running != NULL) { (*running)->state = READY; heap_insert(queue, *running); context_switches++; // Track overhead } // Schedule new process *running = new_proc; (*running)->state = RUNNING; (*running)->last_scheduled = current_time; if (!(*running)->started) { (*running)->start_time = current_time; (*running)->started = 1; } } else { // New process waits; add to queue heap_insert(queue, new_proc); }} /** * Called when running process completes or blocks */void on_process_yield(SRTFReadyQueue* queue, Process** running, int current_time) { // Record completion metrics (*running)->completion_time = current_time; (*running)->turnaround_time = current_time - (*running)->arrival_time; (*running)->state = TERMINATED; // or BLOCKED // Select next process if (queue->size > 0) { *running = heap_extract_min(queue); (*running)->state = RUNNING; (*running)->last_scheduled = current_time; context_switches++; } else { *running = NULL; // CPU idle }} /** * Preemption threshold check (practical optimization) */#define PREEMPTION_THRESHOLD_MS 2 // Only preempt if >2ms difference int should_preempt(Process* running, Process* new_proc) { if (running == NULL) return 1; int difference = running->remaining_time - new_proc->remaining_time; return difference > PREEMPTION_THRESHOLD_MS;}Production schedulers often approximate SRTF rather than implementing it exactly. Linux CFS, for example, doesn't use explicit burst prediction but achieves similar effects by tracking actual CPU time consumption and favoring processes that have used less CPU recently.
Both algorithms optimize for short jobs, but they suit different scenarios.
| Criterion | Winner | Margin |
|---|---|---|
| Average waiting time | SRTF | 5-20% typically |
| Context switch count | SJF | Often 2-3x fewer |
| Implementation complexity | SJF | Significantly simpler |
| Response time (interactive) | SRTF | Much better |
| Throughput (batch) | Comparable | Depends on workload |
| Predictability | SJF | No mid-job interruption |
| Starvation risk | Both suffer | See next page |
Most modern general-purpose operating systems use neither pure SJF nor SRTF. Instead, they use multi-level feedback queues (MLFQ) or fair schedulers (CFS) that approximate the benefits of shortest-job-first while providing better fairness and avoiding starvation.
SRTF needs remaining time, which means:
Prediction errors compound in SRTF because we may preempt based on wrong predictions, then preempt again when predictions are revised.
Linux CFS (Completely Fair Scheduler):
Windows Scheduler:
SRTF (or close approximations) is still used in:
The principle behind SRTF—favor the task that will complete soonest—appears throughout computing wherever queuing occurs. Understanding SRTF's tradeoffs helps in designing efficient systems beyond just OS scheduling.
We have thoroughly examined SRTF, understanding both its theoretical optimality and practical limitations.
Looking Ahead:
Both SJF and SRTF share a critical weakness: they can cause starvation of long processes. If short processes continually arrive, long processes may wait indefinitely. The next page explores the starvation problem in depth, understanding its causes, detecting when it occurs, and learning prevention techniques like aging.
You now understand SRTF's mechanics, optimality, and tradeoffs. The algorithm represents the theoretical best for average waiting time, but practical constraints—especially context switch overhead and the starvation problem—limit its direct application. Real schedulers draw inspiration from SRTF while addressing its weaknesses.