Why Combine pipe() and fork()?

 

πŸ”€ Pipes with fork()
Parent ↔ Child Process Communication via Pipes
Part 2 of 9
fork + pipe
Topic
Parent-Child IPC
Level
Intermediate

Why Combine pipe() and fork()?

A pipe by itself is only useful within a single process. The real power of pipes comes when combined with fork(). After fork(), the child inherits all open file descriptors from the parent β€” including both ends of the pipe. This is how parent and child processes can communicate with each other.

The critical rule: after fork(), each process must close the pipe end it doesn’t use. This is not optional β€” it prevents deadlocks and ensures correct EOF detection.

Key Concepts

fork() fd inheritance close unused ends EOF detection SIGPIPE wait() bidirectional

πŸ“Š How pipe() + fork() Works β€” Step by Step

Step 1: Call pipe()
Parent Process
fd[0]
read
fd[1]
write
Both ends open in parent

Step 2: Call fork()
Parent
fd[0]
fd[1]
πŸ”€
Child
fd[0]
fd[1]
Child inherits copies of both file descriptors

Step 3: Close unused ends (Parent writes β†’ Child reads)
Parent (WRITER)
fd[0]βœ—
fd[1] βœ“
close(fd[0]) β†’ writes to fd[1]
PIPE
β†’β†’β†’
Child (READER)
fd[0] βœ“
fd[1]βœ—
close(fd[1]) β†’ reads from fd[0]

πŸ’» Example 1: Parent Writes, Child Reads (TLPI Listing 44-2)

This is the classic example from TLPI β€” the parent sends a command-line string to the child via a pipe.

/* simple_pipe.c β€” parent writes, child reads */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

#define BUF_SIZE 10

int main(int argc, char *argv[])
{
    int pfd[2];          /* pipe file descriptors */
    char buf[BUF_SIZE];
    ssize_t numRead;

    if (argc != 2) {
        fprintf(stderr, "Usage: %s <string>\n", argv[0]);
        return 1;
    }

    /* Step 1: Create the pipe */
    if (pipe(pfd) == -1) {
        perror("pipe");
        return 1;
    }

    /* Step 2: Fork a child */
    switch (fork()) {
    case -1:
        perror("fork");
        return 1;

    case 0:  /* ===== CHILD: reads from pipe ===== */
        /* Close unused write end */
        if (close(pfd[1]) == -1) {
            perror("close - child write end");
            _exit(1);
        }

        /* Read data in BUF_SIZE chunks until EOF */
        for (;;) {
            numRead = read(pfd[0], buf, BUF_SIZE);
            if (numRead == -1) {
                perror("read");
                _exit(1);
            }
            if (numRead == 0)
                break;  /* EOF: parent closed write end */

            /* Echo data to stdout */
            if (write(STDOUT_FILENO, buf, numRead) != numRead) {
                fprintf(stderr, "child: partial write\n");
                _exit(1);
            }
        }
        write(STDOUT_FILENO, "\n", 1);  /* trailing newline */

        if (close(pfd[0]) == -1) {
            perror("close - child read end");
            _exit(1);
        }
        _exit(EXIT_SUCCESS);

    default:  /* ===== PARENT: writes to pipe ===== */
        /* Close unused read end */
        if (close(pfd[0]) == -1) {
            perror("close - parent read end");
            exit(1);
        }

        /* Write command-line argument to pipe */
        if (write(pfd[1], argv[1], strlen(argv[1])) != (ssize_t)strlen(argv[1])) {
            fprintf(stderr, "parent: partial write\n");
            exit(1);
        }

        /* Close write end β€” child sees EOF after this */
        if (close(pfd[1]) == -1) {
            perror("close - parent write end");
            exit(1);
        }

        /* Wait for child to finish */
        wait(NULL);
        exit(EXIT_SUCCESS);
    }
}
Compile and Run:
gcc -o simple_pipe simple_pipe.c
./simple_pipe "Hello from parent to child!"
Hello from parent to child!

πŸ’» Example 2: Child Writes, Parent Reads

Here we reverse the direction β€” the child sends data up to the parent.

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

int main(void)
{
    int pfd[2];
    char buf[128];
    ssize_t n;
    pid_t child_pid;

    if (pipe(pfd) == -1) {
        perror("pipe");
        return 1;
    }

    child_pid = fork();

    if (child_pid == -1) {
        perror("fork");
        return 1;
    }

    if (child_pid == 0) {
        /* ===== CHILD: writes to pipe ===== */
        close(pfd[0]);   /* close unused read end */

        char *msg = "Message from child process!";
        printf("[Child PID=%d] Sending: %s\n", getpid(), msg);
        write(pfd[1], msg, strlen(msg));

        close(pfd[1]);   /* EOF signal to parent */
        _exit(0);

    } else {
        /* ===== PARENT: reads from pipe ===== */
        close(pfd[1]);   /* close unused write end */

        n = read(pfd[0], buf, sizeof(buf) - 1);
        buf[n] = '\0';
        printf("[Parent PID=%d] Received: %s\n", getpid(), buf);

        close(pfd[0]);
        waitpid(child_pid, NULL, 0);
    }

    return 0;
}
/* Output:
   [Child PID=12346] Sending: Message from child process!
   [Parent PID=12345] Received: Message from child process!
*/

πŸ’» Example 3: Bidirectional β€” Two Pipes

For two-way communication, use two separate pipes β€” one for each direction. Be careful of deadlocks!

/* bidirectional_pipe.c β€” parent and child talk to each other */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
    int p2c[2];   /* parent β†’ child pipe */
    int c2p[2];   /* child β†’ parent pipe */
    char buf[128];
    ssize_t n;

    /* Create both pipes before fork */
    if (pipe(p2c) == -1 || pipe(c2p) == -1) {
        perror("pipe");
        return 1;
    }

    pid_t pid = fork();
    if (pid == -1) { perror("fork"); return 1; }

    if (pid == 0) {
        /* ===== CHILD ===== */
        close(p2c[1]);  /* don't write to parent→child pipe */
        close(c2p[0]);  /* don't read from child→parent pipe */

        /* Read request from parent */
        n = read(p2c[0], buf, sizeof(buf) - 1);
        buf[n] = '\0';
        printf("[Child] Received from parent: \"%s\"\n", buf);

        /* Send response back to parent */
        char *reply = "PONG from child";
        write(c2p[1], reply, strlen(reply));
        printf("[Child] Sent reply: \"%s\"\n", reply);

        close(p2c[0]);
        close(c2p[1]);
        _exit(0);

    } else {
        /* ===== PARENT ===== */
        close(p2c[0]);  /* don't read from parent→child pipe */
        close(c2p[1]);  /* don't write to child→parent pipe */

        /* Send a message to child */
        char *msg = "PING from parent";
        write(p2c[1], msg, strlen(msg));
        printf("[Parent] Sent: \"%s\"\n", msg);
        close(p2c[1]);  /* signal EOF so child can proceed */

        /* Read response from child */
        n = read(c2p[0], buf, sizeof(buf) - 1);
        buf[n] = '\0';
        printf("[Parent] Received reply: \"%s\"\n", buf);

        close(c2p[0]);
        wait(NULL);
    }

    return 0;
}
/* Output:
   [Parent] Sent: "PING from parent"
   [Child] Received from parent: "PING from parent"
   [Child] Sent reply: "PONG from child"
   [Parent] Received reply: "PONG from child"
*/
⚠️ Deadlock Warning: If both parent and child try to read at the same time (both waiting for data), they will deadlock. Always design the protocol so one side writes first and the other reads.

πŸ‘« Pipes Between Sibling Processes

When a shell runs ls | grep .c, it creates a pipe, then forks two children (siblings). The pipe was created by the parent shell. Both siblings inherit both ends, then each closes the end it doesn’t need.

/* sibling_pipe.c β€” pipe between two child processes (siblings) */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
    int pfd[2];

    /* Parent creates pipe BEFORE forking children */
    if (pipe(pfd) == -1) {
        perror("pipe");
        return 1;
    }

    /* Fork first child β€” WRITER */
    pid_t writer = fork();
    if (writer == 0) {
        close(pfd[0]);  /* writer doesn't read */
        char *data = "Data from sibling writer";
        write(pfd[1], data, strlen(data));
        printf("[Writer child PID=%d] Sent data\n", getpid());
        close(pfd[1]);
        _exit(0);
    }

    /* Fork second child β€” READER */
    pid_t reader = fork();
    if (reader == 0) {
        close(pfd[1]);  /* reader doesn't write */
        char buf[128];
        ssize_t n = read(pfd[0], buf, sizeof(buf) - 1);
        buf[n] = '\0';
        printf("[Reader child PID=%d] Got: \"%s\"\n", getpid(), buf);
        close(pfd[0]);
        _exit(0);
    }

    /* Parent: closes both ends (it doesn't use the pipe) */
    close(pfd[0]);
    close(pfd[1]);

    /* Wait for both children */
    waitpid(writer, NULL, 0);
    waitpid(reader, NULL, 0);

    printf("[Parent] Both children done.\n");
    return 0;
}
Important: The parent must close both pipe ends after forking β€” otherwise, the reader child will never see EOF even after the writer child closes its write end (the parent still holds an open write descriptor!).

🎯 Interview Questions β€” pipe() + fork()
Q1. Why must you call pipe() before fork()?
A: Because fork() copies the parent’s open file descriptors. If you call pipe() after fork(), each process would get its own independent pipe with no shared connection between them.
Q2. Why is it important to close unused pipe ends after fork()?
A: Two reasons: (1) If the reader keeps its write end open, it will never see EOF β€” it will block forever even after the writer is done. (2) If the writer keeps its read end open, the kernel won’t send SIGPIPE when the reader is gone, and the writer could block indefinitely trying to fill a full pipe.
Q3. How does a child process detect that the parent has finished writing?
A: When the parent closes its write end of the pipe, read() in the child returns 0 (EOF), signaling there is no more data.
Q4. Can two processes simultaneously read from the same pipe?
A: Technically yes, but it creates a race condition β€” you cannot predict which process will get which data. Normally only one process reads from a pipe.
Q5. How do you implement bidirectional communication using pipes?
A: Create two pipes before fork(). One pipe carries data from parent to child, the other from child to parent. Each process closes the ends it doesn’t use in each pipe.
Q6. What does wait() do after fork() + pipe() usage?
A: The parent calls wait() (or waitpid()) to wait for the child to terminate, preventing zombie processes and ensuring orderly cleanup.

Leave a Reply

Your email address will not be published. Required fields are marked *