The mechanism behind "ls | grep" — connecting the output of one process to the input of another.
You've used fork() to create two processes. They can't share memory. How do you get data from one to the other? The simplest answer: a pipe.
A pipe is a one-way data channel. One end writes, the other reads. Data flows in order — first in, first out. The shell uses pipes every time you type something like ls | grep foo | wc -l. Each | creates a pipe connecting stdout of the left command to stdin of the right.
Click "Send" to push data through the pipe from writer to reader. Watch bytes flow through the buffer.
Before diving into pipes, you need to understand file descriptors. They're just integers that refer to open I/O channels. Every process starts with three:
| FD | Name | Purpose |
|---|---|---|
| 0 | stdin | Standard input (keyboard) |
| 1 | stdout | Standard output (terminal) |
| 2 | stderr | Standard error (terminal) |
When you call open(), you get a new file descriptor (usually 3, 4, 5, ...). When you call pipe(), you get a pair of file descriptors — one for reading, one for writing. The pipe() call is the low-level equivalent of the FILE* streams from stdio.
fopen() returns a FILE*. Under the hood, it uses a file descriptor. fileno(fp) gives you the integer fd. Pipes work at the fd level — with read() and write(), not fread() and fwrite().The pipe() system call creates a pipe and gives you two file descriptors:
c int pfds[2]; pipe(pfds); /* pfds[0] = read end */ /* pfds[1] = write end */
Now you can write to pfds[1] and read from pfds[0]:
c write(pfds[1], "hello", 5); /* put bytes in */ char buf[10]; read(pfds[0], buf, 5); /* take bytes out */ /* buf now contains "hello" */
pfds[0] is the read end (index 0 = input), pfds[1] is the write end (index 1 = output). Think of it as: data goes in at [1] and comes out at [0]. On most systems, the pipe buffer holds about 64KB before blocking.This example is trivial — writing to yourself through a pipe is pointless. The real power comes when you combine pipe() with fork().
Here's where it gets interesting. You create the pipe before forking. The child inherits copies of both file descriptors. Now the child can write to pfds[1] and the parent can read from pfds[0] — data flows between processes!
c int pfds[2]; pipe(pfds); if (!fork()) { /* CHILD: write to pipe */ close(pfds[0]); /* close unused read end */ write(pfds[1], "test", 5); exit(0); } else { /* PARENT: read from pipe */ char buf[30]; close(pfds[1]); /* close unused write end */ read(pfds[0], buf, 5); printf("Got: %s\n", buf); /* prints "test" */ wait(NULL); }
What if you want a child's stdout to go through a pipe instead of to the terminal? You need dup() (or dup2()). It clones a file descriptor into a new number.
The trick: close fd 1 (stdout), then call dup(pfds[1]). Since dup() always uses the lowest available fd, and you just freed fd 1, the pipe's write end becomes the new stdout. Now anything the child printf()s goes into the pipe!
c /* In the child: redirect stdout to pipe */ close(1); /* free up fd 1 (stdout) */ dup(pfds[1]); /* clone pipe write end into fd 1 */ close(pfds[0]); /* close unused read end */ /* Now stdout IS the pipe */ printf("This goes into the pipe!\n");
dup2(pfds[1], 1) which atomically copies pfds[1] into fd 1, closing the old fd 1 if needed. Same result, one call.Now we can build the shell's pipe operator in C. The plan: fork two children. Child 1 runs ls with its stdout redirected to the pipe. Child 2 runs wc -l with its stdin redirected from the pipe.
c int pfds[2]; pipe(pfds); if (!fork()) { /* Child 1: ls */ close(1); /* close stdout */ dup(pfds[1]); /* pipe write end -> stdout */ close(pfds[0]); execlp("ls", "ls", NULL); } else { /* Child 2: wc -l */ close(0); /* close stdin */ dup(pfds[0]); /* pipe read end -> stdin */ close(pfds[1]); execlp("wc", "wc", "-l", NULL); }
Pipes have a built-in buffer (typically 64KB on Linux). Two things can cause blocking:
This blocking behavior is what makes pipes work as a synchronization mechanism too. The reader naturally waits for the writer, and the writer naturally waits if the reader is slow. It's like a conveyor belt that pauses when the other end isn't keeping up.
The showcase simulation. Build a multi-stage pipeline like the shell does. Watch data flow through each pipe, see buffers fill and drain, and observe blocking when a stage falls behind.
Data (shown as colored blocks) flows from left to right through pipes. Click "Produce" to generate data and watch it flow through the pipeline. Adjust processing speed with the slider.
Pipes are elegant but limited. They only work between related processes (parent-child via fork). You can't connect two unrelated programs with a pipe() call. For that, you need FIFOs (named pipes) — the topic of the next chapter.
| Feature | Pipes | FIFOs |
|---|---|---|
| Direction | Unidirectional | Unidirectional |
| Relationship | Parent-child only | Any processes |
| Persistence | In-memory only | File on disk |
| Creation | pipe() | mknod() or mkfifo |
| Typical use | Shell pipelines | IPC between daemons |
"There is no form of IPC that is simpler than pipes." — Beej