mremap(), MAP_FIXED & Overcommitting Resizing mappings, fixed-address placements, and Linux memory overcommit

 

mremap(), MAP_FIXED & Overcommitting
Resizing mappings, fixed-address placements, and Linux memory overcommit

mremap() – Resizing an Existing Mapping

Once a mapping is created with mmap(), its size is fixed. If you need more (or less) space, Linux provides mremap() to grow or shrink it without creating an entirely new mapping. Think of it as realloc() for memory mappings.

mremap() Signature
#define _GNU_SOURCE
#include <sys/mman.h>

void *mremap(void *old_address,   /* Start of existing mapping       */
             size_t old_size,     /* Current size                    */
             size_t new_size,     /* Desired new size                */
             int flags,           /* MREMAP_MAYMOVE and/or MREMAP_FIXED */
             ...);                /* new_address (only if MREMAP_FIXED) */

/* Returns new address on success, MAP_FAILED on error */
Flag Meaning
MREMAP_MAYMOVE Allow the kernel to move the mapping to a new address if there is not enough contiguous space to grow it in place. Without this flag, mremap() fails if it cannot grow in place.
MREMAP_FIXED Move the mapping to the address specified as the 5th argument. Any existing mapping at that address is replaced. Must be combined with MREMAP_MAYMOVE.

How mremap() Works Visually
Before mremap() — 3-page mapping:
Page 0 Page 1 Page 2 [free space? maybe]
After mremap() to 5 pages with MREMAP_MAYMOVE — may relocate:
Page 0 | Page 1 | Page 2 | Page 3 (new) | Page 4 (new) ← at new address

Example 1: Growing a Mapping with mremap()
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>
#include <unistd.h>

int main(void) {
    long ps = sysconf(_SC_PAGESIZE);

    /* Start with a 1-page anonymous mapping */
    char *addr = mmap(NULL, ps,
                      PROT_READ | PROT_WRITE,
                      MAP_PRIVATE | MAP_ANONYMOUS,
                      -1, 0);
    if (addr == MAP_FAILED) { perror("mmap"); exit(1); }

    strcpy(addr, "Original data in page 0");
    printf("Before mremap: %s\n", addr);

    /* Grow to 4 pages — allow moving if needed */
    char *new_addr = mremap(addr, ps, 4 * ps, MREMAP_MAYMOVE);
    if (new_addr == MAP_FAILED) { perror("mremap"); exit(1); }

    /* Data at original offset is preserved after move */
    printf("After mremap: %s\n", new_addr);

    /* New pages are accessible and zero-filled */
    new_addr[ps] = 'B';
    printf("New page 1 first byte: %c\n", new_addr[ps]);

    munmap(new_addr, 4 * ps);
    return 0;
}

Example 2: Shrinking a Mapping with mremap()
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>

int main(void) {
    long ps = sysconf(_SC_PAGESIZE);

    /* Allocate 8 pages */
    char *addr = mmap(NULL, 8 * ps,
                      PROT_READ | PROT_WRITE,
                      MAP_PRIVATE | MAP_ANONYMOUS,
                      -1, 0);
    if (addr == MAP_FAILED) { perror("mmap"); exit(1); }
    printf("Allocated 8 pages at %p\n", (void*)addr);

    /* Shrink to 2 pages — no flag needed for shrinking */
    char *new_addr = mremap(addr, 8 * ps, 2 * ps, 0);
    if (new_addr == MAP_FAILED) { perror("mremap"); exit(1); }
    printf("Shrunk to 2 pages at %p\n", (void*)new_addr);

    /* Pages 2–7 are now unmapped — accessing them = SIGSEGV */
    munmap(new_addr, 2 * ps);
    return 0;
}

MAP_FIXED – Placing a Mapping at a Specific Address

Normally, you pass NULL as addr to mmap() and let the kernel choose where to place the mapping. With MAP_FIXED, the kernel maps exactly at the address you specify, silently removing any existing mapping in the way.

Warning: MAP_FIXED is dangerous. If the specified address overlaps a critical region (stack, code, libc), it silently destroys it. The safe pattern is: first reserve a large contiguous region with a normal mmap(NULL, ...), then use MAP_FIXED within that reserved region to place specific sub-mappings. This guarantees you are only overwriting your own placeholder.

Example 3: Safe Use of MAP_FIXED – Reserve Then Fill
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

int main(void) {
    long ps = sysconf(_SC_PAGESIZE);
    int fd;
    char *region, *slot0, *slot1;

    /* Step 1: Reserve a contiguous 4-page address range */
    region = mmap(NULL, 4 * ps,
                  PROT_NONE,                  /* No access yet */
                  MAP_PRIVATE | MAP_ANONYMOUS,
                  -1, 0);
    if (region == MAP_FAILED) { perror("mmap reserve"); exit(1); }
    printf("Reserved region at %p\n", (void*)region);

    /* Step 2: Open two files to map within the reserved region */
    fd = open("/etc/hostname", O_RDONLY);
    if (fd == -1) { perror("open"); exit(1); }

    /* Step 3: Map file into page 0 of the reserved region using MAP_FIXED */
    slot0 = mmap(region, ps,
                 PROT_READ,
                 MAP_PRIVATE | MAP_FIXED,   /* MAP_FIXED: exactly at region */
                 fd, 0);
    if (slot0 == MAP_FAILED) { perror("mmap fixed slot0"); exit(1); }
    close(fd);
    printf("slot0 (hostname): %p → %.30s\n", (void*)slot0, slot0);

    /* Step 4: Map anonymous memory into pages 2-3 */
    slot1 = mmap(region + 2 * ps, 2 * ps,
                 PROT_READ | PROT_WRITE,
                 MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
                 -1, 0);
    if (slot1 == MAP_FAILED) { perror("mmap fixed slot1"); exit(1); }
    slot1[0] = 'Z';
    printf("slot1 (anon): %p → %c\n", (void*)slot1, slot1[0]);

    munmap(region, 4 * ps);
    return 0;
}

Memory Overcommitting in Linux

When you call mmap() (or malloc()), the kernel does not immediately allocate physical RAM for every page. It just reserves virtual address space. Physical pages are allocated on demand when the page is first accessed. This is called demand paging.

Because processes rarely use all their allocated virtual memory, Linux allows the total committed virtual memory to exceed the available physical RAM + swap. This is overcommitting. It works well in practice because most allocations are never fully used.

Process A
Committed: 500 MB
Actually used: 80 MB
Process B
Committed: 400 MB
Actually used: 60 MB
Process C
Committed: 300 MB
Actually used: 50 MB
Physical RAM
+ Swap: 512 MB
(total committed = 1.2 GB ✓ allowed)

What Happens When Overcommit Fails? The OOM Killer

If too many pages are actually accessed, physical RAM and swap fill up. The kernel’s Out-Of-Memory (OOM) killer steps in and kills one or more processes to free memory. This can happen long after mmap() succeeded, which is a surprise to programs expecting an immediate failure.

Controlling Overcommit via /proc
# View current overcommit policy
cat /proc/sys/vm/overcommit_memory
# 0 = heuristic overcommit (default)
# 1 = always overcommit (never fail mmap)
# 2 = never overcommit beyond commit limit

# View overcommit ratio (used when policy = 2)
cat /proc/sys/vm/overcommit_ratio
# e.g., 50 means total commit limit = 50% of RAM + swap

# Set policy (as root)
echo 2 > /proc/sys/vm/overcommit_memory
echo 80 > /proc/sys/vm/overcommit_ratio

MAP_NORESERVE – Opting Out of Swap Reservation

Normally, when you create a mapping, the kernel reserves swap space as a safety net. MAP_NORESERVE skips this reservation — the mapping succeeds even if swap is nearly full. The trade-off: if you later access a page and there truly is no physical memory or swap available, the OOM killer fires or the access fails with SIGSEGV.

/* Large sparse mapping — don't reserve swap for all pages */
char *sparse = mmap(NULL, 10UL * 1024 * 1024 * 1024,  /* 10 GB virtual */
                    PROT_READ | PROT_WRITE,
                    MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE,
                    -1, 0);
/* This can succeed even on a 4 GB RAM machine.
   Pages are only backed by RAM when actually touched. */

Example 4: Demonstrating Overcommit (Safe Read of /proc)
#include <stdio.h>
#include <stdlib.h>

void print_overcommit_policy(void) {
    FILE *f;
    int policy;

    f = fopen("/proc/sys/vm/overcommit_memory", "r");
    if (!f) { perror("fopen"); return; }
    fscanf(f, "%d", &policy);
    fclose(f);

    printf("Overcommit policy: %d — ", policy);
    switch (policy) {
        case 0: printf("Heuristic (default)\n"); break;
        case 1: printf("Always overcommit\n"); break;
        case 2: printf("Strict limit\n"); break;
        default: printf("Unknown\n");
    }
}

void print_commit_limit(void) {
    FILE *f = fopen("/proc/meminfo", "r");
    if (!f) { perror("fopen"); return; }
    char line[256];
    while (fgets(line, sizeof(line), f)) {
        if (strncmp(line, "CommitLimit", 11) == 0 ||
            strncmp(line, "Committed_AS", 12) == 0) {
            printf("%s", line);
        }
    }
    fclose(f);
}

int main(void) {
    print_overcommit_policy();
    print_commit_limit();
    return 0;
}

Interview Questions & Answers
Q1. What does mremap() do and how is it different from calling munmap() + mmap()?

A: mremap() resizes an existing mapping in place (or moves it with MREMAP_MAYMOVE). It is more efficient than unmap + remap because: (1) it preserves the page table entries for unchanged pages — no page faults needed on the retained data; (2) it avoids the window of time between unmap and remap where the address range is unmapped; (3) it is a single system call.

Q2. What happens without MREMAP_MAYMOVE if there is not enough space to grow a mapping in place?

A: mremap() returns MAP_FAILED with errno set to ENOMEM. Without MREMAP_MAYMOVE, the kernel is not allowed to relocate the mapping, so if the virtual address range immediately after the current mapping is already occupied, the resize fails.

Q3. What is memory overcommitting and what is its benefit?

A: Overcommitting means the kernel allows processes to reserve (commit) more virtual memory in total than the physical RAM + swap available. The benefit is efficiency: most programs allocate large buffers but only use a fraction. By not requiring physical RAM up front, many more processes can run concurrently. The kernel assigns physical pages only when a page is first accessed (demand paging).

Q4. A program calls mmap() and it succeeds, but later the process is killed without any apparent bug. What could cause this?

A: The Linux OOM (Out-Of-Memory) killer. mmap() succeeds because virtual address space is reserved (overcommit), but physical RAM + swap runs out later when many pages are accessed. The kernel kills the process (or another one) to reclaim memory. This can be mitigated by: using overcommit_memory=2 (strict mode) so mmap() fails early, or by mlockall()/mlock() to pre-fault and lock pages into RAM.

Q5. What is the safe pattern for using MAP_FIXED?

A: First, create a large anonymous mapping with MAP_PRIVATE | MAP_ANONYMOUS (no MAP_FIXED) to reserve a contiguous virtual address range. The kernel guarantees this range is yours. Then use MAP_FIXED within that range to place specific file mappings or different-protection sub-regions. This avoids accidentally overwriting critical regions because you are only replacing your own placeholder mapping.

Leave a Reply

Your email address will not be published. Required fields are marked *