From the shm_open
man page:
A new shared memory object initially has zero length. The size of the object can be set using ftruncate(2). [...] The shm_open() function itself does not create a shared object of a specified size because doing so would duplicate an extant function that sets the size of an object referenced by a file descriptor.
Doesn't this expose the application to a race condition? Consider the following pseudo-code:
int fd = shm_open("/foo", CREATE);
if ( fd is valid ) {
// created shm object, so set its size
ftruncate(fd, 128);
} else {
fd = shm_open("/foo", GET_EXISTING);
}
void* mem = mmap(fd, 128);
Since the shm_open
and ftruncate
calls (together) are not atomic, you could have a race condition whereby one process calls shm_open
(CREATE
case) but, before calling ftruncate
, another process calls shm_open
(GET_EXISTING
case) and attempts to mmap
the object of 0 size and possibly even write to it.
I can think of two ways to avoid this race condition:
Use an IPC mutex/semaphore to make the whole thing synchronized, or...
If it's safe (per POSIX), call ftruncate
in both the CREATE
and GET_EXISTING
cases.
Which is the preferred method for avoiding this race condition?
Your approach (calling ftruncate
from both) should work, but you need a way to synchronize use of the contents of the shared memory segment anyway. As the memory is initially empty (zero-filled) and thus does not contain a valid synchronization object, unless you're going to roll your own with atomics, you need a secondary form of synchronization anyway for controlling access to the shared memory.
I would think normally, rather than having multiple process racing to create-or-open a shared memory segment with a fixed name, you'd want to have an owner process responsible for creating a segment with a random name, using O_EXCL
to avoid random or malicious collisions, and then passing the name, once you've successfully opened it, sized it, and created synchronization objects in it, to the other processes that need to access it.
As @R. alluded to, another issue here is that having created the file there is still a window before contents, such as a mutex, are initialised and ready for use.
A slightly different solution to the above is:
Try to open(). If open() succeeds, simply map() and use with the necessary guarantee (see below) that the contents are already initialised and good to go. If open() fails, create and initialise a temporary file, then try to hard link() the temporary file as the desired file and unlink() the temporary name.
If link() succeeds, we have now made the initialised file available for ourselves and other processes. If link() fails with EEXIST, another process got there first (unlike rename(), link() fails if the target name exists). Either way, our repeated open() should now succeed with an initialised ready to use file.
With this strategy a race condition clearly exists for initialisation of the temporary file, however provided that the initialisation process is idempotent, not overly expensive on resources, and processes each choose a unique temporary file, this is of no consequence. If multiple initialisation could be an issue, a solution is to split initialisation into a two stage process, with stage one being just a mutex in the file for use to guard against multiple initialisation of the rest of the file during the second stage.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With