Closing Unused Pipe FDs SIGPIPE, EPIPE, and Why Closing Matters

 

๐Ÿšซ Closing Unused Pipe FDs
SIGPIPE, EPIPE, and Why Closing Matters
Part 3 of 9
FD Lifecycle
Topic
SIGPIPE / EPIPE
Level
Intermediate

Why Closing Unused Ends Is Critical

Forgetting to close unused pipe file descriptors is one of the most common pipe programming bugs. It causes processes to hang forever waiting for data that will never come, or to fill up a pipe that no one will read.

There are three separate reasons to close unused pipe ends โ€” each affects a different end and a different process. We explore all three in this file.

Key Concepts

close() unused ends EOF detection SIGPIPE EPIPE broken pipe signal(SIG_IGN) pipe reference count

๐Ÿ“Œ Rule 1: Reader Must Close Its Write End

When the writer process closes its write end of the pipe, the reader should see EOF (read() returns 0). But this only works if the reader has also closed its own copy of the write end.

What Happens Without Closing Write End in Reader

โŒ Bug: Reader keeps write end open
1. Parent (writer) writes data
2. Parent closes fd[1] โ† EOF signal
3. Child’s fd[1] still open โ†’ kernel sees 1 writer
4. Child calls read() โ†’ BLOCKS FOREVER
5. read() never returns 0 (no EOF)
Symptom: process hangs on read()

โœ… Correct: Reader closes its write end
1. After fork(): child calls close(fd[1])
2. Parent writes data, then closes fd[1]
3. Kernel: 0 write descriptors open
4. Child’s read() returns 0 โ† EOF
5. Child exits cleanly
Result: read() returns 0, loop exits
โš ๏ธ The kernel logic: The kernel counts how many file descriptors reference the write end of a pipe. EOF is only delivered when that count reaches zero. If the reader holds an open write fd, the count never reaches zero.

๐Ÿ“Œ Rule 2: Writer Must Close Its Read End โ€” Prevents Blocking Forever

When the writer’s process tries to write to a pipe and no process has an open read descriptor, the kernel sends SIGPIPE to the writer. By default, SIGPIPE kills the process.

SIGPIPE Scenario
Writer Process
has fd[1] open
also has fd[0] open (BUG)
โ†’
PIPE (FULL)
No reader process
โ†’
write() BLOCKS
pipe full, no reader
waits forever ๐Ÿ˜ฑ
If writer also keeps fd[0] open โ†’ even after reader exits โ†’ write() blocks indefinitely

โšก SIGPIPE: Writing to a Broken Pipe

When a write() is attempted on a pipe where no process has the read end open, the kernel delivers SIGPIPE to the writing process.

Scenario Default Behavior If SIGPIPE caught/ignored
write() to pipe with no readers Process killed by SIGPIPE write() returns -1, errno = EPIPE
Subsequent write() after SIGPIPE Process already dead Still fails with EPIPE

Example 1: Demonstrate SIGPIPE and EPIPE

/* sigpipe_demo.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>

static void sigpipe_handler(int sig)
{
    /* Using write() is signal-safe; printf() is not */
    const char *msg = "SIGPIPE received! Pipe is broken.\n";
    write(STDERR_FILENO, msg, strlen(msg));
}

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

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

    /* Install a SIGPIPE handler instead of default (process kill) */
    struct sigaction sa;
    sa.sa_handler = sigpipe_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGPIPE, &sa, NULL);

    /* Close the read end โ€” no one is reading! */
    close(pfd[0]);

    printf("About to write to a pipe with no reader...\n");

    ssize_t ret = write(pfd[1], "Hello", 5);
    if (ret == -1) {
        if (errno == EPIPE)
            printf("write() failed: errno = EPIPE (Broken pipe)\n");
        else
            perror("write");
    } else {
        printf("Write succeeded: %zd bytes\n", ret);
    }

    close(pfd[1]);
    return 0;
}
/* Output:
   About to write to a pipe with no reader...
   SIGPIPE received! Pipe is broken.
   write() failed: errno = EPIPE (Broken pipe)
*/

Example 2: Ignoring SIGPIPE โ€” check errno instead

/* ignore_sigpipe.c โ€” common pattern in network/pipe programs */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>

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

    /* Ignore SIGPIPE โ€” write() will return -1 with errno=EPIPE */
    signal(SIGPIPE, SIG_IGN);

    /* Close read end โ€” simulate broken pipe */
    close(pfd[0]);

    ssize_t ret = write(pfd[1], "test", 4);
    if (ret == -1) {
        if (errno == EPIPE)
            printf("Pipe broken (EPIPE). Handling gracefully.\n");
        else
            perror("write unexpected error");
    }

    close(pfd[1]);
    return 0;
}
/* Output:
   Pipe broken (EPIPE). Handling gracefully.
*/
Best practice: In production code, either (a) install a SIGPIPE handler, or (b) use signal(SIGPIPE, SIG_IGN) and check for EPIPE from write(). Never rely on default SIGPIPE behavior (process kill) in a multi-pipe program.

๐Ÿ“Œ Rule 3: Pipe is Destroyed Only After All FDs are Closed

The kernel maintains a reference count of all open file descriptors pointing to each end of a pipe. The pipe (and its buffered data) is destroyed only when all references โ€” across all processes โ€” are closed.

Pipe Reference Count Example
After fork()
Write end ref count: 2
(parent + child both open fd[1])
โ†’
Parent closes fd[1]
Write end ref count: 1
(only child has fd[1])
โ†’
Child closes fd[1]
Write end ref count: 0
โ†’ EOF delivered to readers
โ†’ Pipe buffer freed
โš ๏ธ Unread data in the pipe is LOST when the pipe is destroyed

Example 3: Demonstrating pipe lifetime with reference counting

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

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

    /* Fork TWO writers โ€” both inherit pfd[1] */
    for (int i = 0; i < 2; i++) {
        if (fork() == 0) {
            /* Child writer */
            close(pfd[0]);  /* close read end */
            char msg[32];
            snprintf(msg, sizeof(msg), "Writer %d speaking\n", i + 1);
            write(pfd[1], msg, strlen(msg));
            close(pfd[1]);  /* decrement write ref count */
            _exit(0);
        }
    }

    /* Parent becomes reader */
    close(pfd[1]);  /* parent must close write end too! */

    char buf[32];
    ssize_t n;
    /* This read loop won't end until BOTH children close their pfd[1] */
    while ((n = read(pfd[0], buf, sizeof(buf))) > 0) {
        printf("[Parent] Read: %.*s", (int)n, buf);
    }
    printf("[Parent] EOF โ€” all writers closed write end\n");

    close(pfd[0]);
    while (wait(NULL) != -1);  /* reap all children */
    return 0;
}
/* Output (order may vary):
   [Parent] Read: Writer 1 speaking
   [Parent] Read: Writer 2 speaking
   [Parent] EOF โ€” all writers closed write end
*/

๐Ÿ“‹ Summary: What Happens If You Don’t Close
Who forgets to close Which end forgotten Consequence
Reader process Write end (fd[1]) read() blocks forever โ€” never gets EOF
Writer process Read end (fd[0]) write() blocks forever when pipe is full (no SIGPIPE)
Any process Either end Pipe not destroyed; memory not freed; FD leak

๐ŸŽฏ Interview Questions โ€” SIGPIPE, EPIPE, Closing FDs
Q1. What is SIGPIPE?
A: SIGPIPE is a signal sent by the kernel to a process that calls write() on a pipe (or socket) when no process has the read end open. By default, SIGPIPE terminates the process.
Q2. What is EPIPE and when does it occur?
A: EPIPE is the errno value set when write() fails because the pipe is broken (no readers). It occurs when SIGPIPE is caught or ignored and the write() call itself fails.
Q3. Why doesn’t SA_RESTART apply to SIGPIPE?
A: Normally, SA_RESTART auto-restarts a slow system call interrupted by a signal. For SIGPIPE, restarting the write() makes no sense because the pipe is still broken โ€” the next write would also fail.
Q4. How do you gracefully handle SIGPIPE in a server program?
A: Use signal(SIGPIPE, SIG_IGN) to ignore it globally, then check write() return values for -1 and errno == EPIPE. This is standard practice in network and pipe-based servers.
Q5. When exactly is a pipe destroyed?
A: When all file descriptors (across all processes) referring to both ends of the pipe are closed. Any unread data in the pipe buffer at that point is lost.
Q6. If two processes both inherit a pipe’s write end, how does the reader know when both are done writing?
A: The reader’s read() returns 0 (EOF) only when all write-end file descriptors (across all processes including the parent) are closed. The kernel uses a reference count to track this.

Leave a Reply

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