How to Use Named Pipes (FIFOs) for IPC in C

Named pipes, also known as FIFOs (First-In, First-Out), provide a robust mechanism for inter-process communication (IPC) between unrelated local processes on Unix-like operating systems. Unlike standard anonymous pipes, which require a parent-child relationship established through fork(), a named pipe exists as a special device file in the filesystem. This article explains how named pipes operate in the C programming language, demonstrating how independent programs can open, read, and write to a shared FIFO using standard system-level file operations.

The Concept of a FIFO in Unix

A named pipe behaves similarly to a standard pipe: data written to one end is read from the other in strict sequential order. However, because it has an entry in the filesystem directory tree, any process with appropriate permissions can access it using its path. The data itself does not reside on disk; the filesystem entry serves merely as an access point or reference handle, while the operating system kernel manages the communication buffer in memory.

Creating a Named Pipe

A named pipe is created programmatically in C using the mkfifo() function, declared in <sys/stat.h>.

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

The pathname parameter defines where the FIFO will exist in the filesystem, and mode sets file permissions (subject to the calling process's umask). If the FIFO already exists, mkfifo() returns -1 and sets errno to EEXIST.

Inter-Process Communication Mechanism

Once created, processes interact with the FIFO using standard POSIX system calls: open(), read(), write(), and close().

By default, opening a named pipe blocks until both the read and write ends are open:

  • A process opening a FIFO with O_RDONLY will block until another process opens it with O_WRONLY.
  • Conversely, a process opening the FIFO with O_WRONLY will block until a reading process connects.

This synchronization behavior ensures that data is not written into a void and that a reader does not spin idly waiting for an initial connection.

Implementation: The Writer Process

The writer process creates the pipe (or assumes it exists), opens it for writing, sends data, and closes the descriptor.

// writer.c
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>

int main() {
    const char *fifo_path = "/tmp/my_fifo";
    
    // Create the FIFO if it doesn't exist
    mkfifo(fifo_path, 0666);

    printf("Writer: Waiting for reader...\n");
    int fd = open(fifo_path, O_WRONLY);
    
    char message[] = "Hello from the unrelated writer process!";
    write(fd, message, strlen(message) + 1);
    
    close(fd);
    printf("Writer: Data sent and connection closed.\n");
    return 0;
}

Implementation: The Reader Process

The reader process opens the same path for reading, retrieves the data from the kernel buffer, and processes it.

// reader.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    const char *fifo_path = "/tmp/my_fifo";
    char buffer[128];

    printf("Reader: Waiting for writer...\n");
    int fd = open(fifo_path, O_RDONLY);

    ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
    if (bytes_read > 0) {
        printf("Reader received: %s\n", buffer);
    }

    close(fd);
    return 0;
}

Lifecycle and Cleanup

When all writer descriptors are closed, any subsequent read() call by the reader returns 0, signaling End-of-File (EOF). If a process attempts to write to a named pipe that has no readers open, the kernel sends a SIGPIPE signal to the writer, which terminates the process unless handled or ignored.

The FIFO special file remains in the filesystem after the communicating processes terminate. To clean up the resource, a process can remove it using the unlink() system call:

unlink("/tmp/my_fifo");