blog/content/post/shared-memory.md

5.7 KiB

title date draft
Shared Memory: The Basics with shmget 2019-05-18T07:44:24-04:00 false

Shared Memory - The Fastest Way for Two Processes to Talk

Shared memory is one of the many choices available to us for IPC in C. The program asks the kernel for a shared memory segment, and the kernel sets one up, initializing all data values and data structures to 0. Once that step is complete, any process can attach to it and use it in meaningful ways! The only danger of this approach is obviously it is on the application programmer to synchronize the data stored in that segment, but otherwise it is very fast.

Setting up our shared memory.

Prerequisites - Forking

For our shared memory to work, and be meaningful, we must set up a child process that we can use to access the memory in parallel with the parent. To do this, we will use fork. Fork is a wonderful and powerful system call that allows us to create a new process that is an exact copy of the parent. The kernel will set up the new process, copy the parents address space into the address space of the new child, and set the new child free to run with a new process identifier (PID). An interesting point to note is that fork technically returns twice, once in the parent, and once in the child. The return value of fork in the parent is the child's PID. The return value in the child is 0. We use these return values to determine which process we are currently executing in, in order to make decisions based on who we are.

Let's begin then, with this small sample program that does nothing but fork a child that will sleep.

#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv)
{
  pid_t child_pid;
  child_pid = fork ();
  if (child_pid < 0)
    {
      printf ("Error forking, exiting the program\n");
      exit (1);
    }
  else if (child_pid == 0)
    {
      sleep (1);
      printf ("Hello from the child %d!\n", getpid ());
      exit (0);
    }
  else
    {
      printf("Hello from the parent %d!\n", getpid ());
      wait (NULL);
      printf ("Child exited!\n");
    }
}

The program is largely uninteresting, but does what we need it to do. It will start execution by forking, checks to see if there was an error in forking, then sets up the work for a child and the parent to do. The parent uses the system call wait with a NULL pointer to wait for ANY child to exit (in our case just the one child) before the parent exits. This is a good practice and allows us to ensure that no zombie processes are created because we didn't wait for the children. Let's run our sample program, just to see what happens.

$ ./a.out
Hello from the parent 9281!
Hello from the child 9282!
Child exited!

Excellent, now we can focus on setting up shared memory.

Setting up shared memory

For this article, I am going to use the functions shmget, shmat, and shmdt as they are what I'm familiar with. I am aware of more modern techniques like mmap but discussing those is outside the scope of this article.

In order to get a segment of shared memory from the kernel, we first need a unique identifier for the memory segment. The kernel needs an IPC key to initialize and retrieve the segment each time someone asks for it. We create this by using ftok. Once the key has been created, we must use shmget to actually ask for the memory and retrieve an integer identifier to the memory segment. With this identifier, we can then use the functions shmat and shmdt to attach and detach to the memory at will! Here is what our test program looks like modified to include shared memory functions.

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char *argv)
{
  pid_t child_pid;
  child_pid = fork ();
  
  /* Our new IPC variables */
  key_t key;
  int shmid;
  
  int *ipcPointer;
  
  if (child_pid < 0)
    {
      printf ("Error forking, exiting the program\n");
      exit (1);
    }
  else if (child_pid == 0)
    {
      sleep (2);
      key = ftok ("shmfile", 65);
      shmid = shmget (key, sizeof (int), 0666|IPC_CREAT);
      printf ("Hello from the child %d!\n", getpid ());

      /* Attach to shared memory and read */
      ipcPointer = (int *) shmat (shmid, 0, 0);
      printf("Child shm contents: %d\n", *ipcPointer);
      shmdt(ipcPointer);
      exit (0);
    }
  else
    {
      printf("Hello from the parent %d!\n", getpid ());
      key = ftok ("shmfile", 65);
      shmid = shmget (key, sizeof (int), 0666|IPC_CREAT);

      /* Attach to shared memory and write */
      ipcPointer = (int *) shmat (shmid, (void *) 0, 0);
      printf("Parent shm contents: %d\n", *ipcPointer);
      printf("Parent writing 3 to shm\n");
      *ipcPointer = 3;
      printf("Parent wrote to memory\n");

      /* Detach from shared memory */
      shmdt(ipcPointer);
      wait (NULL);
      printf ("Child exited!\n");
    }
}

What does this look like running? Let's find out.

$ sudo ./a.out
Hello from the parent 10395!
Parent shm contents: 3
Parent writing 3 to shm
Parent wrote to memory
Hello from the child 10396!
Child shm contents: 3
Child exited!

I had to use sudo here due to coredumps from permission errors when writing to the shared memory buffer, I will revisit this post in order to correct errors I've had when setting up permissions to shared memory. The basic structure is intact though, have both the parent and the child possibly create and definitely attach to shared memory segments, then have the parent write to the shared memory while the child reads it. Thanks for reading, I hope you enjoyed this post!