Anonymous Pipes in C: Parent and Child Process IPC

This article explains how to create and use anonymous unidirectional pipes for inter-process communication (IPC) between a parent and child process in the C programming language. It details the mechanics of the pipe() and fork() system calls, illustrates how file descriptors are inherited and managed to enforce one-way communication, and provides a concise code example demonstrating practical implementation.

Understanding the pipe() System Call

An anonymous pipe is a unidirectional data channel managed by the operating system kernel. In C (on POSIX-compliant systems), an anonymous pipe is created using the pipe() system call, defined in <unistd.h>.

The function takes an array of two integers as an argument:

int pipe(int pipefd[2]);

When successful, pipe() allocates two new file descriptors and populates the array:

  • pipefd[0] is opened for reading.
  • pipefd[1] is opened for writing.

Data written to pipefd[1] is buffered by the kernel until it is read from pipefd[0]. The call returns 0 on success and -1 on error.

Inheriting Descriptors with fork()

Because anonymous pipes lack names in the filesystem, they can only be shared between related processes. When a parent process calls fork(), the kernel creates a child process that receives duplicate copies of all the parent's open file descriptors, including pipefd[0] and pipefd[1].

To establish a pipe:

  1. The parent must call pipe() before calling fork().
  2. The parent calls fork().
  3. Both processes now hold references to both ends of the pipe.

Enforcing Unidirectional Flow

Although both processes initially have read and write capabilities, standard anonymous pipes are designed for unidirectional communication. To enforce this and avoid undefined behavior or deadlocks:

  1. Decide the direction of communication: For example, parent (sender) to child (receiver).
  2. Close unused ends in both processes:
    • The parent closes the read end (pipefd[0]) because it will only write.
    • The child closes the write end (pipefd[1]) because it will only read.

Closing the unused write end in the reading process is critical. A read() operation returns 0 (End-of-File) only when all open write descriptors for the pipe are closed. If the reader leaves its own write descriptor open, read() will block indefinitely waiting for further input.

Implementation Example

The following program demonstrates a parent process sending a text string to its child process through an anonymous pipe:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

#define BUFFER_SIZE 128

int main(void) {
    int pipefd[2];
    pid_t pid;
    char message[] = "Hello from the parent process!";
    char buffer[BUFFER_SIZE];

    // 1. Create the pipe
    if (pipe(pipefd) == -1) {
        perror("pipe failed");
        exit(EXIT_FAILURE);
    }

    // 2. Fork the child process
    pid = fork();
    if (pid < 0) {
        perror("fork failed");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        // Child process: reads from the pipe
        close(pipefd[1]); // Close unused write end

        ssize_t bytes_read = read(pipefd[0], buffer, sizeof(buffer) - 1);
        if (bytes_read > 0) {
            buffer[bytes_read] = '\0'; // Null-terminate string
            printf("Child received: %s\n", buffer);
        }

        close(pipefd[0]); // Close read end after reading
        exit(EXIT_SUCCESS);
    } else {
        // Parent process: writes to the pipe
        close(pipefd[0]); // Close unused read end

        write(pipefd[1], message, strlen(message));

        close(pipefd[1]); // Close write end to send EOF to child
        wait(NULL);       // Wait for child to terminate
        exit(EXIT_SUCCESS);
    }
}

Summary of Operations

  1. pipe(pipefd) creates read/write descriptors.
  2. fork() duplicates those descriptors into a new child process.
  3. Each process immediately closes the descriptor it will not use.
  4. The sender uses write() to send byte streams into the pipe.
  5. The receiver uses read() to consume byte streams until 0 (EOF) is returned.
  6. Both processes close their active descriptors to free system resources.