How mmap Replaces sbrk in C Heap Allocation
Modern memory allocators in C, such as GNU libc's
malloc, manage dynamic memory using a hybrid strategy that
relies heavily on the mmap system call instead of the
traditional sbrk. While sbrk expands a single
contiguous heap by moving the process data segment pointer, it suffers
from severe limitations, including irreversible heap fragmentation and
poor multithreaded performance. For large memory allocations,
mmap solves these problems by requesting independent,
non-contiguous virtual memory mappings directly from the kernel,
allowing memory to be allocated and deallocated in arbitrary order
without trapping unreleased pages.
The Limitations of sbrk
Historically, UNIX memory management relied on brk() and
sbrk(). The brk system call sets the end of
the process data segment (the "program break"), while sbrk
increments or decrements this pointer. Calling malloc()
internally invoked sbrk() to push the program break
forward, creating a single, contiguous linear heap.
This approach creates a fundamental architectural bottleneck known as
heap pinning. Because sbrk only moves forward or backward
linearly, memory at the top of the heap cannot be returned to the
operating system if lower addresses remain in use. For example, if a
program allocates a 100 MB buffer, allocates a 16-byte integer directly
afterward, and then frees the 100 MB buffer, the 100 MB cannot be
released via sbrk. The single active 16-byte object traps
the program break, causing external memory fragmentation and
artificially inflating the process's Resident Set Size (RSS).
How mmap Handles Dynamic Pages
The mmap system call allocates memory by creating
independent virtual memory regions. Rather than adjusting a global break
pointer, an allocator calls mmap with flags such as
MAP_PRIVATE | MAP_ANONYMOUS:
void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);This requests the kernel to allocate page-aligned virtual memory
anywhere in the process's address space. Because these pages are backed
by anonymous memory (RAM and swap) rather than a file, they function
identically to dynamic heap memory. When the program calls
free() on this block, the allocator immediately invokes
munmap():
munmap(ptr, size);This returns the specific address range directly to the kernel, freeing physical pages immediately regardless of other active allocations in the process.
The Allocation Threshold
Modern allocators do not replace sbrk entirely with
mmap; instead, they use a hybrid approach governed by a
dynamic allocation threshold (traditionally MMAP_THRESHOLD
in glibc, often starting around 128 KB).
Small allocations incur high overhead if mapped directly with
mmap because system calls require context switches to
kernel space, and mappings are rounded up to the system page size
(typically 4 KB). Consequently, small objects are handled inside
contiguous memory pools or arenas. When a request exceeds the threshold,
the allocator automatically redirects the request to mmap.
Glibc can dynamically adjust this threshold based on allocation patterns
to balance system call overhead against fragmentation risks.
Core Advantages of mmap Over sbrk
- Independent Lifecycle Management: Mapped memory
regions are decoupled from each other. Releasing a large allocation
instantly unmaps its virtual pages and frees physical memory,
eliminating the pinning issue inherent to
sbrk. - Concurrency and Multithreading: The program break
is a single global process attribute. Expanding it requires
synchronization, which creates thread contention. Modern allocators use
mmapto create independent memory arenas per thread, enabling concurrent allocations without lock contention on a shared heap boundary. - Address Space Layout Randomization (ASLR): While
the classic heap grows predictably upward from the data segment,
mmapallocations can be placed at randomized locations throughout the virtual address space, providing an added layer of defense against heap-based exploitation techniques. - Demand Paging and Zero-Filling: Anonymous
mmapmappings are lazily backed by the kernel. The operating system only maps physical frames when individual pages are touched, and these pages are guaranteed to be zero-initialized by the kernel, optimizing initialization costs for large data structures.