Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

The correct way of changing the size of a mapped file

Tags:

c

mmap

The Linux man mmap manual warns against changing the size of a file after it has been mapped:

The effect of changing the size of the underlying file of a mapping on the pages that correspond to added or removed regions of the file is unspecified.

Some argue that it's fine as long as you're not accessing the data beyond the end of a file boundary.

If you adopt the following sequence:

  1. msync the file
  2. munmap the file
  3. ftruncate the file
  4. mmap the file again

wouldn't it be extremely costly (if you need to change the file size frequently, you would have to transfer data from HDD to RAM back and forth over and over again)?

So what is the correct way of dealing with changing the size of a mapped file?

like image 868
Fyodor Avatar asked Sep 30 '25 09:09

Fyodor


1 Answers

i tried three minimal programs using your method msync → munmap → ftruncate → mmap, munmap → ftruncate → mmap and mremap. Not using msync was recommended in comments by Aykhan Hagverdili

msync → munmap → ftruncate → mmap

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

#define FILE_PATH "testfile"
#define INITIAL_SIZE 4096
#define EXTENDED_SIZE 8192

void handle_error(const char *msg) {
    perror(msg);
    _exit(1);
}

int main() {
    // Create or open file
    int fd = open(FILE_PATH, O_RDWR | O_CREAT, 0666);
    if (fd == -1) handle_error("open");

    // Set the initial file size
    if(ftruncate(fd, INITIAL_SIZE) == -1) handle_error("ftruncate");
    // Map the initial region
    void *map = mmap(NULL, INITIAL_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (map == MAP_FAILED) handle_error("mmap");
    // Write data to the mapped region
    strncpy((char *)map, "Hello,mmap!", INITIAL_SIZE);
    // Synchronize changes to disk
    if (msync(map, INITIAL_SIZE, MS_SYNC) == -1) handle_error("msync");
    // Unmap the region
    if (munmap(map, INITIAL_SIZE) == -1) handle_error("munmap");
    // Extend the file size
    if (ftruncate(fd, EXTENDED_SIZE) == -1) handle_error("munmap");
    // Remap the file with the new size
    map = mmap(NULL, EXTENDED_SIZE, PROT_READ | PROT_WRITE,MAP_SHARED, fd, 0);
    if (map == MAP_FAILED) handle_error("mmap");
    // Write data to the extended region
    strncpy((char *)map + INITIAL_SIZE, "Extend region!", EXTENDED_SIZE - INITIAL_SIZE);
    // Synchronize changes to disk
    if (msync(map, EXTENDED_SIZE, MS_SYNC) == -1) handle_error("msync");
    // cleanup
    if (munmap(map, EXTENDED_SIZE) == -1) handle_error("munmap");
    close(fd);

    printf("\ntest completed sucessfully\n");
    return 0;
}

the time for the program

$ time ./msync_mmap_test

test completed sucessfully

real    0m0,015s
user    0m0,003s
sys 0m0,000s

with mremap

#define _GNU_SOURCE


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

int main()
{
    int fd = open("testfile_mmap_test", O_RDWR | O_CREAT, 0666);
    if(fd == -1){
        perror("open");
        return 1;
    }

    size_t size = 4096;
    ftruncate(fd, size);

    void *map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if(map == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    //Access the file
    sprintf((char *)map, "Hello, mmap!");
    printf("File contents: %s\n", (char *)map);

    size_t new_size = 8192;
    ftruncate(fd, new_size);

    // remap to new size
    map = mremap(map, size, new_size, MREMAP_MAYMOVE);
    if (map == MAP_FAILED) {
        perror("mremap");
        close(fd);
        return 1;
    }

    //Use new region
    sprintf((char *)map + 4096, "Extended region");
    printf("File contents (extended): %s\n", (char *)map + 4096);

    //clean up
    munmap(map, new_size);
    close(fd);
    return 0;
}

the time for the mremap

$ time ./mmap_test 
File contents: Hello, mmap!
File contents (extended): Extended region

real    0m0,003s
user    0m0,003s
sys 0m0,000s

with no_msync

#define _GNU_SOURCE
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

#define FILE_PATH "testfile_no_msync"
#define INITIAL_SIZE 4096
#define EXTENDED_SIZE 8192

void handle_error(const char *msg) {
    perror(msg);
    _exit(1);
}

int main() {
    // Create or open the file
    int fd = open(FILE_PATH, O_RDWR | O_CREAT, 0666);
    if (fd == -1) handle_error("open");

    // Set the initial file size
    if (ftruncate(fd, INITIAL_SIZE) == -1) handle_error("ftruncate");

    // Map the initial region
    void *map = mmap(NULL, INITIAL_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (map == MAP_FAILED) handle_error("mmap");

    // Write data to the mapped region
    strncpy((char *)map, "Hello, mmap!", INITIAL_SIZE);

    // Unmap the region without msync
    if (munmap(map, INITIAL_SIZE) == -1) handle_error("munmap");

    // Extend the file size
    if (ftruncate(fd, EXTENDED_SIZE) == -1) handle_error("ftruncate");

    // Remap the file with the new size
    map = mmap(NULL, EXTENDED_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (map == MAP_FAILED) handle_error("mmap");

    // Write data to the extended region
    strncpy((char *)map + INITIAL_SIZE, "Extended region!", EXTENDED_SIZE - INITIAL_SIZE);

    // Cleanup
    if (munmap(map, EXTENDED_SIZE) == -1) handle_error("munmap");
    close(fd);

    printf("\nTest completed successfully without msync\n");
    return 0;
}

the time for no msync

$ time ./no_msync 

Test completed successfully without msync

real    0m0,003s
user    0m0,003s
sys 0m0,000s

the execution time for mremap and without msync was same. As per man msync there is no guarantee that changes will be written back to the file before the memory is unmapped using munmap() or the program exits. When working with critical data that must persist even in the event of a crash or power failure, the msync should be used. As per man mremap mremap() system call is used to resize an existing memory mapping created with mmap(). It allows you to expand, shrink, or move the memory mapping to a new location while keeping the data in the memory region intact. It is good way to use for dynamically resizing memory-mapped regions to accommodate additional data and avoiding the overhead of unmapping and remapping.

like image 97
Hermit Avatar answered Oct 02 '25 04:10

Hermit



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!