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

  1. 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.
  2. Concurrency and Multithreading: The program break is a single global process attribute. Expanding it requires synchronization, which creates thread contention. Modern allocators use mmap to create independent memory arenas per thread, enabling concurrent allocations without lock contention on a shared heap boundary.
  3. Address Space Layout Randomization (ASLR): While the classic heap grows predictably upward from the data segment, mmap allocations can be placed at randomized locations throughout the virtual address space, providing an added layer of defense against heap-based exploitation techniques.
  4. Demand Paging and Zero-Filling: Anonymous mmap mappings 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.