How to Sync Memory-Mapped Files to Disk in C
Synchronizing memory-mapped files to persistent storage is critical
for preventing data corruption and data loss in C applications. When
using the POSIX mmap() interface, changes made to the
mapped memory region are written to the kernel's page cache rather than
directly to the physical drive. To safely guarantee that these
modifications are committed to disk, developers must use the
msync() system call with appropriate flags, handle memory
alignment constraints, and maintain proper mapping permissions.
Understanding the Role of
msync()
When a file is mapped into memory using mmap() with the
MAP_SHARED flag, writes to the memory buffer alter dirty
pages in the operating system's page cache. The kernel eventually
flushes these pages asynchronously, but a power failure, system crash,
or abrupt program termination can cause unwritten data to be lost.
The msync() system call forces the kernel to flush
modified pages back to the underlying filesystem. Its signature is
defined in <sys/mman.h>:
int msync(void *addr, size_t length, int flags);addr: The starting address of the memory region to synchronize. This pointer should ideally be aligned with the system's memory page boundary.length: The size, in bytes, of the memory segment being synchronized.flags: Bitwise flags that dictate how the synchronization behaves.
Selecting the Synchronization Flags
The behavior of msync() is determined by the flags
passed to it:
MS_SYNC(Synchronous): The call blocks until all dirty pages within the specified range have been physically written to disk. This is the safest option when data persistence must be guaranteed before moving to the next operation (such as updating a database transaction log).MS_ASYNC(Asynchronous): The call schedules dirty pages to be written out by the operating system and returns immediately. This minimizes execution latency but does not guarantee that the data has reached the physical storage medium when the call returns.MS_INVALIDATE(Cache Invalidation): Instructs the kernel to invalidate other cached copies of the mapped data, ensuring that any subsequent reads by other mappings access the freshly written state from disk.
To ensure strict durability, MS_SYNC is mandatory.
Practical Implementation
The following example demonstrates mapping a file, modifying its
contents, and safely persisting the update using
msync():
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
int main(void) {
const char *filepath = "data.bin";
size_t length = 4096;
int fd = open(filepath, O_RDWR | O_CREAT, 0644);
if (fd == -1) {
perror("open");
return EXIT_FAILURE;
}
// Ensure the file has adequate size before mapping
if (ftruncate(fd, length) == -1) {
perror("ftruncate");
close(fd);
return EXIT_FAILURE;
}
// Map the file into process memory
char *map = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map == MAP_FAILED) {
perror("mmap");
close(fd);
return EXIT_FAILURE;
}
// Modify the mapped memory
snprintf(map, length, "Persistent transaction data.");
// Safely flush changes to disk
if (msync(map, length, MS_SYNC) == -1) {
perror("msync");
}
// Clean up
if (munmap(map, length) == -1) {
perror("munmap");
}
close(fd);
return EXIT_SUCCESS;
}Safety Requirements and Best Practices
- Page Alignment: The
addrparameter passed tomsync()must be aligned to the system page size, which can be verified usingsysconf(_SC_PAGESIZE). Passing a pointer returned directly bymmap()satisfies this requirement. - Mapping Mode:
msync()only writes back changes if the memory was mapped withMAP_SHARED. IfMAP_PRIVATEwas specified, changes are local copy-on-write modifications and will never be written back to the underlying file. - Error Verification: File systems can return I/O
errors during flushing (such as write failures caused by disk
exhaustion). Always check the return value of
msync(); a return value of-1indicates that data persistence failed, anderrnoshould be checked immediately.