4

I need persistence of a uint64_t tag across restarts.

To achieve this I am using boost::interprocess::mapped_region to memory map a file which I create in the same process:

bip::file_mapping file(filename.c_str(), bip::read_write);
auto region = std::make_unique<bip::mapped_region>(file, bip::read_write);

I then cast the address to my uint64_t type

using Tag = uint64_t;
Tag& curr_ = *reinterpret_cast<Tag*>(region->get_address());

Now I can post-increment tag, obtaining the "next tag", and the results are persisted across restarts

Tag next = curr_++;

Note that this file is written to and read from only by this process. It's purpose is purely to provide persistence.

Question:

Is my Tag& curr_, being non-volatile, and performing I/O to a memory mapped region, undefined-behaviour?

To be correct, does my code require the volatile keyword?

Full working example below:

#include <boost/interprocess/mapped_region.hpp>
#include <boost/interprocess/file_mapping.hpp>
#include <sys/stat.h>
#include <fstream>
#include <cstdint>
#include <memory>
#include <iostream>

namespace bip = boost::interprocess;

using Tag = uint64_t;

Tag& map_tag(const std::string& filename,
             std::unique_ptr<bip::mapped_region>& region)
{
    struct stat buffer;
    if (stat(filename.c_str(), &buffer) != 0)
    {
        std::filebuf fbuf;
        fbuf.open(filename.c_str(), std::ios_base::in | 
                                    std::ios_base::out | 
                                    std::ios_base::trunc | 
                                    std::ios_base::binary);

        Tag tag = 1;
        fbuf.sputn((char*)&tag, sizeof(Tag));
    }

    bip::file_mapping file(filename.c_str(), bip::read_write);

    // map the whole file with read-write permissions in this process
    region = std::make_unique<bip::mapped_region>(file, bip::read_write);

    return *reinterpret_cast<Tag*>(region->get_address());
}

class TagBroker
{
public:
    TagBroker(const std::string& filename)
        : curr_(map_tag(filename, region_))
    {}

    Tag next()
    {
        return curr_++;
    }

private:
    std::unique_ptr<bip::mapped_region> region_;
    Tag& curr_;
};

int main()
{
    TagBroker broker("/tmp/tags.bin");

    Tag tag = broker.next();

    std::cout << tag << '\n';
    return 0;
}

Output:

Across runs, persistence is kept.

$ ./a.out
1
$ ./a.out
2
$ ./a.out
3
$ ./a.out
4

I don't know if this is correct, since my process is the only one reading from/writing to Tag& curr_, or if it's just working by accident, and is, in fact, undefined behaviour.

Steve Lorimer
  • 27,059
  • 17
  • 118
  • 213
  • I thought volatile was only necessary if something other than your program was writing to that file? – AndyG Aug 08 '17 at 01:48
  • @AndyG that is what I though too - but since the I/O scheduler will be handling the actual reading from/writing to disk, is this considered another process? My understanding is not, and volatile *isn't* required, but I'm not 100% certain, hence this question – Steve Lorimer Aug 08 '17 at 02:20

1 Answers1

2

In this case, no.

Under the hood, Boost's interprocess/mapped_region.hpp is using mmap which will return you a pointer to you memory mapped region.

You only need to use volatile if you suspect another process (or hardware) might be writing to your file.

(That would be the most basic synchronization you should provide, because volatile enforces a read from memory on each access. If you have control over the processes, you may try more advanced synchronization like a semaphore.)

AndyG
  • 39,700
  • 8
  • 109
  • 143
  • So for this particular use case, which is my process being the **only one writing to the file**, then `volatile` isn't required, and neither is any more advanced synchronisation, such as semaphore etc? – Steve Lorimer Aug 08 '17 at 02:21
  • Right you are, sir – AndyG Aug 08 '17 at 02:29