1. Overview
Shared memory is an IPC (inter-process communication) mechanism that Unix-based operating systems, including Linux, support. It’s a type of memory that multiple processes can use simultaneously to communicate with each other.
In this tutorial, we’ll first discuss two forms of shared memory, namely memory-mapped files and anonymous memory. Firstly, we’ll give brief information about them. We’ll discuss the mmap() system call in this context. Then, we’ll inspect them using examples.
2. Brief Information About Shared Memory
Shared memory is one of the fastest IPC mechanisms because processes using shared memory don’t have to copy data between each other. A process can access the shared memory as if the memory is allocated within its own address space. It doesn’t perform any system calls into the kernel. Processes accessing shared memory must use synchronization primitives, such as semaphores, to prevent race conditions.
One form of shared memory is memory-mapped files. Once multiple processes map the same file to their address spaces, they can access the contents of the file and update the file simultaneously using the mapped memory directly. Another form of shared memory is anonymous memory. It refers to a shared memory region that a program allocates without associating it with a file or persistent storage mechanism.
3. The mmap() System Call
The function to map a file into the address space of a process is the mmap() system call:
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset)
The first parameter, addr, is the starting address of the mapping within the process. Setting the address to a null pointer tells the kernel to choose the starting address. The second parameter, len, is the number of bytes that will be mapped to the address space of the process. The third parameter, prot, specifies the rights on the mapping using some constants. For example, PROT_READ | PROT_WRITE means that the process has read-write access.
The fourth parameter, flags, is also specified by using some constants. For example, using MAP_SHARED means that modifications applied to the mapped memory by the calling process are available to other processes that are accessing the same memory. The changes are also applied to the underlying object, i.e., to the mapped file. If we use MAP_PRIVATE, on the other hand, modifications applied to the mapped memory by the calling process aren’t available to other processes accessing the same memory. The underlying object isn’t changed.
The fifth parameter, fd, is the file descriptor of the mapped file. Finally, the sixth parameter, offset, specifies the offset from the beginning of the file from which len bytes will be mapped, where len is the second parameter.
mmap() returns the starting address of the mapped region.
4. Code Example Using a Memory-Mapped File
We’ll use the following C program, memory_mapped_file.c, for examining memory-mapped files:
#include <stdio.h>
#include <unistd.h>
#include <semaphore.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/wait.h>
void write_to_shared_memory(int *ptr, int value, int count, sem_t *sem) {
for (int i = 0; i < count; i++) {
sem_wait(sem);
/* Assign the value to the memory pointed by ptr */
*ptr = value;
sem_post(sem);
}
}
void main(int ac, char **av) {
int fd, start, zero = 0, status, count = 100000;
int *ptr;
char sem_name[] = "my_named_semaphore";
/* Create a binary file with name output_file */
fd = open("output_file", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
/* Initialize the file using an integer with value 0 */
write(fd, &zero, sizeof(zero));
/* Map the file to the address space of the process */
ptr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
/* Close the file */
close(fd);
/* Create a POSIX named semaphore */
sem_t *sem = sem_open(sem_name, O_CREAT, S_IRUSR | S_IWUSR, 1);
/* Spawn a child process */
if (fork() == 0) {
/* This is the child process */
/* Child process writes 1 to the shared memory */
write_to_shared_memory(ptr, 1, count, sem);
/* Child process exits here */
exit(0);
}
/* Parent process writes 2 to the shared memory */
write_to_shared_memory(ptr, 2, count, sem);
/* Wait for the child process */
wait(&status);
/* Close the semaphore */
sem_close(sem);
/* Delete the named semaphore */
sem_unlink(sem_name);
exit(0);
}
4.1. Explanation of the Source Code
Let’s break down the code and understand it:
void write_to_shared_memory(int *ptr, int value, int count, sem_t *sem) {
for (int i = 0; i < count; i++) {
sem_wait(sem);
/* Assign the value to the memory pointed by ptr */
*ptr = value;
sem_post(sem);
}
}
The function in the code snippet above, write_to_shared_memory(), takes four parameters. It writes the integer, value, to the memory pointed by ptr. It writes the same value count times in a for loop. A parent process and a child process write to the memory simultaneously. Therefore, we must synchronize the processes. The fourth parameter, sem, is the semaphore. We use the sem_wait() and sem_post() functions to decrement and increment the semaphore’s value.
We’ll create a file, and map it to the address space of the process:
/* Create a binary file with name output_file */
fd = open("output_file", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
/* Initialize the file using an integer with value 0 */
write(fd, &zero, sizeof(zero));
/* Map the file to the address space of the process */
ptr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
/* Close the file */
close(fd);
In the code snippet above, we first create a binary file, output_file, using the statement fd = open(“output_file”, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR). The open() function returns a file descriptor, fd. Then, we initialize the memory with 0 using write(fd, &zero, sizeof(zero)). We map the file to the address space of the process using the ptr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0) statement. Finally, we close the file using close(fd).
We’ll spawn a child process now:
/* Spawn a child process */
if (fork() == 0) {
/* This is the child process */
/* Child process writes 1 to the shared memory */
write_to_shared_memory(ptr, 1, count, sem);
/* Child process exits here */
exit(0);
}
In the code snippet above, we spawn a child process using fork(). We map the file into the address space of the parent before forking the child process. Therefore, the child process can also access the same memory region with the same privileges as the parent process. This is a common usage to share memory between a parent and a child process.
The child process writes the value, 1, to the shared memory using the statement write_to_shared_memory(ptr, 1, count, sem). We write the same value count times, which is 100000, to ensure that the parent and child processes write to the same memory segment simultaneously. Finally, the child process exits using exit(0).
Now, the parent process will write to the shared memory:
/* Parent process writes 2 to the shared memory */
write_to_shared_memory(ptr, 2, count, sem);
/* Wait for the child process */
wait(&status);
/* Close the semaphore */
sem_close(sem);
/* Delete the named semaphore */
sem_unlink(sem_name);
exit(0);
In the code snippet above, after forking the child process, the parent process writes the value, 2, to the shared memory using write_to_shared_memory(ptr, 2, count, sem). Then, the parent process waits for the child process using wait(&status). Finally, the parent process exits after closing and deleting the semaphore using sem_close(sem) and sem_unlink(sem_name), respectively.
4.2. Compiling and Running the Example
We’ll use gcc to compile the program:
$ gcc memory_mapped_file.c –o memory_mapped_file –lpthread
The -o flag of gcc specifies the name of the executable, which is memory_mapped_file in our example. We must link the executable with libpthread.so because of using semaphores. Having created the executable memory_mapped_file, let’s run it:
$ ./memory_mapped_file
$ ls output_file
output_file
There are no errors, and we created the binary file, output_file, successfully. Let’s check the content of it using the od command:
$ od –D –An output_file
1
The od command dumps the content of a binary file in octal format by default. The -D option dumps the content of the file as a decimal number. The -An option, on the other hand, prevents printing the byte offsets. Hence, the output is 1, which means that the child process was the last writer. Let’s run the executable a few more times, and check the result:
$ ./memory_mapped_file
$ od –D –An output_file
1
$ ./memory_mapped_file
$ od –D –An output_file
2
As it’s apparent from the output, the parent process was the last writer in the second run since the output is 2.
5. Code Example Using Anonymous Memory
We’ll use the following C program, anonymous_memory.c, for examining anonymous memory:
#include <stdio.h>
#include <unistd.h>
#include <semaphore.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/wait.h>
void write_to_shared_memory(int *ptr, int value, int count, sem_t *sem) {
for (int i = 0; i < count; i++) {
sem_wait(sem);
/* Assign the value to the memory pointed by ptr */
*ptr = value;
sem_post(sem);
}
}
void main(int ac, char **av) {
int start, zero = 0, status, count = 100000;
int *ptr;
char sem_name[] = "my_named_semaphore";
/* Map the file to the address space of the process */
ptr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);
/* Create a POSIX named semaphore */
sem_t *sem = sem_open(sem_name, O_CREAT, S_IRUSR | S_IWUSR, 1);
/* Spawn a child process */
if (fork() == 0) {
/* This is the child process */
/* Child process writes 1 to the
write_to_shared_memory(ptr, 1, count, sem);
/* Child process exits here */
exit(0);
}
/* Parent process writes 2 to the shared memory */
write_to_shared_memory(ptr, 2, count, sem);
/* Wait for the child process */
wait(&status);
/* Close the semaphore */
sem_close(sem);
/* Delete the named semaphore */
sem_unlink(sem_name);
printf("The last written value to shared memory: %d\n", *ptr);
exit(0);
}
5.1. Explanation of the Source Code
The source code for anonymous memory is like the one for memory-mapped file**. We don’t use a file in this case as anonymous memory isn’t associated with a file**. Therefore, the statements for opening, initializing, and closing the file don’t exist.
This time, we map the anonymous memory to the address space of the process using the statement below:
/* Map the file to the address space of the process */
ptr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);
We pass the MAP_ANON flag additionally to specify that we want to use anonymous memory. We use -1 for the file descriptor. mmap() ignores the fifth and sixth parameters, fd and offset, while using anonymous memory. However, some implementations may require the file descriptor to be -1. Therefore, we set it to -1 for portability.
The child process inherits the anonymous memory from the parent process. Since the anonymous memory is shared because of the MAP_SHARED flag, the child process has read-write access.
Finally, the parent process displays the value in the anonymous memory using the code snippet below:
printf("The last written value to shared memory: %d\n", *ptr);
5.2. Compiling and Running the Example
We’ll again use gcc to compile the program:
$ gcc anonymous_memory.c –o anonymous_memory –lpthread
Having created the executable anonymous_memory, let’s run it a few times:
$ ./anonymous_memory
The last written value to shared memory: 2
$ ./anonymous_memory
The last written value to shared memory: 1
As it’s apparent from the output, the last writer was the parent process in the first run. However, the last writer was the child process in the second run.
6. Conclusion
In this article, we learned the two forms of shared memory, memory-mapped files, and anonymous memory. Firstly, we discussed shared memory briefly. We learned that it’s a fast IPC mechanism. Then, we saw that the mmap() system call provides a mechanism for mapping a file to the address space of a process. Multiple processes can use the same mapping. The mapped memory is backed by the file in this case.
Afterward, we learned about anonymous memory. Multiple processes such as parent and child processes can still use the same memory. However, it isn’t backed by a file in this case. Finally, we saw two examples showing the usage of memory-mapped files and anonymous memory.