JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Named Pipes and Message Queues

Guide 1 wired two processes together with an anonymous pipe — but only because they were close relatives sharing inherited descriptors. Here we meet two ways for total strangers to talk: a named pipe that lives in the filesystem like a file, and a message queue that hands over whole messages instead of a raw byte stream.

The problem the anonymous pipe could not solve

Guide 1 left you with a working anonymous pipe: pipe() handed back two file descriptors, one to read and one to write, and a fork() let parent and child each keep one end. That worked because of a quiet assumption — the two processes were relatives. The child inherited the parent's open descriptors across the fork, so both sides already held the same pipe. But an anonymous pipe has no name anywhere; nothing in the filesystem points at it. So if two completely unrelated programs — say a shell command you type now and a daemon that started yesterday — want to talk, neither can name the other's pipe, and inheritance is off the table. They need a rendezvous point both can find independently.

A named pipe — also called a FIFO, for first-in-first-out — is exactly that rendezvous point. It is a pipe that has been given a name in the filesystem. You create one with the mkfifo() call (or the mkfifo shell command), and a special file appears at a path you choose, say /tmp/myfifo. That path is the public meeting place: any process that can see the path can open it, with no kinship and no inheritance required. The data still behaves like a pipe — bytes flow one way, the kernel buffers them, a reader blocks until a writer arrives — but now the two parties find each other through the filesystem instead of through a shared family tree.

Opening a FIFO: the blocking rendezvous

Because a FIFO looks like a file, you talk to it with the same calls you already know from the file-descriptor rung: open(), read(), write(), and close(). One side opens the path for reading, the other for writing, and from then on it is exactly the byte stream of guide 1. The surprising part is the open() itself. Opening a FIFO blocks by default until both a reader and a writer are present. Open it for reading and your process waits, frozen inside open(), until some other process opens the same path for writing — and vice versa. That blocking handshake is a feature: it guarantees neither side starts pushing bytes into the void.

# terminal 1: create the FIFO, then read from it
$ mkfifo /tmp/myfifo
$ cat /tmp/myfifo          # blocks here, waiting for a writer

# terminal 2: open the same name for writing
$ echo "hello from a stranger" > /tmp/myfifo

# terminal 1 now unblocks and prints:
hello from a stranger
Two unrelated shells meeting through one named path. The reader's cat blocks inside open() until the writer arrives — no fork, no shared ancestry, just the name /tmp/myfifo.

Everything else carries straight over from the anonymous pipe, including the sharp edges. A FIFO is still one-directional: if you need replies, you make a second FIFO for the other direction. The kernel buffer is still bounded (typically 64 KiB on Linux), so a writer that outruns its reader will eventually block on write(). And the most important hazard is unchanged: if every reader has closed its end and a process still calls write(), the kernel raises SIGPIPE, whose default action kills the writer outright. That is the same broken-pipe rule guide 1 warned about — a named pipe does not soften it, so a careful program either handles the signal or checks for the EPIPE error.

Streams have no seams: the boundary problem

There is a subtle limitation that both anonymous and named pipes share, and it is worth dwelling on because it is exactly what message queues are built to fix. A pipe is a byte stream: it carries an undivided river of bytes with no message boundaries preserved. Suppose a writer makes three separate write() calls of "AB", "CD", and "EF". The reader is not promised three reads of two bytes each. It might get all six bytes "ABCDEF" in one read(), or "ABC" then "DEF", or any split at all. The kernel only guarantees the order of the bytes, never where one logical message stops and the next begins.

This is not a bug; it is what "stream" means, and the same property bites you on a TCP socket in a later rung. But it pushes work onto you. If your protocol sends discrete records, you must invent a framing scheme on top of the raw stream — a length prefix ("here come 12 bytes, then the next message"), or a delimiter like a newline that you scan for and never allow inside a message. Plenty of real bugs are just a program that forgot the stream can split or merge its writes, assumed one write() equals one read(), and read a half-message as if it were whole.

Message queues: handing over whole messages

A message queue is the kernel object built around exactly this idea: it stores and delivers discrete messages, not a flat byte stream. When you send a 12-byte message, the receiver receives that 12-byte message as one indivisible unit — the boundaries you wrote are the boundaries it reads. On Linux the modern interface is the POSIX message queue family: mq_open() to create or attach to a named queue, mq_send() to enqueue one message, mq_receive() to dequeue one. Like a FIFO it lives under a name (a path-like string such as "/myqueue"), so unrelated processes can find it; unlike a FIFO, the unit of transfer is the whole message.

A message queue throws in two extras a raw pipe never offered. First, persistence within the kernel: the queue and its undelivered messages survive even when no process currently holds it open, so a sender can drop messages now and a receiver started later still finds them waiting — a pipe, by contrast, evaporates the moment its last descriptor closes. Second, priority: each message carries a priority number, and mq_receive() always hands back the highest-priority message available, so an urgent record can jump ahead of ordinary ones rather than waiting at the back of a strict line. Neither is possible with a plain byte-stream pipe.

None of this is free, and honesty matters here. A message queue has a bounded capacity — a maximum number of messages and a maximum message size, fixed when the queue is created — so mq_send() blocks (or fails, if you opened the queue non-blocking) once the queue is full, the same back-pressure a full pipe applies. And every send still copies the message bytes through the kernel, just as a pipe does. That copy cost is precisely why guide 4 of this rung turns to shared memory, where two processes map the same physical pages and exchange data with no copying at all — the fastest IPC, at the price of doing your own synchronization.

Choosing between them, and what comes next

So how do you pick? Reach for a named pipe when the data really is a stream and you want something dead simple that the shell can drive — a FIFO is a few lines and one mkfifo away, and tools like cat, grep, and redirection all speak to it as if it were a file. Reach for a message queue when you are passing discrete records and want the kernel to keep their boundaries, when you need a message to outlive a brief gap with no live reader, or when priority ordering matters. Both let unrelated processes meet through a name — that shared naming is the leap beyond the inherited anonymous pipe of guide 1.

One honest caveat before we move on: neither of these is the only record-preserving option, and in modern code many people skip both. A Unix-domain socket — covered later in this rung — also lets unrelated local processes talk, is bidirectional on one connection, can preserve message boundaries in its datagram mode, and is the same programming model you will reuse for the network. Plenty of production systems prefer it over POSIX message queues for exactly that consistency. None of these is a universal best; each is a different point on the tradeoff between simplicity, message framing, persistence, and raw speed.

You now hold a much fuller map of inter-process communication than guide 1 gave you: anonymous pipes for relatives, named pipes for strangers who want a stream, and message queues for strangers who want bounded, ordered, priority-aware records. What every one of these shares is that they all move data through the kernel. The next guide steps sideways to a different kind of IPC entirely — not data, but a one-bit interruption: a signal, the kernel's way of tapping a process on the shoulder to say something happened. After that comes the fastest mechanism, shared memory, and finally the art of waiting on many of these channels at once with select and poll.