Pipes that live on disk — letting unrelated processes communicate through a named file.
In the last chapter, you learned about pipes. They're great — but they have a fatal limitation: both processes must be related. You need to call pipe() before fork(), so only parent and child can share the connection. Two completely unrelated programs can't use them.
What if you want two independent programs — maybe a logger daemon and a monitoring tool — to communicate? You need a pipe that has a name on the filesystem, so both programs can find it by opening the same file.
That's exactly what a FIFO is. "First In, First Out" (pronounced "Fy-Foh"). Also called a named pipe. It looks like a regular file on disk, but it acts like a pipe: data written to it can be read from the other end, in order, and it doesn't actually store data on disk.
Click "Send" to push data through the FIFO. Unlike anonymous pipes, any process that knows the filename can connect.
Since a FIFO is a special file on disk, you have to create it before any process can open it. There are two ways: from your C code with mknod(), or from the command line with mkfifo.
c /* Create a FIFO from C code */ mknod("myfifo", S_IFIFO | 0644, 0);
The first argument is the filename. The second is the creation mode: S_IFIFO tells the kernel "make this a FIFO" and 0644 sets read/write permissions (just like chmod). The third argument is a device number — ignored for FIFOs, so pass 0.
From the command line, it's even simpler:
shell $ mkfifo myfifo $ ls -l myfifo prw-r--r-- 1 beej beej 0 Jan 1 00:00 myfifo
Notice the p at the start of the permissions — that means "pipe." The file has zero bytes because FIFOs don't store data on disk. They're conduits, not containers.
Let's build Beej's classic producer-consumer example. The writer process (speak.c) creates the FIFO and sends data into it. Here's the code:
c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <sys/stat.h> #include <unistd.h> #define FIFO_NAME "american_maid" int main(void) { char s[300]; int num, fd; mknod(FIFO_NAME, S_IFIFO | 0666, 0); printf("waiting for readers...\n"); fd = open(FIFO_NAME, O_WRONLY); /* BLOCKS here */ printf("got a reader--type some stuff\n"); while (fgets(s, 300, stdin) != NULL) { if ((num = write(fd, s, strlen(s))) == -1) perror("write"); else printf("speak: wrote %d bytes\n", num); } return 0; }
open(FIFO_NAME, O_WRONLY) call will block until another process opens the other end for reading. speak just sits there, frozen, displaying "waiting for readers..." until tick starts up. This is by design — you can't pour water into a pipe with no other end.Notice that mknod() is called every time. If the FIFO already exists, mknod() fails silently (returns -1 with errno set to EEXIST), which is fine — the FIFO is already there.
Now the consumer side. tick.c opens the same FIFO for reading and sucks out whatever speak puts in:
c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <sys/stat.h> #include <unistd.h> #define FIFO_NAME "american_maid" int main(void) { char s[300]; int num, fd; mknod(FIFO_NAME, S_IFIFO | 0666, 0); printf("waiting for writers...\n"); fd = open(FIFO_NAME, O_RDONLY); /* BLOCKS here */ printf("got a writer\n"); do { if ((num = read(fd, s, 300)) == -1) perror("read"); else { s[num] = '\0'; printf("tick: read %d bytes: \"%s\"\n", num, s); } } while (num > 0); return 0; }
The key sequence is: start speak in one terminal (it blocks on open), then start tick in another (it also blocks on open). The moment both sides are open, data flows. Type in the speak window, and tick prints it out.
By default, FIFOs block in two situations, and this blocking is the core synchronization mechanism:
After both sides are connected:
This makes FIFOs self-synchronizing. The producer and consumer naturally pace each other, like a conveyor belt that pauses when either end stops.
| Operation | Blocks when... | Returns when... |
|---|---|---|
| open(O_WRONLY) | No readers yet | A reader opens the FIFO |
| open(O_RDONLY) | No writers yet | A writer opens the FIFO |
| read() | Buffer empty, writers exist | Data arrives or all writers close |
| write() | Buffer full | Reader drains some data |
Sometimes you don't want open() to block. Maybe you want to check if a reader/writer exists without freezing your whole program. The O_NDELAY flag (also called O_NONBLOCK) changes the blocking behavior:
c /* Non-blocking open for reading */ fd = open(FIFO_NAME, O_RDONLY | O_NDELAY);
The effects depend on which side you're on:
| Side | Normal (blocking) | With O_NDELAY |
|---|---|---|
| Writer open() | Blocks until reader exists | Returns -1 immediately if no reader |
| Reader open() | Blocks until writer exists | Returns immediately (success) |
| Reader read() | Blocks until data arrives | Returns 0 if no data in pipe |
Here's something pipes can't do: multiple unrelated processes can write to the same FIFO. Since the FIFO is a file on disk, any process with the right permissions can open it.
Start one tick (reader) and two or three speaks (writers) in different terminals. Type in any writer window, and the reader gets all the messages. When one writer exits, the reader keeps going — it only gets EOF when all writers close their end.
What about multiple readers? That gets weird. Sometimes one reader gets everything, sometimes messages alternate unpredictably between readers. The kernel doesn't guarantee fair distribution. In practice, you usually want a single reader.
Watch messages flow through a named pipe. Multiple producers can feed into the FIFO, and the consumer reads them in order. Notice how data arrives in FIFO order regardless of which producer sent it.
Click a producer to send a message. The consumer processes messages in the order they arrive in the FIFO buffer.
FIFOs solve the "unrelated processes" problem that pipes couldn't. But they're still just byte streams — there's no concept of message boundaries or types. If you need to send structured, typed messages with priority retrieval, you want message queues.
| Feature | Pipes | FIFOs | Message Queues |
|---|---|---|---|
| Processes | Related only | Any | Any |
| Persistence | In-memory | File on disk | Kernel-managed |
| Message types | No | No | Yes (mtype field) |
| Priority | No | No | Yes (retrieve by type) |
| Direction | One-way | One-way | One-way (per queue) |
"Having the name of the pipe right there on disk sure makes it easier, doesn't it?" — Beej