strange mmap behavior

Question

I am making an experiment where I have a program that creates a shared file of 16 bytes, and then every second shows the value of each byte.

I want to be able to change the file while the program is running, and have the program recognize the changes.

The program seems to indeed have writing to the memory write to the file as well; but if I modify the file at runtime, it continues using old values despite the file being changed, and the file stops being updated.

Here is the version that doesn't recognize changes.

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>

// | bag of global state.
struct {
    uint8_t *state;
    char *filename;
    int length;
} global = {
    (uint8_t *)0,
    "state.bin",
    16
};

// | clean up when ctrl-c is pressed.
static void onSIGINT(int unused)
{
    puts("caught SIGINT; cleaning up.");
    munmap(global.state, global.length);
    unlink(global.filename);
    exit(0);
}

// | display each byte of global shared memory
static void inspect()
{
    int i;

    for (i = 0; i < global.length; ++i) {
        printf("state[%d] = %d.\n", i, global.state[i]);
    }
}

int main(int argc, char **argv)
{
    /* anonymous scope: initialize shared memory */
    {
        // | mmap arguments
        void *addr = (void *)0;
        int prot = PROT_READ | PROT_WRITE;
        int flags = MAP_SHARED;
        int offset = 0;
        int fd = open(global.filename, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);

        // | initialize memory to 16 zerod out bytes.
        uint8_t dummy[16];
        memset(&dummy, 0, global.length);
        write(fd, dummy, global.length);

        global.state = (uint8_t *)mmap (
            addr,
            global.length,
            prot,
            flags,
            fd,
            offset
        );

        if (global.state == MAP_FAILED) {
            close(fd);
            perror("could not create map.\n");
            unlink(global.filename);
        }                
    }

    signal(SIGINT, onSIGINT);

    /* anonymous scope: mainloop */
    {
        int count = 0;

        for (;;) {
            system("clear");
            printf("refresh number: %d.\n", count);
            ++count;
            inspect();
            sleep(1);
        }
    }

    return 0;
}

Here is the version that also increments each byte on each display iteration to show that it is actually using the shared file.

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>

// | bag of global state.
struct {
    uint8_t *state;
    char *filename;
    int length;
} global = {
    (uint8_t *)0,
    "state.bin",
    16
};

// | clean up when ctrl-c is pressed.
static void onSIGINT(int unused)
{
    puts("caught SIGINT; cleaning up.");
    munmap(global.state, global.length);
    unlink(global.filename);
    exit(0);
}

// | prints length bytes starting at address given.
static void inspect()
{
    int i;

    for (i = 0; i < global.length; ++i) {
        printf("state[%d] = %d.\n", i, global.state[i]);
        ++global.state[i];
    }
}

int main(int argc, char **argv)
{
    /* anonymous scope: initialize shared memory */
    {
        // | mmap arguments
        void *addr = (void *)0;
        int prot = PROT_READ | PROT_WRITE;
        int flags = MAP_SHARED;
        int offset = 0;
        int fd = open(global.filename, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);

        // | initialize memory to 16 zerod out bytes.
        uint8_t dummy[16];
        memset(&dummy, 0, global.length);
        write(fd, dummy, global.length);

        global.state = (uint8_t *)mmap (
            addr,
            global.length,
            prot,
            flags,
            fd,
            offset
        );

        if (global.state == MAP_FAILED) {
            close(fd);
            perror("could not create map.\n");
            unlink(global.filename);
        }                
    }

    signal(SIGINT, onSIGINT);

    /* anonymous scope: mainloop */
    {
        int count = 0;

        for (;;) {
            system("clear");
            printf("refresh number: %d.\n", count);
            ++count;
            inspect();
            sleep(1);
        }
    }

    return 0;
}

Note that it successfully keeps incrementing each byte UNTIL the moment I change one of the bytes in vim. When I overwrite the file, it stops modifying the file, but continues counting from where it was regardless.

Why is it behaving this way; and how can I make it behave as expected?


Show source
| C   | mmap   2017-01-06 15:01 1 Answers

Answers ( 1 )

  1. 2017-01-06 16:01

    This is caused by the method vim uses to create backup files. This is documented for the backupcopy option and the typical default is to rename the original file and edit a copy. This causes the memory mapped view to be associated with the backup and not the file being edited.

    You can see this happen if you check the inode for the file:

    $ echo z>a && ls -i a
    14551885 a
    $ vim a
    $ ls -i a
    14551887 a
    

    As you can see the inode is now different. If you used python for instance to open and modify the file you can edit the file in-place and it will work as expected.

    Example using python instead:

    $ ls -i a
    14551886 a
    $ python
    Python 2.7.6 (default, Oct 26 2016, 20:30:19)
    [GCC 4.8.4] on linux2
    Type "help", "copyright", "credits" or "license" for more information.
    >>> f = open('a', 'r+')
    >>> f.write('22')
    >>> f.close()
    >>>
    
    $ ls -i a
    14551886 a
    
◀ Go back