File Descriptor / I/O · Linux userspace / kernel ABI

dup3, pipes, and standard-I/O redirection

Shows how a shell connects the pipe buffer and fd tables for cmd1 | cmd2, and which ends must close for EOF to occur.

Series
13 / 38
Build
cc -std=c17 -Wall -Wextra -O2 pipeline.c -o pipeline
Run
./pipeline
Kernel
Linux 6.18.37 LTS

Why does a reader fail to receive EOF after the pipe writer finishes?

Pipe EOF occurs not when one writer process exits, but when every file reference to the write end is closed. If the parent waits while retaining an unused write end, the reader waits for EOF while the parent waits for the reader to exit, creating a deadlock.

If the target fd number is open, dup2/dup3 closes it and attaches the source open file description to that slot. After redirection, the original pipe fd must also close so the reference count decreases correctly.

Structure diagram

Figure 1. Pipe-end references duplicated inside a pipeline

Writer process

  • fd 1 → pipe write
  • close original pipefd[1]
  • read end close

Pipe object

  • ring buffer
  • readers=1
  • writers=1

Reader process

  • fd 0 → pipe read
  • close original pipefd[0]
  • write end close

Parent

  • read end close
  • write end close
  • wait for both children

EOF occurs only after every fd owning a write end closes. Even one write end left in the parent prevents the reader from receiving EOF.

Call path

Figure 2. From userspace code to observable results
pipe2 create read/write fds
fork writer dup3 onto stdout
fork reader dup3 onto stdin
parent close release references to both ends
EOF/wait pipeline exits

Drawing fd numbers held by each process as rows and the shared pipe object as a column makes the cause of missing EOF easy to find. Every duplicate that a process does not use must be closed.

Figure 3. Major points along the kernel-internal path
do_pipe2 pipe_inode_info
copy_files fork fd table
do_dup2 replace target slot
pipe_write/read ring buffer
fput check for last writer

This diagram is not for memorizing function names. Follow which return value, file descriptor, memory mapping, or wait queue is passed to the next stage.

Linux 6.18.37 LTS source locations

Go beyond the glibc function to the files where the syscall implementation meets kernel objects. Each link points to the original file at the same tag.

FileFunction / structureWhat to inspect
fs/pipe.c do_pipe2(), pipe_read(), pipe_write() pipe object, buffer, reader/writer count
fs/file.c do_dup2(), replace_fd() Atomically replace the target fd slot
kernel/fork.c copy_files() Share or duplicate the fd table at fork

Complete runnable example

The code below is not pseudocode with explanatory lines omitted. It is a minimal example that can be built and run as a file.

Buildcc -std=c17 -Wall -Wextra -O2 pipeline.c -o pipeline
01#define _GNU_SOURCE
02#include <fcntl.h>
03#include <stdio.h>
04#include <stdlib.h>
05#include <sys/wait.h>
06#include <unistd.h>
07
08int main(void)
09{
10    int pipefd[2];
11    if (pipe2(pipefd, O_CLOEXEC) < 0)
12        return 1;
13
14    pid_t writer = fork();
15    if (writer == 0) {
16        if (dup2(pipefd[1], STDOUT_FILENO) < 0)
17            _exit(127);
18        close(pipefd[0]);
19        close(pipefd[1]);
20        execlp("printf", "printf", "alpha\nbeta\n", NULL);
21        _exit(127);
22    }
23
24    pid_t reader = fork();
25    if (reader == 0) {
26        if (dup2(pipefd[0], STDIN_FILENO) < 0)
27            _exit(127);
28        close(pipefd[0]);
29        close(pipefd[1]);
30        execlp("wc", "wc", "-l", NULL);
31        _exit(127);
32    }
33
34    close(pipefd[0]);
35    close(pipefd[1]);
36    int status;
37    waitpid(writer, &status, 0);
38    waitpid(reader, &status, 0);
39    return 0;
40}

Code notes

Source line 11pipe2(pipefd, O_CLOEXEC)

Sets close-on-exec when the 2 fds are created. For the copies duplicated onto standard fds, inspect the attributes of the existing STDIN/STDOUT slots separately.

Source line 16dup2(pipefd[1]

Makes the writer's fd 1 refer to the same open file description as the pipe write end. If fd 1 was already open, replacement is atomic.

Source line 19close(pipefd[1]);

The original number is no longer needed after dup2. Leaving it open can preserve a reference in the exec target or delay EOF.

Source line 30execlp("wc"

Replaces the reader image with wc, while the fd 0 connection remains in a standard slot without CLOEXEC and becomes the pipeline input.

Source line 18close(pipefd[0]);

The parent must close both ends too. Closing them only in the children does not reduce the pipe object's reader/writer counts to 0.

Detailed behavior

01

A pipe is a byte stream

It does not preserve message boundaries. Small writes from multiple writers are guaranteed not to interleave up to PIPE_BUF, but the reader is not guaranteed to read in the same sizes.

The protocol must define its own delimiter or length prefix.

02

After dup, the open description is shared

Although the fd numbers differ, file status flags and offsets are shared through the same struct file used by source and target. FD_CLOEXEC is per fd slot, so the dup2 result does not simply copy the source FD_CLOEXEC.

New code can express intent with dup3 and O_CLOEXEC, with the difference that equal oldfd and newfd produce EINVAL.

03

The shell defines pipeline exit-status policy

Writer and reader terminate independently. The parent must decide whether to return only the last command's status or fail when any command fails, as pipefail does.

Whether a writer terminated by SIGPIPE is a service failure or a normal downstream termination likewise depends on pipeline semantics.

Objects and lifetimes

ObjectCreation and releaseValues to inspect
pipe_inode_infoCreated by pipe2 and released when all read/write files closereaders, writers, ring head/tail
fd copyIncremented by fork/dup and decremented by each close/execWhich process owns which end
pipe bufferFilled by write and drained by read; sleeps or returns EAGAIN according to capacitybytes, slots, wakeup

Failure conditions and common misconceptions

Observed symptomLikely causesHow to verify
The reader does not receive EOFA write-end reference remains somewhereInspect /proc/*/fd and lsof
The writer terminates with SIGPIPEEvery read end is closedCheck wait status and signal
The pipeline occasionally hangsA fork failure, missing close, or asymmetric error pathCompare fd lifetimes with strace -ff

Verify it yourself

  1. Comment out the parent's close(pipefd[1]), confirm that wc fails to receive EOF, then restore it.
  2. Change the writer to emit a large data set and delay the reader to observe pipe-capacity backpressure.
  3. Print /proc/PID/fd links at each fork point and make a table of fd copies that refer to the same pipe inode.
Run./pipeline
Tracestrace -f -e trace=pipe2,clone,dup2,close,read,write,wait4 ./pipeline

Primary sources