Unix - UNIX Memory-Mapped Files Using mmap()?

UNIX memory-mapped files are a mechanism that allows a file or another memory object to be mapped directly into a process's virtual memory. This is commonly achieved using the mmap() system call. Instead of repeatedly calling functions such as read() and write() to transfer data between a file and a program's memory, a program can access the mapped region as though it were an ordinary block of memory. The operating system manages the relationship between the virtual memory region and the underlying file.

What Is mmap()?

The mmap() system call creates a mapping between a file and a region of the calling process's virtual address space. Once the mapping is established, the program can access the file's contents through a pointer.

The general syntax is:

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

The important parameters are:

  • addr: Preferred starting address of the mapping. It is normally set to NULL, allowing the operating system to select a suitable address.

  • length: Number of bytes to map.

  • prot: Protection permissions, such as PROT_READ, PROT_WRITE, or both.

  • flags: Specifies how the mapping behaves, such as MAP_PRIVATE or MAP_SHARED.

  • fd: File descriptor of the file being mapped.

  • offset: Position in the file from which the mapping starts.

A successful call returns the starting address of the mapped region. If the operation fails, MAP_FAILED is returned.

How Memory Mapping Works

Consider a file containing a large amount of data. With traditional file access, a program typically opens the file, reads portions of it into a buffer, processes the buffer, and then reads additional portions.

With memory mapping, the operating system establishes a virtual-memory mapping between the process and the file. The application can then access the mapped region using normal memory operations.

For example, conceptually:

File on Disk
     |
     | mmap()
     v
Virtual Memory of Process
     |
     v
Pointer used by Program

The operating system loads required portions of the file into physical memory as they are accessed. This is closely connected with virtual memory and demand paging.

Basic Example

A simple read-only memory-mapping program can look like this:

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

int main(void)
{
    int fd = open("data.txt", O_RDONLY);

    struct stat st;
    fstat(fd, &st);

    char *data = mmap(NULL, st.st_size,
                      PROT_READ,
                      MAP_PRIVATE,
                      fd, 0);

    write(STDOUT_FILENO, data, st.st_size);

    munmap(data, st.st_size);
    close(fd);

    return 0;
}

Here, open() obtains a file descriptor, while fstat() determines the file size. The mmap() call then maps the file into the process's address space. The program can access the file contents through data. Finally, munmap() removes the mapping.

In production code, return values should always be checked for errors.

MAP_SHARED and MAP_PRIVATE

One of the most important concepts in mmap() is the difference between shared and private mappings.

MAP_SHARED

With MAP_SHARED, changes made to the mapped memory can be reflected in the underlying file. Other processes that map the same file using an appropriate shared mapping can also observe changes.

For example:

char *data = mmap(NULL, size,
                  PROT_READ | PROT_WRITE,
                  MAP_SHARED,
                  fd, 0);

If the program modifies the mapped region, the modification can eventually be written back to the file.

MAP_SHARED is particularly useful for applications that need shared access to file-backed data.

MAP_PRIVATE

MAP_PRIVATE creates a private, copy-on-write mapping. A process can modify its mapped memory, but those modifications are not written back to the underlying file.

For example:

char *data = mmap(NULL, size,
                  PROT_READ | PROT_WRITE,
                  MAP_PRIVATE,
                  fd, 0);

Initially, the process may see the same file contents as other processes. However, when it modifies a memory page, the operating system can create a private copy of that page for the process.

This mechanism is known as copy-on-write.

Memory-Mapped Files and Page Faults

Memory mapping relies heavily on the UNIX virtual-memory system.

When a program accesses a mapped region for the first time, the required data might not yet be present in physical memory. The processor generates a page fault, and the operating system determines that the requested page belongs to the mapped file.

The kernel can then load the required page from the file into physical memory and establish the appropriate virtual-to-physical mapping.

This means that an entire large file does not necessarily have to be loaded into RAM immediately.

For example, if a 1 GB file is mapped but the program accesses only a few pages, the operating system can load only the portions that are actually required.

munmap()

A mapping should be removed when the program no longer needs it.

The system call is:

int munmap(void *addr, size_t length);

Example:

munmap(data, size);

This releases the specified memory mapping from the process's virtual address space.

It is important to understand that munmap() is different from closing the file descriptor. A file descriptor and a memory mapping are separate kernel-managed resources.

Synchronizing Changes with the File

For shared mappings, a program may use msync() when it needs to request synchronization of modifications with the underlying file.

The general form is:

msync(addr, length, flags);

For example:

msync(data, size, MS_SYNC);

MS_SYNC requests that updates be synchronized before the call returns.

Another option is MS_ASYNC, which requests asynchronous synchronization.

The exact persistence guarantees also depend on the filesystem and storage system, so msync() should not be treated as a universal guarantee that data has physically reached nonvolatile storage in every environment.

Advantages of Memory-Mapped Files

Memory mapping provides several important advantages.

Efficient Random Access

A program can access different locations in a mapped file directly using memory addresses. This can be convenient for applications that frequently access different portions of a large file.

Reduced Buffer Management

Traditional file processing often requires explicit buffers and repeated read() or write() operations. With memory mapping, the application can work with the mapped region more directly.

Demand Paging

The operating system can load pages as they are needed rather than requiring the application to explicitly read the entire file.

Potentially Efficient Sharing

Multiple processes can map the same file into their address spaces. With suitable shared mappings, they can access common file-backed memory.

Convenient Data Structures

Memory mapping can be useful when working with large structured files. An application can interpret portions of the mapped region according to an appropriate in-memory representation, although it must carefully consider issues such as alignment, portability, file format, and byte order.

Limitations and Risks

Memory mapping is powerful, but it is not always the best choice.

A mapping has a finite length. Accessing beyond the valid mapped region can result in invalid memory access.

The underlying file also needs to be handled carefully. If the file is truncated while a process is accessing a corresponding mapped region, accessing affected pages can result in a SIGBUS signal on systems with behavior typical of UNIX-like environments.

Memory consumption can also become complicated because mapped files occupy virtual address space and may cause physical pages to be brought into memory as they are accessed.

Another consideration is that mmap() does not automatically make concurrent modifications safe. If multiple processes or threads modify shared mapped memory, appropriate synchronization mechanisms may still be required.

mmap() Compared with read() and write()

Traditional file access:

File
 |
read()
 |
Buffer
 |
Application

Memory-mapped access:

File
 |
mmap()
 |
Virtual Memory
 |
Application

With read(), the program explicitly asks the kernel to transfer data into a buffer. With mmap(), the program accesses a virtual memory region and the operating system handles the file-backed pages through the virtual-memory subsystem.

Neither approach is universally faster. Performance depends on factors such as access patterns, file size, workload, operating system behavior, filesystem, storage device, and memory pressure.

Common Applications

Memory-mapped files are commonly useful in areas such as:

  • Large file processing

  • Database and storage systems

  • File indexing

  • Binary file analysis

  • Shared-memory-style communication using file-backed mappings

  • High-performance data processing

  • Large read-only datasets

  • Applications requiring frequent random access to file contents

Important Concepts to Remember

The key ideas behind UNIX memory-mapped files are:

  1. mmap() maps a file or memory object into a process's virtual address space.

  2. The returned address can be used to access the mapped region.

  3. PROT_READ and PROT_WRITE control memory-access permissions.

  4. MAP_SHARED allows modifications to be shared and potentially reflected in the underlying file.

  5. MAP_PRIVATE provides copy-on-write behavior and does not write modifications back to the original file.

  6. Page faults allow the operating system to load file-backed pages when they are needed.

  7. msync() can be used to request synchronization of shared mappings.

  8. munmap() removes a mapping from the process's address space.

  9. Memory mapping does not automatically provide synchronization between concurrent processes.

  10. Programs must carefully manage mapping size, file lifetime, permissions, and error conditions.

Overall, mmap() is an important UNIX system facility because it connects file storage with virtual memory. It allows applications to treat file contents as memory while relying on the operating system's virtual-memory mechanisms to manage the underlying pages. This makes memory-mapped files particularly valuable for applications that work with large datasets or require efficient random access to file-backed information.