Question
POSIX systems provide at least two ways to access file contents:
- Traditional file-descriptor system calls such as
open(),read(),write(), andclose(). - 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()andwrite()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.
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; inspecterrno.
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.
datais the starting virtual address of the mapped file region.data[1000]means “read the byte 1,000 bytes after that address.”- If the page containing that byte is already resident in memory, the CPU reads it normally.
- If it is not resident, the CPU triggers a page fault.
- 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.
- The instruction resumes, and
bytereceives 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()withO_APPENDis straightforward for logs and event data. - Precise I/O error reporting: A failed
read()orwrite()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_SHAREDmappings 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.
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:
- Opens a regular file.
- Uses
fstat()to obtain its size. - Handles an empty file separately because mapping length zero is invalid.
- Validates the file format and every offset before dereferencing it.
- 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
| Aspect | read() / write() | mmap() |
|---|---|---|
| Programming model | Explicit transfer to or from buffers | Access file bytes through pointers |
| Best access pattern | Sequential streaming and chunk processing | Random access or repeated indexing |
| Buffer management | Application supplies buffers | Kernel maps pages into virtual memory |
| Supported targets | Regular files, pipes, sockets, devices, terminals | Primarily regular files and shared-memory use cases |
| Error timing | Reported by read() and write() return values | Setup errors at mmap(); some I/O-related failures can occur later during memory access |
| Partial transfer handling | Must handle short reads and writes | No short reads, but every access must stay within the mapped range |
| File modifications | write() is explicit; call fsync() when durability is required | MAP_SHARED can modify the file; msync() requests write-back but does not replace a full durability design |
| Cleanup | close(fd) | munmap() plus usually close(fd) |
| Simple small-file task | Usually simpler | Often 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.
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,0at EOF, or-1on 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);
lengthmust be greater than zero.- The file offset must be page-aligned.
- Check failure with
MAP_FAILED, notNULL. 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.
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.
Keep learning
Related questions
2D Array Loop Order and Cache Performance in C
Learn why swapping nested loops changes 2D array performance in C, using row-major memory layout, cache locality, and practical benchmarks.
Array-to-Pointer Conversion in C and C++ Explained
Learn what array-to-pointer conversion means in C and C++, how array decay works, and how it differs from a pointer to an array.
Building More Fault-Tolerant Embedded C++ Applications for Radiation-Prone ARM Systems
Learn practical C++ and compile-time techniques to reduce soft-error damage in embedded ARM systems exposed to radiation.