Module 3 · Unit 8 · Lesson 4 of 6
Contiguous Allocation
The simplest scheme for dividing physical memory among processes: give each process one contiguous region. The operating system maintains a list of the free regions, and when a process needs memory, it searches the list for a region large enough. The choice of which region to use — the placement algorithm — determines how well the memory is used. First-fit, best-fit, and worst-fit are the three classical answers, and their trade-offs are the foundation of everything that follows.
01 Introduction
Lessons 8.1 through 8.3 established the physical and conceptual foundations of memory management: the memory hierarchy, address binding, and the mechanisms for loading code on demand. This lesson turns to the first concrete allocation policy. The system has a fixed amount of physical memory, and several processes want to use it. How should the operating system divide the memory among them?
The simplest answer, and the one that dominated early time-sharing systems, is contiguous allocation: each process is given a single region of physical memory large enough to hold its entire address space, and the region is contiguous — the process's logical addresses 0 through size−1 map to consecutive physical addresses base through base+size−1. This is exactly the base-and-limit scheme from Lesson 8.2, and it is the natural way to use the memory-management unit introduced there. Each process has its own base and limit, the MMU translates logical to physical addresses by adding the base, and the limit check enforces protection.
The interesting problem is not the translation but the allocation. Suppose the operating system has been running for a while, and memory is carved into a sequence of regions: some allocated to processes, some free. A new process arrives and needs N bytes. Which free region should the operating system assign? The obvious answer — "any region large enough" — is not a complete policy, because in general there are several regions large enough, and the choice affects how well the remaining memory can serve future requests. This is the placement problem, and the three classical algorithms that solve it — first-fit, best-fit, and worst-fit — are the subject of this lesson.
The lesson also introduces the two structural problems that arise in any contiguous allocation scheme. Fragmentation is the accumulation of small, unusable free regions that no future request can fit, either because they are between allocated blocks (external fragmentation) or because they are inside an allocated block that is larger than the process needed (internal fragmentation). Compaction is the technique of moving the allocated blocks together to consolidate the free regions, which is expensive because it requires copying processes in memory. Fragmentation is the subject of Lesson 8.5; this lesson introduces it and illustrates it in the worked example.
The lesson closes with the relationship between contiguous allocation and the mechanisms of the rest of the unit. Contiguous allocation is the simplest member of the family of allocation schemes, and it has the most severe limitations. The limitations are what motivate the segmentation of Lesson 8.6 and the paging of Unit 9. Understanding the simple scheme thoroughly is what makes the design of the more elaborate ones legible.
Prerequisites
This lesson builds on Lessons 8.1–8.3. The distinction between logical and physical addresses, the memory-management unit, and the base-and-limit scheme from Lesson 8.2 are assumed throughout. The operating system's three responsibilities — allocation, protection, and transparency — from Lesson 8.1 are the framework for the discussion. No prior knowledge of fragmentation or of the placement algorithms is assumed; all are introduced here.
02 Learning objectives
By the end of this lesson you will be able to
- Describe the contiguous allocation model and the free-list data structure that supports it.
- Distinguish fixed partitioning from variable partitioning and explain the trade-offs of each.
- Apply the three placement algorithms — first-fit, best-fit, and worst-fit — to a sequence of allocation requests.
- Explain coalescing and describe how the free list is maintained as blocks are allocated and freed.
- Compare the three algorithms and analyse their behaviour on a realistic workload.
This lesson contributes to U8.3 and U8.4, and to course learning outcomes CLO1, CLO4, and CLO6.
03 Required reading
- Operating System Concepts — Chapter 9, Section 9.2, Contiguous Memory Allocation. The standard treatment of the three placement algorithms and the free-list data structure.
- Silberschatz et al. — Chapter 9, Section 9.2. Read the worked example carefully; the sequence of requests and the placement of each process under the three algorithms is the canonical illustration and appears in almost every treatment of the topic. Work through the numbers yourself before reading the solution.
- Tanenbaum & Bos — Modern Operating Systems, Chapter 3, Section 3.2, A Memory Abstraction: Address Spaces. The section on memory allocation algorithms and the discussion of first-fit, next-fit, best-fit, and worst-fit.
- OSTEP — Chapter 17, Free Space Management. The clearest available treatment of free-space management from the perspective of a memory allocator. OSTEP's treatment of the free list, splitting, coalescing, and the trade-offs between the placement policies is the basis for the worked example in this lesson.
- Knuth, D. E. — The Art of Computer Programming, Volume 1: Fundamental Algorithms, Section 2.5, Dynamic Storage Allocation. The classical treatment of the free-list data structures and the placement policies, written from the perspective of a storage allocator for a language runtime.
- Wilson, P. R., Johnstone, M. S., Neely, M., & Boles, D. — Dynamic Storage Allocation: A Survey and Critical Review. Proceedings of the International Workshop on Memory Management, 1995. A comprehensive survey of the placement policies and their empirical behaviour on real workloads. Read sections 1–3 for the conceptual framework.
-
Linux man pages:
man 3 malloc,man 3 free,man 2 brk,man 2 sbrk,man 2 mmap,man 5 proc(the/proc/<pid>/mapsand/proc/<pid>/statminterfaces).
Reading strategy
Read the Silberschatz section first for the formal statement of the three algorithms and the canonical worked example. Then read the OSTEP chapter on free-space management, which presents the same material from the allocator's perspective and adds the crucial insight that the free list is a data structure with its own overhead, and that the placement policy interacts with the coalescing policy in ways that are not obvious. Finally, skim the Wilson et al. survey for the empirical results: the theoretical analysis of the algorithms does not match their behaviour on real workloads, and the survey is the best available account of the discrepancy.
04 Core content
4.1 The contiguous allocation model
In the contiguous allocation model, each process occupies a single contiguous region of physical memory. The region's size is the size of the process's address space — the sum of its code, data, heap, and stack. The region's starting address is chosen by the operating system when the process is created, and it does not change for the lifetime of the process.
The operating system maintains two pieces of information:
- For each allocated region, the process that owns it, the region's start address, and its size. This is enough to set the base and limit registers when the process runs, and to identify the region when the process terminates.
- For each free region, the region's start address and its size. These free regions are collectively called the free list or hole list. The free list is a data structure — historically a linked list, in modern systems usually a tree or a bitmap — that records the available memory.
When a process terminates, its region is returned to the free list. If its neighbours are also free, the three regions are merged into a single larger free region, a process called coalescing. Coalescing is essential: without it, the free list would accumulate many small regions that individually are too small for any future request but together would be sufficient.
The translation of logical to physical addresses is performed by the base-and-limit mechanism of Lesson 8.2: the base register holds the region's start address, and the limit register holds its size. The MMU adds the base to each logical address and checks the result against the limit. Protection is therefore automatic: a process cannot reach beyond its own region, because the limit check rejects any address at or beyond the boundary.
4.2 Fixed partitioning
The simplest form of contiguous allocation is fixed partitioning. The operating system divides memory into a fixed number of partitions at startup, and each partition is permanently assigned a fixed size and a fixed position. When a process is loaded, it is assigned to one of the partitions that is large enough to hold it.
Fixed partitioning comes in two flavours.
- Equal-size partitions. All partitions are the same size. This is simple to implement, because any free partition is interchangeable with any other. It has two serious problems. First, a process larger than the partition size cannot be run at all, no matter how much memory is available elsewhere in the system. Second, a process smaller than the partition size wastes the difference: if the partition is 1 MB and the process needs 100 KB, 900 KB are wasted. This waste is called internal fragmentation.
- Unequal-size partitions. The partitions are of different sizes, chosen to match the expected distribution of process sizes. A small process can use a small partition, a large process a large one, and the waste is reduced. The problem is that the assignment of processes to partitions is now a bin-packing problem, which is NP-hard, and the operating system must use a heuristic. A process that fits none of the available partitions cannot be run, and a process that fits only one may wait while that partition is occupied by a smaller process that fits elsewhere.
Fixed partitioning was used in the earliest multiprogrammed systems, where the number and size of the partitions were chosen at system configuration time based on the expected workload. It is essentially extinct in modern general-purpose systems, because it cannot adapt to the actual mix of processes: if the workload changes, the partition sizes are wrong, and the system either wastes memory or cannot run the processes. Its historical importance is that it introduced the terminology — partitions, internal and external fragmentation — that later schemes inherited.
4.3 Variable partitioning
The alternative is variable partitioning. The operating system does not divide memory into partitions at startup. Instead, it allocates each process exactly as much memory as it needs, taking the space from the free list. A process of 200 KB is given a 200 KB region; a process of 1 MB is given a 1 MB region. There is no fixed upper limit on the size of a process, and there is no internal waste within a region.
Variable partitioning is the model used by modern contiguous allocation systems, and it is what the rest of this lesson assumes. Its advantages are flexibility — the partition sizes track the process sizes automatically — and the absence of internal fragmentation. Its disadvantage is that the free list fragments: as processes are allocated and freed, the free memory accumulates in small regions scattered between the allocated regions. This is external fragmentation, and it is the principal problem of variable partitioning.
4.4 The placement problem
When a process requests N bytes of memory, the operating system searches the free list for a region of at least N bytes. In general, several such regions exist, and the choice of which to use is the placement problem. The three classical answers are first-fit, best-fit, and worst-fit.
- First-fit
- Allocate the first region in the free list that is large enough. The search stops as soon as a suitable region is found. First-fit is the simplest algorithm, and it is also the fastest, because it does not examine the entire list. Its name is slightly misleading: the region chosen is the first one that fits, not the first one that is exactly the right size. If the first region happens to be exactly N bytes, it is used with no waste; if it is larger, it is split and the remainder goes back on the free list.
- Best-fit
- Allocate the smallest region in the free list that is large enough. The search must examine the entire list, because the best fit may be anywhere. The intuition is that best-fit leaves the smallest remainder, and therefore preserves the larger regions for future requests that need them. The intuition is partly right and partly wrong: best-fit does minimise the immediate waste, but the small remainders it produces are often too small to be useful, and they accumulate as unusable fragments. This is the fragmentation problem, and it is discussed in Lesson 8.5.
- Worst-fit
- Allocate the largest region in the free list. The search must examine the entire list, because the largest region may be anywhere. The intuition is the opposite of best-fit: by taking the largest region, the algorithm leaves the largest possible remainder, which is most likely to be useful for a future request. In practice, worst-fit performs poorly, because it consumes the large regions that future large requests will need, and it leaves remainders that are themselves large and quickly fragmented. The algorithm is instructive precisely because its intuition is plausible but its behaviour is not what the intuition suggests.
All three algorithms share a common structure. They search the free list for a region of sufficient size, allocate N bytes from it, and split the region if it is larger than needed. The difference is only in which region each one chooses when several qualify.
4.5 Splitting and coalescing
The free list is a dynamic data structure, and it must be updated on every allocation and every free. Two operations are essential.
- Splitting
- When a region of size S is chosen for a request of N bytes, and S > N, the region is split: N bytes are allocated to the process, and the remaining S − N bytes are returned to the free list as a new region. If S == N, the entire region is allocated and no remainder is produced. Some allocators also require a minimum region size — typically enough to hold the metadata for a free region — and if the remainder would be smaller than this minimum, the entire region is allocated to the process and the small remainder is wasted. This is one source of internal fragmentation.
- Coalescing
- When a process terminates and its region is returned to the free list, the operating system checks whether the region's immediate neighbours in physical memory are also free. If so, the regions are merged into a single larger region, and the list is updated accordingly. Coalescing is what prevents the free list from accumulating into an unusable collection of small fragments. Without coalescing, a system that repeatedly allocated and freed would end up with the free memory scattered in pieces too small for any future request — even though the total free memory would be large.
Coalescing is easy in a contiguous allocation scheme, because the free list can be kept sorted by start address, and a region's neighbours can be identified by their positions in the sorted list. In more sophisticated schemes — with non-contiguous regions, or with the allocator managing a heap with its own metadata — the coalescing operation is more involved.
4.6 Comparing the three algorithms
The three algorithms differ in their search time, in their memory utilisation, and in the size of the fragments they leave.
| Aspect | First-fit | Best-fit | Worst-fit |
|---|---|---|---|
| Search cost | Low — stops at the first fit | High — must scan the entire list | High — must scan the entire list |
| Immediate waste | Variable — depends on where the fit is found | Minimum — leaves the smallest remainder | Maximum — leaves the largest remainder |
| Fragment sizes | Mixed — small near the head of the list, larger further along | Very small — leaves tiny remainders that are often unusable | Very large — leaves large remainders that fragment quickly |
| Empirical behaviour | Generally the best of the three on real workloads | Comparable to first-fit; slightly worse on some workloads | The worst of the three on most workloads |
| Typical use | The default in most real allocators | Used when the size distribution is known and uniform | Rarely used; valuable as a pedagogical counterexample |
The empirical result is the most important and the least intuitive. First-fit is generally the best of the three algorithms on real workloads, despite being the simplest and despite not scanning the entire list. The reason is that first-fit tends to allocate from the head of the free list, so the free regions near the head are used and split, while the regions further along remain large and intact. The result is a distribution of region sizes that is skewed: many small regions at the front (useful for small requests) and a few large regions at the back (useful for large requests). This distribution happens to match the distribution of request sizes in most workloads, where many small requests and a few large requests are the norm.
Best-fit's behaviour is the opposite: by always choosing the smallest sufficient region, it produces small remainders, and the free list accumulates many tiny fragments that cannot serve any future request. The algorithm's name suggests that it produces the best fit, but the fit is best for the current request, not for the system as a whole. Worst-fit's behaviour is straightforwardly poor: by always consuming the largest region, it destroys the large regions that future large requests need, and the algorithm rapidly fragments the memory into pieces too small for anything.
The lesson from this comparison is that the relationship between an algorithm's local decision rule and its global behaviour is not obvious. Best-fit's rule minimises waste at each step, but produces a global outcome that is worse than first-fit's. Worst-fit's rule is the opposite of best-fit's, but produces a global outcome that is worse than both. First-fit's rule is the simplest, and it produces the best outcome. This is a recurring theme in operating-systems design: the intuition of a local rule is often a poor guide to its global behaviour, and empirical measurement is necessary to know which policy is actually best.
4.7 Compaction
A second technique for dealing with fragmentation is compaction. The operating system moves all the allocated regions to one end of memory, consolidating the free regions into a single large region. After compaction, the free memory is a single contiguous block, and any future request can be satisfied from it, regardless of its size.
Compaction is expensive. It requires the operating system to copy each process's entire address space to a new location, and to update the process's base register to reflect its new location. If the process is running, it must be suspended during the copy. For a system with many processes and large address spaces, the total cost of a compaction can be milliseconds — significant for an interactive system, and prohibitive if compaction happens frequently.
Compaction is only possible if addresses are bound at execution time. In a system with compile-time or load-time binding, the process's internal addresses are absolute, and moving the process would require adjusting every reference inside it, which is impractical at runtime. With execution-time binding, the process's internal addresses are logical, and the translation is performed by the MMU using the base register, so moving the process requires only updating the base register. This is a significant advantage of execution-time binding, and it is the reason compaction is feasible at all in a modern system.
In practice, compaction is rarely used. It is too expensive for an interactive system, and the memory savings are usually available through other means. Paging, the subject of Unit 9, eliminates external fragmentation entirely by allowing a process's address space to be split across non-contiguous physical pages. Once paging is available, compaction is unnecessary.
4.8 Protection with base and limit
The protection mechanism that accompanies contiguous allocation is the base-and-limit scheme from Lesson 8.2. Each process has its own base and limit, and the MMU checks every access against the limit before performing the translation. Because each process's region is contiguous and disjoint from every other process's region, this mechanism is sufficient: a process can only reach its own memory, and any attempt to reach outside its region causes a fault.
The base and limit registers are part of the process's state, saved and restored on every context switch. When the scheduler switches from one process to another, it writes the new process's base and limit into the MMU before resuming execution. If this write were omitted, the new process would run with the old process's translation, and its accesses would reach the old process's memory — a catastrophic protection failure. The update of the base and limit registers is therefore a mandatory and non-optional part of the context switch.
The protection mechanism has one significant limitation. It enforces that a process can reach only its own region, but it cannot enforce finer-grained protection within the region. In particular, it cannot distinguish the process's code from its data, so a process that accidentally writes to its own code section — corrupting its own instructions — is not caught by the protection mechanism. Modern systems address this by using segmentation (Lesson 8.6) or paging (Unit 9), which allow each region to have its own permissions.
4.9 Common misconceptions
"First-fit is called first-fit because it finds the exact fit."
First-fit finds the first region that fits, not the first region that fits exactly. If the first region is larger than needed, it is split, and the remainder goes back on the free list.
"Best-fit minimises fragmentation."
Best-fit minimises the immediate waste for each request, but the small remainders it produces accumulate into fragments too small to be useful. On real workloads, first-fit usually produces better overall utilisation than best-fit.
"Worst-fit leaves the largest remainder, so it is best for large requests."
Worst-fit leaves a large remainder for the current request, but it destroys the large regions that future large requests need. On the canonical example, worst-fit fails to allocate the last process, while first-fit and best-fit succeed.
"Coalescing is automatic in all allocators."
Coalescing must be implemented explicitly. An allocator that does not coalesce adjacent free regions will accumulate an unusable collection of small fragments, even though the total free memory may be large.
"Compaction is cheap because it only updates base registers."
Updating the base register is cheap, but the process must first be copied to its new location, which means copying the entire address space and suspending the process during the copy. For a large process, this takes milliseconds.
"Fragmentation is a minor issue in a modern system."
Fragmentation is the central problem of contiguous allocation, and it is what motivates the more sophisticated schemes of the rest of the unit and of Unit 9. Modern systems reduce it through paging and through careful allocator design, but it is not eliminated in all cases.
"The three placement algorithms all produce the same fragmentation."
The algorithms produce different fragmentation patterns even from the same starting state and the same sequence of requests. The canonical example shows first-fit and best-fit both succeeding on the same workload where worst-fit fails.
"Contiguous allocation is obsolete."
Contiguous allocation is the foundation of every
other allocation scheme, and the same placement
algorithms are used inside modern memory allocators
(malloc), inside the kernel's own
allocators (slab, buddy), and inside file systems.
Understanding contiguous allocation is
understanding the vocabulary of all the schemes that
follow.
05 Worked example
The following example traces the three placement algorithms on the canonical workload, showing where each process is placed and what the free list looks like after each allocation.
The canonical workload
Main memory has five holes, in the order they appear in the free list:
| Hole | Size |
|---|---|
| H1 | 600 KB |
| H2 | 500 KB |
| H3 | 200 KB |
| H4 | 300 KB |
| H5 | 600 KB |
Four processes request memory, in this order: P1 = 212 KB, P2 = 417 KB, P3 = 112 KB, P4 = 426 KB. We trace each algorithm's placement decisions.
First-fit
First-fit scans the free list from the head and allocates from the first hole large enough.
| Request | Size | Hole chosen | Reason | Remainder | Holes after allocation |
|---|---|---|---|---|---|
| P1 | 212 KB | H1 (600 KB) | The first hole ≥ 212 KB | 388 KB | 388, 500, 200, 300, 600 |
| P2 | 417 KB | H2 (500 KB) | The first hole ≥ 417 KB (H1's remainder is 388, too small) | 83 KB | 388, 83, 200, 300, 600 |
| P3 | 112 KB | Remainder of H1 (388 KB) | The first hole ≥ 112 KB | 276 KB | 276, 83, 200, 300, 600 |
| P4 | 426 KB | H5 (600 KB) | The first hole ≥ 426 KB (276, 83, 200, 300 are too small) | 174 KB | 276, 83, 200, 300, 174 |
First-fit allocates all four processes. The order in which it chooses the holes is the order they appear in the list, and after the first allocation, the remainder of H1 moves to the front of the list. This is what produces first-fit's characteristic skew: the front of the list is populated with small remainders, while the back retains the large holes.
Best-fit
Best-fit scans the entire free list and allocates from the smallest hole that is large enough.
| Request | Size | Hole chosen | Reason | Remainder | Holes after allocation |
|---|---|---|---|---|---|
| P1 | 212 KB | H4 (300 KB) | Smallest hole ≥ 212 KB | 88 KB | 600, 500, 200, 88, 600 |
| P2 | 417 KB | H2 (500 KB) | Smallest hole ≥ 417 KB (H1 and H5 are 600, larger) | 83 KB | 600, 83, 200, 88, 600 |
| P3 | 112 KB | H3 (200 KB) | Smallest hole ≥ 112 KB | 88 KB | 600, 83, 88, 88, 600 |
| P4 | 426 KB | H1 or H5 (600 KB) | Smallest hole ≥ 426 KB (H1 and H5 are equal) | 174 KB | 174, 83, 88, 88, 600 |
Best-fit also allocates all four processes. Its holes are smaller on average than first-fit's — the remainders of 88, 88, and 83 KB are all small — and the largest hole is preserved (600 KB after P4's allocation). This is the sense in which best-fit "preserves large holes": it consumes small holes where possible and leaves the large ones intact. But the small remainders it produces are a liability: 83 KB and 88 KB are too small to serve any but the smallest future requests, and they will accumulate as unusable fragments.
Worst-fit
Worst-fit scans the entire free list and allocates from the largest hole.
| Request | Size | Hole chosen | Reason | Remainder | Holes after allocation |
|---|---|---|---|---|---|
| P1 | 212 KB | H1 (600 KB) | The largest hole (tie between H1 and H5, first wins) | 388 KB | 388, 500, 200, 300, 600 |
| P2 | 417 KB | H5 (600 KB) | The largest hole (600 KB; H2 is 500) | 183 KB | 388, 500, 200, 300, 183 |
| P3 | 112 KB | H2 (500 KB) | The largest hole (500 KB; H1's remainder is 388) | 388 KB | 388, 388, 200, 300, 183 |
| P4 | 426 KB | — | No hole ≥ 426 KB (largest remaining is 388 KB) | — | 388, 388, 200, 300, 183 |
Worst-fit fails to allocate P4. After the first three allocations, the largest remaining hole is 388 KB, which is smaller than P4's request of 426 KB. The memory is not "full" — the total free space is 388 + 388 + 200 + 300 + 183 = 1,459 KB, which is enough for P4 — but the free space is fragmented into pieces too small to satisfy the request. This is external fragmentation in its purest form.
The failure is the direct consequence of worst-fit's policy. By always consuming the largest hole, the algorithm destroyed the large holes — H1 and H5 were both 600 KB, and both were consumed by P1 and P2 — and left only their remainders. If worst-fit had instead taken the 500 KB hole for P2, the second 600 KB hole would have remained available, and P4 could have been allocated. The algorithm's "leave the largest remainder" intuition does not account for the fact that consuming a large hole removes it from the list entirely, and the remainder is always smaller than the hole it came from.
Comparing the outcomes
| Algorithm | Processes allocated | Largest final hole | Smallest final hole | Outcome |
|---|---|---|---|---|
| First-fit | 4 of 4 | 300 KB | 83 KB | All requests satisfied |
| Best-fit | 4 of 4 | 600 KB | 83 KB | All requests satisfied; large hole preserved |
| Worst-fit | 3 of 4 | 388 KB | 183 KB | P4 cannot be allocated |
The comparison makes the trade-offs concrete. First-fit produces the most balanced distribution: its holes are neither very large nor very small. Best-fit preserves the large hole, which is useful if future large requests arrive. Worst-fit produces only small holes and cannot satisfy large requests at all.
On this workload, first-fit and best-fit both succeed; worst-fit fails. This is the canonical illustration of worst-fit's weakness, and it is why the algorithm is rarely used in practice. It is valuable as a pedagogical counterexample: an algorithm whose intuition is plausible but whose behaviour is poor.
What coalescing changes
The traces above assume that no processes free their memory during the sequence. If a process frees its memory, its region returns to the free list, and if its neighbours are free, the regions are coalesced. Consider the first-fit outcome: after all four allocations, the holes are 276, 83, 200, 300, and 174 KB, arranged in physical order (the order of the free list may differ from the physical order). If the process in the region between two of these holes terminates, its region becomes free, and if the hole on one side is free, the two are merged.
Coalescing is what makes the allocation scheme work over time. Without it, the free list would accumulate more and more small holes, and the system would eventually be unable to allocate any process of substantial size, even if the total free memory was large. With coalescing, the free list is kept in a state that reflects the actual free memory: adjacent free regions are merged, and the holes are as large as the pattern of allocation allows.
An interactive simulator
To explore the three algorithms further, write a small simulator. The simulator maintains a free list and processes a sequence of allocation and free operations. For each operation, it prints the state of the free list and the placement decision.
# Python sketch
def first_fit(holes, request):
for i, (start, size) in enumerate(holes):
if size >= request:
return i
return None
def best_fit(holes, request):
best = None
for i, (start, size) in enumerate(holes):
if size >= request:
if best is None or size < holes[best][1]:
best = i
return best
def worst_fit(holes, request):
worst = None
for i, (start, size) in enumerate(holes):
if size >= request:
if worst is None or size > holes[worst][1]:
worst = i
return worst
def allocate(holes, request, policy):
i = policy(holes, request)
if i is None:
return False, holes
start, size = holes[i]
if size == request:
holes.pop(i)
else:
holes[i] = (start + request, size - request)
return True, holes
# Test with the canonical workload
for name, policy in [("first-fit", first_fit),
("best-fit", best_fit),
("worst-fit", worst_fit)]:
holes = [(0, 600), (600, 500), (1100, 200),
(1300, 300), (1600, 600)]
print(f"--- {name} ---")
for pid, req in enumerate([212, 417, 112, 426]):
ok, holes = allocate(holes, req, policy)
print(f"P{pid+1} = {req} KB: {'allocated' if ok else 'FAILED'}")
print(f" holes: {holes}")
The simulator's output should match the traces above. Use it to explore other workloads: different sequences of requests, different initial hole configurations, and the effect of coalescing when processes are freed.
06 Interactive activity
Activity · implement and stress-test a contiguous allocator
The goal of this activity is to implement the three placement algorithms, test them on a variety of workloads, and observe how their behaviour differs. Work through the steps in your course VM.
-
Implement the allocator.
Write a program that maintains a free list and
implements the three placement algorithms. The program
should accept a sequence of allocation and free
operations and print the state of the free list after
each.
Test it with the canonical workload from the worked example and verify that the results match.struct hole { unsigned long start; unsigned long size; struct hole *next; }; struct hole *first_fit(struct hole *list, unsigned long n); struct hole *best_fit (struct hole *list, unsigned long n); struct hole *worst_fit(struct hole *list, unsigned long n); void *allocate(struct hole **list, unsigned long n, struct hole *(*policy)(struct hole *, unsigned long)); void release(struct hole **list, unsigned long start, unsigned long size); -
Implement coalescing.
Extend the allocator so that when a region is freed, it
is merged with its adjacent free neighbours. The free
list can be kept sorted by start address to make the
coalescing operation a simple check of each neighbour.
Test the coalescing by allocating a sequence of processes and then freeing them in various orders. Verify that the free list correctly consolidates adjacent regions.void release(struct hole **list, unsigned long start, unsigned long size) { /* insert the new hole in sorted order */ /* then coalesce with the previous and next holes if adjacent */ } - Compare the algorithms on the canonical workload. Run all three algorithms on the canonical workload and tabulate the results: how many processes each one allocates, what the largest and smallest final holes are, and how the free memory is distributed. Your results should match the worked example.
-
Stress test with a random workload.
Generate a workload of 1,000 random allocation
requests, with sizes drawn from a realistic
distribution (for example, a log-normal distribution,
which is closer to the distribution of real process
sizes than a uniform distribution). Measure, for each
algorithm:
- The number of requests that fail to allocate.
- The total number of holes at the end.
- The average hole size.
- The largest hole size.
import random random.seed(42) for i in range(1000): size = int(random.lognormvariate(6, 1)) # allocate or free based on some policy - Observe fragmentation directly. Modify the simulator to print the free list after each operation and to compute the fragmentation ratio: the ratio of the largest hole to the total free memory. A ratio of 1.0 means all free memory is in one hole; a ratio near 0 means the free memory is scattered. Plot the ratio over time for each of the three algorithms. Which algorithm keeps the fragmentation ratio highest?
- Investigate the effect of coalescing. Modify the simulator to disable coalescing and re-run the stress test. Compare the results with and without coalescing. How much worse is the fragmentation without it? What is the smallest workload on which the difference is visible?
-
Examine the real allocator.
The C library's
mallocis a real contiguous allocator. Write a program that allocates and frees blocks of various sizes and usesmalloc_usable_sizeto see how much memory each allocation actually consumes.
Compile and run. Notice that the usable size is larger than the requested size, and that the difference is not constant — this is the allocator's internal fragmentation and metadata overhead. Then observe how the allocator behaves when allocating many small blocks and then freeing them.#include <malloc.h> #include <stdio.h> #include <stdlib.h> int main(void) { for (size_t n = 1; n <= 128; n *= 2) { void *p = malloc(n); printf("request %3zu -> usable %3zu\n", n, malloc_usable_size(p)); free(p); } return 0; } - Read the OSTEP chapter. Read OSTEP Chapter 17, Free Space Management. Compare its presentation of the placement algorithms to the presentation in this lesson. What additional considerations does OSTEP raise — for example, segregated free lists, binary buddy allocation, and the role of header metadata? Write a short paragraph on the additional complexities that arise when the allocator is a general-purpose memory manager rather than an OS-level process allocator.
Reflect: the three placement algorithms are simple to describe and to implement, and their behaviour on a small workload is easy to trace by hand. But their behaviour on a realistic workload — thousands of allocations, with sizes drawn from a heavy-tailed distribution, interspersed with frees and coalescing — is far harder to predict. This is a recurring theme in systems design: the local rules are simple, but the global behaviour emerges from the interaction of the rules with the workload, and empirical measurement is the only way to know which rule is best. What would a workload need to look like for best-fit to outperform first-fit? What would it take for worst-fit to be the right choice?
07 Knowledge check
Check your understanding
Question 1
Describe the contiguous allocation model. What is a free list, and what information does it record? What is coalescing, and why is it necessary?
Show answer
Question 2
Describe the three placement algorithms — first-fit, best-fit, and worst-fit. For each, state the search strategy, the search cost, and the characteristic size of the remainder it leaves.
Show answer
Question 3
On the canonical workload — holes of 600, 500, 200, 300, and 600 KB, and requests of 212, 417, 112, and 426 KB — trace first-fit and worst-fit by hand. Which allocates all four requests? Explain why the other fails.
Show answer
Question 4
What is compaction? What does it require of the binding scheme, and why is it rarely used in modern systems?
Show answer
Question 5
The empirical result on real workloads is that first-fit performs better than best-fit, despite best-fit's local optimality. Explain why the local rule and the global behaviour diverge, and what this tells us about the design of allocation policies.
Show answer
08 Unit discussion
Discussion prompt
The three placement algorithms are the classical solutions to the free-space management problem, and they have been studied for over half a century. And yet the empirical result — that first-fit, the simplest of the three, is generally the best on real workloads — is not what the theory alone would suggest. Best-fit's rule is locally optimal; worst-fit's rule is a plausible response to best-fit's weakness; first-fit's rule is the simplest of the three. But the simplest rule produces the best outcome.
This pattern appears throughout operating-systems design. The shortest-job-first scheduler is optimal for average waiting time but starves long jobs; the Completely Fair Scheduler produces good interactive behaviour without any single optimality criterion; the simple round-robin scheduler is used in every general-purpose system despite its poor average-case performance. In each case, the property that matters is not the optimality of the algorithm's local decision rule, but the distribution of states that the rule produces over time. This distribution is what determines the algorithm's practical behaviour, and it is not visible from the algorithm's statement.
What does this tell us about how we should design operating-system policies? Should we prefer simple rules, on the grounds that their behaviour is more predictable and their overhead is lower? Or should we use more sophisticated rules, on the grounds that real-world workloads are complex and the sophistication may pay off? What role should empirical measurement play in the design process, and how should we reconcile the empirical results with the theoretical analysis?
Post your response to the Unit 8 discussion forum and reply to at least two classmates. Supports CLO1, CLO4, and CLO6.
09 Practice exercise
This ungraded exercise consolidates the lesson and prepares you for Lesson 8.5. Aim to spend 60–90 minutes on it.
- Trace the three algorithms by hand. Memory has the following free holes, in the order they appear in the free list: 100, 500, 200, 300, 600 KB. The processes request memory in this order: P1 = 212 KB, P2 = 417 KB, P3 = 112 KB, P4 = 426 KB. Trace each of the three algorithms, showing the hole chosen for each process and the state of the free list after each allocation. Which algorithms allocate all four processes? Which fail? For each failure, explain why.
- Design a workload that favours best-fit. Construct a sequence of allocation requests and an initial configuration of holes such that best-fit succeeds on more requests than first-fit. Then design a workload that favours worst-fit. For each, explain what property of the workload makes the algorithm succeed.
- Simulate coalescing. Write a small program that maintains a free list, allocates from it, and coalesces adjacent free regions when a region is freed. Test it with a sequence of allocate and free operations, and verify that the free list correctly consolidates adjacent regions. Then modify the program to disable coalescing and compare the behaviour.
-
Compute the fragmentation ratio.
For the canonical workload from the worked example,
compute the fragmentation ratio after each
allocation for each of the three algorithms. The
fragmentation ratio is the size of the largest hole
divided by the total free memory. Tabulate the
results and comment on the trend.
A ratio of 1.0 means the free memory is entirely in one hole; a ratio near 0 means it is scattered.Fragmentation ratio = largest_hole / total_free_memory - Analyse the effect of different allocations. Take the canonical workload and change the order of the requests — for example, process P4 (426 KB) first, then P2, then P1, then P3. Re-run the three algorithms on the new order. Do the outcomes change? Does worst-fit still fail? What does this tell you about the sensitivity of the algorithms to the order of requests?
- Design a workload that defeats first-fit. First-fit is generally the best of the three algorithms, but it is not universally so. Construct a sequence of requests on which first-fit fails to allocate a request that best-fit succeeds on. Explain what property of the workload makes first-fit fail.
- Compare the two-step allocation. Some allocators use a two-step policy: first-fit for small requests and best-fit for large ones. The intuition is that first-fit's characteristic distribution works well for the many small requests, while best-fit's preservation of large holes works well for the few large requests. Design a small experiment to test whether this policy outperforms either of the single policies on a realistic workload.
- Reflect. In your own words, explain why the local optimality of best-fit does not translate into global optimality. Then describe one situation in your own experience — a program you have written or a system you have used — where the interaction between local decisions and global state produced an outcome that was not what the local decisions would suggest. What was the local rule, what was the global outcome, and why did they diverge?
10 Summary
- Contiguous allocation gives each process a single region of physical memory. The region's base and limit are set in the MMU, and the translation is performed by the base-and-limit scheme from Lesson 8.2.
- The operating system maintains a free list that records each free region's start address and size. When a process requests memory, the free list is searched for a region large enough.
- Fixed partitioning divides memory into a fixed set of partitions at startup. It is simple, but it cannot adapt to the actual sizes of the processes and it wastes memory through internal fragmentation. It is essentially extinct in modern general-purpose systems.
- Variable partitioning allocates each process exactly as much memory as it needs, taking the space from the free list. It is the model used by modern contiguous allocation systems. Its disadvantage is external fragmentation: the free memory accumulates in small regions scattered between the allocated regions.
- The three classical placement algorithms are first-fit (allocate from the first region large enough), best-fit (allocate from the smallest region large enough), and worst-fit (allocate from the largest region).
- First-fit is the simplest and the fastest. Best-fit minimises the immediate waste but produces small remainders that accumulate as fragments. Worst-fit leaves the largest remainders but destroys the large regions that future large requests need.
- The empirical result, on real workloads, is that first-fit is generally the best of the three. Its characteristic distribution — many small holes at the front of the list, a few large holes at the back — matches the distribution of request sizes in most workloads.
- Splitting is the operation of dividing a hole when a request is smaller than the hole. Coalescing is the operation of merging a freed region with its adjacent free neighbours. Coalescing is essential to prevent the free list from accumulating into an unusable collection of small fragments.
- Compaction moves all allocated regions to one end of memory, consolidating the free memory into a single large region. It requires execution-time binding, and it is rarely used because it is expensive and paging provides a better solution.
- Contiguous allocation is the foundation of every other allocation scheme. The vocabulary it introduces — holes, free lists, splitting, coalescing, external and internal fragmentation, the placement problem — is used throughout the rest of the unit and in every discussion of memory allocation.
Looking ahead
This lesson presented contiguous allocation and the three placement algorithms. Lesson 8.5 presents fragmentation in detail: the two forms of fragmentation — external and internal — and the techniques for measuring and reducing them. The lesson extends the discussion of this lesson's placement algorithms by examining their long-run behaviour, and it introduces the 50-percent rule, the statistical observation that a first-fit allocator loses about half its memory to fragmentation. The lesson also presents the buddy system and the slab allocator, two techniques that reduce fragmentation by allocating in fixed-size blocks. These techniques are used in the Linux kernel and in modern memory allocators, and they represent the practical response to the fragmentation problem.
11 References
- Silberschatz, A., Galvin, P. B., & Gagne, G. Operating System Concepts, 10th ed., Chapter 9, Section 9.2.
-
Arpaci-Dusseau, R. H., & Arpaci-Dusseau, A. C.
Operating Systems: Three Easy Pieces, Chapter 17,
Free Space Management. Available online at
ostep.org. - Tanenbaum, A. S., & Bos, H. Modern Operating Systems, 4th ed., Chapter 3, Section 3.2.
- Knuth, D. E. The Art of Computer Programming, Volume 1: Fundamental Algorithms, 3rd ed., Section 2.5, Dynamic Storage Allocation. Addison-Wesley, 1997.
- Wilson, P. R., Johnstone, M. S., Neely, M., & Boles, D. Dynamic Storage Allocation: A Survey and Critical Review. Proceedings of the International Workshop on Memory Management, 1995.
- Peterson, J. L., & Norman, T. A. Buddy Systems. Communications of the ACM, 1977. The original paper on the buddy system, which is presented in Lesson 8.5.
- Bonwick, J. The Slab Allocator: An Object-Caching Kernel Memory Allocator. USENIX Summer Conference, 1994. The original paper on the slab allocator.
- Love, R. Linux Kernel Development, 3rd ed., Chapter 12, Memory Management. The kernel's own allocators: the buddy system and the slab allocator.
-
Linux man-pages:
man 3 malloc,man 3 free,man 3 malloc_usable_size,man 2 brk,man 2 mmap,man 5 proc.