Setting up shared memory with the techniques in this section is very similar to using memory-mapped files, with the exception that there is no pre-defined persistent file. That is, shared memory can be set up to allow for immediate data exchange between processes without having a persistent record of the communication. Figure 3.7.1 illustrates the logical structure of a shared memory region that is mapped into two different processes.
For applications that exchange large amounts of data, shared memory is far superior to message-passing techniques like message queues, which require system calls for every data exchange. The major disadvantage of shared memory is that the processes must take extra precaution to synchronize access to the region. If process A writes into the shared region, that might cause unstable behavior in process B, or vice versa.
Bug Warning
When a shared memory region is established in two or more processes, there is no guarantee that the
regions will be placed at the same base address. For instance, one process might have the shared
region starting at address 0x40000000
while the other process uses 0x50008000
. It is
critical to understand that these two addresses refer to the exact same piece of data. So storing
the number 1 in the first process’s address 0x40000000
means the second process has the value
of 1 at 0x50008000
. The two (different) addresses refer to the exact same location.
Given that the base addresses are different, all elements in the region must also have different
addresses. The implication of this is that shared memory regions cannot use pointers to refer to
other parts of the region. For instance, assume the shared memory contains a linked list with the
first node at the beginning of the region. The next node appears 256 bytes later. Using the
addresses above, in one process, 0x40000000
must contain a pointer to address 0x40000100
;
in the other address, the pointer must point to 0x50008100
, given the different base address.
This means that the beginning of the shared memory region must simultaneously store two different
values, which is impossible.
The solution is to avoid the use of pointers within the shared memory region, using pointer
arithmetic instead. That is, if char *baseaddr
is declared to point to the base address of the
region, then the two processes would need to use *(baseaddr + 256)
to refer to the next node of
the linked list. Or, as an alternative, avoid linked lists entirely and use a contiguous data
structure instead.
The POSIX interface for shared memory is very simple, consisting of two primary functions. The
shm_open()
takes the POSIX IPC object name, a bit-mask of flags (oflag
) and a permission
mode to apply new objects. [1] Similarly, shm_unlink()
deletes the shared memory object.
C library functions – <sys/mman.h>
int shm_open (const char *name, int oflag, mode_t mode);
int shm_unlink (const char *name);
The following code sample uses a struct
declared as follows:
1 2 3 4 5 | struct permission {
int user;
int group;
int other;
};
|
Code Listing 3.10 sets up a POSIX shared memory object, which is identified by a given name. Note
that the call to shm_open()
creates the object but does not specify a size; instead,
ftruncate()
resizes the object to be large enough to store one instance of the struct
permission. Next, the process maps the shared memory object into memory with mmap()
before using
fork()
to create a child process (which inherits both the shared memory identifier and the
memory mapped region). The child writes to the region before exiting; the parent waits until the
child exits and reads the data.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | /* Code Listing 3.10:
Using POSIX shared memory to exchange data between processes
*/
/* Create unsized shared memory object;
return value is a file descriptor */
int shmfd = shm_open ("/OpenCSF_SHM", O_CREAT | O_EXCL | O_RDWR, S_IRUSR | S_IWUSR);
assert (shmfd != -1);
/* Resize the region to store 1 struct instance */
assert (ftruncate (shmfd, sizeof (struct permission)) != -1);
/* Map the object into memory so file operations aren't needed */
struct permission *perm = mmap (NULL, sizeof (struct permission),
PROT_READ | PROT_WRITE, MAP_SHARED, shmfd, 0);
assert (perm != MAP_FAILED);
/* Create a child process and write to the mapped/shared region */
pid_t child_pid = fork();
if (child_pid == 0)
{
perm->user = 6;
perm->group = 4;
perm->other = 0;
/* Unmap and close the child's shared memory access */
munmap (perm, sizeof (struct permission));
close (shmfd);
return 0;
}
/* Make the parent wait until the child has exited */
wait (NULL);
/* Read from the mapped/shared memory region */
printf ("Permission bit-mask: 0%d%d%d\n", perm->user, perm->group, perm->other);
/* Unmap, close, and delete the shared memory object */
munmap (perm, sizeof (struct permission));
close (shmfd);
shm_unlink ("/OpenCSF_SHM");
|
Combining shm_open()
and mmap()
in this way is a common technique that often confuses
novices. Specifically, it seems redundant to use both IPC mechanisms, because the same task could be
accomplished with either shm_open()
or mmap()
(not both). While it is true that both are not
required, combining them leads to certain advantages:
- The
shm_open()
parameters adhere to the standard POSIX IPC conventions. This allows the processes to use POSIX names to identify the objects, rather than creating files in arbitrary locations.- Using
shm_unlink()
provides a safety check that is not guaranteed with creating and mapping arbitrary files. Specifically,shm_unlink()
will not delete the object immediately if any other process also has an open connection; this delay allows decreases the likelihood of other processes experiencing random and unexpected failures. In contrast, if an arbitrary non-IPC file was used for themmap()
, the timing of the deletion would be determined by the underlying file system; this introduces unnecessary maintenance and possibly restricts the portability of the program.- Using
mmap()
allows the developer to cast the shared memory object to a more meaningful data structure type and to avoid using file operations. Specifically, observe that the return type fromshm_open()
is a file descriptor, which does not provide any information about the type of data stored in the object. By mapping it into memory, we can cast the pointer returned and access theuser
,group
, andother
fields contained in thestruct
.
[1] | As a minor point of terminology, all forms of POSIX IPC, including shared memory, are referred to as objects. In contrast, System V shared memory is called a “segment” for historical purposes. |