Pipelines are not sequential

Consider the classic shell pipeline:

cmd1 | cmd2

At first glance, it’s easy to assume these run in sequence: cmd1 runs to completion, writes its output somewhere, then cmd2 starts and reads it.

That interpretation is wrong. Pipelines don’t run sequentially, they run concurrently. When the shell encounters a pipeline, it creates a one-way communication channel using the pipe() system call. The shell then forks two child processes, one for each command. One process connects its standard output to the pipe’s write end, and the other connects its standard input to the pipe’s read end.

Data flows from one to the other in a continuous stream. The kernel manages this transfer through an internal pipe buffer1, typically 64 KB in size. If cmd1 writes to the pipe and the buffer is full, it blocks in write(). If cmd2 tries to read and the buffer is empty, it blocks in read(). Each process blocks only on its own I/O operations, not on the other process’s lifecycle. They are independent entities scheduled by the kernel.

If your system has multiple CPU cores, the kernel can even run both cmd1 and cmd2 at the exact same time on different cores. In that case, the pipeline is not just concurrent but also parallel.

This concurrent nature of pipes becomes clear with a long-running command. For example:

tail -f logfile | grep error

Here, tail -f continuously watches a log file for new lines. As soon as a new line appears, tail writes it to its standard output. The grep process, which is already running and waiting for input, immediately reads the new data from the pipe and checks if it contains the string “error”. This pair of processes runs indefinitely, side by side, until they are explicitly stopped.

Below is a minimal C program that demonstrates how a shell pipeline works under the hood:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    int pipefd[2]; // pipefd[0] is read end, pipefd[1] is write end

    // Create an unnamed pipe
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(1);
    }


    // Fork the first process (the writer)
    if (fork() == 0) {
        dup2(pipefd[1], STDOUT_FILENO); // Direct stdout to the pipe's write end
        close(pipefd[0]);
        close(pipefd[1]);
        execlp("echo", "echo", "hello", NULL);
        perror("exec echo");
        exit(1);
    }

    // Fork the second process (the reader)
    if (fork() == 0) {
        dup2(pipefd[0], STDIN_FILENO); // Direct stdin to the pipe's read end
        close(pipefd[1]);
        close(pipefd[0]);
        execlp("grep", "grep", "h", NULL);
        perror("exec grep");
        exit(1);
    }

    close(pipefd[0]); // Parent closes its pipe ends
    close(pipefd[1]);
    wait(NULL); // Wait for children to finish
    wait(NULL);
    return 0;
}

This code creates the pipe, forks two child processes, and connects them. The result is identical to echo hello | grep h. Both processes are created and started immediately. Data streams from echo to grep through the pipe as it becomes available.

TL;DR

Pipelines may look sequential, but they are not. Each command is started right away, and the kernel ties them together with a pipe. What you get is a chain of processes running at the same time, passing data through a buffer, with reads and writes blocking only when necessary. The stream is ordered, but the execution is concurrent.


  1. Internally, the pipe buffer is a circular queue of pipe_buffer structures, each pointing to a memory page in the kernel. Data is written at the tail, read from the head, and the buffer wraps around when full. ↩︎