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:

  1. 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).
  2. 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.
  3. 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 addr parameter passed to msync() must be aligned to the system page size, which can be verified using sysconf(_SC_PAGESIZE). Passing a pointer returned directly by mmap() satisfies this requirement.
  • Mapping Mode: msync() only writes back changes if the memory was mapped with MAP_SHARED. If MAP_PRIVATE was 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 -1 indicates that data persistence failed, and errno should be checked immediately.