Memory Mappings in Linux munmap() and Unmapping Regions

 

Memory Mappings in Linux
Chapter 49 โ€” Part 2: munmap() and Unmapping Regions
๐Ÿ”“ munmap() System Call
โœ‚ Partial Unmapping
๐Ÿ”€ Memory Locks Removed
โšก exec() Auto-unmap

Why Unmapping Matters

Every mmap() call reserves virtual address space in your process. Failing to unmap regions causes virtual address space leaks. On 32-bit systems this can exhaust the 3 GB user space quickly. Even on 64-bit systems, it wastes kernel resources (page table entries, VMA structures). The munmap() call is the counterpart to mmap() โ€” it releases a mapping from the process’s virtual address space.

Understanding the exact behaviour of munmap() โ€” especially what happens when you unmap part of a mapping โ€” is important for building memory allocators, database buffer pools, and low-level runtime libraries.

Key Terms in This Module

munmap() Virtual Address Space Page Boundary Memory Locks mlock() mlockall() msync() exec() Partial Unmap VMA Split

๐Ÿš€ munmap() System Call โ€” Signature and Behaviour
#include <sys/mman.h>

int munmap(void *addr, size_t length);

/* Returns: 0 on success, -1 on error (sets errno) */

addr: Starting address of the region to unmap. Must be page-aligned (a multiple of the system page size). This should be the address returned by a previous mmap() call when unmapping a whole mapping.

length: Size in bytes of the region to unmap. The kernel rounds this up to the next page boundary internally. A value of 0 is an error (EINVAL).

Key behaviours of munmap():

  • If addr and length do not cover any mapped region, munmap() returns 0 (success) silently โ€” it is not an error to unmap an already-unmapped region.
  • All memory locks (mlock() or mlockall()) in the unmapped range are automatically removed by the kernel.
  • All of a process’s mappings are automatically unmapped when the process terminates or calls exec().
  • For shared file mappings, always call msync() before munmap() to ensure changes reach the file.

Basic example โ€” full unmap after mmap:

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(void)
{
    int fd;
    struct stat sb;
    char *addr;

    fd = open("data.txt", O_RDWR);
    if (fd == -1) { perror("open"); exit(EXIT_FAILURE); }

    if (fstat(fd, &sb) == -1) { perror("fstat"); exit(EXIT_FAILURE); }

    /* Map entire file */
    addr = mmap(NULL, (size_t)sb.st_size,
                PROT_READ | PROT_WRITE,
                MAP_SHARED,
                fd, 0);
    if (addr == MAP_FAILED) { perror("mmap"); exit(EXIT_FAILURE); }

    /* fd can be closed after mmap - mapping persists independently */
    close(fd);

    /* ... work with the mapping ... */
    addr[0] = 'A';

    /*
     * For MAP_SHARED: sync changes to disk before unmapping.
     * MS_SYNC: wait until write is complete.
     * MS_ASYNC: schedule write but don't wait.
     */
    if (msync(addr, (size_t)sb.st_size, MS_SYNC) == -1)
        perror("msync");

    /* Unmap: use the same addr and length from mmap() */
    if (munmap(addr, (size_t)sb.st_size) == -1) {
        perror("munmap");
        exit(EXIT_FAILURE);
    }

    printf("Mapping released successfully.\n");
    return 0;
}

โœ‚ Partial Unmapping โ€” Shrink or Split a Mapping

munmap() does not have to release the entire mapping. You can unmap a portion of an existing mapping. The result depends on where the unmapped region falls:

Case 1: Unmap from the front (shrinks mapping at start)

UNMAPPED
munmap(addr, partial)
REMAINING MAPPING
addr + partial, length – partial

Case 2: Unmap from the back (shrinks mapping at end)

REMAINING MAPPING
addr, length – partial
UNMAPPED
addr + (length – partial)

Case 3: Unmap the middle (splits mapping into two)

MAPPING A
addr, first_part
HOLE (UNMAPPED)
addr + first_part, hole_size
MAPPING B
addr + first_part + hole_size
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>

int main(void)
{
    size_t page = (size_t)sysconf(_SC_PAGESIZE);
    /* Allocate 4 pages */
    size_t total = 4 * page;
    char *base;

    base = mmap(NULL, total,
                PROT_READ | PROT_WRITE,
                MAP_PRIVATE | MAP_ANONYMOUS,
                -1, 0);
    if (base == MAP_FAILED) { perror("mmap"); exit(EXIT_FAILURE); }

    printf("4-page mapping at: %p (size %zu bytes)\n", (void *)base, total);

    /* --- Case 1: Unmap the first page (shrink from front) --- */
    if (munmap(base, page) == -1) { perror("munmap front"); exit(EXIT_FAILURE); }
    printf("Unmapped first page. Remaining starts at %p\n", (void *)(base + page));
    /* Now: base + page .. base + 4*page is still mapped */

    /* --- Case 2: Unmap the last page (shrink from back) --- */
    if (munmap(base + 3 * page, page) == -1) {
        perror("munmap back");
        exit(EXIT_FAILURE);
    }
    printf("Unmapped last page. Remaining: %p to %p\n",
           (void *)(base + page),
           (void *)(base + 3 * page));
    /* Now: base + page .. base + 3*page is still mapped (2 pages) */

    /* Accessing base+page and base+2*page is still valid */
    base[page]       = 'X';   /* OK */
    base[2 * page]   = 'Y';   /* OK */

    /* --- Case 3: Unmap the middle page (splits mapping into two) --- */
    /* Remap fresh 4 pages for this demo */
    char *m2 = mmap(NULL, 4 * page,
                    PROT_READ | PROT_WRITE,
                    MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (m2 == MAP_FAILED) { perror("mmap2"); exit(EXIT_FAILURE); }

    /* Unmap pages 1 and 2 (middle), creating a hole */
    if (munmap(m2 + page, 2 * page) == -1) {
        perror("munmap middle");
        munmap(m2, 4 * page);
        exit(EXIT_FAILURE);
    }
    printf("Middle unmapped. Two separate regions: [%p] and [%p]\n",
           (void *)m2,
           (void *)(m2 + 3 * page));

    /* m2[0]         is valid (page 0 still mapped) */
    /* m2[3*page]    is valid (page 3 still mapped) */
    /* m2[page]      would SIGSEGV (page 1 unmapped - the hole) */

    /* Cleanup */
    munmap(m2, page);           /* free page 0 */
    munmap(m2 + 3 * page, page); /* free page 3 */
    munmap(base + page, 2 * page); /* free the 2-page region from case 1/2 demo */

    return 0;
}

๐Ÿ” Unmapping Multiple Mappings in One Call

The address range passed to munmap() can span multiple separate mappings. All mappings that fall within the specified range will be unmapped in a single call. This is useful for releasing a contiguous virtual address range that was populated with several calls to mmap().

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>

int main(void)
{
    size_t page = (size_t)sysconf(_SC_PAGESIZE);

    /*
     * Strategy: Reserve a large virtual range first with PROT_NONE,
     * then overlay individual mappings within it.
     * This technique is used by JVM, .NET CLR, and custom allocators.
     */

    /* Step 1: Reserve 4 contiguous pages of virtual space */
    char *base = mmap(NULL, 4 * page,
                      PROT_NONE,
                      MAP_PRIVATE | MAP_ANONYMOUS,
                      -1, 0);
    if (base == MAP_FAILED) { perror("mmap reserve"); exit(EXIT_FAILURE); }
    printf("Reserved 4 pages at %p\n", (void *)base);

    /* Step 2: Map page 0 and page 1 as read-write */
    char *m0 = mmap(base, page,
                    PROT_READ | PROT_WRITE,
                    MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
                    -1, 0);
    char *m1 = mmap(base + page, page,
                    PROT_READ | PROT_WRITE,
                    MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
                    -1, 0);

    if (m0 == MAP_FAILED || m1 == MAP_FAILED) {
        perror("mmap fixed");
        munmap(base, 4 * page);
        exit(EXIT_FAILURE);
    }

    /* Step 3: Map page 2 from a file */
    int fd = open("/etc/hostname", O_RDONLY);
    if (fd != -1) {
        mmap(base + 2 * page, page,
             PROT_READ,
             MAP_PRIVATE | MAP_FIXED,
             fd, 0);
        close(fd);
    }

    /* Step 4: Release ALL 4 pages in one munmap call
     * Even though they were created by different mmap() calls,
     * munmap() will remove all mappings in the range. */
    printf("Releasing all 4 pages with single munmap()...\n");
    if (munmap(base, 4 * page) == -1) {
        perror("munmap all");
        exit(EXIT_FAILURE);
    }
    printf("All mappings released.\n");

    return 0;
}

๐Ÿ”’ Memory Locks and exec() Auto-unmap

Two important side effects of munmap() that are often overlooked in interviews:

๐Ÿ”’ Memory Locks Released

When a region is unmapped, any memory locks established with mlock() or mlockall() on pages in that region are automatically removed by the kernel. You do not need to call munlock() before munmap().

โšก exec() Auto-unmap

When a process calls any of the exec() family of functions, the kernel replaces the entire process image โ€” all memory mappings are automatically unmapped. Similarly, when a process terminates normally (exit(), returning from main()), all mappings are released.

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>

int main(void)
{
    size_t page = (size_t)sysconf(_SC_PAGESIZE);
    char *addr;

    addr = mmap(NULL, page,
                PROT_READ | PROT_WRITE,
                MAP_PRIVATE | MAP_ANONYMOUS,
                -1, 0);
    if (addr == MAP_FAILED) { perror("mmap"); exit(EXIT_FAILURE); }

    /*
     * Lock the page in RAM - prevents it from being swapped out.
     * Useful for security-sensitive data (passwords, keys).
     */
    if (mlock(addr, page) == -1) {
        perror("mlock (need CAP_IPC_LOCK or low RLIMIT_MEMLOCK)");
        /* Non-fatal - continue without locking */
    } else {
        printf("Page locked in RAM (mlock succeeded)\n");
    }

    /* ... use the mapping ... */
    addr[0] = 'S';

    /*
     * munmap() automatically releases the memory lock.
     * No need for a separate munlock() call.
     */
    if (munmap(addr, page) == -1) {
        perror("munmap");
        exit(EXIT_FAILURE);
    }
    printf("munmap() removed the memory lock automatically.\n");

    return 0;
}

/*
 * Note on exec():
 * If this process called execve("/bin/ls", ...), all mappings
 * would be automatically unmapped by the kernel before the new
 * program image is loaded.
 */
โš  Important for shared file mappings: Even though exec() and exit() unmap all mappings, there is a risk with MAP_SHARED mappings. If the process was writing data to a shared mapping and is killed before calling msync(), writes that are buffered in the page cache may or may not have been flushed to disk. Always call msync(MS_SYNC) before relying on the data being durable on disk.

โŒ Common munmap() Errors and How to Debug Them
errno Cause Fix
EINVAL addr is not page-aligned, or length is 0 Use the exact addr returned by mmap(); ensure length > 0
ENOMEM Partial unmap would require more VMAs than kernel allows Increase /proc/sys/vm/max_map_count; or redesign to avoid VMA splits
Double-free munmap() called twice on same addr/length Set pointer to NULL after munmap; second call is silently ignored but dangerous if the range was remapped
Use-after-free Accessing memory after munmap() Use Valgrind or AddressSanitizer to detect; set pointer to NULL after munmap
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

/* Safe wrapper for munmap that sets the pointer to NULL */
int safe_munmap(void **paddr, size_t length)
{
    if (paddr == NULL || *paddr == NULL) return 0;

    if (munmap(*paddr, length) == -1) {
        fprintf(stderr, "munmap(%p, %zu) failed: %s\n",
                *paddr, length, strerror(errno));
        return -1;
    }

    *paddr = NULL;  /* Prevent use-after-free and double-free */
    return 0;
}

int main(void)
{
    size_t page = (size_t)sysconf(_SC_PAGESIZE);
    char *addr;

    addr = mmap(NULL, page,
                PROT_READ | PROT_WRITE,
                MAP_PRIVATE | MAP_ANONYMOUS,
                -1, 0);
    if (addr == MAP_FAILED) { perror("mmap"); exit(EXIT_FAILURE); }

    addr[0] = 'A';

    /* Safe unmap using wrapper */
    if (safe_munmap((void **)&addr, page) == -1)
        exit(EXIT_FAILURE);

    printf("addr after safe_munmap: %p (NULL = safe)\n", (void *)addr);

    /* Second call on NULL pointer is harmless */
    safe_munmap((void **)&addr, page);

    return 0;
}

๐Ÿซ Interview Questions โ€” munmap()

Q1: Does munmap() deallocate physical memory immediately?

Not necessarily immediately. munmap() removes the virtual address space mapping. For anonymous private mappings that were modified, the kernel frees the physical page frames. For file mappings and shared mappings, the physical pages may remain in the page cache for a while because other processes or the kernel itself may still reference them. The kernel’s page reclaim mechanism eventually frees them.

Q2: What happens if you call munmap() on an address range that is not mapped?

It is not an error. munmap() returns 0 (success) when the specified address range contains no mappings. This is by design โ€” it makes cleanup code easier because you don’t need to track whether every subrange was actually mapped before unmapping it.

Q3: When you close the fd after mmap(), does the mapping become invalid?

No. Once mmap() succeeds, the mapping is independent of the file descriptor. The kernel increases an internal reference count on the file’s inode. Closing the file descriptor does not affect the mapping. You can safely close fd immediately after mmap() returns, which is a common pattern to avoid file descriptor leaks.

Q4: What is the difference between munmap() and free()?

free() returns heap memory (allocated with malloc()) to the C library’s allocator, which may or may not release it to the kernel. munmap() directly removes a virtual memory mapping from the process’s address space by making a system call to the kernel. Memory allocated with mmap() must be freed with munmap(), and memory from malloc() must be freed with free(). They are not interchangeable.

Q5: Why should you call msync() before munmap() for a MAP_SHARED file mapping?

MAP_SHARED writes go to the kernel page cache, not directly to disk. When munmap() is called, the kernel is not required to flush the page cache to disk synchronously. If the system crashes after munmap() but before the page cache is flushed, data written to the mapping may be lost. Calling msync(addr, length, MS_SYNC) before munmap() ensures all dirty pages are flushed to disk before the mapping is released.

Q6: Can you call munmap() with a length larger than the original mapping?

Yes. If the range covers the entire mapping plus some unmapped areas, munmap() still succeeds. It simply skips unmapped holes. The call only affects pages that are actually part of a mapping. This is why a single munmap() can span several separately created mappings โ€” all of them within the specified range will be removed.

Q7: How does the kernel handle partial unmapping internally (VMA split)?

The kernel represents mappings as VMAs (Virtual Memory Areas) โ€” entries in the process’s mm_struct. When you unmap the middle of a VMA, the kernel splits it into two VMAs. This operation requires allocating a new VMA structure. The kernel has a limit on the number of VMAs (/proc/sys/vm/max_map_count, default 65536). Excessive VMA splits can exhaust this limit and cause ENOMEM from subsequent mmap() or munmap() calls.

Continue Learning Memory Mappings

Next: File Mappings โ€” MAP_SHARED and MAP_PRIVATE file-backed mappings in detail

Next: File Mappings โ†’ โ† Back: mmap() Flags

Leave a Reply

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