C Programming

POSIX Shared Memory in C Language

The interprocess communication (IPC) is a concept that encompasses several methods that are used by operating systems to share the data or interact between processes. The most commonly used IPC methods are signals, pipes, and the use of shared memory.

When it comes to sharing the data quickly between processes, using a shared memory is the most versatile method for doing so. This method consists of allocating a block of memory that is shared between processes and in which data and variables can be read or written by one or another. Shared memory can be allocated and used with System V system calls or POSIX system calls.

In this Linuxhint article, you’ll learn how to allocate and use the shared memory with POSIX system calls. We’ll look at the functions used for this purpose and show you how to create the necessary variables to use in their input arguments. Then, using a practical example with code snippets and pictures, we’ll show you how to create a shared memory between two simple processes and establish a communication between them.

Headers and Libraries Needed to Use the POSIX Shared Memory

In order to use the function group that we will see in the following, we need to include the headers that define them and the variables that are used by them in our code. Let’s see how to insert the necessary headers correctly:

#include <fcntl.h>
#include <sys/shm.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>

When we compile the programs that use the POSIX shared memory functions, the compiler may print errors if we use an outdated version. In this case, we must manually link the library that contains the functions that we want to use. Otherwise, the compiler will print errors like the one in the following figure:

The following command shows in green the correct way to link the needed library to compile the programs that use the shared memory:

~$ gcc Documents/name.c -o out -lrt

Method to Allocate the Shared Memory with POSIX System Call Functions

The use of shared memory with POSIX system calls is based on input and output operations on an object. A shared memory object is a temporary file that is normally created in the “/dev/shm” directory. Any program that wants to share the memory with another process must create or open the shared memory object and map it in its virtual memory address space.

To create a shared memory object, we must perform the following three steps, each of which involves using a system call function.

Step 1: Create a Shared Memory Object with the Shm_open() Function

The first step in allocating a shared memory area is to create or open the temporary file that will be the object on which we perform the read and write operations. For this purpose, POSIX provides the shm_open() function whose syntax is shown in the following:

int shm_open(const char *name, int flags, mode_t mode);

The shm_open() function opens or creates a shared memory object and returns an “fd” descriptor as a result. The name input argument may contain the path of the file to be opened or the name of the object to be created. In the last case, the temporary file is created in the “/dev/shm” directory with the specified name in the name string.

The operation of shm_open() is identical to that of the open() function since both use the same flags and modes that are defined in the “stat.h” and “fcntl.h” headers in the flags and mode input arguments. The flags input argument specifies the attributes that are used to open the file such as whether it is read-only or read-write, etc.

The mode argument specifies the access permissions for the file. For example: users, process group, etc.

Step 2: Allocate the Size of Shared Memory with the Ftruncate() POSIX System Call Function

When we create a shared memory object with the shm_open() function, its size is 0 bytes. Therefore, we must specify the number of bytes that we want to allocate to the shared memory area. The ftruncate() function allocates the size in bytes that is specified in length to the file that is specified by its descriptor in the “fd” input argument. The syntax of ftruncate() is described as follows:

int ftruncate(int fd, off_t length);

Step 3: Map a Shared Memory Area with the Mmap() POSIX System Function

Once the object is created and its size is established, the next step is to map it into the virtual memory of the process and get its pointer to access it. This is done with the mmap() function whose syntax is as follows:

void *mmap(void *addr, size_t length, int prot, int flags,

int fd, off_t offset);

The mmap() function creates a memory map in the virtual address space of the calling process. The memory area is created at the specified address by the “addr” input argument and has the specified length. To avoid overwriting the occupied memory areas, it is recommended to send the “addr” argument with 0. This way, the system allocates the address of a free memory area next to the last used.

The “prot” input argument specifies the permissions to be assigned to the memory area, whether it may be written to, read, or executed, or whether the access should be denied.

The mmap() function can create two types of mappings, shared or private. This is selected by the MAP_PRIVATE or MAP_SHARED flag in the flags input argument. If you want to map the memory that is shared, you must select the MAP_SHARED flag.

Exampleꓽ How to Create a Shared Memory Area to Write to and Read from by Different Processes with POSIX System Calls

In this example, we will create the “process1” and “process2” programs which performs read and write operations in shared memory.

In this case, they share a structure because that is the way to include multiple variables in a block of shared memory, but the same method can be applied to a single variable, an array, or any other data type that is supported by the C language.

Step 1: Include the Headers

Everything we do from here on, except for step 6, we copy into two files with the “.c” extension, corresponding to “process1” and “process2”, respectively. We start with these two files and add to them the “stdio.h”, “string.h”, “fcntl.h”, “sys/shm.h”, “unistd.h”, “signal.h”, “sys/stat.h”, and “sys/mman.h” headers.

Step 2: Declare the Variables and Structures

In this step, we open a main function of type void main() and create a name map structure in it. In this structure, we declare two members – the integer flag and the array buffer – with 1024 elements of type char. The members of this structure are the variables that our programs will share in the memory.

Next, we create a pointer to the map structure which we call “Ptr_1” and the “sh_fd” integer which will be the descriptor of the shared memory object.

Step 3: Create the Object

In this step, we create a shared memory object. To do this, we call the shm_open() function and pass the name of the object as the first argument which, in this case, we call as “shared_1”. In the second argument, we pass the OR logical operation between the O_ CREAT and O_RDWR flags which tells the function to create the “shared_1” file and give it the read and write attributes. In mode, we send the value 666 which is the result of the OR operation between the S_IWOTH, S_IROTH, S_IWGRP, S_IRGRP, S_IWUSR, and S_IRUSR flags. This set of flags grants the read and write permissions to the user, the process group, and all other processes in the system. As the output argument of this call, we send the “sh_fd” integer.

Step 4: Assign a Size

In this step, we call the ftruncate() function, passing the “sh_fd” identifier as the first argument that is returned by the shm_open() function and the size in bytes we want to assign to object as the second argument which, in this case, is the sizeof() of the map structure.

Step 5: Map the Object

In this step, we map the object by calling the mmap() function, passing the value of 0 as the first argument so that the system assigns a mapping address to the object. The second argument is the sizeof() of the map structure. The third argument is an OR operation between the PROT_WRITE and PROT_READ flags so that we can read and write the mapped object. As the fourth argument, we pass the MAP_SHARED flag to make the map visible and allow the operations on it by other processes. In the fifth argument, we pass the “sh_id” object identifier and specify the value of 0 in the last argument to map the entire file. As an output argument, we send the “Ptr_1” pointer which we will use later to access the shared memory.

Here is the code to create a shared memory area, specify its size in bytes, and map it:

//Step 1:
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/shm.h>
#include <unistd.h>
#include <signal.h>
#include <sys/mman.h>
#include <sys/stat.h>

//Step 2:
void main() {

struct map {
int flag;
char buffer[1024];
};

struct map * Ptr_1;
int sh_fd;

//Step 3:
sh_fd = shm_open("shared_1", O_CREAT | O_RDWR, 0666);

//Step 4:
ftruncate(sh_fd, sizeof(struct map));

//Step 5:
Ptr_1 = (struct map*) mmap(0, sizeof(struct map), PROT_READ | PROT_WRITE, MAP_SHARED, sh_fd, 0);

}

Up to this point, the code that we describe is the same for both programs, but they differ from here on. Next, we see step 6 for each program.

Step 6.1: Shm1 (Process 1)

In this step, process1 goes into an infinite loop where we use the stdin() function to input the data from the command console. The characters that are entered are written to the shared memory buffer into the map structure. If you then press ENTER, the flag is activated and the cycle repeats. Then, we see the complete code for “shm1”.

//Step 1:
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/shm.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>

//Step 2:
void main() {

struct map {
int flag;
char buffer[1024];
};
struct map * Ptr_1;char c;
int sh_fd;

//Step 3:
sh_fd = shm_open("shared_1", O_CREAT | O_RDWR, 0666);

//Step 4:
ftruncate(sh_fd, sizeof(struct map));

//Step 5:
Ptr_1 = (struct map*)mmap(0, sizeof(struct map), PROT_READ | PROT_WRITE, MAP_SHARED, sh_fd, 0);

//Step 6:
while (1)
  {
  scanf ("%s",Ptr_1->buffer);
  Ptr_1->flag=1;
  }

}

Step 6.2: Shm2 (Process 2)

In this step, the program enters an infinite loop in which an “if” condition analyzes the state of the flag shared variable. If this flag is set, we use the printf() function to print the contents of the buffer that “shm1” wrote and set the flag to 0. Then, we see the complete code for “shm2”.

//Step 1:
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/shm.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>

//Step 2:
void main() {

struct map {
int flag;
char buffer[1024];
};
struct map * Ptr_1;char c;
int sh_fd;

//Step 3:
sh_fd = shm_open("shared_1", O_CREAT | O_RDWR, 0666);

//Step 4:
ftruncate(sh_fd, sizeof(struct map));

//Step 5:
Ptr_1 = (struct map*)mmap(0, sizeof(struct map), PROT_READ | PROT_WRITE, MAP_SHARED, sh_fd, 0);

//Step 6:
while (1)
     {  
      if (Ptr_1->flag==1){
         printf ( "%s\n" , Ptr_1->buffer);
         memset (Ptr_1->buffer, 0,1024);
         Ptr_1->flag = 0;
         }
     sleep(1);
     }
}

Now, we compile both codes and run each from a different terminal. From a third terminal, we can see the shared memory files for the four processes, their identifiers, and the amount of memory used by executing the following commandː

~$ lsof +D /path

As we learned in the description of the shm_open() function, if we specify only the filename, the file will be created in the “/dev/shm” directory. Now, we execute the command that is described previously and specify this path. The following image shows the execution of this command with the “shm1” and “shm2” processes that use the “shared_1” file to share the memory.

Once this is verified, we go to the terminals that run the processes and write a message to “shm1”. Everything that we write here will be transferred to the shared buffer. When we press ENTER, the flag is set. When “shm2” detects this, it reads the contents of the shared buffer and displays it on the screen. The following image shows how the characters entered in “shm1” are written to the shared memory buffer which is then read by “shm2”.

Conclusion

In this Linuxhint article, we learned how to implement the shared memory for inter-process communication using the POSIX system call functions. We go through the steps that are required to create a shared memory object file, assign it with a size, and allocate it to the process’s virtual address space. We also discuss the functions used for this purpose and explain how their input and output arguments and method call are composed. We then apply what we learned in an example in which we create two programs that allocate the shared memory and thus establish the communication between them.

About the author

Julio Cesar

Julio Cesar is a 42 years old programmer with 8 years of experience in embedded systems development, 6 years developing firmware for user interfaces in C and C++. Additionally he has 2 years of experience developing scripts for network devices and 3 years as developer of high frequency PCB (Printed Circuit Board).