COMP 3150Operating Systems

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>/maps and /proc/<pid>/statm interfaces).

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.

Contiguous allocation with a free list A diagram showing a block of main memory divided into contiguous regions. Several regions are allocated to processes (drawn as solid coloured blocks), and several are free (drawn as dashed-outline blocks). The free regions form a linked list, the free list, shown on the right side of the diagram with pointers from each free region to the next. The allocated regions belong to processes P1 through P5. Addresses are shown on the left, and the diagram illustrates how the free memory is scattered between the allocated regions. CONTIGUOUS ALLOCATION WITH A FREE LIST Physical memory 0 256 512 640 896 1152 1536 P1 — allocated start 0, size 256 KB Hole H1 start 256, size 192 KB P2 — allocated start 448, size 192 KB Hole H2 — size 96 KB P3 — allocated start 640, size 256 KB Hole H3 start 896, size 288 KB P4 — allocated Free list H1 → H2 → H3 → … linked list of holes H1: start 256, size 192 KB H2: start 544, size 96 KB H3: start 896, size 288 KB each free region is a node in the list The free list records every free region with its start address and size. When a process terminates, its region is returned to the free list and merged with any adjacent free regions (coalescing).
Figure 8.4 — Contiguous allocation with a free list. Main memory is divided into contiguous regions; some are allocated to processes, and some are free. The free regions form a linked list — the free list — that records each free region's start address and size. When a new process needs memory, the operating system searches the free list for a region large enough, using one of the placement algorithms described in this lesson.

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.

Comparison of first-fit, best-fit, and worst-fit
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.

First-fit, best-fit, and worst-fit on the same workload A diagram comparing the three placement algorithms on the same initial state and the same sequence of allocation requests. The initial state has five holes: 600K, 500K, 200K, 300K, 600K. The requests are P1=212K, P2=417K, P3=112K, P4=426K. Each algorithm is shown as a column with the initial holes at the top and the placement of each process in turn below. First-fit and best-fit successfully allocate all four processes; worst-fit fails to allocate P4 because the largest remaining hole is too small. FIRST-FIT, BEST-FIT, AND WORST-FIT ON THE SAME WORKLOAD Initial holes: 600K · 500K · 200K · 300K · 600K Requests, in order: P1 = 212K, P2 = 417K, P3 = 112K, P4 = 426K First-fit Best-fit Worst-fit Initial holes 600K 500K 200K 300K 600K P1 = 212K first 600K → P1 leaves 388K P2 = 417K 500K → P2 leaves 83K P3 = 112K 388K → P3 P4 = 426K → fits Initial holes 600K 500K 200K 300K 600K P1 = 212K 300K → P1 leaves 88K P2 = 417K 500K → P2 leaves 83K P3 = 112K 200K → P3 P4 = 426K → fits Initial holes 600K 500K 200K 300K 600K P1 = 212K first 600K → P1 leaves 388K P2 = 417K second 600K → P2 leaves 183K P3 = 112K 500K → P3 P4 = 426K → FAILS Result of the comparison First-fit places each process in the first hole large enough. It succeeds on all four requests. Best-fit places each process in the smallest hole large enough. It also succeeds on all four requests. Worst-fit places each process in the largest hole. After P1, P2, and P3, the largest remaining hole is 388K — too small for P4 (426K).
Figure 8.4b — First-fit, best-fit, and worst-fit on the same workload. Starting from the same five holes and processing the same four requests in the same order, the three algorithms produce different results. First-fit and best-fit allocate all four processes successfully; worst-fit fails on P4 because its policy of always consuming the largest hole destroys the large regions that future large requests need.

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
H1600 KB
H2500 KB
H3200 KB
H4300 KB
H5600 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.

  1. 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.
    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);
    Test it with the canonical workload from the worked example and verify that the results match.
  2. 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.
    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 */
    }
    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.
  3. 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.
  4. 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.
    Run the stress test with five different random seeds and average the results. Does the empirical result match the theory — that first-fit is generally the best, and worst-fit the worst?
    import random
    
    random.seed(42)
    for i in range(1000):
        size = int(random.lognormvariate(6, 1))
        # allocate or free based on some policy
  5. 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?
  6. 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?
  7. Examine the real allocator. The C library's malloc is a real contiguous allocator. Write a program that allocates and frees blocks of various sizes and uses malloc_usable_size to see how much memory each allocation actually consumes.
    #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;
    }
    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.
  8. 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
In the contiguous allocation model, each process occupies a single region of physical memory, contiguous and disjoint from every other process's region. The operating system uses the base-and-limit mechanism from Lesson 8.2 to translate the process's logical addresses to its physical region and to enforce protection. The free list is the data structure that records the free regions of memory. For each free region, it stores the region's start address and its size. The free list can be a linked list, a tree, or a bitmap; the choice depends on the allocator and on the operations it needs to support. When a new process needs memory, the operating system searches the free list for a region large enough to hold it, using one of the placement algorithms. When a process terminates, its region is returned to the free list. Coalescing is the operation of merging a newly-freed region with its adjacent free neighbours. If the region immediately before or immediately after the freed region is also free, the two are merged into a single larger region, and the free list is updated accordingly. Coalescing is necessary because without it, the free list would accumulate many small regions that are individually too small for any future request but together would be sufficient. Over time, the free memory would become so fragmented that no process of substantial size could be allocated, even though the total free memory would be large. Coalescing prevents this by keeping the free regions as large as the pattern of allocation allows. It is easy in a contiguous scheme, because the free list can be kept sorted by start address, and each region's neighbours can be identified by their positions in the list.

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
First-fit allocates from the first region in the free list that is large enough. The search stops as soon as a suitable region is found, so the search cost is low — it depends on where the fit is, but it is generally a small fraction of the list. The remainder it leaves is variable: if the first region happens to be exactly the right size, no remainder is produced; if the region is larger, the remainder's size depends on how much larger it was. In practice, first-fit produces mixed remainder sizes, with a tendency toward small remainders near the head of the list (where the algorithm has been allocating) and larger holes further along. Best-fit allocates from 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, so the search cost is high. The remainder it leaves is the smallest possible for the given request — best-fit minimises the immediate waste. But the small remainders it produces are often too small to be useful, and they accumulate as fragments. Worst-fit allocates from the largest region in the free list. The search must also examine the entire list, because the largest region may be anywhere, so the search cost is high. The remainder it leaves is the largest possible for the given request. The intuition is that by taking the largest hole, worst-fit leaves the largest remainder for future requests. In practice, worst-fit performs poorly: by consuming the largest holes, it destroys the large regions that future large requests need. On the canonical workload, worst-fit fails to allocate the last process, while first-fit and best-fit both succeed.

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
First-fit traces as follows. P1 (212 KB) takes the first hole ≥ 212 KB, which is the 600 KB hole, leaving 388 KB. P2 (417 KB) takes the next hole ≥ 417 KB; the 388 KB remainder is too small, but the 500 KB hole is not, so P2 takes the 500 KB hole, leaving 83 KB. P3 (112 KB) takes the first hole ≥ 112 KB; the 388 KB remainder from the first hole qualifies, leaving 276 KB. P4 (426 KB) takes the first hole ≥ 426 KB; the 276, 83, 200, and 300 KB holes are all too small, so P4 takes the last 600 KB hole, leaving 174 KB. First-fit allocates all four processes. Worst-fit traces as follows. P1 (212 KB) takes the largest hole. The largest hole is 600 KB (two are tied); the first one is chosen, leaving 388 KB. P2 (417 KB) takes the largest remaining hole. The second 600 KB hole is the largest, so P2 takes it, leaving 183 KB. P3 (112 KB) takes the largest remaining hole. The 500 KB hole is now the largest, so P3 takes it, leaving 388 KB. P4 (426 KB) cannot be allocated: the largest remaining hole is 388 KB, which is smaller than the 426 KB request. Worst-fit fails. The reason is that worst-fit's policy of always consuming the largest hole destroyed both of the 600 KB holes — P1 and P2 consumed them — and left only their remainders, which are too small to satisfy P4. The total free memory is 388 + 388 + 200 + 300 + 183 = 1,459 KB, which is more than enough for P4, but the free memory is fragmented into pieces too small to be useful. This is external fragmentation, and it is the direct consequence of the placement policy.

Question 4

What is compaction? What does it require of the binding scheme, and why is it rarely used in modern systems?

Show answer
Compaction is the technique of moving all the allocated regions of memory to one end, so that all the free memory is consolidated into a single large contiguous block. After compaction, the free memory is a single region, and any future request can be satisfied from it, regardless of its size. Compaction requires the operating system to copy each process's entire address space to its 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. Compaction requires execution-time binding: the process's internal addresses must be logical, so that moving the process requires only updating the base register. 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. This is one of the advantages of execution-time binding: it makes compaction possible. Compaction is rarely used in modern systems because it is expensive. Copying a large process's address space takes milliseconds, during which the process cannot run, and if compaction is performed frequently, the total cost is significant. The memory savings it provides are usually available through other means. In particular, 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, there is no need for compaction, because the free memory does not need to be contiguous to be usable. Modern systems use paging for the memory they manage dynamically, and compaction survives only in specialised contexts — for example, in the heap of a managed language runtime, where it can be performed as part of garbage collection.

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
The divergence between best-fit's local optimality and its global behaviour arises from the interaction of the placement rule with the pattern of future requests. Best-fit's rule — choose the smallest sufficient hole — is locally optimal in the sense that it minimises the waste on the current request. It leaves the smallest possible remainder, which seems good. But the remainders it produces are systematically small: because best-fit always chooses the smallest sufficient hole, the remainder is always the difference between the smallest sufficient hole and the request, and this difference is typically small. The small remainders accumulate as fragments that are too small for any future request of substantial size. Over time, the free memory becomes populated with tiny holes, and the total number of holes grows, even as the total free memory shrinks. First-fit's rule is locally suboptimal in the same sense: it takes the first hole that fits, which may be much larger than needed, leaving a large remainder. But the remainders it produces are located near the head of the list, where the algorithm keeps allocating. The result is that the front of the free list accumulates small remainders, while the back retains large intact holes. This distribution matches the distribution of request sizes in most workloads: many small requests, and a few large ones. The many small requests are served by the small remainders at the front, and the few large requests are served by the large holes at the back. The distribution that first-fit produces is therefore better suited to the workload than the distribution that best-fit produces. This is a general lesson in the design of allocation policies: the relationship between a local decision rule and its global behaviour is not obvious. A rule that is optimal at each step may produce a globally suboptimal outcome, because the steps interact with each other through the shared state — the free list — and the distribution of future requests. The right policy is the one that produces a good distribution of free regions over time, and this can only be determined empirically, by measuring the policy's behaviour on realistic workloads.

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
    Fragmentation ratio = largest_hole / total_free_memory
    A ratio of 1.0 means the free memory is entirely in one hole; a ratio near 0 means it is scattered.
  5. 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?
  6. 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.
  7. 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.
  8. 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.
👤