﻿# Shared memory and semaphores

> The life of a shared memory segment from shmget to IPC_RMID, why two processes reading and writing it race, System V semaphores as kernel counters with P and V, the difference between a semaphore that signals (0) and one that locks (1), cleanup with ipcs and ipcrm – and the solutions of the three pipe tasks.

Source: https://laszlonemes.com/university/operating-systems/shared-memory-semaphores · Author: László Nemes

Session 6 · Operating Systems

The life of a shared memory segment from shmget to IPC_RMID, why two processes reading and writing it race, System V semaphores as kernel counters with P and V, the difference between a semaphore that signals (0) and one that locks (1), cleanup with ipcs and ipcrm – and the solutions of the three pipe tasks.

14 min read `shmget / shmat / shmdt` `Race without synchronisation` `semop P and V` `Signal (0) vs lock (1)` `ipcs and ipcrm`

Pipes and message queues copy data: the sender's bytes go into the kernel, and the receiver copies them out. Shared memory skips the copying. Two processes attach the same piece of memory, and what one writes, the other can read straight away – the fastest way processes can exchange data.

The price is that nothing in shared memory says *when* the data is ready. That is the job of a semaphore: a counter in the kernel that makes a process wait. This session builds both up from the examples of the course, shows the race they solve, and closes with the solutions of the three pipe tasks from [the previous session](https://laszlonemes.com/university/operating-systems/pipes-message-queues).

After this session you can

- create, attach, detach and remove a shared memory segment (`shmget`, `shmat`, `shmdt`, `shmctl`)
- explain why two processes reading and writing shared memory need synchronisation
- create a System V semaphore, set its initial value and use it with `semop` as P and V
- choose the initial value: 0 for "wait until the data is there", 1 for "one at a time"
- find and remove IPC objects a crashed program left behind (`ipcs`, `ipcrm`)

## Shared memory: one segment, many processes

Every process has its own address space: the same pointer value in two processes means two different places. A shared memory **segment** is memory owned by the kernel that can be mapped into several address spaces at once. Each process gets its own pointer to it, but the bytes behind the pointers are the same (Fig. 1).

> **Fig. 1** · One segment in the kernel, a pointer to it in each process.
>
> *Diagram:* shmat maps the same kernel segment into the address space of each process that attaches it. A child created by fork inherits the attachment. A write through s in one process is visible through s in the other – nothing is copied.

- **`key_t`** – A number that names the IPC object system-wide. `ftok(path, id)` makes one from an existing file and a small project id: same file and same id give the same key in every process.
- **shmid** – The id `shmget` returns for the segment. All further calls use it.
- **attach** – `shmat` maps the segment into the calling process and returns the address. A child created by `fork` inherits the attachment.
- **detach** – `shmdt` removes the mapping from this process. The segment itself stays.

The examples of the course create the key from the program's own file name, `argv[0]`, with project id `1` for the memory and `2` for the semaphore, so the two keys never collide:

osztmem.c – create and attach

```c
key_t kulcs = ftok(argv[0], 1);           // same file + same id → same key
if (kulcs == -1) { perror("ftok"); exit(EXIT_FAILURE); }

int oszt_mem_id = shmget(kulcs, 500, IPC_CREAT | S_IRUSR | S_IWUSR);
if (oszt_mem_id == -1) { perror("shmget"); exit(EXIT_FAILURE); }

char *s = shmat(oszt_mem_id, NULL, 0);    // NULL: the kernel picks the address
if (s == (void *)-1) {                    // the error value is (void *)-1, not NULL
    perror("shmat");
    shmctl(oszt_mem_id, IPC_RMID, NULL);
    exit(EXIT_FAILURE);
}
```

`IPC_CREAT` creates the segment if it doesn't exist yet and opens it if it does. `S_IRUSR | S_IWUSR` are the permissions (read and write for the owner). A newly created segment is filled with zero bytes, so a string read from it is empty until someone writes one.

Common mistake

**Checking shmat against NULL**

`shmat` signals an error with `(void *)-1`, not with `NULL`. A check for `NULL` never fires, and the first write through the bad pointer crashes the program.

## The lifecycle of a segment

A segment is not tied to the program that created it. It exists in the kernel from `shmget` until someone removes it with `shmctl(id, IPC_RMID, NULL)` – and it survives the end of every process that used it (Fig. 2).

> **Fig. 2** · Attach and detach happen in each process; create and remove happen once.
>
> *Diagram:* The life of a shared memory segment: a key from ftok, shmget creates the segment, every process attaches it with shmat and detaches with shmdt, and shmctl with IPC_RMID removes it. Between shmget and IPC_RMID the segment exists even when no process is running.

| Call | Affects | After it |
| --- | --- | --- |
| `shmdt(s)` | only this process | the pointer `s` is invalid here; the data is still in the segment |
| `shmctl(id, IPC_RMID, NULL)` | the segment | marked for removal; the kernel frees it when the last process has detached |

The simplest correct order uses no synchronisation at all: write *before* `fork`. The child inherits the attachment and the text is already there when it starts.

osztmem_sec.c – write first, then fork

```c
strcpy(s, "Valami iras!\n");   // 1. write ...
pid_t child_pid = fork();       // 2. ... then fork: the child sees the finished text
if (child_pid == 0) {
    printf("A gyerek ezt olvasta: %s", s);
    shmdt(s);
    exit(EXIT_SUCCESS);
}
```

osztmem_sec.c – the parent's cleanup

```c
shmdt(s);                          // this process no longer uses it
waitpid(child_pid, &status, 0);    // the child has detached and exited
shmctl(oszt_mem_id, IPC_RMID, NULL); // remove the segment itself
```

Good to know

**Why the commented-out s[0] = 'B' crashes**

After `shmdt(s)` the address range is no longer mapped in this process. Writing through `s` is a write to memory the process doesn't have – a segmentation fault, even though the segment itself still exists.

## Why access needs synchronisation

Writing before `fork` works once, in one direction. As soon as the parent writes *after* the fork, there are two processes and the scheduler decides which runs when. The first example of the course has the child `sleep(1)` and then read – usually the parent has written by then, but nothing guarantees it (Fig. 3).

> **Fig. 3** · A race: the result depends on timing. `sleep` hides it, it doesn't fix it.
>
> *Diagram:* Shared memory gives no ordering. The child sleeps one second and reads; if the parent is delayed for longer than that, the child reads before the parent wrote and prints an empty string. sleep makes the bad order unlikely, not impossible.

A machine under load, a slow terminal or a debugger is enough to stretch the parent's delay beyond one second. A program that works "almost always" is the hardest kind to debug, so the rule is: whenever two processes touch shared memory and the order matters, something has to enforce the order. With shared memory that something is usually a semaphore.

> **Shared memory transports data; it does not synchronise.** A pipe read blocks until there is data; a read from shared memory never waits – it returns whatever is there right now.

## System V semaphores

A semaphore is a non-negative counter kept by the kernel, with two operations. **P** (down, wait) decreases it by one – but if the value is 0, the process blocks until it can. **V** (up, signal) increases it by one and wakes a process waiting in P (Fig. 4). Both are atomic: two processes can't both see the value 1 and both decrease it.

> **Fig. 4** · P waits while the value is 0; V lets one waiting process through.
>
> *Diagram:* A semaphore is a counter kept by the kernel. semop −1 (P, down) decreases it, but if that would make it negative the process blocks. semop +1 (V, up) increases it and wakes a waiting process, whose −1 then completes.

### Creating and initialising

`semget` creates a *set* of semaphores – the second argument is how many. The new value is set with `semctl(…, SETVAL, …)`, whose fourth argument is a `union semun`. POSIX leaves defining that union to the program, so you write it yourself:

szemafor.c – a set with one semaphore, value 0

```c
union semun {               // you define it – <sys/sem.h> does not
    int val;                // for SETVAL
    struct semid_ds *buf;   // for IPC_STAT, IPC_SET
    unsigned short *array;  // for GETALL, SETALL
};

int semid = semget(ftok(argv[0], 2), 1, IPC_CREAT | S_IRUSR | S_IWUSR);
union semun arg;
arg.val = 0;                          // initial value
if (semctl(semid, 0, SETVAL, arg) == -1) { perror("semctl"); exit(EXIT_FAILURE); }
```

### P and V with semop

`semop` takes an array of `struct sembuf` operations and performs them atomically. The course's helper wraps a single operation:

szemafor.c – P and V in one helper

```c
void szemafor_muvelet(int semid, int op)   // op = -1: P (down), +1: V (up)
{
    struct sembuf muvelet;
    muvelet.sem_num = 0;     // index in the set – we have one semaphore
    muvelet.sem_op = op;
    muvelet.sem_flg = 0;     // or SEM_UNDO, IPC_NOWAIT
    if (semop(semid, &muvelet, 1) == -1) {   // 1 = number of operations
        perror("semop");
        exit(EXIT_FAILURE);
    }
}
```

| sem_op | Name | What happens |
| --- | --- | --- |
| `-1` | P, down, wait | value > 0: decrease and continue; value 0: block until it can |
| `+1` | V, up, signal | increase; a blocked process may continue |
| `0` | wait for zero | block until the value is 0 – "everyone has finished" |

Good to know

**SEM_UNDO**

With `SEM_UNDO` in `sem_flg` the kernel reverts the process's operations when it exits. If a process dies between P and V, the semaphore is not left at 0 forever.

## A semaphore guarding shared memory

The initial value decides what the semaphore means. The two examples of the course show both uses.

### Value 0: a signal – "the data is there"

The semaphore starts closed. The child calls P and blocks. The parent writes the segment and then calls V. Whichever process the scheduler runs first, the child reads only after the write (Fig. 5).

> **Fig. 5** · The V comes after the write, the read after the P – the order is enforced.
>
> *Diagram:* Initialised to 0, the semaphore is a signal. The child's −1 blocks until the parent's +1, and the parent calls +1 only after writing – so the child always reads the finished data, however the scheduler orders the two processes.

szemafor.c – write, V / P, read

```c
// semaphore created with value 0 BEFORE fork()
if (child > 0) {                    // parent
    strcpy(s, buffer);
    szemafor_muvelet(semid, 1);     // V: "the data is there"
} else {                            // child
    szemafor_muvelet(semid, -1);    // P: blocks until the parent's V
    printf("Gyerek olvasta: %s", s);
}
```

Create the semaphore *before* `fork`, so both processes have the same `semid` and the value is 0 before either can touch it.

### Value 1: a lock – "one at a time"

In the second example the parent reads lines from the keyboard and writes them to a struct in the segment; the child reads the text and writes back its length. Both sides wrap their access in P … V, so neither can see the struct half-updated (Fig. 6).

> **Fig. 6** · A mutex makes the critical section exclusive, not ordered.
>
> *Diagram:* Initialised to 1, the semaphore is a lock (mutex): only one process at a time can be between its −1 and +1. It prevents reading half-written data, but it does not decide the order – either process may enter first.

szemafor_sec.c – the parent's critical section

```c
struct sharedData { char text[MEMSIZE]; int textSize; };

szemafor_muvelet(semid, -1);        // enter – value 1 → 0
strcpy(s->text, buffer);            // parent → child
printf("previous length: %i\n", s->textSize);   // child → parent
szemafor_muvelet(semid, 1);         // leave – value 0 → 1
```

The lock does not decide who goes first. The child in the example checks the segment every two seconds; if lines arrive faster – pasted or piped in – it sees only some of them. That is fine for the demonstration, but a real hand-over of every message needs two semaphores: one saying "a message is waiting", one saying "the slot is free".

Common mistake

**Cutting the newline**

`fgets` keeps the `\n`, so `"exit\n"` never equals `"exit"`. The fragile fix, `buffer[strlen(buffer) - 1] = '\0'`, cuts the last real character when the line has no newline – the last line of piped input, or a line longer than the buffer. Cut only a newline that is there:

```c
if (fgets(buffer, MEMSIZE, stdin) == NULL) { /* EOF: send "exit" and stop */ }
buffer[strcspn(buffer, "\n")] = '\0';   // cut the newline – only if there is one
```

In your assignment

**Signal or lock?**

When you add shared memory to your assignment, decide for each piece of shared data: does one process have to wait until another has produced it (start at 0, the producer calls V), or do several processes update it and must not interleave (start at 1, everyone does P … V)? Write the answer as a comment next to the `SETVAL` line.

## Cleaning up

Shared memory segments, semaphore sets and message queues all outlive the program. Remove them in the process that created them, after the others have finished:

szemafor.c – the end of the parent

```c
waitpid(child, &status, 0);              // 1. nobody uses the objects any more
semctl(semid, 0, IPC_RMID);              // 2. remove the semaphore set
shmctl(sh_mem_id, IPC_RMID, NULL);       // 3. remove the segment
```

Removing a semaphore set takes effect immediately: a process still blocked in `semop` on it wakes up with an error (`EIDRM`). That is why the parent waits for the child first.

If a program crashes or you stop it with Ctrl+C, the cleanup never runs. `ipcs` lists what you left behind, `ipcrm` removes it:

```text
$ ipcs -m -s
------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x0105a2c1 32769      abc123     600        1028       0

------ Semaphore Arrays --------
key        semid      owner      perms      nsems
0x0205a2c1 3          abc123     600        1

$ ipcrm -m 32769          # remove a segment by its id
$ ipcrm -s 3              # remove a semaphore set by its id
```

Common mistake

**A stale object with the same key**

`IPC_CREAT` without `IPC_EXCL` silently reuses an existing object with the same key. A leftover message queue still holds old messages; a leftover segment that is smaller than the size you ask for makes `shmget` fail with `EINVAL`. When a program behaves strangely on its second run, look at `ipcs` first.

## The pipe tasks, solved

The previous session ended with three pipe tasks. Their solutions use three different shapes of pipes (Fig. 7); here is the structure of each and the few lines that matter.

> **Fig. 7** · Who writes to whom: one pipe per direction and per pair of processes.
>
> *Diagram:* The three pipe tasks use three shapes. Counting together needs a pipe in each direction: the count goes down, an acknowledgement comes back. The chain passes data from the parent to child 1 and on to child 2. The worker task sends a name, a count and an array of structs through one pipe, in that order.

### 1 · Counting together

Parent and child count to three, alternately, the parent first; the parent sends an `enum` value, not a character. One pipe carries "your turn" to the child. A `sleep(1)` in the parent's loop makes the output alternate in practice, but only a second pipe makes it certain: the child answers after printing, and the parent waits for that answer before it counts on.

kozos_szamlalas.c – the parent waits for the acknowledgement

```c
// parent: io_pipes1 = parent → child, io_pipes = child → parent
while (mainszamlalo < 4) {
    printf("Parent mondja: %i \n", mainszamlalo);
    fflush(stdout);
    write(io_pipes1[1], &puffer, sizeof(enum message));
    if (read(io_pipes[0], &valasz, sizeof(enum message)) != sizeof(enum message))
        break;                      // wait until the child has printed too
    mainszamlalo++;
}
```

The `fflush(stdout)` matters when the output goes to a file or a pipe: there `printf` is buffered, and two processes' buffers would come out in the wrong order.

### 2 · Child to child

The parent takes a number N, sends it to child 1, child 1 prints it and forwards it to child 2, followed by N random numbers; child 2 prints all of them. Two pipes, both created before the forks, so both children inherit them. Each child closes the ends it doesn't use and checks that every `read` returned a whole `int`:

child_communications.c – child 2

```c
/* child 2 inherited the write end of the child1 → child2 pipe:
   close it, or read never sees EOF if child 1 dies */
close(pipe_id_unused);

int arrived;
if (read(pipe_id_rec, &arrived, sizeof(int)) != sizeof(int)) {
    perror("Child2 read");          // 0 = EOF, -1 = error: no number arrived
    exit(EXIT_FAILURE);
}
```

### 3 · Structs through a pipe

The parent hands out a job: its name (a fixed-size `char` array), the number of workers, then the workers themselves as an array of structs – in one `write`. The child reads in the same order and with the same sizes. That works because the struct has no pointers: its bytes mean the same in the child.

pipe_struct.c – one write, one read

```c
/* parent: one write for all workers */
write(io_pipes_out[1], daily_datas, daily_num_ready * sizeof(struct worker));

/* child: allocate for the count it received, then one read */
struct worker *data = malloc(daily_num * sizeof(struct worker));
ssize_t expected = daily_num * (ssize_t)sizeof(struct worker);
if (read(pipe_id_rec, data, expected) != expected) { /* incomplete data */ }
```

Good to know

**When one read is enough**

A write of at most `PIPE_BUF` bytes (4096 on Linux) goes into a pipe atomically, so here – three structs of about 1 KB – the child's single `read` gets everything. For more data, read in a loop until all bytes have arrived.

## Practice

Task 1

**Replace the sleep with a semaphore**

Start from the first shared memory example: the parent writes after `fork`, the child `sleep(1)`s and reads. Remove the `sleep` and make the order certain with a semaphore. Then put a `sleep(3)` into the parent before its write – the child must still print the text, not an empty line.

**Approach:**

Initial value 0, created before the fork. The parent calls V after `strcpy`, the child calls P before `printf`. Don't forget to remove the semaphore at the end, next to the segment.

Task 2

**No line lost**

Change the keyboard example so the child processes *every* line, also when the input is piped in (`printf 'a\nbb\nexit\n' | ./a.out`), and the parent prints the length of the line it has just sent, not the previous one.

**Approach:**

Two signals instead of one lock: a set of two semaphores, both starting at 0. "Line ready" goes from parent to child, "length ready" from child to parent. The parent: write, V(line), P(length), print. The child: P(line), read, write the length, V(length). Address them with `sem_num` 0 and 1.

```c
int semid = semget(kulcs, 2, IPC_CREAT | S_IRUSR | S_IWUSR);   // a set of two
// sem 0 = "line ready", sem 1 = "length ready" – both SETVAL 0

struct sembuf v_line   = { 0, +1, 0 };  // parent: a line is in the segment
struct sembuf p_length = { 1, -1, 0 };  // parent: wait for the child's answer
semop(semid, &v_line, 1);
semop(semid, &p_length, 1);
```

Task 3

**Find what a crash leaves behind**

Run the keyboard example and stop it with Ctrl+C while it waits for input. List your IPC objects, find the segment and the semaphore set the program created, and remove both. Then explain why the next run would have worked anyway.

**Approach:**

`ipcs -m -s`, then `ipcrm -m` and `ipcrm -s` with the ids. For the explanation: the same `argv[0]` gives the same key, `IPC_CREAT` reopens the old objects, and `SETVAL` resets the semaphore.

## Summary

- `ftok` → `shmget` → `shmat` → use → `shmdt` → `shmctl(IPC_RMID)`; check `shmat` against `(void *)-1`.
- Shared memory moves data without copying, but never waits – the order of reads and writes has to be enforced.
- A semaphore is a kernel counter: P (`-1`) waits at 0, V (`+1`) lets a waiter through. Set its value with `SETVAL` and a `union semun`.
- Initial value 0: a signal that data is ready. Initial value 1: a lock for one-at-a-time access – exclusion, not order.
- Create IPC objects before `fork`, remove them after `wait`; after a crash, `ipcs` and `ipcrm`.
