Loading content...
Every process uses memory, but not all of its memory is actively in use at any given moment. A web browser may have dozens of tabs loaded, but you're only actively viewing one. A compiler may have parsed an entire source file, but is currently processing just one function. Intuitively, we understand that processes have an 'active' subset of their memory—the portion they're currently working with.
But intuition isn't enough for operating systems. To build effective memory management policies, we need a precise, formal definition that can be measured, tracked, and used to make allocation decisions. This is where Peter Denning's working set model provides its crucial contribution.
By the end of this page, you will understand the formal definition of the working set W(t, Δ), its mathematical properties, how to compute and track it, and why this definition transformed memory management from ad-hoc policies to principled engineering. You'll see how Denning's deceptively simple definition captures exactly the right information for preventing thrashing.
Peter Denning introduced the working set concept in 1968 with a precise mathematical definition that has stood the test of time.
Definition (Working Set):
The working set of a process at time t with window parameter Δ (delta), denoted W(t, Δ), is the set of all distinct pages referenced by the process during the time interval [t - Δ, t].
Formally:
W(t, Δ) = { p : page p was accessed during interval [t - Δ, t] }
The Working Set Size (WSS) at time t is simply the cardinality of this set:
WSS(t, Δ) = |W(t, Δ)|
This definition has several elegant properties that make it ideal for memory management:
| Property | Explanation | Practical Implication |
|---|---|---|
| Time-based | Defined over a time window, not static allocation | Automatically adapts as process behavior changes |
| History-based | Uses past behavior to predict future needs | Exploits temporal locality for prediction |
| Set-based | Tracks distinct pages, not access counts | Memory cost proportional to distinct pages, not total accesses |
| Parameterized | Window Δ is adjustable parameter | Allows tuning for different workload characteristics |
| Monotonic in Δ | W(t, Δ₁) ⊆ W(t, Δ₂) when Δ₁ ≤ Δ₂ | Larger window always includes smaller window's pages |
The window parameter Δ is crucial. It represents 'how far back we look' when determining the working set. Δ is typically measured in memory references (page accesses) rather than wall-clock time, as this makes the definition independent of execution speed. A typical Δ value might be 10,000 to 50,000 memory references, translating to perhaps millions of instructions.
The window parameter Δ (delta) is the key tuning knob of the working set model. Its value profoundly affects both the accuracy of the working set approximation and the resources consumed tracking it.
What Δ Represents:
Conceptually, Δ represents "how much history matters" for predicting future behavior. A process accessed page P at time t-1000. Should P be in the working set at time t? The answer depends on whether 1000 references is "recent" or "ancient" for this workload.
Δ as a Memory Reference Count:
In Denning's original formulation, Δ is measured in page references. If Δ = 10,000:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
# Demonstration of how window size affects working set behavior class WorkingSetTracker: """ Track working set over time with configurable window. Key insight: Same access trace produces different working sets depending on the window parameter Δ. """ def __init__(self, delta): self.delta = delta self.access_history = [] # All page accesses: [(time, page), ...] self.current_time = 0 def access_page(self, page): """Record a page access.""" self.access_history.append((self.current_time, page)) self.current_time += 1 def get_working_set(self, at_time=None): """ Compute W(t, Δ) - the working set at specified time. W(t, Δ) = {p : page p accessed in interval [t-Δ, t]} """ if at_time is None: at_time = self.current_time - 1 window_start = at_time - self.delta # Collect all distinct pages accessed in window working_set = set() for access_time, page in self.access_history: if window_start <= access_time <= at_time: working_set.add(page) return working_set def get_wss(self, at_time=None): """Get Working Set Size = |W(t, Δ)|""" return len(self.get_working_set(at_time)) def demonstrate_window_effects(): """ Show how the same access pattern produces different working sets with different window sizes. """ # Simulate a process with two phases: # Phase 1 (time 0-49): Access pages 0-4 in a loop # Phase 2 (time 50-99): Access pages 5-9 in a loop access_trace = [] # Phase 1: pages 0-4 for _ in range(10): # 10 cycles through pages 0-4 for p in range(5): access_trace.append(p) # 50 accesses total # Phase 2: pages 5-9 for _ in range(10): # 10 cycles through pages 5-9 for p in range(5, 10): access_trace.append(p) # 50 accesses total # Test different window sizes print("Working Set Analysis with Different Window Sizes") print("=" * 60) print() print("Trace: Phase 1 (t=0-49): pages 0-4, Phase 2 (t=50-99): pages 5-9") print() for delta in [10, 25, 50, 75, 100]: tracker = WorkingSetTracker(delta=delta) for page in access_trace: tracker.access_page(page) # Measure working set at different times print(f"Window Δ = {delta}:") # At time 25 (middle of phase 1) ws_25 = tracker.get_working_set(at_time=25) print(f" t=25 (Phase 1): W(25,{delta}) = {sorted(ws_25)}, WSS = {len(ws_25)}") # At time 75 (middle of phase 2) ws_75 = tracker.get_working_set(at_time=75) print(f" t=75 (Phase 2): W(75,{delta}) = {sorted(ws_75)}, WSS = {len(ws_75)}") # At time 60 (just after phase transition) ws_60 = tracker.get_working_set(at_time=60) print(f" t=60 (Transition): W(60,{delta}) = {sorted(ws_60)}, WSS = {len(ws_60)}") print() print("Observations:") print("- Small Δ (10): Only sees current phase, quick adaptation") print("- Medium Δ (50): Exactly captures one phase") print("- Large Δ (75+): Working set includes pages from BOTH phases") print("- At transitions: Larger Δ means more 'carry-over' from old phase") # Example output:# Window Δ = 10:# t=25: W(25,10) = [0, 1, 2, 3, 4], WSS = 5# t=75: W(75,10) = [5, 6, 7, 8, 9], WSS = 5# t=60: W(60,10) = [5, 6, 7, 8, 9], WSS = 5 ← Quick adaptation## Window Δ = 75:# t=25: W(25,75) = [0, 1, 2, 3, 4], WSS = 5# t=75: W(75,75) = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], WSS = 10# t=60: W(60,75) = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], WSS = 10 ← Old pages lingerThere is no single 'correct' value of Δ. The optimal window depends on the workload's locality characteristics. Some systems use adaptive Δ values that adjust based on observed page fault rates. Others use heuristics based on process type. The key insight is that Δ represents a trade-off between memory consumption and page fault reduction.
The working set definition has several important mathematical properties that inform both theoretical analysis and practical implementation.
Property 1: Monotonicity in Δ
For any time t and windows Δ₁ ≤ Δ₂:
W(t, Δ₁) ⊆ W(t, Δ₂)
A larger window always produces a superset of a smaller window's working set. This is because the larger window sees all references the smaller window sees, plus additional older references.
Corollary: WSS(t, Δ) is monotonically non-decreasing in Δ.
Property 2: Bounded Size
For any process with P total pages:
1 ≤ |W(t, Δ)| ≤ min(P, Δ)
The working set contains at least one page (the most recently accessed) and at most min(P, Δ) pages. The upper bound is Δ because we can have at most one distinct page per reference in the window.
Property 3: Transition Behavior
When a process transitions between phases:
W(t, Δ) = W_old(t, Δ) ∪ W_new(t, Δ)
During transitions, the working set temporarily includes pages from both the old and new phases until old pages age out of the window.
Property 4: The Inclusion Property (Working Set Policy Foundation)
Denning observed that keeping W(t, Δ) resident minimizes page faults over windows of size Δ. More formally:
If a page p was accessed at time t-k where k ≤ Δ, and p is evicted at time t, then any access to p in [t, t+Δ-k] will cause a page fault that could have been avoided.
This property is why the working set policy states: keep exactly the working set resident. Any larger set wastes memory; any smaller set causes additional avoidable page faults.
Property 5: Convexity of Miss Rate Function
For most workloads, the page fault rate as a function of allocated memory is convex (or at least monotonically decreasing). This means:
∂²(fault_rate) / ∂(memory)² ≥ 0
The first few frames added provide large fault rate reduction (diminishing the most critical misses); additional frames provide progressively smaller improvements. This justifies allocating memory proportional to working set sizes.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
# Demonstrating mathematical properties of working sets class WorkingSetMathProperties: """ Verify and demonstrate mathematical properties of working sets. """ def __init__(self, access_trace): """Initialize with a trace of page accesses.""" self.trace = access_trace self.total_pages = len(set(access_trace)) def compute_working_set(self, t, delta): """ Compute W(t, Δ). Returns set of pages accessed in [t-Δ, t]. """ window_start = max(0, t - delta) return set(self.trace[window_start:t+1]) def verify_monotonicity(self, t): """ Verify Property 1: W(t, Δ₁) ⊆ W(t, Δ₂) when Δ₁ ≤ Δ₂ """ print(f"Property 1: Monotonicity at t={t}") print("-" * 50) prev_ws = set() for delta in range(1, min(t+2, 20)): ws = self.compute_working_set(t, delta) # Verify subset property if not prev_ws.issubset(ws): print(f" VIOLATION at Δ={delta}") return False print(f" Δ={delta:2d}: |W| = {len(ws):2d}, W = {sorted(ws)}") prev_ws = ws print(" ✓ Monotonicity verified: larger Δ ⊇ smaller Δ") return True def verify_bounded_size(self, t): """ Verify Property 2: 1 ≤ |W(t, Δ)| ≤ min(P, Δ) """ print(f"Property 2: Bounded Size (P={self.total_pages})") print("-" * 50) for delta in [5, 10, 20, 50, 100]: ws = self.compute_working_set(t, delta) wss = len(ws) upper_bound = min(self.total_pages, delta) lower_bound = 1 valid = lower_bound <= wss <= upper_bound status = "✓" if valid else "✗" print(f" Δ={delta:3d}: |W| = {wss:2d}, bound = [{lower_bound}, {upper_bound}] {status}") return True def demonstrate_transition_behavior(self): """ Demonstrate Property 3: Working set during phase transitions contains pages from both old and new phases. """ print("Property 3: Transition Behavior") print("-" * 50) # Create trace with distinct phases # Phase 1 (t=0-99): pages 0-9 # Phase 2 (t=100-199): pages 10-19 phase1 = [i % 10 for i in range(100)] phase2 = [10 + (i % 10) for i in range(100)] trace = phase1 + phase2 analyzer = WorkingSetMathProperties(trace) # Examine working set at transition points for t in [90, 100, 110, 120, 150]: ws = analyzer.compute_working_set(t, delta=30) old_pages = set(range(10)).intersection(ws) new_pages = set(range(10, 20)).intersection(ws) print(f" t={t:3d}: old_phase={sorted(old_pages)}, new_phase={sorted(new_pages)}") print(" During transition, working set includes BOTH phases") print(" Old pages gradually age out as time advances") def analyze_miss_curve(self): """ Demonstrate Property 5: Diminishing returns of additional memory. """ print("Property 5: Convexity of Miss Rate Function") print("-" * 50) test_point = len(self.trace) - 1 prev_wss = 0 prev_marginal = float('inf') print(" Memory | WSS | Marginal Benefit") print(" " + "-" * 35) for delta in [10, 20, 50, 100, 200, 500]: ws = self.compute_working_set(test_point, delta) wss = len(ws) # Marginal benefit = pages gained per unit of additional window if delta > 10: marginal = (wss - prev_wss) / (delta - 10) else: marginal = wss / delta print(f" Δ={delta:3d} | {wss:3d} | {marginal:.4f} pages/ref") prev_wss = wss prev_marginal = marginal # Example usage with sample tracedef demonstrate_properties(): # Create varied access trace trace = [] # Looping phase for _ in range(50): trace.extend([0, 1, 2, 3, 4]) # Scattered phase import random random.seed(42) for _ in range(100): trace.append(random.randint(0, 20)) analyzer = WorkingSetMathProperties(trace) # Verify properties analyzer.verify_monotonicity(t=200) analyzer.verify_bounded_size(t=200) analyzer.demonstrate_transition_behavior() analyzer.analyze_miss_curve()The theoretical definition of workings sets is elegant, but tracking W(t, Δ) exactly for every process presents significant practical challenges. Real operating systems use approximations that balance accuracy with overhead.
The Measurement Challenge:
To track W(t, Δ) exactly, we would need to:
With modern systems making billions of memory references per second, this exact tracking is prohibitively expensive. Real systems use sampling and approximation techniques.
| Approach | Mechanism | Accuracy | Overhead |
|---|---|---|---|
| Timer-based sampling | Sample reference bits at fixed intervals | Approximate | Very low |
| Reference bit aging | Shift reference bits to track recency | Moderate | Low |
| Multiple reference bits | N bits encode last N intervals | Good | Low-Moderate |
| Access counters | Per-page counters updated on reference | Good | Moderate |
| Full trace | Record all references (debugging only) | Exact | Extremely high |
The Timer-Based Approximation:
Most real implementations use a timer-based approximation. Instead of tracking the last Δ memory references, they track pages accessed in the last Δ time units:
This approximation works because:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
# Practical working set tracking implementations from collections import defaultdictimport time class ExactWorkingSetTracker: """ Exact working set tracking (expensive, for analysis only). Maintains complete history of page references and computes W(t, Δ) precisely on demand. """ def __init__(self, delta): self.delta = delta self.reference_history = [] # [(time, page), ...] self.reference_count = 0 def record_reference(self, page): """Record a page reference. O(1) per reference.""" self.reference_history.append((self.reference_count, page)) self.reference_count += 1 def get_working_set(self): """Compute exact W(t, Δ). O(Δ) per query.""" current = self.reference_count - 1 window_start = max(0, current - self.delta) working_set = set() for ref_time, page in self.reference_history[-self.delta:]: working_set.add(page) return working_set class TimerBasedWorkingSetTracker: """ Timer-based working set approximation (practical). Uses periodic timer interrupts to sample reference bits. This is similar to how real OS kernels track working sets. """ def __init__(self, num_pages, window_intervals=10): self.num_pages = num_pages self.window_intervals = window_intervals # Δ in timer intervals # Reference bit for each page (set by hardware on access) self.reference_bit = [False] * num_pages # Age bits: bitmap showing which intervals page was accessed # Bit 0 = current interval, bit N = N intervals ago self.age_bits = [0] * num_pages self.current_interval = 0 def access_page(self, page): """ Simulate a page access (in real OS, hardware sets this). """ self.reference_bit[page] = True def timer_interrupt(self): """ Called on timer interrupt to update working set tracking. This is O(num_pages) but done only periodically (e.g., 100Hz), so amortized overhead is low. """ for page in range(self.num_pages): # Shift age bits right (age all references by one interval) self.age_bits[page] = self.age_bits[page] >> 1 # Add current reference bit as most significant if self.reference_bit[page]: self.age_bits[page] |= (1 << (self.window_intervals - 1)) # Clear reference bit for next interval self.reference_bit[page] = False self.current_interval += 1 def get_working_set(self): """ Return approximate working set. A page is in working set if ANY bit is set in age_bits, meaning it was accessed in one of the last window_intervals. """ working_set = set() for page in range(self.num_pages): if self.age_bits[page] > 0: working_set.add(page) return working_set def get_working_set_size(self): """Return WSS = |W(t, Δ)|""" return len(self.get_working_set()) def is_in_working_set(self, page): """Check if specific page is in working set. O(1).""" return self.age_bits[page] > 0 class ReferenceCountTracker: """ Counter-based working set tracking. Each page has a counter that's incremented on access and decremented on timer interrupts. Pages with counter > 0 are in the working set. """ def __init__(self, num_pages, max_count=10): self.num_pages = num_pages self.max_count = max_count # Maximum counter value (= window intervals) # Counter per page: higher = more recently/frequently used self.counters = [0] * num_pages self.reference_bit = [False] * num_pages def access_page(self, page): """Record page access.""" self.reference_bit[page] = True def timer_interrupt(self): """ Update counters on timer interrupt. Referenced pages: boost counter (capped at max) Unreferenced pages: decrement counter toward zero """ for page in range(self.num_pages): if self.reference_bit[page]: # Page was accessed: boost counter self.counters[page] = min(self.counters[page] + 1, self.max_count) else: # Page not accessed: decay counter self.counters[page] = max(self.counters[page] - 1, 0) self.reference_bit[page] = False def get_working_set(self): """Pages with counter > 0 are in working set.""" return {p for p in range(self.num_pages) if self.counters[p] > 0} def get_page_age(self, page): """ Get relative age of page. Higher counter = more recently used = younger. """ return self.max_count - self.counters[page] # Demonstrationdef compare_tracking_methods(): print("Comparing Working Set Tracking Methods") print("=" * 60) num_pages = 20 # Create access pattern: phases with locality accesses = [] for _ in range(100): accesses.extend([0, 1, 2, 3, 4]) # Hot set 1 for _ in range(50): accesses.extend([10, 11, 12, 13, 14]) # Hot set 2 for _ in range(100): accesses.extend([0, 1, 2, 3, 4]) # Back to hot set 1 # Test timer-based tracker timer_tracker = TimerBasedWorkingSetTracker(num_pages, window_intervals=8) for i, page in enumerate(accesses): timer_tracker.access_page(page) if i % 50 == 49: # "Timer interrupt" every 50 accesses timer_tracker.timer_interrupt() ws = timer_tracker.get_working_set() print(f"After {i+1} accesses: WS = {sorted(ws)}, WSS = {len(ws)}")Modern CPUs provide hardware reference bits in page table entries that are automatically set when a page is accessed. This makes working set tracking practical—the OS only needs to periodically check and clear these bits, rather than trapping on every memory access. Without this hardware support, working set tracking would be prohibitively expensive.
The working set size (WSS) of a process is not static—it varies over time as the process moves through different phases of execution. Understanding WSS dynamics is crucial for effective memory allocation.
WSS Over Time:
A typical process exhibits WSS variations that correspond to its computational phases:
Initialization: WSS grows as the process loads libraries, initializes data structures, and establishes its initial working set.
Steady-State Computation: WSS stabilizes around a characteristic level for the current algorithm or task.
Phase Transitions: WSS may spike temporarily as the process accesses both old and new pages, then settles to a new level.
Cleanup: WSS typically decreases as the process releases resources and winds down.
Implications for Memory Management:
WSS dynamics have direct implications for memory allocation:
During WSS Growth:
During Stable WSS:
During WSS Decline:
At Phase Transitions:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
# Simulating and visualizing WSS dynamics over time import random class WSSSimulator: """ Simulate realistic WSS dynamics for a multi-phase process. """ def __init__(self, delta): self.delta = delta self.history = [] # Page access trace self.time = 0 def simulate_phase(self, duration, hot_pages, access_dist='uniform'): """ Simulate a phase with given characteristics. Args: duration: Number of references in this phase hot_pages: List of pages that are "hot" in this phase access_dist: 'uniform' or 'skewed' access pattern """ for _ in range(duration): if access_dist == 'uniform': page = random.choice(hot_pages) elif access_dist == 'skewed': # 80% to first half, 20% to second half (80-20 rule) if random.random() < 0.8: page = random.choice(hot_pages[:len(hot_pages)//2]) else: page = random.choice(hot_pages[len(hot_pages)//2:]) else: page = random.choice(hot_pages) self.history.append(page) self.time += 1 def compute_wss_over_time(self): """ Compute WSS at each point in time. Returns list of (time, wss) tuples. """ wss_trace = [] for t in range(len(self.history)): window_start = max(0, t - self.delta) ws = set(self.history[window_start:t+1]) wss_trace.append((t, len(ws))) return wss_trace def compute_wss_statistics(self): """ Compute statistics about WSS behavior. """ wss_trace = self.compute_wss_over_time() wss_values = [wss for _, wss in wss_trace] return { 'min_wss': min(wss_values), 'max_wss': max(wss_values), 'avg_wss': sum(wss_values) / len(wss_values), 'wss_variance': sum((w - sum(wss_values)/len(wss_values))**2 for w in wss_values) / len(wss_values), } def simulate_multi_phase_process(): """ Simulate a realistic multi-phase process and analyze WSS dynamics. """ print("Simulating Multi-Phase Process WSS Dynamics") print("=" * 60) sim = WSSSimulator(delta=100) # Phase 1: Initialization (growing WSS) print("Phase 1: Initialization (pages 0-9, building up)") for i in range(10): sim.simulate_phase(50, list(range(i+1))) # Gradually add pages # Phase 2: Stable computation (steady WSS) print("Phase 2: Steady-state computation (pages 0-9)") sim.simulate_phase(500, list(range(10))) # Phase 3: Transition to new task (WSS spike) print("Phase 3: Task transition (pages 0-9 + 20-29)") sim.simulate_phase(100, list(range(10)) + list(range(20, 30))) # Phase 4: New steady state (different WSS) print("Phase 4: New steady-state (pages 20-24)") sim.simulate_phase(400, list(range(20, 25))) # Phase 5: Cleanup (declining WSS) print("Phase 5: Cleanup (pages 20-22)") sim.simulate_phase(200, list(range(20, 23))) # Analyze WSS dynamics wss_trace = sim.compute_wss_over_time() stats = sim.compute_wss_statistics() print() print("WSS Statistics:") print(f" Minimum WSS: {stats['min_wss']}") print(f" Maximum WSS: {stats['max_wss']}") print(f" Average WSS: {stats['avg_wss']:.1f}") print(f" WSS Variance: {stats['wss_variance']:.1f}") print() # Sample key points print("WSS at Key Points:") key_points = [100, 500, 700, 1000, 1400] for t in key_points: if t < len(wss_trace): _, wss = wss_trace[t] print(f" t={t:4d}: WSS={wss:2d}") # Phase identification heuristic print() print("Detected Phases (WSS changes > 20%):") prev_wss = wss_trace[0][1] for t, wss in wss_trace[::100]: # Sample every 100 references if abs(wss - prev_wss) / max(prev_wss, 1) > 0.2: change = "↑" if wss > prev_wss else "↓" print(f" t={t}: WSS {prev_wss} → {wss} ({change})") prev_wss = wss # Example output visualization concept:# # WSS │ **** # │ * * # 15 ├ * * # │ * * ****** # 10 ├ * ******** **** # │* **** # 5 ├ *** # │ # 0 └────────────────────────────────────────# 0 500 1000 1500 2000 Time# └─Init─┴─Steady──┴─Trans─┴─New─┴CleanupIf WSS could be predicted accurately, memory allocation would be trivial: simply allocate the predicted WSS. Unfortunately, perfect prediction is impossible. The working set model uses recent past as a proxy for near future—a reasonable heuristic given temporal locality, but not a guarantee. Adaptive systems continuously monitor WSS and adjust allocation accordingly.
The working set model provides a principled answer to the frame allocation problem: allocate to each process a number of frames equal to its current working set size. This policy, while simple to state, has profound implications.
The Working Set Allocation Policy:
This policy directly connects memory demand (WSS) to memory supply (frames), preventing both under-allocation (causing thrashing) and over-allocation (wasting memory).
| Scheme | Allocation Basis | Adapts to Phase? | Prevents Thrashing? |
|---|---|---|---|
| Equal allocation | Total frames / processes | No | No guarantee |
| Proportional to size | Virtual address space size | No | No guarantee |
| Proportional to priority | Process priority | No | No guarantee |
| Working set policy | Current WSS | Yes | Yes (if enforced) |
| Page Fault Frequency | Observed fault rate | Yes | Yes (reactive) |
Why Working Set Policy Prevents Thrashing:
Thrashing occurs when processes collectively demand more memory than available. Without working set tracking:
With working set policy:
The Key Insight:
The working set model transforms memory management from a reactive crisis (thrashing) to a proactive policy (admission control). By knowing how much memory each process needs before thrashing begins, the system can prevent thrashing rather than recovering from it.
The working set policy implies a natural load control mechanism: the multiprogramming level is set by how many processes can have their working sets simultaneously resident. This self-regulates the system—when memory pressure increases (processes demand more), multiprogramming decreases (fewer processes run). When memory pressure decreases, more processes can run.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
# Working set-based frame allocation and admission control class WorkingSetAllocator: """ Frame allocator using working set policy. Implements Denning's key insight: allocate to each process its working set size, and control multiprogramming to ensure all active processes' working sets can be satisfied. """ def __init__(self, total_frames, delta): self.total_frames = total_frames self.delta = delta # Per-process state self.processes = {} # pid -> ProcessState self.active_processes = set() self.suspended_processes = set() def add_process(self, pid, initial_wss=1): """Add a new process to the system.""" self.processes[pid] = { 'wss': initial_wss, 'allocated_frames': 0, 'history': [], } self.try_activate_process(pid) def update_working_set(self, pid, new_wss): """ Update process's working set size. Called when WSS tracking detects a change. May trigger reallocation or process suspension. """ if pid not in self.processes: return old_wss = self.processes[pid]['wss'] self.processes[pid]['wss'] = new_wss # Check if system is overcommitted self._rebalance_allocation() def _total_active_wss(self): """Sum of working set sizes for all active processes.""" return sum( self.processes[pid]['wss'] for pid in self.active_processes ) def _rebalance_allocation(self): """ Rebalance frame allocation to maintain working set policy. Key principle: Active processes MUST have their working sets fully resident. If this is impossible, suspend processes. """ total_demand = self._total_active_wss() while total_demand > self.total_frames and self.active_processes: # Overcommitted: must suspend a process # Choose victim (here: largest WSS, but policies vary) victim = max( self.active_processes, key=lambda p: self.processes[p]['wss'] ) self._suspend_process(victim) total_demand = self._total_active_wss() # Allocate frames to remaining active processes for pid in self.active_processes: self.processes[pid]['allocated_frames'] = self.processes[pid]['wss'] # Try to resume suspended processes if space available self._try_resume_suspended() def _suspend_process(self, pid): """Suspend a process (swap out its pages).""" if pid in self.active_processes: self.active_processes.remove(pid) self.suspended_processes.add(pid) self.processes[pid]['allocated_frames'] = 0 print(f"Suspending process {pid} (WSS={self.processes[pid]['wss']})") def _try_resume_suspended(self): """Try to resume suspended processes if memory available.""" available = self.total_frames - self._total_active_wss() for pid in list(self.suspended_processes): wss = self.processes[pid]['wss'] if wss <= available: self.suspended_processes.remove(pid) self.active_processes.add(pid) self.processes[pid]['allocated_frames'] = wss available -= wss print(f"Resuming process {pid} (WSS={wss})") def try_activate_process(self, pid): """Try to activate a new process.""" wss = self.processes[pid]['wss'] available = self.total_frames - self._total_active_wss() if wss <= available: self.active_processes.add(pid) self.processes[pid]['allocated_frames'] = wss print(f"Activated process {pid} (WSS={wss})") else: self.suspended_processes.add(pid) print(f"Process {pid} suspended on creation (insufficient memory)") def get_multiprogramming_level(self): """Current number of active processes.""" return len(self.active_processes) def get_memory_utilization(self): """Fraction of frames currently allocated.""" allocated = sum( self.processes[pid]['allocated_frames'] for pid in self.active_processes ) return allocated / self.total_frames def status_report(self): """Print current allocation status.""" print("" + "=" * 50) print("Working Set Allocator Status") print("=" * 50) print(f"Total frames: {self.total_frames}") print(f"Active processes: {len(self.active_processes)}") print(f"Suspended processes: {len(self.suspended_processes)}") print(f"Total active WSS: {self._total_active_wss()}") print(f"Memory utilization: {self.get_memory_utilization():.1%}") print() print("Active Processes:") for pid in self.active_processes: p = self.processes[pid] print(f" PID {pid}: WSS={p['wss']}, allocated={p['allocated_frames']}") print("Suspended Processes:") for pid in self.suspended_processes: p = self.processes[pid] print(f" PID {pid}: WSS={p['wss']} (waiting)") print() # Demonstrationdef demonstrate_ws_allocation(): print("Working Set Allocation Demonstration") print("=" * 60) allocator = WorkingSetAllocator(total_frames=100, delta=1000) # Add processes with various working set sizes allocator.add_process(1, initial_wss=20) allocator.add_process(2, initial_wss=30) allocator.add_process(3, initial_wss=25) allocator.add_process(4, initial_wss=40) # This might not fit allocator.status_report() # Process 1 enters a new phase with larger WSS print("Process 1 enters memory-intensive phase...") allocator.update_working_set(1, 50) # WSS grows from 20 to 50 allocator.status_report() # Process 2 completes task, WSS shrinks print("Process 2 completes task, WSS shrinks...") allocator.update_working_set(2, 10) allocator.status_report()The working set definition transforms the intuitive notion of 'memory in active use' into a precise, measurable, and actionable concept. With this formal foundation, memory management becomes principled rather than ad-hoc.
You now understand the precise definition of the working set and its mathematical properties. In the next page, we'll explore the working set window parameter in depth—how to choose it, how it affects behavior, and the trade-offs involved in window size selection.
Next Up: Working Set Window (Δ)
The window parameter Δ is the most important tunable aspect of the working set model. We'll explore how to select appropriate values, analyze the effects of different window sizes, and understand adaptive approaches that dynamically adjust Δ based on workload characteristics.