3.7. Shared Memory¶

Figure 3.7.1: A shared memory region present in two processes

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.

3.7.1. POSIX Shared Memory¶

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);
Open a connection to a POSIX shared memory object.
int shm_unlink (const char *name);
Delete a POSIX shared memory object.

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 the mmap(), 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 from shm_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 the user, group, and other fields contained in the struct.
 [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.