The wall between two processes
By now you carry a hard-won fact in your bones: each process has its own private address space. The number 0x7fffe3a0 in one process and the same number in another point at two completely different bytes; the kernel and the MMU make sure of it. That isolation is a gift — a runaway program cannot scribble on yours — but it raises a sharp question the moment you want two programs to cooperate. If process A computes something and process B needs it, and neither can reach into the other's memory, how does the value cross the wall? That whole problem has a name: inter-process communication, or IPC, and this rung is a tour of the answers Unix gives.
The very first answer, and the one almost everything else is measured against, is the pipe. The picture is exactly the household word: a pipe is a one-way channel. Bytes go in one end and come out the other, in order, and that is all it does. The crucial twist is where the pipe lives. It is not in process A's memory, and not in process B's — it is a small buffer the kernel holds, sitting in the no-man's-land both processes can ask the kernel to touch. A writes by calling write() into its end; B reads by calling read() from its end; the kernel shuttles the bytes between them across the wall neither process can cross directly.
Building a pipe by hand: pipe() and fork()
Let us make one, because the mechanics are short and they explain everything that follows. A single call, pipe(), asks the kernel to create the buffer and hand back two descriptors at once: one for the read end and one for the write end. You pass it a two-element array and it fills it in — by convention fd[0] is the end you read from and fd[1] is the end you write to (a small mnemonic: 0 looks like the input slot you already know, 1 like output). At this instant both descriptors live in one process, which is not yet useful: a pipe you talk to yourself with is just a slow way to copy memory.
The magic happens at the next step, and it reuses something you already met: fork(). Recall that fork() duplicates the process, and the child inherits a copy of the parent's whole descriptor table. So after pipe() then fork(), both processes hold both ends of the same pipe — fd[0] and fd[1] now name the same kernel buffer from inside two different processes. That shared buffer, inherited across the fork, is the channel. From here the recipe is just bookkeeping: each side closes the end it will not use, so that the pipe becomes strictly one-way. The parent might keep only the write end and the child only the read end, and now bytes flow one direction, parent to child.
int fd[2];
if (pipe(fd) == -1) { perror("pipe"); return 1; } /* fd[0]=read, fd[1]=write */
pid_t pid = fork();
if (pid == -1) { perror("fork"); return 1; }
if (pid == 0) { /* CHILD: will only READ */
close(fd[1]); /* close the write end we won't use */
char buf[64];
ssize_t n = read(fd[0], buf, sizeof buf); /* receive from parent */
if (n > 0) write(1, buf, n); /* echo to our stdout */
close(fd[0]);
_exit(0);
}
/* PARENT: will only WRITE */
close(fd[0]); /* close the read end we won't use */
const char *msg = "hello across the wall\n";
write(fd[1], msg, strlen(msg)); /* send to child */
close(fd[1]); /* closing write end signals end-of-file */
waitpid(pid, NULL, 0); /* reap the child */Why each side must close its spare end
That closing is not tidiness — it is load-bearing, and getting it wrong is the classic pipe bug. Here is the rule the kernel follows: a read() on a pipe returns 0, the signal for end-of-file, only once every write descriptor for that pipe has been closed, everywhere. The reader is asking "is anyone still able to send?", and the kernel answers "no more is coming" only when no writable end remains open in any process. So if the parent forgets to close its own copy of the read end, or the child keeps a stray copy of the write end alive, there is still a writer in the world — and the reader's read() blocks forever, waiting for bytes from a sender that will never speak.
The mirror-image failure is just as instructive, and it is where a signal you will meet two guides from now first shows its face. Suppose the reader has gone away — it exited, or closed its read end — but a writer keeps calling write(). Those bytes have nowhere to land; no one will ever read them. The kernel refuses to let the writer pile up garbage forever, so it does two things at once: write() fails with the error code EPIPE, and the kernel delivers the SIGPIPE signal to the writing process, whose default action is to kill it on the spot. This is the famous "broken pipe", and it is exactly why a long pipeline halts the instant you quit the program reading its output.
From pipe to pipeline: what `|` really does
Now we can read the most ordinary line you type. When you run `$ ls | wc -l`, the bar is not magic punctuation — it is an instruction to the shell to wire one program's standard output into another's standard input through a pipe. The shell does this with exactly the pieces you now hold: it calls pipe() to make the channel, forks twice (once per program), and in each child performs a tiny act of surgery in the seam between fork and exec before becoming ls or wc. That surgery is the last idea from the files rung, dup2() and redirection: rebind a standard descriptor to point at a pipe end.
- The shell calls pipe(fd) once, getting a read end fd[0] and a write end fd[1] for the channel between ls and wc.
- It forks the first child for ls. In that child it calls dup2(fd[1], 1) — make descriptor 1 (stdout) point at the pipe's write end — then closes both fd[0] and fd[1], and finally exec()s ls. Now ls writes to its normal stdout, unaware the bytes go into the pipe.
- It forks the second child for wc. In that child it calls dup2(fd[0], 0) — make descriptor 0 (stdin) point at the pipe's read end — closes both fd[0] and fd[1], and exec()s wc. Now wc reads from its normal stdin, unaware the bytes come from ls.
- Crucially, the parent shell closes BOTH its own copies of fd[0] and fd[1]. Only then can wc ever see end-of-file — because only then is every write end of the pipe finally closed once ls finishes.
Step back and the whole shell pipeline is just this dance repeated. For `ls | grep foo | wc -l`, the shell makes two pipes, forks three children, and dup2()s each child's stdin and stdout onto the right pipe ends so the output of one becomes the input of the next. Every program in the chain is written the plain, boring way — read stdin, write stdout — and knows nothing about pipes at all. The composition lives entirely in the shell's plumbing. That is the quiet genius of Unix: small tools that each do one thing, glued by a kernel buffer, into combinations none of their authors ever imagined.
What a pipe really is, and where it stops
A pipe is a small, fixed-size buffer in the kernel — on Linux, typically 64 KiB by default — and that finite size is what makes it well-behaved instead of a runaway. The buffer creates back-pressure: if the writer is fast and the reader is slow, the buffer fills, and the next write() simply blocks, parking the writer until the reader drains some room. Symmetrically, a reader that calls read() on an empty pipe blocks until a byte arrives. Nobody has to coordinate speeds by hand; the pipe's full/empty states throttle the two ends into lockstep automatically. This is the blocking behavior from the files rung doing real synchronization work for free.
Be honest about the limits, because they shape why the rest of this rung exists. A pipe is one-way: bytes flow from write end to read end and never back, so two-way conversation needs two pipes. It is a raw byte stream with no message boundaries — if a writer does three write() calls, the reader may receive all the bytes glued together in one read(), or split differently; the pipe does not preserve "these N bytes were one message". And the pipe created by pipe() is anonymous: it has no name anywhere, so the only way a second process can reach it is to inherit the descriptor across fork(). That is why every example here forks. Two unrelated programs, already running, cannot find this pipe at all.
That last limit is the doorway to the rest of the rung. The very next guide, on named pipes and message queues, lifts the "must be related by fork" restriction so two strangers can rendezvous. The signals guides handle a different need — a tiny asynchronous nudge rather than a stream of data. Shared memory will chase raw speed by removing the kernel copy entirely. And the final guide tackles waiting on many descriptors at once. But hold the shape of the pipe firmly, because it is the measuring stick: every later mechanism is, in part, an answer to something a plain anonymous pipe cannot do.