Loading learning content...
FCFS is fair but inefficient. SSTF is efficient but unfair. Is there a way to achieve both high throughput AND bounded waiting times?
The answer comes from an unlikely source: elevators. Consider how an elevator services floor requests:
This simple discipline—move in one direction until you can't, then reverse—eliminates starvation while maintaining excellent seek optimization. An elevator doesn't skip floors to jump back and forth; neither should a disk head.
This insight gives us SCAN, also known as the Elevator Algorithm—the most influential disk scheduling algorithm ever designed.
By the end of this page, you will master the SCAN algorithm—its mechanics, performance characteristics, bounded wait time guarantee, and variations. You'll understand why SCAN became the foundation for disk scheduling in virtually every production operating system.
SCAN moves the disk arm in one direction, servicing all requests along the way, until it reaches the end of the disk. Then it reverses direction and repeats.
Algorithm Definition:
Critical Detail: The arm travels to the physical end of the disk, not just the extreme request. This is what distinguishes SCAN from LOOK (covered later).
Formal Description:
Let $H$ be the current head position, $D \in {-1, +1}$ be the direction, and $N$ be the total cylinders.
The next cylinder to service is: $$\text{next} = \begin{cases} \min{c : c > H} & \text{if } D = +1 \text{ and } \exists c > H N-1 \text{ (end reached)} & \text{if } D = +1 \text{ and } exists c > H \max{c : c < H} & \text{if } D = -1 \text{ and } \exists c < H 0 \text{ (end reached)} & \text{if } D = -1 \text{ and } exists c < H \end{cases}$$
After reaching an end, reverse: $D \leftarrow -D$
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
"""SCAN (Elevator) Disk Scheduling Algorithm Implements the classic SCAN algorithm with direction trackingand comprehensive metrics for analysis.""" from dataclasses import dataclass, fieldfrom typing import List, Tuple, Optionalfrom enum import Enum class Direction(Enum): """Arm movement direction.""" INWARD = -1 # Toward cylinder 0 OUTWARD = 1 # Toward cylinder N-1 @dataclassclass DiskRequest: """Represents a disk I/O request.""" request_id: int cylinder: int arrival_time: float @dataclassclass SCANScheduler: """ SCAN Disk Scheduling Implementation Moves the disk arm in one direction until reaching the disk end, servicing all requests along the way, then reverses direction. """ initial_head_position: int total_cylinders: int initial_direction: Direction = Direction.OUTWARD # State head_position: int = field(init=False) direction: Direction = field(init=False) pending: List[DiskRequest] = field(default_factory=list) # Metrics total_seek_distance: int = 0 requests_serviced: int = 0 service_order: List[int] = field(default_factory=list) seek_sequence: List[Tuple[int, int, str]] = field(default_factory=list) direction_reversals: int = 0 def __post_init__(self): self.head_position = self.initial_head_position self.direction = self.initial_direction def submit_request(self, request: DiskRequest) -> None: """Add a request to pending queue.""" self.pending.append(request) def get_requests_in_direction(self) -> List[DiskRequest]: """ Get requests in the current direction, sorted by cylinder. If moving OUTWARD (toward N-1): requests with cylinder > head, ascending If moving INWARD (toward 0): requests with cylinder < head, descending """ if self.direction == Direction.OUTWARD: # Get requests ahead (higher cylinders), sort ascending ahead = [r for r in self.pending if r.cylinder >= self.head_position] return sorted(ahead, key=lambda r: r.cylinder) else: # Get requests behind (lower cylinders), sort descending behind = [r for r in self.pending if r.cylinder <= self.head_position] return sorted(behind, key=lambda r: r.cylinder, reverse=True) def move_to_end(self) -> int: """ Move to the end of the disk in the current direction. Returns the seek distance for this move. """ if self.direction == Direction.OUTWARD: seek = (self.total_cylinders - 1) - self.head_position target = self.total_cylinders - 1 else: seek = self.head_position - 0 target = 0 self.seek_sequence.append(( self.head_position, target, f"End ({self.direction.name})" )) self.head_position = target self.total_seek_distance += seek return seek def reverse_direction(self) -> None: """Reverse the arm direction.""" self.direction = ( Direction.INWARD if self.direction == Direction.OUTWARD else Direction.OUTWARD ) self.direction_reversals += 1 def service_request(self, request: DiskRequest) -> dict: """Service a single request.""" seek = abs(request.cylinder - self.head_position) self.seek_sequence.append(( self.head_position, request.cylinder, f"Request {request.request_id}" )) self.head_position = request.cylinder self.pending.remove(request) self.total_seek_distance += seek self.requests_serviced += 1 self.service_order.append(request.cylinder) return { "request_id": request.request_id, "cylinder": request.cylinder, "seek_distance": seek, } def run_scheduling(self) -> dict: """ Execute SCAN scheduling until all requests serviced. """ results = [] while self.pending: # Get requests in current direction direction_requests = self.get_requests_in_direction() if direction_requests: # Service all requests in this direction for request in direction_requests: result = self.service_request(request) results.append(result) # No more requests in this direction if self.pending: # Move to end and reverse self.move_to_end() self.reverse_direction() return { "algorithm": "SCAN", "service_order": self.service_order, "total_seek_distance": self.total_seek_distance, "average_seek_distance": ( self.total_seek_distance / self.requests_serviced if self.requests_serviced > 0 else 0 ), "requests_serviced": self.requests_serviced, "direction_reversals": self.direction_reversals, "seek_sequence": self.seek_sequence, "detailed_results": results, } def demonstrate_scan(): """ Demonstrate SCAN with the standard example for comparison. Disk: 200 cylinders (0-199) Initial head: 50 Initial direction: OUTWARD (toward 199) Requests: 95, 180, 34, 119, 11, 123, 62, 64 """ scheduler = SCANScheduler( initial_head_position=50, total_cylinders=200, initial_direction=Direction.OUTWARD ) requests = [95, 180, 34, 119, 11, 123, 62, 64] for i, cyl in enumerate(requests): scheduler.submit_request(DiskRequest(i, cyl, 0.0)) results = scheduler.run_scheduling() print("=== SCAN (Elevator) Disk Scheduling Demo ===") print(f"Disk: 200 cylinders, Head starts at 50") print(f"Initial direction: OUTWARD (toward 199)") print(f"Requests: {requests}") print("Execution trace:") for from_cyl, to_cyl, note in results['seek_sequence']: print(f" {from_cyl} -> {to_cyl} [{note}]") print(f"Service order: {results['service_order']}") print(f"Total seek distance: {results['total_seek_distance']} cylinders") print(f"Average seek: {results['average_seek_distance']:.2f} cylinders") print(f"Direction reversals: {results['direction_reversals']}") # Comparison print("=== Algorithm Comparison ===") print(f"FCFS: 644 cylinders (baseline)") print(f"SSTF: 208 cylinders (67.7% improvement)") print(f"SCAN: {results['total_seek_distance']} cylinders", end="") print(f" ({(1 - results['total_seek_distance']/644)*100:.1f}% improvement)") return results if __name__ == "__main__": demonstrate_scan()Let's trace SCAN execution using our standard example, with initial direction OUTWARD (toward cylinder 199).
Scenario:
| Step | Action | Head Movement | Seek Distance | Cumulative |
|---|---|---|---|---|
| Initial | Start | Head at 50 | — | 0 |
| 1 | Service request | 50 → 62 | 12 | 12 |
| 2 | Service request | 62 → 64 | 2 | 14 |
| 3 | Service request | 64 → 95 | 31 | 45 |
| 4 | Service request | 95 → 119 | 24 | 69 |
| 5 | Service request | 119 → 123 | 4 | 73 |
| 6 | Service request | 123 → 180 | 57 | 130 |
| 7 | Move to end | 180 → 199 | 19 | 149 |
| 8 | Reverse direction | Direction → INWARD | — | 149 |
| 9 | Service request | 199 → 34 | 165 | 314 |
| 10 | Service request | 34 → 11 | 23 | 337 |
SCAN Performance Metrics:
$$\text{Total Seek Distance} = 12 + 2 + 31 + 24 + 4 + 57 + 19 + 165 + 23 = 337 \text{ cylinders}$$
$$\text{Average Seek Distance} = \frac{337}{8} = 42.1 \text{ cylinders}$$
Service Order: 62 → 64 → 95 → 119 → 123 → 180 → 34 → 11
| Algorithm | Total Seek | Avg Seek | vs FCFS | Starvation? |
|---|---|---|---|---|
| FCFS | 644 cyl | 80.5 cyl | Baseline | No |
| SSTF | 208 cyl | 26.0 cyl | -67.7% | Yes |
| SCAN | 337 cyl | 42.1 cyl | -47.7% | No |
SCAN uses 337 cylinders vs SSTF's 208—about 62% more seek distance. However, SCAN guarantees that every request will be serviced within at most two full disk sweeps. This bounded latency often outweighs the throughput difference in production systems.
SCAN's most important property is its bounded wait time guarantee—no request can be starved, and the maximum wait time is calculable.
Maximum Wait Analysis:
Consider the worst-case scenario for a request:
Maximum Wait Time Formula:
$$W_{max} = 2 \cdot N \cdot T_{seek_per_cylinder}$$
Where:
For a 10,000-cylinder disk with 0.01ms per cylinder: $$W_{max} = 2 \times 10,000 \times 0.01ms = 200ms$$
This is a hard upper bound—no request will ever wait longer, regardless of workload.
Expected Wait Time:
For uniformly distributed requests, the expected wait is approximately:
$$E[W] \approx N \cdot T_{seek_per_cylinder}$$
Half the maximum, because on average a request arrives when the head is halfway through one direction.
Variance:
Unlike SSTF where variance can be unbounded, SCAN's wait time variance is:
$$Var[W] \approx \frac{N^2 \cdot T^2}{12}$$
Bounded variance means predictable latency distribution—essential for SLA compliance.
With SSTF, you cannot predict worst-case latency—it could be seconds, minutes, or infinite. With SCAN, you can compute the absolute worst case mathematically and design systems accordingly. This predictability is invaluable for real-time systems, databases with latency SLAs, and any application where tail latency matters.
While SCAN eliminates starvation, it introduces a subtle fairness bias that's important to understand.
The Edge Cylinder Problem:
Consider how often different cylinders are visited:
This means requests at disk edges wait, on average, longer than requests at the center.
Wait Time by Position:
For a request at cylinder $c$ (disk has $N$ cylinders, head at center):
$$E[W_c] \propto \frac{|c - N/2|}{N/2} \cdot W_{max}$$
Center cylinders have wait ~50% of max; edge cylinders approach $W_{max}$.
| Cylinder | Position | Expected Wait (relative) | Probability of Long Wait |
|---|---|---|---|
| 0 | Edge (minimum) | ~100% of max | High |
| 50 | Quarter | ~75% of max | Medium-High |
| 100 | Center | ~50% of max | Medium |
| 150 | Three-quarter | ~75% of max | Medium-High |
| 199 | Edge (maximum) | ~100% of max | High |
Impact on Workloads:
This bias becomes problematic when:
Hot data at edges: If frequently-accessed data is stored at cylinder 0 (e.g., file system metadata in first sectors), those accesses experience higher average latency
Uneven aging: Edge requests age unevenly—they always wait for a complete sweep in one direction
Burst patterns: If bursts of requests alternately hit edges, the center benefits from both sweeps while edges wait
C-SCAN addresses this fairness issue, as we'll see in the next page.
In real file systems, cylinder 0 often contains critical metadata (boot sectors, superblocks). This means the most critical data experiences the worst average latency under SCAN. This motivated the development of C-SCAN.
SCAN's performance characteristics depend heavily on workload distribution and queue depth.
Total Seek Distance Formula:
For a set of requests spanning from cylinder $min$ to cylinder $max$, starting at position $H$ with direction $D$:
If moving toward $max$ first: $$D_{SCAN} = (max - H) + (max - min) + (min \text{ to end penalty})$$
General case (moving to end each direction): $$D_{SCAN} \leq 2 \times (N-1)$$
SCAN never traverses more than twice the disk width—a hard upper bound.
| Workload | Expected Seek | Comparison to SSTF | Notes |
|---|---|---|---|
| Uniform random | ~1.33N per pass | 10-30% worse | Competitive performance |
| Clustered single | ~cluster width | Similar | Both excellent |
| Two clusters | Distance between + widths | Slightly worse | SSTF may jump; SCAN sweeps |
| Edge-heavy | ~2N per cycle | Much worse | SCAN always goes to ends |
| Sequential | ~N | Identical | Both optimal |
Queue Depth Impact:
Like SSTF, SCAN benefits from deeper queues:
The Sweep Efficiency Principle:
SCAN's efficiency improves as the "density" of requests along the sweep path increases. With many requests, the head makes few wasted movements—most cylinder transitions service a pending request.
For clustered workloads where data locality is high, SCAN can actually outperform SSTF. SSTF may waste seeks bouncing within a cluster, while SCAN efficiently sweeps through it once. Combined with SCAN's fairness, this makes it superior for many real workloads.
Efficient SCAN implementation requires maintaining sorted request structures for quick access to the next request in the current direction.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
"""Efficient SCAN Implementation Uses two sorted lists for O(log n) insertion and O(1) selection.Demonstrates production-quality implementation patterns.""" import bisectfrom dataclasses import dataclass, fieldfrom typing import List, Optional, Tuplefrom enum import Enum class Direction(Enum): INWARD = -1 OUTWARD = 1 @dataclassclass EfficientSCANScheduler: """ Optimized SCAN using two sorted lists. Maintains: - outward_queue: cylinders > head, sorted ascending - inward_queue: cylinders <= head, sorted descending (stored ascending, read reversed) This allows O(1) selection of next request and O(log n) insertion. """ total_cylinders: int head_position: int direction: Direction = Direction.OUTWARD # Two sorted queues outward_queue: List[int] = field(default_factory=list) # Sorted ascending inward_queue: List[int] = field(default_factory=list) # Sorted ascending, pop from end # Metrics total_seek: int = 0 service_order: List[int] = field(default_factory=list) def submit(self, cylinder: int) -> None: """ O(log n) insertion into appropriate queue. """ if cylinder > self.head_position: bisect.insort(self.outward_queue, cylinder) elif cylinder < self.head_position: bisect.insort(self.inward_queue, cylinder) else: # At head position - goes in current direction's queue if self.direction == Direction.OUTWARD: bisect.insort(self.outward_queue, cylinder) else: bisect.insort(self.inward_queue, cylinder) def get_next(self) -> Optional[int]: """ O(1) selection of next request. Returns None if no requests pending. """ if self.direction == Direction.OUTWARD: if self.outward_queue: return self.outward_queue[0] # Smallest cylinder > head elif self.inward_queue: # Must go to end and reverse self._go_to_end() self._reverse() return self.inward_queue[-1] if self.inward_queue else None else: if self.inward_queue: return self.inward_queue[-1] # Largest cylinder < head elif self.outward_queue: # Must go to end and reverse self._go_to_end() self._reverse() return self.outward_queue[0] if self.outward_queue else None return None def _go_to_end(self) -> None: """Move head to disk end in current direction.""" if self.direction == Direction.OUTWARD: end = self.total_cylinders - 1 else: end = 0 seek = abs(end - self.head_position) self.total_seek += seek self.head_position = end def _reverse(self) -> None: """Reverse direction.""" self.direction = ( Direction.INWARD if self.direction == Direction.OUTWARD else Direction.OUTWARD ) def service_next(self) -> Optional[Tuple[int, int]]: """ Service the next request. Returns (cylinder, seek_distance) or None if empty. """ next_cyl = self.get_next() if next_cyl is None: return None # Remove from appropriate queue if self.direction == Direction.OUTWARD: self.outward_queue.pop(0) else: self.inward_queue.pop() seek = abs(next_cyl - self.head_position) self.total_seek += seek self.head_position = next_cyl self.service_order.append(next_cyl) return (next_cyl, seek) def run_all(self) -> dict: """Run until all requests serviced.""" while self.outward_queue or self.inward_queue: self.service_next() return { "service_order": self.service_order, "total_seek": self.total_seek, } # Example usageif __name__ == "__main__": scheduler = EfficientSCANScheduler( total_cylinders=200, head_position=50, direction=Direction.OUTWARD ) for cyl in [95, 180, 34, 119, 11, 123, 62, 64]: scheduler.submit(cyl) result = scheduler.run_all() print(f"Service order: {result['service_order']}") print(f"Total seek: {result['total_seek']} cylinders")| Operation | Naive | Optimized (Two Lists) | Notes |
|---|---|---|---|
| Insert request | O(1) | O(log n) | Sorted insertion |
| Select next | O(n) | O(1) | Head of appropriate list |
| Service request | O(n) | O(1) | Pop from list |
| Direction reversal | O(1) | O(1) | Flag toggle |
| Memory | O(n) | O(n) | Same total storage |
SCAN and its variants are the foundation of disk scheduling in virtually every production operating system.
With SSDs, SCAN's benefits diminish—there's no physical head to optimize. Modern Linux uses the 'noop' or 'none' scheduler for SSDs, essentially FCFS. However, SCAN remains critical for HDDs, which still dominate cold storage and high-capacity deployments.
We have thoroughly examined the SCAN (Elevator) algorithm. Let's consolidate the key insights:
What's Next:
SCAN's positional unfairness—where edge cylinders experience longer waits—motivates an enhancement. In the next page, we'll study C-SCAN (Circular SCAN), which provides uniform wait time across all cylinder positions by treating the disk as a circular structure.
You now understand the SCAN algorithm at an expert level—its elevator-inspired design, bounded wait guarantee, positional fairness characteristics, and implementation strategies. SCAN represents the fundamental breakthrough in disk scheduling that made bounded-latency storage systems possible.