Shared Memory, in Code That Runs
Chapter Twenty-Two
Syllabus topic Module 1, "Processes - Inter-process Communication"; Computer Science Practical 3, Module 1, "Process Communication using Shared Memory"
Pages 85 to 88 of 452
In one line
Shared memory is one piece of physical memory that appears in two processes' address spaces at once, so that a write by one is immediately visible to the other.
Why it looks the way it does
Chapter fourteen said each process has its own memory. The memory management unit of Chapter sixty eight is what makes that true: it maps each process's addresses to different physical memory. Shared memory is that same machinery used deliberately the other way: the kernel maps one piece of physical memory into two processes' page tables, at whatever address each of them happens to have free.
The two processes need not see it at the same address, and usually do not. That is the single most important practical consequence, and it is examined: you may not store a pointer in shared memory, because a pointer is an address and the other process's addresses are different. Store offsets, or plain data.
The four calls
There are two families. System V shared memory is what MU's text book describes and what ipcs reports, so it is taught here; POSIX shared memory is mentioned at the end.
| Call | What it does |
|---|---|
shmget(key, size, flags) | make or find a segment, and return its id |
shmat(id, NULL, 0) | attach it: map it into this process, and return the address |
shmdt(address) | detach it: unmap it from this process |
shmctl(id, IPC_RMID, NULL) | remove it from the system |
A key is a name and an id is a handle. The key is a number two unrelated programs agree on in advance, so that both can find the same segment. IPC_PRIVATE as the key means that no name is needed and the kernel should give out a fresh segment, which is enough when the other process is a child and inherits the id.
shmdt and shmctl(IPC_RMID) are not the same thing, and this is where marks are lost. Detaching removes it from one process. Removing destroys it for everybody. A program that detaches and forgets to remove leaves the segment in the system for ever, and the next chapter's ipcs output is how you find them.
Seen from outside, with no program at all
The shell can make a segment, list it and remove it, which is the quickest way to see that a segment is a thing the system owns rather than a thing a process owns.
$ ipcmk -M 4096
Shared memory id: 0
$ ipcs -m | awk '$3 == "student" {print "id", $2, "owner", $3, "perms", $4, "bytes", $5, "attached", $6}'
id 0 owner student perms 644 bytes 4096 attached 0
$ ipcrm -m 0
$ ipcs -m | awk '$3 == "student" {print "a segment is still there"} END {print "the table has been read"}'
the table has been readShared Memory, in Code That Runs
A segment of 4096 bytes existed, owned by student, with nattch of 0 because no process had attached it. Then it was removed and the table was empty again. No program was running at any point. The segment outlived the command that made it, which is exactly the property that makes System V shared memory useful and dangerous.
Two processes, one segment
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <unistd.h>
struct board {
int ready;
int count;
char message[64];
};
int main(void)
{
int id = shmget(IPC_PRIVATE, sizeof(struct board), IPC_CREAT | 0600);
if (id < 0) {
perror("shmget");
return 1;
}
printf("the segment id is %d, and it is %zu bytes\n", id, sizeof(struct board));
pid_t child = fork();
if (child == 0) {
struct board *b = shmat(id, NULL, 0);
strcpy(b->message, "the child wrote this straight into memory");
b->count = 7;
b->ready = 1; /* last, so the parent sees a whole message */
printf("child : written, and detaching\n");
shmdt(b);
_exit(0);
}
wait(NULL);
struct board *b = shmat(id, NULL, 0);
printf("parent: ready is %d, count is %d\n", b->ready, b->count);
printf("parent: message is \"%s\"\n", b->message);
shmdt(b);
shmctl(id, IPC_RMID, NULL); /* without this the segment stays */
printf("parent: segment removed\n");
return 0;
}$ gcc -std=c17 -Wall -Wextra -o shareit shareit.c
$ ./shareit
the segment id is 1, and it is 72 bytes
child : written, and detaching
parent: ready is 1, count is 7
parent: message is "the child wrote this straight into memory"
parent: segment removed
$ ipcs -m | awk '$3 == "student" {print "a segment is still there"} END {print "the table has been read"}'
the table has been readNotice what is not in that program: no send, no receive, no system call for the transfer. The child assigned to a structure and the parent read the structure. Between those two lines the kernel did nothing at all, and that is the whole reason shared memory exists.
Notice also the order of the child's three writes: the message first, the count next, the ready flag last. That is not an accident and it is the right habit: the flag says "everything else is valid", so it must be written after everything else. Chapters thirty three to forty two are about doing this properly rather than carefully.
The danger, named now and solved later
The program above is safe only because the parent called wait, so the two processes never touched the segment at the same moment. Take that away and the program is broken in a way that usually works, which is the worst kind of broken.
Shared Memory, in Code That Runs
Shared memory with no synchronisation is a race condition waiting to be reported as an intermittent bug. Chapter thirty four makes one happen on purpose; Chapters thirty seven to forty two are the tools that fix it. The practical's own instruction is "Explore issues of race conditions and how to avoid them", and that is the honest order: see the mechanism here, see the danger in Chapter thirty four, see the fix in Chapter thirty nine.
The POSIX family, in one paragraph
POSIX shared memory does the same job with the file system as its naming scheme. shm_open("/name", ...) gives a descriptor, ftruncate sets the size, and mmap maps it. The name looks like a file name and appears under /dev/shm, so an ordinary ls finds leftovers. It is newer and easier to clean up; the System V family is what an examination question will name, because it is what MU's text book uses.
Distinctions that carry marks
shmdt | shmctl(id, IPC_RMID, NULL) | |
|---|---|---|
| Effect | detaches from this process | destroys the segment for everybody |
| Other processes | unaffected | lose it |
| Forget it and | this process's mapping goes when it exits | the segment stays in the system for ever |
| Shared memory | Message passing | |
|---|---|---|
| Kernel involved per transfer | no | yes |
| Speed | memory speed | two copies and two system calls |
| Synchronisation | yours to arrange | the kernel's |
| May contain pointers | no: the addresses differ per process | not applicable |
| Key | Id | |
|---|---|---|
| Is | a name two programs agree on | a handle the kernel gives back |
| Chosen by | the programmer, or IPC_PRIVATE | the kernel |
| Used by | shmget to find the segment | every other call |
What it does not mean
Shared memory is not a shared variable. There is no declaration that makes two processes share a variable. There is a region of bytes, and both programs must agree on its layout.
Attaching is not copying. shmat maps; nothing is copied, which is why it is fast however large the segment is.
Removing it is not immediate if somebody is attached. IPC_RMID marks it for destruction; it goes when the last process detaches. So a segment can be removed and still be usable by the processes already holding it, which is the safe way to clean up.
Quick revision
- Shared memory maps one piece of physical memory into two address spaces. No kernel
involvement per access, so it is the fastest form of communication.
shmgetto make or find (returns an id),shmatto attach (returns an address),shmdt
to detach, shmctl with IPC_RMID to remove.
- A key is an agreed name;
IPC_PRIVATEasks for an unnamed one, enough when a child
inherits the id.
- The two processes may see the segment at different addresses, so never store a pointer in
Shared Memory, in Code That Runs
it.
shmdtis notIPC_RMID. Detach affects one process; remove destroys it for all. A
forgotten remove leaves the segment in the system, and ipcs -m finds it.
- Write the data first and the ready flag last.
- Shared memory with no synchronisation is a race condition. Chapters thirty four and thirty
nine.
- The POSIX family is
shm_open,ftruncate,mmap, and its names appear under/dev/shm.
Test yourself
- Name the four System V shared memory calls and what each does.
shmgetmakes or finds a
segment and returns its id; shmat attaches it and returns an address; shmdt detaches it from this process; shmctl with IPC_RMID removes it from the system.
- Why must a pointer never be stored in shared memory? Because each process may map the
segment at a different address, so an address valid in one is meaningless in the other. Store offsets.
- Distinguish detaching from removing. Detaching unmaps it from the calling process only.
Removing destroys it for everybody, once the last process detaches.
- What happens if a program forgets to remove a segment? It stays in the system after the
program ends. ipcs -m lists it and ipcrm -m removes it.
- Why is the ready flag written last? Because it tells the reader that everything else is
valid, so it must not become true before the data it vouches for is there.
- Why is shared memory faster than message passing? Because after the one time setup the
kernel is not involved: a transfer is an ordinary memory access rather than two system calls and two copies.
- What is the price of that speed? Nothing co-ordinates the two processes, so preventing
race conditions becomes the programmer's job.
The rest of this subject
These notes are cut from the University's printed syllabus. Open the syllabus itself, or the past papers, for the same subject.