Loading content...
Anonymous pipes require processes to share a common ancestor—typically established through fork(). This limitation makes them unsuitable for many real-world scenarios: a web server communicating with a logging daemon, a CLI tool controlling a background service, or multiple independent applications coordinating work.
Named Pipes (FIFOs) eliminate this constraint. By existing as filesystem entries, FIFOs allow any process with appropriate permissions to participate in communication, regardless of ancestry. This architectural freedom enables powerful patterns like client-server IPC, producer-consumer systems, and loosely coupled service architectures.
By the end of this page, you will understand:
• How unrelated processes discover and connect via FIFOs • The open() semantics and blocking behavior during connection • Patterns for client-server communication using FIFOs • Bidirectional communication with paired FIFOs • Handling multiple concurrent clients
For two unrelated processes to communicate, they must solve the rendezvous problem: how do they find each other? With anonymous pipes, the answer is inheritance—file descriptors pass from parent to child. With FIFOs, the answer is the filesystem.
The FIFO as a Meeting Point:
Both processes must agree on a well-known pathname. This agreement can be:
/var/run/myapp/control.fifo)/tmp/app-$USER.fifo)| Mechanism | Anonymous Pipe | Named Pipe (FIFO) | Socket |
|---|---|---|---|
| Discovery | File descriptor inheritance | Agreed filesystem path | Path, port, or abstract namespace |
| Setup timing | Before fork() | Either process can create first | Server binds, clients connect |
| Process relationship | Must share ancestor | Any processes | Any processes (even networked) |
| Typical pattern | Pipeline (linear) | Client-server | Client-server or peer-to-peer |
Production systems typically use well-known paths like /var/run/<service>/control for system services or $XDG_RUNTIME_DIR/<app>/fifo for user applications. This allows client tools to connect without additional discovery mechanisms.
The critical moment in FIFO-based IPC is when processes open the FIFO. Unlike regular files, opening a FIFO involves synchronization with the other party. The default behavior creates a natural handshake:
Default Open Behavior (Blocking):
| Open Mode | Behavior |
|---|---|
O_RDONLY | Blocks until a writer opens the FIFO |
O_WRONLY | Blocks until a reader opens the FIFO |
O_RDWR | Never blocks (process is both reader and writer) |
This blocking behavior serves as implicit synchronization—a reader knows a writer exists (and vice versa) once open() returns.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
#include <stdio.h>#include <stdlib.h>#include <fcntl.h>#include <sys/stat.h>#include <unistd.h>#include <time.h> #define FIFO_PATH "/tmp/open_demo.fifo" void log_time(const char *msg) { time_t now = time(NULL); struct tm *t = localtime(&now); printf("[%02d:%02d:%02d] %s\n", t->tm_hour, t->tm_min, t->tm_sec, msg);} // Run as: ./program reader OR ./program writerint main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "Usage: %s <reader|writer>\n", argv[0]); return 1; } // Create FIFO if it doesn't exist mkfifo(FIFO_PATH, 0666); if (strcmp(argv[1], "reader") == 0) { log_time("Reader: Attempting to open FIFO..."); int fd = open(FIFO_PATH, O_RDONLY); // BLOCKS here log_time("Reader: FIFO opened! Writer connected."); char buf[256]; ssize_t n = read(fd, buf, sizeof(buf) - 1); if (n > 0) { buf[n] = '\0'; printf("Reader: Received: %s\n", buf); } close(fd); } else if (strcmp(argv[1], "writer") == 0) { log_time("Writer: Attempting to open FIFO..."); int fd = open(FIFO_PATH, O_WRONLY); // BLOCKS here log_time("Writer: FIFO opened! Reader connected."); const char *msg = "Hello from writer!"; write(fd, msg, strlen(msg)); printf("Writer: Sent message\n"); close(fd); } return 0;}Demonstration:
# Terminal 1 - Start reader first (it will block)
$ ./program reader
[10:30:15] Reader: Attempting to open FIFO...
# ... waiting ...
# Terminal 2 - Start writer (both unblock)
$ ./program writer
[10:30:20] Writer: Attempting to open FIFO...
[10:30:20] Writer: FIFO opened! Reader connected.
Writer: Sent message
# Terminal 1 continues simultaneously:
[10:30:20] Reader: FIFO opened! Writer connected.
Reader: Received: Hello from writer!
Notice how both processes unblock at the same moment (10:30:20) when the second one opens the FIFO. This synchronization is built into the FIFO semantics.
Sometimes blocking indefinitely isn't acceptable. The O_NONBLOCK flag changes open behavior:
| Mode | Without O_NONBLOCK | With O_NONBLOCK |
|---|---|---|
O_RDONLY | Blocks until writer | Returns immediately (fd valid) |
O_WRONLY | Blocks until reader | Returns -1 with ENXIO if no reader |
Asymmetric Behavior Explained:
The asymmetry exists because:
Kernel protects against silent data loss by refusing write-only opens when no reader exists.
123456789101112131415161718192021222324252627282930313233343536373839
#include <stdio.h>#include <fcntl.h>#include <sys/stat.h>#include <unistd.h>#include <errno.h>#include <string.h> #define FIFO_PATH "/tmp/nonblock_demo.fifo" int main(void) { mkfifo(FIFO_PATH, 0666); // Non-blocking read-only open: succeeds immediately printf("Opening for read (O_NONBLOCK)...\n"); int rd_fd = open(FIFO_PATH, O_RDONLY | O_NONBLOCK); if (rd_fd >= 0) { printf("Read-only open succeeded (fd=%d)\n", rd_fd); } // Non-blocking write-only open: fails with ENXIO printf("\nOpening for write (O_NONBLOCK)...\n"); int wr_fd = open(FIFO_PATH, O_WRONLY | O_NONBLOCK); if (wr_fd < 0) { printf("Write-only open failed: %s\n", strerror(errno)); // ENXIO = "No such device or address" (no reader) } // With reader open, writer succeeds printf("\nNow trying write again with reader present...\n"); wr_fd = open(FIFO_PATH, O_WRONLY | O_NONBLOCK); if (wr_fd >= 0) { printf("Write-only open succeeded (fd=%d)\n", wr_fd); close(wr_fd); } close(rd_fd); unlink(FIFO_PATH); return 0;}Opening a FIFO with O_RDWR never blocks because the process counts as both reader and writer. While useful for servers that need non-blocking opens, it has caveats:
• Reads never return EOF (there's always a "writer"—yourself) • Must handle the FIFO lifecycle carefully • Not the intended FIFO semantics—use judiciously
The most common FIFO usage pattern is client-server communication. A server creates a well-known FIFO, reads requests from clients, and processes them. For responses, clients create their own FIFOs.
Architecture:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <fcntl.h>#include <sys/stat.h>#include <unistd.h>#include <signal.h> #define SERVER_FIFO "/tmp/calc_server.fifo" typedef struct { pid_t client_pid; int operand1; int operand2; char operation; char response_fifo[64];} Request; volatile sig_atomic_t running = 1; void handle_signal(int sig) { running = 0; } int main(void) { signal(SIGINT, handle_signal); signal(SIGTERM, handle_signal); unlink(SERVER_FIFO); if (mkfifo(SERVER_FIFO, 0666) == -1) { perror("mkfifo"); return 1; } printf("Calculator server started on %s\n", SERVER_FIFO); // Open for reading (blocks until first client) int server_fd = open(SERVER_FIFO, O_RDONLY); // Keep a dummy writer to prevent EOF int dummy_fd = open(SERVER_FIFO, O_WRONLY); while (running) { Request req; ssize_t n = read(server_fd, &req, sizeof(req)); if (n == sizeof(req)) { int result; switch (req.operation) { case '+': result = req.operand1 + req.operand2; break; case '-': result = req.operand1 - req.operand2; break; case '*': result = req.operand1 * req.operand2; break; case '/': result = req.operand2 ? req.operand1 / req.operand2 : 0; break; default: result = 0; } printf("Request from PID %d: %d %c %d = %d\n", req.client_pid, req.operand1, req.operation, req.operand2, result); // Send response to client's FIFO int resp_fd = open(req.response_fifo, O_WRONLY); if (resp_fd >= 0) { write(resp_fd, &result, sizeof(result)); close(resp_fd); } } } close(dummy_fd); close(server_fd); unlink(SERVER_FIFO); printf("\nServer shutdown\n"); return 0;}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <fcntl.h>#include <sys/stat.h>#include <unistd.h> #define SERVER_FIFO "/tmp/calc_server.fifo" typedef struct { pid_t client_pid; int operand1; int operand2; char operation; char response_fifo[64];} Request; int main(int argc, char *argv[]) { if (argc != 4) { fprintf(stderr, "Usage: %s <num1> <op> <num2>\n", argv[0]); return 1; } // Create client-specific response FIFO Request req; req.client_pid = getpid(); req.operand1 = atoi(argv[1]); req.operation = argv[2][0]; req.operand2 = atoi(argv[3]); snprintf(req.response_fifo, sizeof(req.response_fifo), "/tmp/calc_client_%d.fifo", getpid()); unlink(req.response_fifo); if (mkfifo(req.response_fifo, 0666) == -1) { perror("mkfifo"); return 1; } // Send request to server int server_fd = open(SERVER_FIFO, O_WRONLY); if (server_fd < 0) { perror("Cannot connect to server"); unlink(req.response_fifo); return 1; } write(server_fd, &req, sizeof(req)); close(server_fd); // Read response int resp_fd = open(req.response_fifo, O_RDONLY); int result; read(resp_fd, &result, sizeof(result)); close(resp_fd); printf("%d %c %d = %d\n", req.operand1, req.operation, req.operand2, result); unlink(req.response_fifo); return 0;}Usage:
# Terminal 1: Start server
$ ./server
Calculator server started on /tmp/calc_server.fifo
# Terminal 2: Run clients
$ ./client 10 + 5
10 + 5 = 15
$ ./client 100 '*' 3
100 * 3 = 300
When multiple clients write to the same server FIFO, proper handling requires understanding atomicity guarantees:
POSIX Atomicity Guarantee:
Writes of PIPE_BUF bytes or less (typically 4096 bytes on Linux) are guaranteed atomic—they won't be interleaved with other writes. This is crucial for multi-client scenarios.
Design Strategies:
If your messages exceed PIPE_BUF, concurrent writes can interleave:
Client A writes: [Header-A][Payload-A-part1]
Client B writes: [Header-B][Payload-B]
Client A continues: [Payload-A-part2]
Server reads corrupted data: [Header-A][Payload-A-part1][Header-B][Payload-B][Payload-A-part2]
Always design message sizes ≤ PIPE_BUF for atomic multi-writer scenarios.
FIFOs are unidirectional by design. For bidirectional communication, use two FIFOs—one for each direction. This pattern is common in client-server architectures where both request and response channels are needed.
Two-FIFO Pattern:
┌────────┐ request_fifo ┌────────┐
│ Client │ ─────────────────> │ Server │
│ │ <───────────────── │ │
└────────┘ response_fifo └────────┘
The client-server calculator example above demonstrates this pattern—the server reads from a single request FIFO, but each client creates its own response FIFO for receiving results.
While a single FIFO opened O_RDWR by both processes could theoretically work, it creates problems:
• Echo effect: process reads its own writes • No clear ownership of data direction • Complex synchronization to prevent confusion
Two unidirectional FIFOs provide cleaner semantics and fewer surprises.
You now understand how FIFOs enable communication between independent processes. Next, we'll dive deep into FIFO blocking behavior—understanding exactly when and why reads and writes block, and how to handle these scenarios effectively.