Loading content...
Before we can intelligently allocate frames to processes, we must answer a fundamental question: How many frames does each process actually need?
This question sounds simple but reveals profound complexity. A process's memory requirements are not static—they vary over time as the process enters different phases of execution. They depend on the algorithm being executed, the data being processed, and the interaction patterns of the user or system. Moreover, there's a difference between what a process wants, what it needs to function, and what it needs to run efficiently.
Understanding these distinctions is crucial. Allocating based on wants leads to waste. Allocating below functional minimums causes failure. Finding the sweet spot—the number of frames that provides good performance without excess—is the art of frame allocation.
By the end of this page, you will understand how to analyze process memory requirements, distinguish between minimum, optimal, and maximum needs, and recognize how different process types exhibit different memory patterns. This knowledge is essential for designing allocation policies that work in practice.
The most important concept for understanding process requirements is the working set—the set of pages a process is actively using during a given time window.
Formal definition:
The working set W(t, Δ) at time t with window Δ is the set of pages referenced by the process in the time interval [t - Δ, t].
Why the working set matters:
The working set represents the pages a process needs in memory to run efficiently at a given moment. If a process has all its working set pages in physical memory, it will experience few page faults. If pages from the working set are missing, the process will fault frequently and performance will degrade dramatically.
Working set size (WSS):
The number of pages in the working set, |W(t, Δ)|, indicates how many frames the process needs. This number is not constant—it varies as the process moves between phases of execution.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
/* * Working Set Calculation * * This demonstrates how an OS might track and calculate * the working set of a process. */ #include <stdio.h>#include <stdbool.h>#include <string.h> #define MAX_PAGES 256#define WINDOW_SIZE 10 // Δ value - size of the observation window typedef struct { int page_number; int last_access_time;} PageInfo; typedef struct { int pid; PageInfo pages[MAX_PAGES]; int page_count; // Reference history for working set calculation int reference_history[WINDOW_SIZE]; int history_index; int current_time; // Working set statistics int working_set_size; int working_set_pages[WINDOW_SIZE];} ProcessWorkingSet; void initialize_process(ProcessWorkingSet *p, int pid) { p->pid = pid; p->page_count = 0; p->history_index = 0; p->current_time = 0; p->working_set_size = 0; for (int i = 0; i < WINDOW_SIZE; i++) { p->reference_history[i] = -1; // -1 indicates no reference }} void record_page_reference(ProcessWorkingSet *p, int page) { // Record this reference in the sliding window p->reference_history[p->history_index] = page; p->history_index = (p->history_index + 1) % WINDOW_SIZE; p->current_time++; // Update last access time for this page bool found = false; for (int i = 0; i < p->page_count; i++) { if (p->pages[i].page_number == page) { p->pages[i].last_access_time = p->current_time; found = true; break; } } if (!found && p->page_count < MAX_PAGES) { p->pages[p->page_count].page_number = page; p->pages[p->page_count].last_access_time = p->current_time; p->page_count++; }} int calculate_working_set(ProcessWorkingSet *p) { // Count unique pages in the reference window bool seen[MAX_PAGES] = {false}; int unique_count = 0; for (int i = 0; i < WINDOW_SIZE; i++) { int page = p->reference_history[i]; if (page >= 0 && page < MAX_PAGES && !seen[page]) { seen[page] = true; p->working_set_pages[unique_count] = page; unique_count++; } } p->working_set_size = unique_count; return unique_count;} void print_working_set(ProcessWorkingSet *p) { calculate_working_set(p); printf("Process %d Working Set Analysis", p->pid); printf("================================"); printf("Window size (Δ): %d references", WINDOW_SIZE); printf("Current time: %d", p->current_time); printf("Working set size: %d pages", p->working_set_size); printf("Pages in working set: "); for (int i = 0; i < p->working_set_size; i++) { printf("%d ", p->working_set_pages[i]); } printf("");} /* * Example usage demonstrating working set evolution: * * Phase 1 (Initialization): * References: 1, 2, 3, 4, 5 * WSS: 5 (loading initial code and data) * * Phase 2 (Tight loop): * References: 2, 3, 2, 3, 2, 3 * WSS: 2 (only loop body and data) * * Phase 3 (Function call): * References: 10, 11, 12, 13, 14 * WSS: 5 (new function + its data) * * The working set size changes as the process * moves through different execution phases. */Choosing the window size Δ is critical. Too small, and the working set misses pages that will be needed shortly. Too large, and the working set includes pages no longer relevant. The optimal Δ depends on the locality characteristics of the workload—typically ranging from thousands to millions of memory references.
The reason working sets are typically small relative to total address space is a fundamental property of program behavior: locality of reference.
Temporal locality:
Pages referenced recently are likely to be referenced again soon. A loop iterating over a data structure will access the same pages repeatedly. Function calls return to caller code. Variables are read and written multiple times.
Spatial locality:
Pages near recently accessed pages are likely to be accessed soon. Arrays are processed sequentially. Instructions execute in order (mostly). Data structures are allocated contiguously.
The locality hypothesis:
Most programs spend most of their time accessing a small fraction of their address space. A program with 1000 pages might, at any given moment, be actively using only 10-50 pages. This is why virtual memory works at all—only the active pages need to be in physical memory.
Locality phases:
Programs typically move through phases with different locality characteristics:
Initialization phase: Load libraries, parse configurations, initialize data structures. Often touches many pages briefly.
Main computation phase: Execute the core algorithm. If the algorithm has good locality, the working set is small and stable.
I/O phase: Read or write data, potentially touching different pages than computation phases.
Transition phases: Moving between major code sections, loading new functions, switching data structures. Working set changes rapidly.
Understanding these phases helps predict when a process will need more or fewer frames.
If programs accessed memory uniformly randomly across their address space, virtual memory would be useless—every access would cause a page fault. Locality is the empirical observation that makes paging viable. Programs that violate locality assumptions can cause severe performance problems.
Every process has a minimum number of frames below which it cannot execute at all. This minimum is determined by the instruction set architecture, not by the process itself.
Why minimums exist:
Consider what happens during a single instruction execution:
If any of these pages faults mid-instruction, the instruction cannot complete. The OS must load the missing page, then restart the instruction from the beginning. For this restart to be possible, the partially completed instruction must not have corrupted state.
Architecture-dependent minimums:
Different instruction sets require different minimums:
| Architecture | Typical Minimum | Reason |
|---|---|---|
| Simple RISC | 2 frames | Instruction page + data page |
| x86 (CISC) | 6 frames | Complex addressing modes, REP prefix |
| VAX | 8+ frames | Extremely complex instructions |
| Modern x86-64 | 3-4 frames | Simpler than x86 but still complex |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
/* * Architectural Minimum Frame Analysis * * This demonstrates why certain instructions require * multiple frames to be simultaneously resident. */ #include <stdio.h> /* * Example: x86 MOVE STRING instruction (REP MOVSB) * * This single instruction can span up to 6 pages: * * 1. Instruction page: Contains the REP MOVSB opcode * 2. Source start page: Beginning of source string * 3. Source end page: End of source string (may differ from start) * 4. Dest start page: Beginning of destination buffer * 5. Dest end page: End of destination buffer * 6. Stack page: For interrupt handling during the operation * * If the string is large and spans page boundaries, * multiple pages from both source and destination * may be needed simultaneously. */ // Pseudocode for the worst case:void analyze_rep_movsb_pages() { printf("REP MOVSB Instruction Frame Analysis"); printf("==================================== "); printf("Consider: memcpy(dest, src, 8192)"); printf("Where 8192 bytes = 2 pages, and both buffers"); printf("are misaligned, crossing page boundaries. "); printf("Required pages during execution:"); printf(" 1. Instruction page (contains REP MOVSB)"); printf(" 2. Source page 1 (first half of source)"); printf(" 3. Source page 2 (second half of source)"); printf(" 4. Dest page 1 (first half of dest)"); printf(" 5. Dest page 2 (second half of dest)"); printf(" 6. Potential for indirect addressing pages "); printf("Minimum frames for this architecture: 6"); printf("If fewer frames are available, the instruction"); printf("cannot complete without page faults, and repeated"); printf("restarts make no forward progress.");} /* * Example: Indirect addressing chain * * Consider: MOV AX, [[[[ptr]]]] * (Four levels of indirection) * * Pages needed: * 1. Instruction page * 2. Page containing 'ptr' * 3. Page containing *ptr * 4. Page containing **ptr * 5. Page containing ***ptr * 6. Final data page containing ****ptr * * Each level of indirection may reference a different page. */ void analyze_indirect_addressing() { printf("Indirect Addressing Frame Analysis"); printf("=================================== "); printf("For: value = ****ptr (4 levels of indirection) "); printf("Pages potentially needed:"); printf(" 1. Instruction page"); printf(" 2. Page containing ptr"); printf(" 3. Page containing *ptr"); printf(" 4. Page containing **ptr"); printf(" 5. Page containing ***ptr"); printf(" 6. Page containing final value "); printf("Plus potentially:"); printf(" - Stack page for exception handling"); printf(" - Page table pages (if hardware uses them)");} /* * Key insight: The minimum is set by the worst-case * instruction that the architecture supports. Even if * most instructions need only 2 frames, the process * must have enough frames for ANY instruction it might * execute. * * The OS must guarantee at least this minimum to every * running process, or the process cannot make progress. */If a process has fewer frames than the architectural minimum, it enters a livelock state. Each attempt to execute an instruction faults. The page is loaded, but loading it evicts another page needed for the same instruction. The process continuously faults without ever completing a single instruction. This is an absolute lower bound.
Beyond the minimum, we distinguish between optimal and maximum frame requirements.
Optimal frames:
The optimal number of frames is the smallest allocation that allows the process to run at near-full efficiency—experiencing only occasional page faults that don't significantly impact throughput.
Optimal ≈ Working Set Size + Buffer
The buffer accounts for transitional pages during phase changes. Typically, optimal is 10-20% above the average working set size.
Maximum frames:
The maximum is theoretically the entire address space of the process—every page it could ever access. However, this is rarely practical or beneficial:
In practice, maximum useful frames is governed by the largest working set the process ever exhibits, plus pages it might access during the session.
| Level | Definition | Efficiency | System Impact |
|---|---|---|---|
| Minimum | Architectural lower bound | 0% - can't execute | Must always be provided |
| Marginal | Slightly above minimum | 10-30% - high fault rate | Process runs but thrashes |
| Adequate | Covers current working set | 70-90% - occasional faults | Reasonable performance |
| Optimal | Working set + buffer | 95-100% - rare faults | Best performance/memory ratio |
| Maximum | All potentially needed pages | 100% but wasteful | No faults but excess allocation |
The efficiency curve:
As frames increase from minimum to optimal, efficiency improves rapidly at first, then levels off:
Efficiency |
100% | ___________
| / Optimal zone
| /
50% | /
| /
| /
| /
0% |_____________/
+---+---+---+---+---+---+---+-> Frames
Min Adequate Optimal
Allocating beyond optimal provides diminishing returns. Those excess frames might be better used by other processes.
The goal of frame allocation is to hit the 'knee' of the efficiency curve for each process—where adding more frames provides minimal additional benefit. Identifying this point requires understanding each process's working set characteristics.
Different types of processes exhibit vastly different memory requirement patterns. Understanding these patterns helps in designing allocation policies.
Interactive processes:
Batch processes:
Server processes:
Memory-intensive processes:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
/* * Process Type Memory Profiles * * This illustrates the different memory access patterns * exhibited by various process types. */ #include <stdio.h> typedef struct { const char *name; int min_frames; int typical_wss; // Working set size (pages) int peak_wss; double locality_score; // 0.0 = random, 1.0 = perfect locality const char *pattern;} ProcessProfile; ProcessProfile process_types[] = { { .name = "Text Editor", .min_frames = 3, .typical_wss = 50, .peak_wss = 200, .locality_score = 0.9, .pattern = "Spiky during file ops, stable during editing" }, { .name = "Web Browser Tab", .min_frames = 4, .typical_wss = 500, .peak_wss = 2000, .locality_score = 0.6, .pattern = "Highly variable, depends on page content" }, { .name = "Database Query", .min_frames = 4, .typical_wss = 1000, .peak_wss = 100000, .locality_score = 0.3, .pattern = "Index traversal then table scan" }, { .name = "Numerical Computation", .min_frames = 3, .typical_wss = 20, .peak_wss = 50, .locality_score = 0.95, .pattern = "Very stable, tight loops over arrays" }, { .name = "Compiler", .min_frames = 4, .typical_wss = 300, .peak_wss = 1500, .locality_score = 0.7, .pattern = "Phases: parse, analyze, codegen" }, { .name = "Video Encoder", .min_frames = 4, .typical_wss = 800, .peak_wss = 2000, .locality_score = 0.8, .pattern = "Frame buffer + working area" }, { .name = "ML Training", .min_frames = 4, .typical_wss = 50000, .peak_wss = 200000, .locality_score = 0.5, .pattern = "Mini-batch iteration over large dataset" }, { .name = "Web Server Worker", .min_frames = 4, .typical_wss = 100, .peak_wss = 500, .locality_score = 0.7, .pattern = "Request handling with shared code" },}; void analyze_process_requirements() { int n = sizeof(process_types) / sizeof(process_types[0]); printf("Process Type Memory Requirements"); printf("================================ "); printf("%-20s %8s %10s %10s %8s", "Process Type", "Min", "Typical", "Peak", "Locality"); printf("------------------------------------------------"); printf("------------------"); for (int i = 0; i < n; i++) { ProcessProfile *p = &process_types[i]; printf("%-20s %8d %10d %10d %8.2f", p->name, p->min_frames, p->typical_wss, p->peak_wss, p->locality_score); } printf("Detailed Patterns: "); for (int i = 0; i < n; i++) { ProcessProfile *p = &process_types[i]; printf("%s: %s ", p->name, p->pattern); }} /* * Key observations: * * 1. Minimum frames are similar across types (architectural limit) * 2. Working set sizes vary by 3 orders of magnitude * 3. Locality scores vary significantly - affects allocation strategy * 4. Peak WSS is often much larger than typical WSS * * Implication for allocation: * - Cannot use one-size-fits-all allocation * - Must adapt to both process type and current phase * - High-locality processes are forgiving of fewer frames * - Low-locality processes need more generous allocation */A process with high locality (0.9+) can often perform well with slightly fewer frames than its working set because pages stay relevant longer. A process with low locality (0.3-0.5) needs more frames to avoid constant page replacement. This is why database workloads and ML training are particularly sensitive to memory allocation.
How does the operating system actually determine process requirements? Several techniques are used, each with tradeoffs.
1. Page fault rate monitoring:
The simplest approach: if a process faults frequently, it needs more frames. If it never faults, it might have excess frames.
2. Reference bit sampling:
Periodically sample reference bits on pages to estimate the working set.
3. Page fault frequency (PFF):
Track inter-fault time. If faults occur too frequently, allocate more frames. If too infrequently, reclaim some.
4. Working set window (WSClock):
Maintain timestamps on pages; working set = pages accessed within window.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
/* * Process Requirement Measurement Techniques * * This demonstrates how an OS measures memory requirements * through various monitoring approaches. */ #include <stdio.h>#include <stdbool.h>#include <time.h> #define MAX_PAGES 1000#define SAMPLE_INTERVAL_MS 100 typedef struct { int pid; // Page fault monitoring int fault_count; int fault_count_last_interval; double avg_inter_fault_time; time_t last_fault_time; // Frame allocation int allocated_frames; int resident_pages; // Working set tracking int wss_estimate; int reference_bits_sum; // Thresholds for PFF double pff_upper_threshold; // Faults/sec above this = need more double pff_lower_threshold; // Faults/sec below this = can reduce} ProcessStats; typedef enum { NEED_MORE_FRAMES, ALLOCATION_OK, COULD_REDUCE_FRAMES} AllocationAdvice; /* * Page Fault Frequency Analysis */AllocationAdvice analyze_pff(ProcessStats *p) { // Calculate fault rate (faults per second) double fault_rate = p->fault_count_last_interval * (1000.0 / SAMPLE_INTERVAL_MS); printf("Process %d: Fault rate = %.2f faults/sec", p->pid, fault_rate); if (fault_rate > p->pff_upper_threshold) { printf(" -> Above upper threshold (%.2f), need more frames", p->pff_upper_threshold); return NEED_MORE_FRAMES; } if (fault_rate < p->pff_lower_threshold) { printf(" -> Below lower threshold (%.2f), could reduce", p->pff_lower_threshold); return COULD_REDUCE_FRAMES; } printf(" -> Within acceptable range"); return ALLOCATION_OK;} /* * Reference Bit Working Set Estimation * * Periodically scan pages and count those with reference bit set. * This approximates the number of pages accessed since last scan. */int estimate_wss_from_reference_bits(ProcessStats *p, bool ref_bits[], int num_pages) { int referenced_count = 0; for (int i = 0; i < num_pages; i++) { if (ref_bits[i]) { referenced_count++; ref_bits[i] = false; // Clear for next interval } } // Working set estimate is running average p->wss_estimate = (p->wss_estimate + referenced_count) / 2; printf("Process %d: Referenced pages this interval: %d, " "WSS estimate: %d", p->pid, referenced_count, p->wss_estimate); return p->wss_estimate;} /* * Combined recommendation */void recommend_allocation(ProcessStats *p) { printf("=== Allocation Recommendation for PID %d ===", p->pid); printf("Current allocation: %d frames", p->allocated_frames); printf("Estimated WSS: %d pages", p->wss_estimate); int target = p->wss_estimate + (p->wss_estimate / 10); // WSS + 10% if (p->allocated_frames < target) { printf("Recommendation: INCREASE to %d frames", target); printf("Deficit: %d frames", target - p->allocated_frames); } else if (p->allocated_frames > target * 1.5) { printf("Recommendation: DECREASE to %d frames", target); printf("Excess: %d frames", p->allocated_frames - target); } else { printf("Recommendation: MAINTAIN current allocation"); }} /* * Practical consideration: * * Measurement is imperfect. The OS typically uses multiple * signals together: * * 1. Fault rate trending up → immediate signal of need * 2. WSS estimate → medium-term allocation target * 3. Memory pressure system-wide → constraints on allocation * 4. Process priority → determines who gets frames when scarce * * The goal is to find a stable allocation before performance * degrades significantly. */Predicting future requirements is challenging because process behavior changes. The best approaches combine reactive measurement (page faults) with predictive estimation (working set tracking) and smooth changes to avoid oscillation.
Process requirements are fundamentally dynamic—they change over time as the process moves through execution phases.
Phase transitions:
Startup phase:
Steady-state phase:
Transition phase:
Cleanup phase:
| Phase | Duration | WSS (pages) | Primary Pages |
|---|---|---|---|
| Startup | ~100ms | 50 | Executable, shared libs |
| Parsing | ~2s | 200 | Parser code, AST buffers |
| Semantic Analysis | ~3s | 350 | Symbol tables, type info |
| Optimization | ~5s | 500 | IR, analysis data |
| Code Generation | ~2s | 300 | Codegen, output buffers |
| Cleanup | ~100ms | 50 | Just executable |
Implications for allocation:
Because requirements change, static allocation is inherently suboptimal:
Modern OSes use dynamic allocation, adjusting each process's frame allocation based on observed behavior. The challenge is making adjustments quickly enough to prevent thrashing but slowly enough to avoid oscillation.
Phase transitions are dangerous moments. The working set changes faster than the OS can adapt. A process entering a new phase may fault heavily until its new working set is resident. Good allocation policies anticipate phase changes when possible (e.g., detecting function call patterns) or respond very quickly when they occur.
We've explored how to understand and measure what each process requires from the limited pool of physical frames. This understanding is essential for making intelligent allocation decisions.
What's next:
With understanding of both the frame constraint (Page 1) and process requirements (this page), we're ready to explore minimum frames in more depth—the absolute floor below which processes cannot function, determined by architecture and instruction complexity.
You now understand how to analyze and measure process memory requirements—from working sets and locality to the difference between minimum, optimal, and maximum needs. This knowledge transforms frame allocation from guesswork into principled engineering.