| by Arround The Web | No comments

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 the 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 the shared memory is the most versatile method for doing so. This method consists of allocating a block of memory that is shared between the processes and in which data and variables can be read or written by one or another. Shared memory can be allocated and used with the 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 that are 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 see in the following image, we need to include the headers that define them and the variables that are used by them in our code. In the following, we will 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 the 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 that is shown in the following figure:

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

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

Methods to Allocate the Shared Memory with the 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 a 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 name that is specified 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 the 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 in the following:

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 shown in the following:

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 address that is specified by the “addr” input argument and has the length that is specified in 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 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.

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

In this example, we create the “process1” and “process2” programs which perform the read and write operations in the 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 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 is 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 as the first argument with the name of the object 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 666 value 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. Then, we pass the size in bytes that we want to assign to the 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 the other processes. In the fifth argument, we pass the “sh_id” object identifier. In the last argument, we specify the value of 0 to map the entire file. As an output argument, we send the “Ptr_1” pointer which we 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 previously described 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 and 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 that are 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 went through the required steps to create a shared memory object file, assign it with a size, and allocate it to the process’ virtual address space. We also discussed the functions that are used for this purpose and explained how their input and output arguments and method call are composed. We then applied what we learned in an example in which we created two programs that allocated the shared memory and thus established a communication between them.

Share Button

Source: linuxhint.com

Leave a Reply