๐ฅ๏ธ FIFO Client-Server
Request-Response Pattern with Named Pipes
Part 8 of 9
Client-Server
Client-Server
Topic
FIFO IPC Pattern
FIFO IPC Pattern
Level
Advanced
Advanced
FIFO Client-Server Pattern
FIFOs are well-suited for a client-server architecture on a single machine. A common design:
- The server creates a well-known FIFO (e.g.,
/tmp/server_fifo) - Each client creates its own private FIFO using its PID (e.g.,
/tmp/client_12345_fifo) - Clients send requests to the server’s FIFO, including their own PID
- Server reads requests, processes them, and writes responses back to each client’s private FIFO
Key Concepts
well-known FIFO per-client FIFO PID in request request struct response struct multiple writers PIPE_BUF
๐๏ธ Architecture: Multiple Clients โ One Server
FIFO Client-Server Architecture
๐ฅ๏ธ
SERVER
/tmp/server_fifo
(well-known FIFO)
(well-known FIFO)
reads requests from all clients
Client 1
PID=1001
/tmp/client_1001_fifo
โ request to server_fifo
โ response via client_1001_fifo
โ response via client_1001_fifo
Client 2
PID=1002
/tmp/client_1002_fifo
โ request to server_fifo
โ response via client_1002_fifo
โ response via client_1002_fifo
Client 3
PID=1003
/tmp/client_1003_fifo
โ request to server_fifo
โ response via client_1003_fifo
โ response via client_1003_fifo
๐ Shared Header: Request/Response Structures
/* fifo_cs.h โ shared header for client and server */
#ifndef FIFO_CS_H
#define FIFO_CS_H
#include <sys/types.h>
#include <limits.h>
#define SERVER_FIFO "/tmp/ep_server_fifo"
#define CLIENT_FIFO_TEMPLATE "/tmp/ep_client_%d_fifo"
#define CLIENT_FIFO_LEN 64
/* Message sent from client โ server */
typedef struct {
pid_t client_pid; /* Client's PID (to know which FIFO to reply to) */
int seq_num; /* Sequence number for the request */
} RequestMsg;
/* Message sent from server โ client */
typedef struct {
int seq_num; /* Echoes back the sequence number */
char response[64]; /* Server's response text */
} ResponseMsg;
#endif /* FIFO_CS_H */
๐ป Example 1: FIFO Server
/* fifo_server.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <signal.h>
#include "fifo_cs.h"
static void cleanup(void)
{
unlink(SERVER_FIFO);
}
int main(void)
{
int server_fd, client_fd, dummy_fd;
RequestMsg req;
ResponseMsg resp;
char client_fifo[CLIENT_FIFO_LEN];
ssize_t n;
/* Create the well-known server FIFO */
if (mkfifo(SERVER_FIFO, S_IRUSR | S_IWUSR | S_IWGRP | S_IWOTH) == -1) {
perror("mkfifo server");
return 1;
}
atexit(cleanup);
/* Open server FIFO for reading.
* O_RDWR trick: keeps write end open so server never gets EOF
* when all clients temporarily disconnect. */
server_fd = open(SERVER_FIFO, O_RDONLY);
if (server_fd == -1) {
perror("open server fifo");
return 1;
}
/* Open an extra write end to prevent EOF when no clients exist */
dummy_fd = open(SERVER_FIFO, O_WRONLY);
if (dummy_fd == -1) {
perror("open dummy write end");
return 1;
}
printf("[Server] Started. Listening on %s\n", SERVER_FIFO);
/* Main server loop */
for (;;) {
n = read(server_fd, &req, sizeof(RequestMsg));
if (n != sizeof(RequestMsg)) {
if (n == 0) continue; /* No clients yet */
fprintf(stderr, "[Server] Bad request (n=%zd)\n", n);
continue;
}
printf("[Server] Request from PID=%d, seq=%d\n",
req.client_pid, req.seq_num);
/* Build path to this client's response FIFO */
snprintf(client_fifo, CLIENT_FIFO_LEN, CLIENT_FIFO_TEMPLATE,
req.client_pid);
/* Open client's FIFO to send response */
client_fd = open(client_fifo, O_WRONLY);
if (client_fd == -1) {
perror("open client fifo");
continue;
}
/* Build response */
resp.seq_num = req.seq_num;
snprintf(resp.response, sizeof(resp.response),
"Hello client PID=%d! Processed seq=%d",
req.client_pid, req.seq_num);
write(client_fd, &resp, sizeof(ResponseMsg));
close(client_fd);
}
return 0;
}
๐ป Example 2: FIFO Client
/* fifo_client.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include "fifo_cs.h"
static char client_fifo[CLIENT_FIFO_LEN];
static void cleanup(void)
{
unlink(client_fifo);
}
int main(int argc, char *argv[])
{
int server_fd, client_fd;
RequestMsg req;
ResponseMsg resp;
int seq_num = 1;
/* If seq number provided as arg, use it */
if (argc > 1) seq_num = atoi(argv[1]);
/* Build this client's FIFO path using our PID */
snprintf(client_fifo, CLIENT_FIFO_LEN, CLIENT_FIFO_TEMPLATE, getpid());
/* Create our private response FIFO */
if (mkfifo(client_fifo, S_IRUSR | S_IWUSR | S_IWGRP) == -1) {
perror("mkfifo client");
return 1;
}
atexit(cleanup);
/* Open server FIFO to send request */
server_fd = open(SERVER_FIFO, O_WRONLY);
if (server_fd == -1) {
fprintf(stderr, "Cannot open server FIFO %s โ is server running?\n",
SERVER_FIFO);
return 1;
}
/* Build and send request */
req.client_pid = getpid();
req.seq_num = seq_num;
if (write(server_fd, &req, sizeof(RequestMsg)) != sizeof(RequestMsg)) {
fprintf(stderr, "[Client] Failed to write request\n");
return 1;
}
close(server_fd);
printf("[Client PID=%d] Sent request seq=%d\n", getpid(), seq_num);
/* Open our private FIFO to receive response */
client_fd = open(client_fifo, O_RDONLY);
if (client_fd == -1) {
perror("open client fifo for response");
return 1;
}
if (read(client_fd, &resp, sizeof(ResponseMsg)) != sizeof(ResponseMsg)) {
fprintf(stderr, "[Client] Failed to read response\n");
return 1;
}
printf("[Client PID=%d] Response: \"%s\" (seq=%d)\n",
getpid(), resp.response, resp.seq_num);
close(client_fd);
return 0;
}
/* Usage:
Terminal 1: ./fifo_server
Terminal 2: ./fifo_client 42
Terminal 3: ./fifo_client 99
Server output:
[Server] Request from PID=5001, seq=42
[Server] Request from PID=5002, seq=99
Client 1 output:
[Client PID=5001] Sent request seq=42
[Client PID=5001] Response: "Hello client PID=5001! Processed seq=42" (seq=42)
*/
Why per-client FIFOs? Multiple clients could be sending requests to the server’s FIFO simultaneously. If they all shared one response FIFO, responses would get mixed up. Each client creates its own private FIFO keyed to its PID to ensure responses are delivered to the right client.
โ๏ธ Multiple Writers on a FIFO โ Atomicity
When multiple processes write to the same FIFO, you need to worry about messages getting interleaved. The kernel guarantees atomic writes up to PIPE_BUF bytes (at least 512 bytes on POSIX; 4096 bytes on Linux).
/* fifo_atomic_write.c โ demonstrate PIPE_BUF atomicity */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <limits.h>
#define FIFO_PATH "/tmp/ep_atomic_fifo"
#define NUM_WRITERS 4
int main(void)
{
mkfifo(FIFO_PATH, 0666);
printf("PIPE_BUF = %d bytes (writes <= this are atomic)\n", (int)PIPE_BUF);
for (int i = 0; i < NUM_WRITERS; i++) {
if (fork() == 0) {
/* Child writer */
int fd = open(FIFO_PATH, O_WRONLY);
/* Build a message SMALLER than PIPE_BUF โ atomic! */
char msg[64];
int len = snprintf(msg, sizeof(msg), "[Writer %d] Line #%d\n", i, i * 10);
/* This write is guaranteed to be atomic (len < PIPE_BUF) */
write(fd, msg, len);
close(fd);
_exit(0);
}
}
/* Parent reads all output */
int fd = open(FIFO_PATH, O_RDONLY);
char buf[4096];
ssize_t n = read(fd, buf, sizeof(buf) - 1);
buf[n] = '\0';
printf("\nAll messages received (no interleaving):\n%s", buf);
close(fd);
while (wait(NULL) != -1);
unlink(FIFO_PATH);
return 0;
}
โ ๏ธ Writes > PIPE_BUF are NOT atomic: If a single write() call tries to write more than PIPE_BUF bytes, the kernel may split it into multiple operations, allowing interleaving with writes from other processes. Keep messages under PIPE_BUF for safe multi-writer FIFOs.
๐ฏ Interview Questions โ FIFO Client-Server
Q1. How does a FIFO-based server distinguish responses for different clients?
A: Each client creates a private FIFO using its PID as part of the filename (e.g., /tmp/client_1234_fifo). The client includes its PID in the request. The server opens the per-client FIFO to send the response back to the correct client.
A: Each client creates a private FIFO using its PID as part of the filename (e.g., /tmp/client_1234_fifo). The client includes its PID in the request. The server opens the per-client FIFO to send the response back to the correct client.
Q2. What is PIPE_BUF and why does it matter for multi-writer FIFOs?
A: PIPE_BUF is the maximum number of bytes guaranteed to be written atomically to a pipe/FIFO. On Linux it’s 4096 bytes. If multiple processes write to the same FIFO, writes โค PIPE_BUF bytes are guaranteed not to be interleaved with other writes.
A: PIPE_BUF is the maximum number of bytes guaranteed to be written atomically to a pipe/FIFO. On Linux it’s 4096 bytes. If multiple processes write to the same FIFO, writes โค PIPE_BUF bytes are guaranteed not to be interleaved with other writes.
Q3. Why does the server open the FIFO with an extra dummy write descriptor?
A: To prevent receiving EOF when temporarily no clients have the server’s FIFO open for writing. Without this, the server would see EOF from read() between client connections and might exit or behave incorrectly.
A: To prevent receiving EOF when temporarily no clients have the server’s FIFO open for writing. Without this, the server would see EOF from read() between client connections and might exit or behave incorrectly.
Q4. Can a FIFO handle multiple concurrent writers safely?
A: Yes, if each write is โค PIPE_BUF bytes. Larger writes are not atomic and may be interleaved, corrupting the data stream. Design your protocol so each message fits within PIPE_BUF.
A: Yes, if each write is โค PIPE_BUF bytes. Larger writes are not atomic and may be interleaved, corrupting the data stream. Design your protocol so each message fits within PIPE_BUF.
Q5. What are the limitations of FIFO-based IPC compared to sockets?
A: FIFOs: (1) work only on the same machine, (2) are unidirectional, (3) require per-client FIFOs for bidirectional communication, (4) have no built-in connection management. Sockets support network communication, are bidirectional, and have richer connection semantics.
A: FIFOs: (1) work only on the same machine, (2) are unidirectional, (3) require per-client FIFOs for bidirectional communication, (4) have no built-in connection management. Sockets support network communication, are bidirectional, and have richer connection semantics.
