blog/content/post/shared-memory.md

158 lines
5.7 KiB
Markdown

---
title: "Shared Memory: The Basics with shmget"
date: 2019-05-18T07:44:24-04:00
draft: 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.
```c
#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.
```c
#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!