FD Lifecycle
SIGPIPE / EPIPE
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
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.
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)
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
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.
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.
*/
signal(SIGPIPE, SIG_IGN) and check for EPIPE from write(). Never rely on default SIGPIPE behavior (process kill) in a multi-pipe program.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.
(parent + child both open fd[1])
(only child has fd[1])
โ EOF delivered to readers
โ Pipe buffer freed
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
*/
| 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 |
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.
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.
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.
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.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.
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.
