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.
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).
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.
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:
shm
stuff to create a shared memory segment of
sufficient sizeT
-container” type;T
-container
“in-place” at the pointed at address, this will include the construction
of a T
object thereinshared_ptr
constructor.
T
-container, so we
use the aliasing constructor to obtain a pointer to the contained
T
.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.