Creating and Using Pipes pipe() syscall, fork(), parent-child IPC

 

πŸ”§ Creating and Using Pipes
Chapter 44 β€” Part 2: pipe() syscall, fork(), parent-child IPC
πŸ“– Section 44.2
πŸ”§ pipe() + fork()
🐧 Linux IPC
πŸ’» Code Examples

🏷️ Key Terms

pipe(int filedes[2]) filedes[0] read end filedes[1] write end fork() close unused fds EOF detection dup2() SIGPIPE fdopen() ioctl FIONREAD

1. The pipe() System Call

The pipe() system call creates a new pipe and returns two file descriptors:

#include <unistd.h>

int pipe(int filedes[2]);
/* Returns: 0 on success, -1 on error */

/* After successful pipe():
   filedes[0] = read end  (you read from here)
   filedes[1] = write end (you write to here)
*/

πŸ“Š After pipe() call β€” single process view
Calling Process

Process
filedes[0] ──┐
filedes[1] ───

β†’

[ PIPE BUFFER ]
← filedes[0] reads from here
β†’ filedes[1] writes to here
A single process holds both ends β€” not very useful alone

A pipe within a single process has limited use. The real power comes when we use fork() to create a child process β€” both parent and child get copies of both file descriptors.

πŸ’‘ ioctl tip: You can check how many bytes are waiting unread in a pipe using:
ioctl(fd, FIONREAD, &count);
This returns the number of bytes currently in the pipe buffer (not in SUSv3 standard, but works on Linux and many UNIX systems).

2. Using pipe() + fork(): The Standard Pattern

The standard way to use a pipe between two processes:

1
Call pipe() to create the pipe β€” you now have fd[0] (read) and fd[1] (write)
2
Call fork() β€” now both parent and child have copies of both file descriptors
3
Parent (writer): Close the read end fd[0]. Write data to fd[1]. Close fd[1] when done.
4
Child (reader): Close the write end fd[1]. Read data from fd[0]. Gets EOF when parent closes fd[1].
⚠️ CRITICAL: Always close unused ends!
If the child keeps fd[1] (write end) open, it will never see EOF when it reads β€” because the write end is still “open” (even if no one writes). This causes the child to hang forever waiting for data that never comes.

Rule: Each process should close the end it doesn’t use, immediately after fork().

πŸ“Š Pipe State after fork() β€” before closing unused ends

PARENT
fd[0] (read) βœ“
fd[1] (write) βœ“
(wants to write β†’ close fd[0])

PIPE
kernel buffer
← fd[0] reads
β†’ fd[1] writes

CHILD
fd[0] (read) βœ“
fd[1] (write) βœ“
(wants to read β†’ close fd[1])
⚠️ Each process must close the end it won’t use!
πŸ“Š After closing unused ends β€” correct setup
PARENT (writer)
fd[0] β€” CLOSED
fd[1] (write) ← uses this
β†’β†’β†’
PIPE
β†’β†’β†’
CHILD (reader)
fd[0] (read) ← uses this
fd[1] β€” CLOSED

3. Coding Example 1 β€” Parent Writes, Child Reads

Classic pipe usage: parent sends a message to child using a pipe.

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

#define MSG "Hello child! Message via pipe.\n"

int main(void)
{
    int pfd[2];   /* pfd[0]=read end, pfd[1]=write end */
    pid_t pid;
    char buf[128];
    ssize_t n;

    /* Step 1: Create pipe BEFORE fork */
    if (pipe(pfd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    /* Step 2: Fork */
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        /* ===== CHILD PROCESS: Reader ===== */

        /* Close write end β€” child only reads */
        close(pfd[1]);

        /* Read from pipe until EOF */
        while ((n = read(pfd[0], buf, sizeof(buf) - 1)) > 0) {
            buf[n] = '\0';
            printf("[child PID %d] received: %s", getpid(), buf);
        }

        if (n == 0) {
            printf("[child] Got EOF β€” parent closed write end\n");
        }

        close(pfd[0]);
        exit(EXIT_SUCCESS);

    } else {
        /* ===== PARENT PROCESS: Writer ===== */

        /* Close read end β€” parent only writes */
        close(pfd[0]);

        /* Write message to pipe */
        printf("[parent PID %d] sending message...\n", getpid());
        write(pfd[1], MSG, strlen(MSG));

        /* Close write end β€” this sends EOF to child */
        close(pfd[1]);

        /* Wait for child to finish */
        wait(NULL);
        printf("[parent] child finished, exiting\n");
    }

    return 0;
}

/*
Compile: gcc -o pipe_basic pipe_basic.c
Run:     ./pipe_basic

Expected output:
[parent PID 1234] sending message...
[child PID 1235] received: Hello child! Message via pipe.
[child] Got EOF - parent closed write end
[parent] child finished, exiting
*/

Step-by-step explanation:

  • pipe(pfd) β€” creates pipe before fork, so child inherits both fds
  • fork() β€” duplicates process; child gets same pfd[] values
  • Child closes pfd[1] β€” it only reads, so write end is unnecessary. Important for EOF detection.
  • Parent closes pfd[0] β€” it only writes, read end is unnecessary
  • Parent closes pfd[1] after writing β€” this signals EOF to child

4. Coding Example 2 β€” Bidirectional Communication (Two Pipes)

Since a single pipe is unidirectional, to have parent and child talk both ways, you need two pipes.

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

/* Two pipes for bidirectional communication:
   p2c[]: parent writes to child  (parent→child)
   c2p[]: child writes to parent  (child→parent)
*/

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

    if (pipe(p2c) == -1 || pipe(c2p) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

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

    if (pid == 0) {
        /* ===== CHILD ===== */

        /* Child reads from p2c, writes to c2p */
        close(p2c[1]);   /* don't need write end of p2c */
        close(c2p[0]);   /* don't need read end of c2p */

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

        /* Send answer back to parent via c2p */
        const char *answer = "42 is the answer!";
        write(c2p[1], answer, strlen(answer));

        close(p2c[0]);
        close(c2p[1]);
        exit(EXIT_SUCCESS);

    } else {
        /* ===== PARENT ===== */

        /* Parent writes to p2c, reads from c2p */
        close(p2c[0]);   /* don't need read end of p2c */
        close(c2p[1]);   /* don't need write end of c2p */

        /* Ask a question */
        const char *question = "What is the meaning of life?";
        printf("[parent] sending: %s\n", question);
        write(p2c[1], question, strlen(question));
        close(p2c[1]);   /* done writing */

        /* Read the answer */
        n = read(c2p[0], buf, sizeof(buf) - 1);
        buf[n] = '\0';
        printf("[parent] child replied: %s\n", buf);

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

    return 0;
}

/*
Expected output:
[parent] sending: What is the meaning of life?
[child] received question: What is the meaning of life?
[parent] child replied: 42 is the answer!
*/
πŸ“ Two-pipe pattern: This is a standard Unix design. Many programs like popen() internally use this pattern. In real applications you must be careful of deadlock: if both parent and child try to write to their respective pipes at the same time, and both pipes are full, they both block waiting for the other to read β€” causing a deadlock.

5. Coding Example 3 β€” Simulating “ls | wc -l” in C

This is exactly what the shell does when you type ls | wc -l. We use dup2() to redirect stdout/stdin to the pipe.

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

/* Simulate: ls | wc -l */

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

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

    /* Fork first child: runs 'ls' */
    pid_t pid1 = fork();
    if (pid1 == -1) { perror("fork"); exit(EXIT_FAILURE); }

    if (pid1 == 0) {
        /* Child 1: ls
         * Redirect stdout (fd 1) to write end of pipe (pfd[1])
         * dup2(old_fd, new_fd): makes new_fd a copy of old_fd
         */
        close(pfd[0]);               /* close read end, not needed */
        dup2(pfd[1], STDOUT_FILENO); /* ls output goes into pipe */
        close(pfd[1]);               /* original fd no longer needed */

        execlp("ls", "ls", NULL);   /* run ls */
        perror("execlp ls");        /* reached only on error */
        exit(EXIT_FAILURE);
    }

    /* Fork second child: runs 'wc -l' */
    pid_t pid2 = fork();
    if (pid2 == -1) { perror("fork"); exit(EXIT_FAILURE); }

    if (pid2 == 0) {
        /* Child 2: wc -l
         * Redirect stdin (fd 0) to read end of pipe (pfd[0])
         */
        close(pfd[1]);               /* close write end, not needed */
        dup2(pfd[0], STDIN_FILENO);  /* wc reads from pipe */
        close(pfd[0]);               /* original fd no longer needed */

        execlp("wc", "wc", "-l", NULL);
        perror("execlp wc");
        exit(EXIT_FAILURE);
    }

    /* Parent: close both ends β€” it uses neither */
    /* IMPORTANT: if parent keeps pfd[1] open, wc will never get EOF! */
    close(pfd[0]);
    close(pfd[1]);

    /* Wait for both children */
    waitpid(pid1, NULL, 0);
    waitpid(pid2, NULL, 0);

    return 0;
}

/*
Compile: gcc -o ls_wc ls_wc.c
Run:     ./ls_wc

Output: (number of files in current directory)
   7
*/

Key concepts demonstrated:

  • dup2(pfd[1], STDOUT_FILENO) β€” makes fd 1 (stdout) point to the pipe write end. Now when ls writes to stdout, data goes into the pipe.
  • dup2(pfd[0], STDIN_FILENO) β€” makes fd 0 (stdin) point to the pipe read end. wc -l reads from stdin which is now the pipe.
  • Parent closes both ends β€” critical so wc gets EOF when ls finishes.

πŸ“Š How dup2() redirects fd to pipe

Before dup2()
fd 0 (stdin) β†’ terminal
fd 1 (stdout) β†’ terminal
fd 2 (stderr) β†’ terminal
pfd[1] β†’ pipe write
β†’

After dup2(pfd[1], 1)
fd 0 (stdin) β†’ terminal
fd 1 (stdout) β†’ pipe write βœ“
fd 2 (stderr) β†’ terminal
pfd[1] closed
ls now writes to pipe instead of terminal screen

6. Common Mistakes and How to Avoid Them
Mistake Effect Fix
Child keeps write end open (pfd[1]) Child’s read() never returns EOF β€” hangs forever Child must close(pfd[1]) immediately after fork
Parent keeps pipe fds open after fork Reader never gets EOF even after all writers close Parent closes both ends after fork if it won’t use them
Writing to pipe whose read end is closed SIGPIPE signal sent to writer; write() returns EPIPE Handle SIGPIPE or check errno == EPIPE after write()
Not calling waitpid() for children Zombie processes accumulate Always wait() or waitpid() for each forked child
Using pipe in single process only Writing to full pipe blocks forever (deadlock) Use non-blocking I/O or threads for single-process use

🎯 Interview Questions β€” Creating and Using Pipes
Q1. What does pipe() return? How do you know which fd is for reading and which for writing?
pipe() fills an array of 2 ints. filedes[0] is always the read end; filedes[1] is always the write end. Remember: 0 = read (like stdin = 0), 1 = write (like stdout = 1).
Q2. Why must you call pipe() BEFORE fork()? What happens if you call it after?
pipe() must be called before fork() so that the child inherits the same file descriptors. If called after fork(), parent and child get different, unconnected pipes β€” they cannot communicate via that pipe.
Q3. Why should a child close the write end of a pipe if it only intends to read?
Because the child holds an open reference to the write end. The reader gets EOF only when ALL file descriptors to the write end are closed. If the child keeps it open, it will never see EOF β€” even after the parent closes its write end. The reader will block indefinitely.
Q4. How does dup2() help in implementing “ls | wc -l”?
dup2(pfd[1], STDOUT_FILENO) makes fd 1 (stdout) point to the pipe’s write end. So when ls calls printf or write(1, ...), the data goes into the pipe, not the terminal. Similarly, dup2(pfd[0], STDIN_FILENO) makes wc‘s stdin come from the pipe read end.
Q5. What is SIGPIPE and when is it sent?
SIGPIPE is sent to a process when it tries to write to a pipe whose read end is closed (no readers). By default, SIGPIPE terminates the process. You can ignore it with signal(SIGPIPE, SIG_IGN) and check errno == EPIPE after write() instead.
Q6. How do you implement bidirectional communication between parent and child using pipes?
Use two separate pipes: one for parentβ†’child and one for childβ†’parent. Each process closes the ends it doesn’t use. Be careful of deadlock: if both try to write large amounts simultaneously before reading, they may both block waiting for the other to drain the pipe.
Q7. Write the pseudocode to connect two commands with a pipe (like the shell does for “cmd1 | cmd2”).
pipe(pfd);
pid1 = fork();
if (pid1 == 0) {
    close(pfd[0]);
    dup2(pfd[1], STDOUT_FILENO);
    close(pfd[1]);
    exec(cmd1);
}
pid2 = fork();
if (pid2 == 0) {
    close(pfd[1]);
    dup2(pfd[0], STDIN_FILENO);
    close(pfd[0]);
    exec(cmd2);
}
close(pfd[0]); close(pfd[1]);  /* parent closes both */
waitpid(pid1, ...); waitpid(pid2, ...);
Q8. Can you use stdio functions (printf, scanf) with a pipe?
Yes, by using fdopen(pfd[0], "r") or fdopen(pfd[1], "w") to get a FILE* stream. However, be careful of stdio buffering β€” output may not be flushed to the pipe immediately. Use fflush() or set the stream to unbuffered with setbuf(fp, NULL) to avoid hangs.

Continue Learning

You’ve learned how to create pipes and connect processes. Next: FIFOs (Named Pipes)

← Part 1: Overview β†’ Part 3: FIFOs (Named Pipes) πŸ“‹ Chapter Index

Leave a Reply

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