Question

mmap vs read and write for File Access in POSIX C

cfile-ioposixmmap

Question

POSIX systems provide at least two ways to access file contents:

  • Traditional file-descriptor system calls such as open(), read(), write(), and close().
  • Memory mapping with mmap(), which maps a file into a process's virtual address space.

When is mmap() preferable to read() and write(), and when are traditional file I/O calls a better choice? What advantages and trade-offs justify providing both interfaces?

Short Answer

You will learn how traditional POSIX file I/O and memory-mapped I/O work, when each is a good fit, and why neither is universally faster or simpler. You will also learn important concerns such as error handling, file-size changes, page faults, and flushing modified mappings.

Concept

Traditional file I/O treats a file as a sequence of bytes accessed through a file descriptor. Your program explicitly asks the kernel for bytes with read() and provides bytes with write().

ssize_t count = read(fd, buffer, sizeof buffer);

With mmap(), the kernel maps a selected file region into your process's virtual memory. After setup, the region is accessed like an ordinary array in C.

char *data = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, 0);
char first = data[0];

Both interfaces ultimately use the operating system's file cache. Therefore, mmap() does not automatically mean that an entire file is copied into RAM, nor does it guarantee better performance. Mapped pages are usually loaded on demand when your program touches them, which can cause page faults.

Why two interfaces exist

They support different access styles:

  • read() and write() are explicit, sequential, buffer-oriented operations. They are excellent for streams, pipes, sockets, simple file processing, and controlled error handling.
  • mmap() provides random, memory-like access to a file region. It can simplify code that repeatedly indexes, searches, or shares large file-backed data.

A useful rule is: choose the interface that makes the required access pattern and correctness requirements clearest. Measure performance in the real workload before choosing based on speed alone.

Mental Model

Think of read() as requesting pages from a librarian one batch at a time. You bring your own box (buffer), ask for a specific amount, and the librarian tells you immediately whether the request succeeded or how much was available.

Think of mmap() as placing a file-backed reference book on your desk. You can turn directly to any page using normal memory access such as data[offset]. Pages are fetched only when you open them. This is convenient for random access, but a problem with a missing or invalid page can appear later while you are reading, rather than at the original setup call.

Take Quiz

Syntax and Examples

Traditional read() syntax

ssize_t read(int fd, void *buffer, size_t count);
ssize_t write(int fd, const void *buffer, size_t count);

read() returns:

  • A positive number: the number of bytes actually read.
  • 0: end of file.
  • -1: an error occurred; inspect errno.

A read may return fewer bytes than requested, so production code must handle partial reads.

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

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

    char buffer[128];
    ssize_t bytesRead = read(fd, buffer, sizeof buffer - 1);
    if (bytesRead == -1) {
        perror("read");
        close(fd);
        return 1;
    }

    buffer[bytesRead] = '\0';
    printf("%s", buffer);

    close(fd);
    return 0;
}

mmap() syntax

void *mmap(void *address, size_t length, int protection,
           int flags, int fd, off_t offset);

For file mappings, the file descriptor is usually opened first. The mapping offset must be a multiple of the system page size. On failure, mmap() returns MAP_FAILED, not NULL.

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

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

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

    if (info.st_size == 0) {
        puts("The file is empty.");
        close(fd);
        return 0;
    }

    char *data = mmap(NULL, (size_t)info.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (data == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    printf("First byte: %c\n", data[0]);

    munmap(data, (size_t)info.st_size);
    close(fd);
    return 0;
}

After a successful mapping, the file descriptor can usually be closed; the mapping remains valid until it is unmapped. The mapping itself must still be released with munmap().

Step by Step Execution

Consider reading a byte at offset 1000 from a mapped file:

char byte = data[1000];

Assume data was returned by a successful read-only mmap() call.

  1. data is the starting virtual address of the mapped file region.
  2. data[1000] means “read the byte 1,000 bytes after that address.”
  3. If the page containing that byte is already resident in memory, the CPU reads it normally.
  4. If it is not resident, the CPU triggers a page fault.
  5. The kernel checks that the access is valid, obtains the relevant file page through its cache or storage device, and maps it into the process.
  6. The instruction resumes, and byte receives the requested value.

With traditional I/O, the equivalent is more explicit:

char byte;
if (lseek(fd, 1000, SEEK_SET) == -1) {
    perror("lseek");
}

if (read(fd, &byte, 1) != 1) {
    perror("read");
}

Here, lseek() changes the descriptor's current file position, and read() requests one byte into the program's buffer. Errors are reported directly through these calls rather than potentially occurring during a later memory access.

Real World Use Cases

Good uses for read() and write()

  • Streaming a large upload or download: Process fixed-size chunks without mapping the full file size into virtual address space.
  • Reading text line by line: Parsers, log processors, and command-line tools often naturally consume a stream.
  • Working with pipes, terminals, and sockets: These are not ordinary seekable files; mmap() is generally not the appropriate interface.
  • Appending records: write() with O_APPEND is straightforward for logs and event data.
  • Precise I/O error reporting: A failed read() or write() is reported at the operation boundary.

Good uses for mmap()

  • Random access in a large, mostly read-only file: For example, looking up records in a sorted data file or inspecting a large binary index.
  • Parsing a whole regular file as bytes: A compiler, search tool, or binary-file inspector can use pointers and offsets instead of repeatedly copying data into buffers.
  • Shared memory between processes: MAP_SHARED mappings of the same file can be used for shared file-backed data, with appropriate synchronization.
  • Updating fixed-layout files: A program can modify known fields in a database-like file format through a shared writable mapping, provided it carefully handles consistency and durability.

For a tiny file read once from beginning to end, ordinary read() is often simpler and just as suitable.

Take Quiz

Real Codebase Usage

In production code, the choice is normally based on access pattern, failure behavior, memory limits, and maintainability rather than a blanket performance rule.

Common read() pattern: buffered loop

Code that must process an arbitrary-sized file generally reads chunks until end of file. It does not assume one call fills the buffer.

char buffer[8192];
ssize_t n;

while ((n = read(fd, buffer, sizeof buffer)) > 0) {
    // Process exactly n bytes.
}

if (n == -1) {
    perror("read");
}

For write(), real code also loops because a write can be partial:

size_t written = 0;
while (written < length) {
    ssize_t n = write(fd, data + written, length - written);
    if (n == -1) {
        perror("write");
        break;
    }
    written += (size_t)n;
}

Common mmap() pattern: validate before mapping

Code using mappings typically:

  1. Opens a regular file.
  2. Uses fstat() to obtain its size.
  3. Handles an empty file separately because mapping length zero is invalid.
  4. Validates the file format and every offset before dereferencing it.
  5. Calls munmap() during cleanup.

For writable MAP_SHARED mappings, code should not treat msync() as a complete transaction system. Applications that need crash consistency commonly use explicit file formats, checksums, temporary files plus rename(), logging, or other durability strategies.

Guard clauses

Both styles benefit from early validation:

if (offset > fileSize || requestedLength > fileSize - offset) {
    fprintf(stderr, "Requested range is outside the file\n");
    return 1;
}

This avoids integer-overflow-prone checks such as offset + requestedLength > fileSize.

Common Mistakes

Assuming mmap() loads the full file immediately

A mapping reserves virtual address space, but pages are typically loaded when accessed. A large mapping can still cause page faults during later reads.

Avoid it: Use mmap() for a suitable access pattern, not as a promise that all data is already in RAM.

Checking mmap() against NULL

This is incorrect:

char *data = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, 0);
if (data == NULL) {  // Wrong
    perror("mmap");
}

Use MAP_FAILED:

if (data == MAP_FAILED) {
    perror("mmap");
}

Mapping an empty file

A zero-length mapping is invalid.

Avoid it: Check st_size before calling mmap().

Accessing beyond the mapped range

This is undefined behavior and may crash:

char value = data[fileSize]; // One byte past the valid range

Avoid it: Keep and validate the mapping length, especially when parsing file-provided offsets and lengths.

Ignoring file truncation

If another process truncates a mapped file, later access to pages beyond the new end can raise SIGBUS on POSIX systems.

Avoid it: Do not map files that untrusted concurrent writers may shrink, or coordinate access with locks, stable-file replacement strategies, or application-level synchronization.

Assuming MAP_PRIVATE writes update the file

MAP_PRIVATE uses copy-on-write behavior. Changes are private to the process and are not written back to the file.

Avoid it: Use MAP_SHARED only when you intentionally want changes to affect the underlying file, and handle synchronization and durability.

Assuming one read() or write() completes the request

This is unsafe:

write(fd, message, messageLength); // Return value ignored

Avoid it: Check return values and loop until all required bytes have been transferred or an error occurs.

Comparisons

Aspectread() / write()mmap()
Programming modelExplicit transfer to or from buffersAccess file bytes through pointers
Best access patternSequential streaming and chunk processingRandom access or repeated indexing
Buffer managementApplication supplies buffersKernel maps pages into virtual memory
Supported targetsRegular files, pipes, sockets, devices, terminalsPrimarily regular files and shared-memory use cases
Error timingReported by read() and write() return valuesSetup errors at mmap(); some I/O-related failures can occur later during memory access
Partial transfer handlingMust handle short reads and writesNo short reads, but every access must stay within the mapped range
File modificationswrite() is explicit; call fsync() when durability is requiredMAP_SHARED can modify the file; msync() requests write-back but does not replace a full durability design
Cleanupclose(fd)munmap() plus usually close(fd)
Simple small-file taskUsually simplerOften extra setup with little benefit

mmap() versus malloc() is also important: malloc() creates ordinary process memory, while mmap() can create anonymous memory or a mapping backed by a file. A file-backed mapping lets file contents appear at memory addresses.

Take Quiz

Cheat Sheet

Traditional POSIX I/O

int fd = open("file.txt", O_RDONLY);
ssize_t n = read(fd, buffer, bufferSize);
close(fd);
  • read() returns bytes read, 0 at EOF, or -1 on error.
  • A successful read() may return fewer bytes than requested.
  • Check every return value.
  • Use a loop for full reads and writes.
  • Natural choice for streams, sockets, pipes, and sequential processing.

Memory mapping

void *p = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, 0);
if (p == MAP_FAILED) {
    // handle error
}

munmap(p, length);
  • length must be greater than zero.
  • The file offset must be page-aligned.
  • Check failure with MAP_FAILED, not NULL.
  • MAP_PRIVATE: modifications are private, not saved to the file.
  • MAP_SHARED: modifications can affect the file and be visible to other mappings.
  • Keep all pointer access within [0, length).
  • A concurrently truncated mapped file can cause SIGBUS.
  • Use for regular files with random/repeated access; benchmark if performance matters.

FAQ

Is mmap() always faster than read()?

No. Both commonly rely on the kernel's file cache. mmap() can reduce copying and simplify random access, but page faults, mapping setup, access pattern, and memory pressure can make it slower or no better. Benchmark the actual workload.

Should I use mmap() to read a small file?

Usually not. open(), read(), and close() are often easier for a small file read once. Mapping may be reasonable when it substantially simplifies an existing parser.

Can mmap() be used with sockets or pipes?

Generally no. mmap() is intended for mappable objects such as regular files and shared-memory objects. Use read(), write(), recv(), and send() for streams and sockets.

Does closing the file descriptor invalidate an mmap() mapping?

Normally, no. Once mmap() succeeds, the mapping remains valid until munmap() is called or the process exits. You should still close the descriptor when it is no longer needed.

What is the difference between MAP_PRIVATE and MAP_SHARED?

MAP_PRIVATE makes writes copy-on-write and does not update the file. MAP_SHARED allows changes to update the underlying file and potentially become visible to other processes mapping it.

Do I need msync() after modifying a shared mapping?

Use msync() when you need to request that modified pages be synchronized with the backing file at a specific point. However, reliable persistence and crash recovery can require additional application-level design and, where needed, file synchronization calls.

Why can mapped-file access produce SIGBUS?

One common reason is that a mapped file was truncated and the program accessed data beyond its new end. Coordinate with writers and validate file stability when this is possible.

Take Quiz

Mini Project

Description

Build a small POSIX C program that opens a text file, maps it read-only, and counts newline characters. This demonstrates a practical mmap() use case: repeatedly scanning a regular file as a contiguous byte range without manually filling a read buffer.

Goal

Count and print the number of lines in a non-empty text file using a read-only memory mapping.

Requirements

Open the file path supplied as the first command-line argument. Determine the file size before creating the mapping. Handle open, metadata, mapping, and cleanup errors. Treat an empty file as containing zero lines. Count newline bytes without reading beyond the mapping.

Take Quiz

Keep learning

Related questions

View more