Linux syscalls - mmap / munmap

mmap() and munmap() are POSIX-standard system calls used to map files or device objects into a process's virtual memory address space.

Instead of copying data back and forth between kernel buffers and user-space memory using repeated read() and write() calls, mmap() creates a direct mapping between a region of virtual memory and a file (or anonymous memory). Once mapped, accessing memory addresses via pointers reads and writes data directly to/from the underlying backing store through the kernel's virtual memory manager (page cache).

mmap() Function

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

Parameters

  • addr: A hint for the starting address in virtual memory. Passing NULL lets the kernel select the optimal page-aligned address (recommended in almost all cases).

  • length: The number of bytes to map (automatically rounded up to the system page size, e.g., 4096 bytes).

  • prot: Memory protection flags (bitwise OR):

    • PROT_READ: Pages may be read.
    • PROT_WRITE: Pages may be written.
    • PROT_EXEC: Pages may be executed.
    • PROT_NONE: Pages may not be accessed.
  • flags: Controls the visibility and mapping type. Standard POSIX flags include:

    • MAP_SHARED: Writes to the region are visible to other processes mapping the same region and are committed to the underlying file.
    • MAP_PRIVATE: Writes create a private Copy-On-Write (COW) duplicate page. Changes are not visible to other processes and are not written to the underlying file.
  • fd: Open file descriptor pointing to the file being mapped.

  • offset: Starting byte offset in the file (must be a multiple of the system page size).

Return Value

Returns a pointer to the mapped memory area on success, or MAP_FAILED ((void *) -1) on error (setting errno).

munmap() Function

#include <sys/mman.h>

int munmap(void *addr, size_t length);

Unmaps pages starting at address addr for length bytes. Subsequent accesses to the unmapped address range trigger a SIGSEGV (segmentation fault).

Main Use Cases in POSIX Programming

  1. Memory-Mapped File I/O: Reading or writing large files without buffer copies (read/write syscall overhead).
  2. Shared Memory (IPC): Combining MAP_SHARED with shm_open() or regular files to allow multiple processes to share zero-copy memory buffers.
  3. Dynamic Memory Allocation: Heap allocators (such as malloc) allocate large chunks of memory from the kernel using anonymous mmap() calls instead of moving the heap break pointer (brk/sbrk).

How mmap Differs in Linux vs. Pure POSIX

While Linux conforms to the POSIX specification for mmap() and munmap(), Linux extends mmap() with non-standard flags, kernel-level optimizations, and architecture-specific behaviors.

A. Non-Standard Linux Extension Flags

Linux introduces several flags not present in the POSIX standard:

Linux Flag Purpose / Description
MAP_ANONYMOUS (or MAP_ANON) Creates an anonymous mapping not backed by any file. The fd argument is ignored (traditionally set to -1), and memory pages are initialized to zero. (POSIX historically used /dev/zero for this).
MAP_POPULATE Prefaults page tables for the mapping (read-ahead). Instead of relying on lazy page faulting, the kernel eagerly loads pages into RAM at call time.
MAP_LOCKED Locks the mapped pages into physical RAM (similar to mlock()), preventing them from being swapped out to disk.
MAP_NORESERVE Disables swap space reservation for this mapping (allows overcommitting memory).
MAP_HUGETLB Allocates memory using HugeTLB pages (e.g., 2MB or 1GB pages instead of 4KB) to reduce Translation Lookaside Buffer (TLB) misses in high-performance applications.
MAP_FIXED_NOREPLACE Safe version of POSIX MAP_FIXED. Fails with EEXIST if the requested target virtual address range is already mapped, preventing accidental memory stomping.

B. Linux System Call vs. glibc Function Wrappers

On modern Linux architectures, the low-level kernel system call interface differs slightly from the C library signature:

  1. Page-Offset Syscall (sys_mmap2):
  • On 32-bit architectures, the kernel provides mmap2() where the offset parameter is passed in 4KB page units rather than raw bytes, allowing 32-bit systems to map offsets up to 16 TiB within files.
  1. Standard 64-bit Syscall (sys_mmap):
  • On 64-bit architectures (x86_64, aarch64), sys_mmap accepts byte offsets directly.

glibc translates standard byte-offset mmap() calls into the correct underlying architecture-specific system call automatically.

C. Kernel File Desynchronization & msync()

POSIX specifies that modifying a MAP_SHARED region modifies the file, but doesn't guarantee when dirty pages are written to physical disk storage.

To explicitly flush memory modifications back to storage across POSIX systems, developers call:

int msync(void *addr, size_t length, int flags); // MS_SYNC or MS_ASYNC

On Linux, munmap() or process termination will automatically queue dirty pages for asynchronous writing by the page cache flusher threads (pdflush/writeback), but calling msync() with MS_SYNC remains mandatory if immediate durability is required.

4. Practical C Example (POSIX File Mapping)

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

int main(void) {
    int fd = open("example.txt", O_RDWR);
    if (fd == -1) {
        perror("open failed");
        return 1;
    }

    struct stat sb;
    if (fstat(fd, &sb) == -1) {
        perror("fstat failed");
        close(fd);
        return 1;
    }

    // Map the file into memory
    char *map = mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (map == MAP_FAILED) {
        perror("mmap failed");
        close(fd);
        return 1;
    }

    // File descriptor can be closed immediately after mapping
    close(fd);

    // Read first byte directly from pointer
    printf("First char: %c\n", map[0]);

    // Unmap memory
    if (munmap(map, sb.st_size) == -1) {
        perror("munmap failed");
        return 1;
    }

    return 0;
}

mmap vs read/write

To understand mmap, you have to compare it to the "Standard I/O" way of doing things.

The Standard Way (read / write)

Imagine you want to read a 1GB file.

  1. Syscall: You call read().
  2. Context Switch: The CPU switches to Kernel Mode.
  3. Disk Read: The Kernel reads data from the disk into the Kernel Page Cache.
  4. Copy: The Kernel copies that data from the Page Cache into your application's buffer (User Space).
  5. Return: The CPU switches back to User Mode.
  • Cost: High CPU usage due to copying memory and context switching.

The mmap Way

  1. Syscall: You call mmap().
  2. Mapping: The Kernel modifies the process's Page Table. It marks a range of virtual memory addresses as "pointing" to that file on the disk.
  3. Return: The call returns a pointer. No data has been read yet.
  4. Access: You try to read pointer[0].
  5. Page Fault: The CPU realizes that memory address is empty. It pauses your program and triggers a "Page Fault."
  6. Lazy Load: The Kernel catches the fault, transparently reads the necessary block from the disk into the Page Cache, maps it to your pointer, and resumes your program.
  • Benefit: Zero-copy. The application reads directly from the Kernel's Page Cache.

The Two Main Types of mmap

When you create a map, you must choose how it behaves using flags:

A. File-Backed Mapping (MAP_SHARED vs MAP_PRIVATE)

  • MAP_SHARED:
    • Changes you make to the memory are written back to the disk.
    • Other processes mapping the same file see your changes instantly.
    • Use Case: Inter-Process Communication (IPC) or high-performance database writes.
  • MAP_PRIVATE (Copy-on-Write):
    • You can read the file, but if you write to the memory, the OS creates a private copy of that specific page just for you.
    • The file on disk is never modified.
    • Use Case: Loading code libraries (.so files) or initializing a process.

B. Anonymous Mapping (MAP_ANONYMOUS)

  • This doesn't involve a file at all. It just asks the Kernel for a big chunk of raw RAM, filled with zeros.
  • Fun Fact: This is actually how malloc() works! If you ask malloc for a tiny amount of RAM, it uses the heap (brk). If you ask for a huge amount (e.g., 100MB), malloc uses mmap to grab a fresh block from the OS.

Why use mmap? (The Pros)

  1. Speed (Zero Copy): You avoid the CPU overhead of copying data from kernel space to user space.
  2. Lazy Loading: If you mmap a 1TB file but only read the last byte, the OS only reads the last 4KB page from the disk. The rest of the file never touches RAM.
  3. Random Access: Jumping around a file is as easy as moving a pointer/index (data[5000]). You don't have to lseek() back and forth.
  4. IPC (Inter-Process Communication): If two programs mmap the same file with MAP_SHARED, they can talk to each other extremely fast by just reading/writing memory.

The Downsides (The Cons)

  1. Page Fault Overhead: mmap is great for large files, but for small files, the overhead of setting up the page tables and handling page faults is slower than a simple read().
  2. Lack of Control: With write(), you know exactly when the I/O happens. With mmap, the OS decides when to flush dirty pages back to the disk (unless you force it with msync).
  3. The "SIGBUS" Error: If you mmap a file, and someone else truncates (shortens) that file while you are using it, and you try to access the part that was cut off, your program crashes with a SIGBUS signal.

Summary Analogy

  • read(): You go to the library (Disk), verify your ID (Syscall), the librarian finds the book, photocopies the page you want (Buffer Copy), and hands you the copy.
  • mmap(): The librarian gives you a magic pair of glasses. You look at an empty desk, but the glasses make the book appear there. When you try to read a specific page, the librarian quietly slips that real page onto the desk before your eyes focus. You read the original page directly.
Loading...