﻿# Pipes, named pipes and message queues

> Moving data between processes: pipes and why every process closes the ends it does not use, sending ints, strings and structs with the right sizes, named pipes with mkfifo, and System V message queues with typed messages.

Source: https://laszlonemes.com/university/operating-systems/pipes-message-queues · Author: László Nemes

Session 5 · Operating Systems

Moving data between processes: pipes and why every process closes the ends it does not use, sending ints, strings and structs with the right sizes, named pipes with mkfifo, and System V message queues with typed messages.

17 min read `pipe() and EOF` `Closing unused ends` `Framing data` `mkfifo` `msgsnd / msgrcv by type`

[Signals](https://laszlonemes.com/university/operating-systems/signals) can tell a process that something happened, but they carry no data. This session is about moving data between processes. After `fork()` the parent and the child have separate memory – a variable changed in one is not changed in the other – so they need the kernel to pass bytes between them.

Three tools do that: an anonymous **pipe** between related processes, a **named pipe** (FIFO) that any process can open by its file name, and a **message queue** that keeps whole, typed messages. They look similar, but they differ in exactly the details that make programs hang: who holds which end, when a read returns, and what happens to the boundaries between messages.

> **The model in one breath:** a pipe is a one-way byte stream in the kernel; every process closes the ends it does not use, and the reader must know how many bytes to expect. A message queue keeps message boundaries and lets the receiver pick messages by type.

After this session you can

- Create a pipe, fork, and close the right ends in each process so the reader sees end of file.
- Set up parent → child, child → parent and child → child pipes.
- Send ints, strings and structs with the correct sizes, and read them back completely.
- Create, open and remove a named pipe, and predict when open() blocks.
- Use a System V message queue: ftok, msgget, msgsnd, msgrcv with a type, msgctl(IPC_RMID).
- Choose between a pipe, a FIFO and a message queue for a given task.

## pipe(): two ends of a kernel buffer

`pipe(fd)` creates a buffer inside the kernel and gives you two file descriptors for it: `fd[0]` to read and `fd[1]` to write (think of 0 as stdin, 1 as stdout). Bytes written into `fd[1]` come out of `fd[0]` in the same order (Fig. 1). You use them with the ordinary `read()`, `write()` and `close()` calls from the first session.

```c
int fd[2];
if (pipe(fd) == -1) {
    perror("pipe");
    exit(EXIT_FAILURE);
}
/* fd[0]: the read end    fd[1]: the write end */
```

> **Fig. 1** · A pipe is a first-in first-out byte buffer in the kernel, with a write end and a read end.
>
> *Diagram:* A pipe is a byte buffer inside the kernel with two ends: whatever is written into fd[1] comes out of fd[0] in the same order. read() waits while the pipe is empty, write() waits while it is full, and read() returns 0 – end of file – once every write end is closed.

The rules that decide whether your program works or hangs:

| Situation | What happens |
| --- | --- |
| pipe empty, `read()` | Blocks until some data arrives – then returns what is there, possibly less than you asked for. |
| pipe full, `write()` | Blocks until the reader makes room. |
| every write end closed, `read()` | Returns `0`: end of file. This is how the reader learns that no more data will come. |
| every read end closed, `write()` | The writer gets `SIGPIPE` (default: terminate), or `-1` with `EPIPE` if it ignores the signal. |

A pipe has no name, so the only way to share it is inheritance: create it **before** `fork()`, and the child gets copies of both descriptors.

## Close what you do not use

Right after `fork()` both processes hold both ends: four descriptors point at one pipe (Fig. 2). For a pipe from parent to child, the parent closes the read end and the child closes the write end. What remains is one writer and one reader.

> **Fig. 2** · Left: after fork() each process has both ends. Right: each closed the end it does not need.
>
> *Diagram:* After fork() the parent and the child both hold both ends of the pipe: four file descriptors refer to it. For a parent-to-child pipe, the parent closes its read end and the child closes its write end, so exactly one writer and one reader remain.

This is not tidiness. `read()` returns end of file only when **every** write end is closed – in every process. A child that loops until EOF but forgot to close its own inherited write end is waiting for itself: the writer that keeps the pipe alive is the reader (Fig. 3).

> **Fig. 3** · One forgotten write end – even in the reader itself – and the read loop never ends.
>
> *Diagram:* The reader gets end of file only when every write end of the pipe is closed – including the copy the reader itself inherited from fork(). If the child forgets to close its own write end, its read() waits forever for data that can never come.

Parent writes, child reads until end of file

```c
pid_t pid = fork();
if (pid == 0) {                                /* child: reader */
    close(fd[1]);                              /* not writing – close it, or EOF never comes */
    char buf[64];
    ssize_t n;
    while ((n = read(fd[0], buf, sizeof(buf))) > 0)
        write(STDOUT_FILENO, buf, n);
    close(fd[0]);
    exit(EXIT_SUCCESS);
}
close(fd[0]);                                  /* parent: writer, not reading */
write(fd[1], "hello\n", 6);
close(fd[1]);                                  /* this is the child's EOF */
waitpid(pid, NULL, 0);
```

Common mistake

**The program hangs at the end**

If a program prints everything and then does not exit, look for a read loop that is waiting for EOF and count the write ends: the parent's, the child's own, other children's. Close each one in every process that does not write – ideally on the line right after `fork()`, where it is easy to check.

## Parent and child: one pipe per direction

A pipe is one-way. If the parent asks and the child answers, you need two pipes (Fig. 4). Using one pipe for both directions does not work: a process can read back its own message before the other side gets it.

> **Fig. 4** · The example of the session: the parent sends a random number through one pipe, the child replies through the other.
>
> *Diagram:* A pipe carries data in one direction only. For a question and an answer the parent and the child need two pipes: io_pipes1 from parent to child carries the number, io_pipes from child to parent carries the reply. Each process uses one end of each pipe.

In the session example the parent sends a random number, the child reads it and sends back a sentence. The parent closes the child's ends right after `fork()`, so if the child dies without answering, the parent's `read()` returns 0 instead of waiting forever, and the buffer is terminated by the number of bytes actually read:

pipe_more.c – the parent side

```c
pid_t child_pid = child(io_pipes1[0], io_pipes[1]);   /* child reads 1[0], writes [1] */

close(io_pipes1[0]);            /* the parent uses neither of the child's ends */
close(io_pipes[1]);

write(io_pipes1[1], &r, sizeof(r));                  /* send an int */

ssize_t n = read(io_pipes[0], pufferchar, sizeof(pufferchar) - 1);
if (n == -1) { perror("read"); exit(EXIT_FAILURE); }
pufferchar[n] = '\0';           /* n == 0 (EOF): empty string, not garbage */
```

pipe_more.c – the child side

```c
int arrived;
ssize_t got = read(pipe_id_rec, &arrived, sizeof(int));
if (got != sizeof(int)) {                     /* -1: error, 0: EOF, less: partial */
    fprintf(stderr, "no complete number arrived\n");
    exit(EXIT_FAILURE);
}
char str[] = "Child kuldi a szulonek: Megkaptam a szamot!";
write(pipe_id_send, str, sizeof(str));        /* sizeof an ARRAY: all chars + '\0' */
```

The child checks that it got a **whole** `int`: `read()` returning 0 or 2 bytes would leave `arrived` half-filled with garbage. Finally the parent waits for the child with `waitpid()` – a pipe does not replace waiting.

## Sending numbers, strings and structs

A pipe moves raw bytes. You can write the memory of any variable – an `int`, an array, a struct – as long as the reader reads the same number of bytes into a variable of the same type: `write(fd, &r, sizeof(r))` on one side, `read(fd, &x, sizeof(x))` on the other. Strings need care, because the size is easy to get wrong:

sizeof of an array vs a pointer

```c
char  text[] = "hello";       /* an array of 6 bytes */
char *msg    = "hello";       /* a pointer to them */

write(fd, text, sizeof(text));      /* 6 bytes: h e l l o \0 – correct */
write(fd, msg, sizeof(msg));        /* 8 bytes on 64-bit: the size of the POINTER – wrong */
write(fd, msg, strlen(msg) + 1);    /* 6 bytes – correct for a pointer */
```

The second problem is **boundaries**. A pipe is a stream: it does not remember where one `write()` ended. Two writes can arrive in one `read()`, and one large write can arrive in several (Fig. 5). Writes of up to `PIPE_BUF` bytes (4096 on Linux) are atomic – they are not mixed with other writers' data – but a read may still return less than you asked for.

> **Fig. 5** · The pipe forgets write boundaries. Agree on sizes: fixed-size records, or a length followed by the data.
>
> *Diagram:* A pipe is a stream of bytes: it does not remember where one write() ended and the next began, so two writes can come out of one read(). The reader must know how many bytes belong together – a fixed size, or a length sent first and then exactly that many bytes.

So the reader must know how many bytes belong together. Either both sides use fixed sizes (an `int`, a `char[100]`, a struct), or the sender sends the length first:

Framing a string with its length

```c
/* sender */
int len = strlen(name) + 1;
write(fd, &len, sizeof(len));       /* first: how many bytes follow */
write(fd, name, len);               /* then: exactly that many */

/* receiver */
int len;
read_full(fd, &len, sizeof(len));
read_full(fd, buf, len);            /* buf must be at least len bytes */
```

And the reader loops until it has the whole thing:

read_full – read exactly size bytes

```c
/* read exactly size bytes, or fail: a pipe may return less than asked for */
int read_full(int fd, void *buf, size_t size)
{
    char *p = buf;
    while (size > 0) {
        ssize_t n = read(fd, p, size);
        if (n == -1 && errno == EINTR) continue;   /* interrupted by a signal: retry */
        if (n <= 0) return -1;                     /* error, or EOF too early */
        p += n;
        size -= n;
    }
    return 0;
}
```

### Structs and arrays of structs

A struct is sent as its bytes. That works only if the struct contains values, not pointers: a `char *name` is an address in the sender's memory and means nothing in the receiver. Use `char name[50]`. An array of structs is one block of memory, so a single `write()` sends all of them:

Sending a count, then an array of structs

```c
struct worker {
    int  id;
    char name[50];          /* arrays, not char * – a pointer means nothing in another process */
    char address[50];
};

struct worker team[3] = { {1, "Bela", "Budapest"}, {2, "Janos", "Miskolc"}, {3, "Mark", "Szeged"} };
int count = 3;
write(fd, &count, sizeof(count));
write(fd, team, count * sizeof(struct worker));      /* the whole array in one write */
```

Good to know

**Same program, same layout**

Writing a struct's bytes is safe here because both processes come from the same program, compiled once: the field sizes, the padding and the byte order are identical. Between different programs or machines you would send the fields one by one in an agreed format.

## Child to child

Two children cannot create a pipe for each other – a pipe created inside child 1 is invisible to child 2. The parent creates **all** pipes before it forks, and every process closes the ends it does not use (Fig. 6). The parent too: if it keeps the write end of the child-to-child pipe open, child 2 never sees end of file.

> **Fig. 6** · Parent → child 1 → child 2: two pipes, created before the forks, and a list of what each process closes.
>
> *Diagram:* Child-to-child communication goes through pipes the parent creates before it forks, so both children inherit them. Parent to child 1 through p1, child 1 to child 2 through p12. Every process closes every end it does not use – the parent closes both ends of p12, which it never touches.

Who keeps which end

```c
int p1[2], p12[2];            /* parent -> child1, child1 -> child2 */
pipe(p1);
pipe(p12);                      /* both BEFORE the forks, so every child inherits them */

if (fork() == 0) {              /* child1 */
    close(p1[1]); close(p12[0]);        /* keeps: p1 read, p12 write */
    /* read from p1[0], write to p12[1] */
}
if (fork() == 0) {              /* child2 */
    close(p1[0]); close(p1[1]); close(p12[1]);   /* keeps: p12 read */
    /* read from p12[0] until EOF */
}
close(p1[0]); close(p12[0]); close(p12[1]);      /* parent keeps: p1 write */
```

In your assignment

**In the simulation tasks**

The simulation tasks at the end of the semester combine everything: children report ready with a signal, then the parent talks to one of them through a pipe and to another through a message queue. Draw the processes and the pipes first, write next to each process which ends it keeps, and turn that list into `close()` calls. See [Simulation tasks](https://laszlonemes.com/university/operating-systems/simulation-tasks).

## Named pipes (FIFO)

An anonymous pipe exists only as inherited descriptors. A **named pipe**, or FIFO, also has a name in the file system, so any process that knows the name can open it – related or not (Fig. 7). `mkfifo(path, mode)` creates the name; from then on it is used with the ordinary `open()`, `read()`, `write()` and `close()`.

> **Fig. 7** · The FIFO's name lives in the file system; the data goes through the kernel, as with an ordinary pipe.
>
> *Diagram:* A named pipe (FIFO) has a name in the file system, so unrelated processes can find it with open(). The name is only a meeting point: the data still goes through a buffer in the kernel and never reaches the disk. open() blocks until both a reader and a writer have opened it.

pipe_named.c – creating the FIFO

```c
char pipename[32];
snprintf(pipename, sizeof(pipename), "/tmp/%d", getpid());   /* unique per run */

if (mkfifo(pipename, S_IRUSR | S_IWUSR) == -1) {             /* 0600: only the owner */
    perror("mkfifo");           /* EEXIST: a file with this name is already there */
    exit(EXIT_FAILURE);
}
```

The mode `S_IRUSR | S_IWUSR` (0600) lets only the owner open it – reduced further by the umask, as for any new file. The name contains the PID, so two runs (or two students on the same server) do not collide in `/tmp`. If the name already exists, `mkfifo` fails with `EEXIST`.

The important difference from a file: **open() blocks**. Opening for reading waits until someone opens it for writing, and the other way round. That is the synchronisation point – the two processes meet at `open()`.

pipe_named.c – writer (child) and reader (parent)

```c
/* child – writer */
int fd = open(pipename, O_WRONLY);     /* blocks until someone opens it for reading */
write(fd, "Test value\n", 12);
close(fd);

/* parent – reader */
int fd = open(pipename, O_RDONLY);     /* blocks until someone opens it for writing */
ssize_t n = read(fd, pufferchar, sizeof(pufferchar) - 1);
pufferchar[n > 0 ? n : 0] = '\0';
close(fd);
unlink(pipename);                       /* remove the name – the FIFO file stays otherwise */
```

The FIFO's name stays in the file system after the processes exit, so remove it with `unlink()` when you are done (the open descriptors keep working until closed). From the shell you can try the same thing with two commands:

```text
$ mkfifo /tmp/demo
$ ls -l /tmp/demo
prw-r--r-- 1 student student 0 … /tmp/demo     <- type p, size 0: nothing is stored on disk
$ cat /tmp/demo &                              <- the reader waits
$ echo hello > /tmp/demo                       <- the writer: cat prints hello
$ rm /tmp/demo
```

Common mistake

**Both sides open for reading – or both for writing**

If both processes open the FIFO with `O_RDONLY`, both wait for a writer that never comes – the program hangs at `open()`, before a single byte is sent. One side opens with `O_WRONLY`, the other with `O_RDONLY`.

## System V message queues

A message queue is a list of messages kept by the kernel. Unlike a pipe it keeps **message boundaries** – one `msgsnd` is one `msgrcv` – and every message carries a **type**, a positive `long` that the receiver can select by (Fig. 8).

> **Fig. 8** · Messages wait in sending order; the type argument of msgrcv decides which one a receiver takes.
>
> *Diagram:* A message queue keeps whole messages, each with a type, in the order they were sent. The receiver chooses by type: msgrcv with type 5 takes the oldest type-5 message, type 0 takes the oldest message of any type, and a negative type -4 takes the message with the lowest type that is at most 4.

### Creating or opening a queue

A queue is identified by a numeric **key**. `ftok(path, id)` makes one from an existing file's identity and a non-zero project number – every process that calls it with the same path and number gets the same key. `msgget(key, 0600 | IPC_CREAT)` creates the queue if it does not exist yet and returns its ID (`msqid`); the `0600` are the permissions, as for a file.

```c
key_t key = ftok(".", 1);             /* an existing path + a non-zero project id */
if (key == -1) { perror("ftok"); exit(EXIT_FAILURE); }

int queue = msgget(key, 0600 | IPC_CREAT);   /* create it, or open the existing one */
if (queue < 0) { perror("msgget"); exit(EXIT_FAILURE); }
```

### The message and its size

The message is your own struct, with one rule: it starts with a `long mtype`. After that comes the payload – text, an array, anything. The size you pass to `msgsnd` and `msgrcv` counts **only the payload**, never the `long` in front of it (Fig. 9).

```c
#define MSG_SIZE 1024

struct message {
    long mtype;               /* must be > 0 – the receiver selects by it */
    char mtext[MSG_SIZE];     /* the payload: any bytes, any layout you like */
};
```

> **Fig. 9** · msgsz counts the payload after mtype – passing sizeof(struct message) is one long too many.
>
> *Diagram:* A message is a struct that starts with a long mtype followed by the payload. The size argument of msgsnd and msgrcv counts only the payload, never the long mtype in front of it.

msg-queue.c – the child sends a type-5 text message

```c
struct message ms;
ms.mtype = 5;
strcpy(ms.mtext, "This is a test sentence.");

/* size = the payload only, WITHOUT the long mtype; +1 for the closing '\0' */
if (msgsnd(queue, &ms, strlen(ms.mtext) + 1, 0) < 0) {    /* 0: block if the queue is full */
    perror("msgsnd");
    exit(EXIT_FAILURE);
}
```

`mtype` must be greater than 0, otherwise `msgsnd` fails with `EINVAL`. The last argument is the flag: `0` blocks while the queue is full, `IPC_NOWAIT` returns `-1` with `EAGAIN` instead.

### Receiving by type

msg-queue.c – the parent waits for type 5

```c
struct message ms;
/* the first message of type 5; blocks until there is one */
ssize_t len = msgrcv(queue, &ms, MSG_SIZE, 5, 0);
if (len < 0)
    perror("msgrcv");
else
    printf("type %ld: %s\n", ms.mtype, ms.mtext);
```

| msgtyp argument | msgrcv takes |
| --- | --- |
| `0` | the oldest message, whatever its type |
| `> 0` | the oldest message of exactly that type |
| `< 0` | the message with the lowest type that is ≤ the absolute value |

`msgrcv` blocks until a matching message exists (unless `IPC_NOWAIT`), and returns the number of payload bytes. If the message is longer than the size you gave, it fails with `E2BIG` and the message stays in the queue. With types, one queue can serve several conversations: in `msg-queue2.c` the parent sends type 5 and listens for type 4, the child the other way round, so neither reads back its own message. The payload there is an `int` array:

msg-queue2.c – an int array as payload

```c
struct uzenet2 {
    long mtype;
    int  marr[5];
};
struct uzenet2 uz2 = {5, {1, 2, 3, 4, 5}};

msgsnd(queue, &uz2, sizeof(uz2.marr), 0);                  /* 20 bytes: the array only */
msgrcv(queue, &uz2, sizeof(uz2.marr), 5, 0);               /* in the other process */
```

### Removing the queue

A queue is not tied to a process. It stays in the kernel – with any unread messages – after every process has exited, until someone removes it or the machine restarts. A stale queue from a crashed run is found again by the next run with the same key, old messages included. Remove it when the work is done:

```c
/* after every process is done with it – usually the parent, after waitpid */
if (msgctl(queue, IPC_RMID, NULL) < 0)
    perror("msgctl");
```

Finding and removing leftover queues

```text
$ ipcs -q                       # your message queues
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages
0x0118063d 3          student    600        25           1
$ ipcrm -q 3                    # remove the one with msqid 3
```

Common mistake

**Output printed twice**

If `printf` output sits in the stdout buffer when you call `fork()`, both processes have a copy and both print it. On a terminal the buffer is flushed at every newline, so you never see it – redirect the output to a file and the line appears twice. Call `fflush(stdout)` right before `fork()`:

```c
printf("Key: %d\n", key);
fflush(stdout);                 /* empty the buffer, or the child inherits an unwritten copy */
pid_t child = fork();
```

## Which one to use

|  | Pipe | Named pipe (FIFO) | Message queue |
| --- | --- | --- | --- |
| Who can use it | related processes (inherited over fork) | any process that knows the path | any process that knows the key |
| Unit | byte stream | byte stream | whole messages with a type |
| Boundaries | lost – you frame the data | lost – you frame the data | kept: one send, one receive |
| Select what to read | no – strictly in order | no – strictly in order | yes – by `mtype` |
| End of data | `read()` returns 0 | `read()` returns 0 | no EOF – agree on a "last" message |
| Lifetime | until the last descriptor is closed | the name: until `unlink()` | until `IPC_RMID` or restart |

A rule of thumb: a pipe for a stream from one process to another, two pipes for a question and an answer, a message queue when several kinds of messages share a channel or a receiver must pick a particular one. Shared memory, the fastest of all, comes in [the next session](https://laszlonemes.com/university/operating-systems/shared-memory-semaphores).

- **pipe** – A one-way byte buffer in the kernel with a read end and a write end, shared by inheritance.
- **EOF** – End of file: read() returns 0 because every write end is closed.
- **framing** – Telling the reader where one piece of data ends – fixed sizes or a length prefix.
- **FIFO** – A named pipe: a pipe with a name in the file system, created with mkfifo.
- **mtype** – The positive long at the start of a message; msgrcv selects messages by it.
- **IPC_RMID** – The msgctl command that deletes a message queue.

## Practice

Task 1

**Counting together**

The parent and one child count to three together, taking turns – the parent first. The parent does not send text to tell the child to count: it sends a value of an `enum` (for example `COUNTING`) through a pipe. A `sleep(1)` in the parent's loop helps keep the output in order. The parent waits for the child to finish.

```text
Parent says: 1
Child says: 1
Parent says: 2
Child says: 2
Parent says: 3
Child says: 3
```

**Approach:**

An `enum` is an integer, so it is sent like one, with `sizeof`. The child loops on `read()` and prints its own counter for every command; the loop ends at end of file, when the parent closes its write end. `sleep(1)` only makes the right order very likely; a second pipe on which the child answers “done” makes it certain.

```c
enum command { COUNT, STOP };

/* parent */
for (int i = 1; i <= 3; i++) {
    printf("Parent says: %d\n", i);
    fflush(stdout);
    enum command c = COUNT;
    write(to_child[1], &c, sizeof(c));
    read(from_child[0], &c, sizeof(c));      /* wait for "done" – stricter than sleep(1) */
}
```

Task 2

**Child to child**

The parent has two children. It reads a number from the keyboard (assume the input is valid) and sends it through a pipe to child 1, which prints it. Child 1 then sends the number on to child 2, followed by that many random numbers. Child 2 prints the number and then every random number it receives. The parent waits for both children.

```text
$ ./child_communications
3
Child_1 read message: 3
Child_2 read message: 3
Child_2 read random number: 749
Child_2 read random number: 489
Child_2 read random number: 529
```

**Approach:**

Two pipes, both created before the first `fork()` (Fig. 6). Read the number in the parent before forking or after – but `fflush(stdout)` before each `fork()`. Seed the random generator in child 1 (for example with `getpid()`). Child 2 reads the count first, then exactly that many values.

```c
/* child2: first the count, then exactly that many numbers */
int n;
read_full(p12[0], &n, sizeof(n));
for (int i = 0; i < n; i++) {
    int value;
    if (read_full(p12[0], &value, sizeof(value)) == -1) break;
    printf("Child_2 read random number: %d\n", value);
}
```

Task 3

**Structs through a pipe**

The parent hands out work. Workers are structs with an ID, a name and an address – fill in a few by hand. The parent sends the name of the job (a string), then how many workers it needs (an `int`), then the workers themselves – if you can, all of them with a single `write()`. The child prints the job and the number, then every worker's data. The parent waits for the child.

```text
Child: the job Disinfection is done by 3 workers.
Received: Id: 1 and Name: Bela and Address: Budapest
Received: Id: 2 and Name: Janos and Address: Miskolc
Received: Id: 3 and Name: Mark and Address: Szeged
```

**Approach:**

Three pieces, three agreed sizes: a fixed `char[100]` (or a length prefix) for the job name, an `int`, then `count * sizeof(struct worker)` bytes. The struct holds arrays, not pointers. On the receiving side allocate the array after the count has arrived, and read it with a full-read loop.

```c
char job[100];
int count;
read_full(fd, job, sizeof(job));                    /* fixed-size name: both sides agree on 100 */
read_full(fd, &count, sizeof(count));

struct worker *list = malloc(count * sizeof(struct worker));
read_full(fd, list, count * sizeof(struct worker));
/* … print them … */
free(list);
```

## Summary

- `pipe(fd)`: `fd[0]` reads, `fd[1]` writes; create it before `fork()` so the child inherits it.
- Right after `fork()`, every process closes the ends it does not use. `read()` returns 0 only when every write end is closed.
- One pipe, one direction: two-way talk needs two pipes; child-to-child pipes are created by the parent before both forks.
- A pipe forgets write boundaries. Use fixed sizes or a length prefix, check what `read()` returned, and loop until you have it all. `sizeof` of a pointer is not the length of the string.
- A FIFO is a pipe with a name: `mkfifo`, `open()` blocks until the other side opens, `unlink()` at the end.
- Message queues keep boundaries and types: `ftok` → `msgget` → `msgsnd`/`msgrcv` (size without `mtype`, type > 0) → `msgctl(IPC_RMID)`.
- `fflush(stdout)` before `fork()`, and wait for every child – no IPC replaces `waitpid`.
