IPCpipefifonamed pipeproducer consumersigpipe

Pipes: A Kernel Buffer Between Two Processes

A pipe is a small kernel-owned ring buffer with a write end and a read end; the kernel blocks the writer when it is full and the reader when it is empty, turns the last close into EOF, and that is enough to build every shell pipeline and every subprocess.PIPE.

Unix-styleLinux
▶ InteractiveInterview question
Progress

The problem

Process A produces bytes at its own pace and process B consumes them at a different pace. Neither should have to know about the other’s speed, and neither should be able to corrupt the other. Where do the bytes wait, and who decides when each side sleeps?

What `pipe()` gives you

Unix-style

pipe(fds) asks the kernel for a buffer and returns two descriptors in the caller’s File Descriptors table: fds[0] reads from the buffer, fds[1] writes to it. The buffer is unidirectional, byte-oriented and lives in kernel memory; on Linux its default capacity is 16 pages, 64 kB, adjustable per pipe with fcntl(F_SETPIPE_SZ) up to /proc/sys/fs/pipe-max-size (1 MB by default). Bytes written appear to the reader in order and exactly once.

The two descriptors are ordinary descriptors. They survive fork(), which is the whole trick: a parent creates the pipe, forks, and now both processes hold both ends. The parent closes the end it does not need, the child closes the other, and the two ends are in different processes. Nothing about the pipe was ever “sent” anywhere — the descriptor table was copied (Creating Processes: fork, exec, wait).

That mechanism also explains the limitation. Because the ends are shared only by inheritance, an anonymous pipe can only connect a process with its ancestor, descendant or sibling. Two unrelated processes need a named pipe — a FIFO — which is the same kernel buffer given a path in the file system (mkfifo /tmp/events), so any process with permission can open() an end.

The three-call idiom: pipe, fork, close the end you do not own
1int fds[2];
2pipe(fds); // fds[0] = read end, fds[1] = write end
3pid_t pid = fork();
4if (pid == 0) { // child: producer
5 close(fds[0]); // not reading
6 dup2(fds[1], STDOUT_FILENO); // stdout now goes into the pipe
7 close(fds[1]);
8 execlp("cat", "cat", "big.log", nullptr);
9}
10close(fds[1]); // parent: consumer — MUST close its write end,
11char buf[4096]; // or read() never sees EOF
12ssize_t n;
13while ((n = read(fds[0], buf, sizeof buf)) > 0) { /* consume */ }
14// n == 0: every write end is closed → EOF

Full, empty, closed: the three blocking rules

Linux

The pipe is a producer/consumer (Semaphores and Condition Variables) queue implemented by the kernel, and its rules are the classic ones. A write() into a full pipe blocks until the reader drains enough space (with O_NONBLOCK it returns EAGAIN instead). A read() on an empty pipe blocks until a writer adds bytes. Blocking is how a 4 GB cat and a slow grep coexist in 64 kB of memory: cat is asleep in write() almost all the time, and the kernel wakes it each time grep frees space. That is backpressure with no code — the same idea as TCP’s receive window (Flow Control: The Receive Window, What Happens When the Receiver Is Slow).

The third rule is about closing. When every write end is closed, read() returns 0 — EOF — after the buffer is drained. This is the signal that ends every pipeline, and it is why the parent in the code above must close its own copy of the write end: if it does not, the pipe has a live writer (the parent itself) forever, and read() blocks forever. The most common pipe bug in the world is a leaked write end.

The mirror case: writing when every read end is closed. The kernel sends the writer SIGPIPE (Signals: Asynchronous Notifications From the Kernel), whose default action terminates the process; if SIGPIPE is ignored, write() fails with EPIPE. This is why yes | head -1 terminates: head exits after one line, its read end closes, and yes dies of SIGPIPE on the next write. Servers ignore SIGPIPE for exactly this reason — a client disconnecting must not kill the server.

  • Writes of at most PIPE_BUF bytes (4096 on Linux) are atomic: they are not interleaved with other writers. Larger writes may be split.
  • O_NONBLOCK plus I/O Multiplexing: select, poll, epoll, kqueue, IOCP turns a pipe into just another readable descriptor for an event loop.
  • A pipe has no size in the file-system sense and no seek; lseek() returns ESPIPE — the errno is named after this.

The shell pipeline, fully expanded

Unix-style

cat access.log | grep 500 | sort is three processes and two pipes. The shell calls pipe() twice, fork()s three children, and in each child uses dup2() to wire descriptor 1 (stdout) or 0 (stdin) to the right pipe end before execing the program. cat writes into pipe 1; grep reads pipe 1 and writes pipe 2; sort reads pipe 2 and writes the terminal. The three run concurrently: grep is filtering while cat is still reading the file. Only sort has to wait for EOF, because it cannot emit anything until it has seen the last line.

None of the three programs knows it is in a pipeline. cat writes to descriptor 1 and grep reads descriptor 0 exactly as if they were a terminal or a file — this is the payoff of Everything Is I/O. It also means a program that buffers stdout differently when it is not a terminal (C stdio switches from line-buffered to fully-buffered; Python does the same) will appear to “hang” in a pipeline until its 4–8 kB buffer fills. stdbuf -oL, python -u and PYTHONUNBUFFERED=1 exist for this.

cat access.log | grep 500 | sort
  1. shell: pipe() ×2, fork() ×3, dup2(), exec()wiring happens before exec; the programs never see it
  2. cat → fd 1 → pipe #1 (64 kB)blocks in write() when grep is slower
  3. grep: fd 0 ← pipe #1, fd 1 → pipe #2runs concurrently with cat
  4. sort: fd 0 ← pipe #2must see EOF (all writers closed) before it can output
  5. exit status: last command’s, unless `set -o pipefail`a failing cat is invisible by default

Pipes behind `subprocess.PIPE` and `spawn`

Runtime-specific

Python’s subprocess.Popen(cmd, stdout=PIPE, stderr=PIPE) calls os.pipe() for each requested stream, forks, and wires the child’s descriptors with dup2 exactly as the shell does. Node’s child_process.spawn(cmd, { stdio: 'pipe' }) does the same through libuv (which on Unix actually uses a Unix-domain socket pair for the stdio channels — bidirectional, but the blocking rules are the same). In both, the object you get back exposes the *parent’s* ends of the buffers.

The rules from the previous section apply directly and produce a famous deadlock. Capture both stdout and stderr, then read stdout to EOF, then read stderr: if the child writes more than 64 kB to stderr *before* finishing stdout, it blocks in write() on the stderr pipe, never reaches EOF on stdout, and the parent blocks in read() on stdout. Both wait forever. Python’s Popen.communicate() exists to read both pipes concurrently (with threads or select) precisely to avoid this; Node avoids it because spawn reads both streams from the event loop as data arrives. The lesson: whoever reads a pipe must keep reading it, or the writer stalls.

The deadlock and its fix
1import subprocess
2p = subprocess.Popen(["make", "-j8"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
3
4# WRONG: if make emits > 64 kB on stderr before finishing, it blocks in write()
5# while we block in read() on stdout. Nobody wakes up.
6# out = p.stdout.read(); err = p.stderr.read()
7
8# RIGHT: read both concurrently until EOF
9out, err = p.communicate()
10print(p.returncode)

Key points

  • A pipe is a kernel ring buffer (64 kB by default on Linux) with a read descriptor and a write descriptor; it is unidirectional and byte-oriented.
  • Ends are shared by fork() inheritance; unrelated processes need a FIFO (named pipe) with a path.
  • Writer blocks when full, reader blocks when empty: backpressure for free.
  • EOF is “all write ends closed”. A leaked write end is the classic pipe hang.
  • Writing with no readers raises SIGPIPE (or EPIPE if ignored); servers must ignore SIGPIPE.
  • A shell pipeline is N processes and N−1 pipes running concurrently; subprocess.PIPE and spawn build the same thing.

Why does this exist?

Mechanisms are answers to constraints. Open each question before reading the answer.

Why does the kernel own the buffer instead of one of the processes?

Because neither process can be trusted with the other’s memory. A kernel buffer lets the two stay isolated and lets the kernel enforce the blocking rules that make speed mismatches harmless.

Why block instead of growing the buffer?

A growing buffer converts a slow consumer into unbounded memory in the kernel. A fixed buffer converts it into a sleeping producer, which costs nothing. Every flow-control design makes this choice.

Why is EOF tied to closing rather than an explicit “done” message?

Because a process that crashes never sends “done”, but the kernel always closes its descriptors. Tying EOF to the last close makes crashes terminate pipelines correctly for free.

Pipe simulator

A pipe is a kernel buffer with two blocking ends
The producer writes 512 KB. The pipe buffer lives in the kernel; when it is full the writer sleeps, when it is empty the reader sleeps.
Producer (writer)
running
write(1, …) · sent 24.0 KB
Kernel pipe buffer
fill8 KB
capacity 64 KB · Linux default 64 KB (16 pages), F_SETPIPE_SZ to change
Consumer (reader)
running
read(0, …) · got 16.0 KB
$ cat big.log | grep ERROR | sort
cat (pid 4101)
reads the file, writes stdout → pipe 1
pipe 1 ▶
grep (pid 4102)
stdin ← pipe 1 · stdout → pipe 2 · streams line by line
pipe 2 ▶
sort (pid 4103)
must read to EOF before emitting anything
The shell forks three processes and creates two pipes with `pipe()` + `dup2()` before `exec`. They run concurrently; the pipes bound how far ahead `cat` can get. `sort` sees EOF only when `grep` closes its stdout, which happens when `grep` itself reads EOF from `cat`.
1/33 · tick 0SimulatedUnix-style

How it fails

What the failure looks like from inside real software.

  • Parent forgets to close its copy of the write end after fork(): the child exits, but read() never returns 0 and the parent hangs forever.
  • Reading stdout to EOF before touching stderr in a subprocess: the child blocks writing stderr, the parent blocks reading stdout — a two-process deadlock with no lock in sight.
  • A server that did not ignore SIGPIPE dies the moment a client disconnects mid-response; the log shows nothing because the process was killed by a signal (exit status 141).
  • A program appears to hang in a pipeline because stdio switched to full buffering when stdout stopped being a terminal — output appears only at exit.
  • Treating pipe data as messages: the reader gets a partial line and parses garbage; frame with newlines or length prefixes.