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
#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).
- If
addrandlengthdo 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()ormlockall()) 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()beforemunmap()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;
}
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;
}
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;
}
Two important side effects of munmap() that are often overlooked in interviews:
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().
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.
*/
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.| 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;
}
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
