Inter-process Shared Pointers

José Goudet Alvim

2024-02-05

Yet again taken by an intrusive thought I’ve found myself wondering if shared memory segments can be effectively integrated into the C++ memory model to provide a reasonably idiomatic version of mmap etc. in modern C++. The results are, sadly, positive.

Refresher on Shared Memory

POSIX specifies a shared memory model, see shm_overview(7) for more details but the gist is it allows you to (shm_)open a file descriptor corresponding to a region in memory (which also gets mapped to a file in /dev/shm in linux); you can then truncate that file to a specified size, you can (m)map that file descriptor to a region of your process’ virtual memory. Multiple processes can do that on the same key/file name and thus you have a model wherein multiple processes see the same memory segment (evidently at different positions in their virtual address space).

Refresher on std::shared_ptr

Shared pointers are a part of the C++ standard, they are managed-lifetime pointers which guarantee the destruction of objects pointed to by them through reference counting. While I do not think it is specified to be implemented this way, in practice I believe the obvious implementation is that a std::shared_ptr<T> points to a “control block” in the heap which itself contains a T* as well as a reference counter plus some other stuff (like a weak reference counter, probably a mutex for poking around in those etc). The invariance being maintained is that copies of a shared pointer increment the reference counter, and as those copies die, the counter decreases.

When the counter reaches zero, a deleter, possibly stored within the control block, is invoked on the T*, then the control block is deleted provided no weak references exist.

Mashing them together

Shared pointers have many little nuances, one of which is that you can give its constructor a pointer to T along with a shared pointer of an unrelated type and pinky promise that you’ll make sure the T* stays around for as long as the provided shared pointer is valid. This is the aliasing constructor, which effectively says that references to your T must keep the other object alive. This is useful for creating a shared pointer that refers to a field of something you have a shared pointer to. When all references are gone, the original (from the shared pointer you passed in) deleter is invoked on its pointer.

The other idiosyncrasy is that you can pass custom deleters to shared pointer constructors along with a pointer you wish them to manage. Now you’re not expected to do any of this; all of the properties we have listed are sort of the “advanced” foot-shotgun interface of those pointers, usually you just want to std::make_shared<T>(params...) but this is no ordinary program, and there is no one to stop us in code review!

The sketch is as follows:

There are some subtleties involved in making this work properly, as the program must also be aware of how many intra-process references there are, which therefore cannot be done within the shared memory segment, so as to know when to munmap the address. Specifically when a process is done with a shared object, it may unmap it from its virtual memory, but it may also (shm_)unlink that file entirely, thereby destroying the shared object once all other processes have unmapped their segments.

Our system must recognize when to unmap but also when to unlink and these are not the same event. So my current solution isn’t truly finished enough to publish. I may edit it if I ever get it to the point where it’s not wrong, but merely awful.