How Bash Process Substitution Works in Linux

Process substitution is a Bash feature that allows the output of a command to be passed to another program as a standard file argument rather than a stream via standard input. Using the <(command) syntax, Linux enables programs that typically require explicit file paths—such as diff or comm—to read directly from the output of background processes. This mechanism relies on core Linux operating system primitives: subshells, anonymous pipes, the /dev/fd/ virtual filesystem, and asynchronous inter-process communication.

Subshell Forking and Anonymous Pipes

When Bash parses a command containing <(command), it prepares an inter-process communication channel before executing the enclosed commands.

  1. Creating the Pipe: Bash invokes the pipe() system call to create a unidirectional data channel in kernel memory. This system call returns two file descriptors: one for reading and one for writing.
  2. Spawning the Subshell: Bash forks a child process (a subshell) to run the command inside the parentheses.
  3. Redirecting Standard Output: Inside this child subshell, Bash replaces the standard output (file descriptor 1) with the write end of the pipe using the dup2() system call, then closes the unused read descriptor. The subshell then executes the command, sending its stdout directly into the pipe buffer.

Path Generation via /dev/fd/

Traditional shell pipes (|) only connect the stdout of one command directly to the stdin of another. However, many commands expect input to be provided as a file path argument (for example, diff file1 file2).

To satisfy this requirement without creating temporary files on disk, Bash uses the /dev/fd/ filesystem. In Linux, /dev/fd/ is a symbolic link to /proc/self/fd/, which exposes open file descriptors for the current process as file paths.

Bash takes the read end of the anonymous pipe, assigns it to an open file descriptor in the main shell (often a high descriptor number, such as 63), and substitutes the <(...) expression with the string path to that descriptor:

diff <(cat list1.txt) <(cat list2.txt)

To the operating system, this command is transformed into:

diff /dev/fd/63 /dev/fd/62

Execution and Kernel Data Transfer

Once the string substitution is complete, the outer command executes with /dev/fd/<n> passed as normal command-line arguments.

When the target program calls open() on /dev/fd/<n>, the Linux Virtual File System (VFS) resolves the path to the corresponding open file description in the kernel, which points directly to the read end of the pipe.

Data flows entirely through the Linux kernel's memory buffer (typically 64 KB by default) from the writing child subshell to the reading command. If the writing subshell produces data faster than the reader consumes it, the kernel blocks the subshell until buffer space becomes available. Conversely, if the reader reads faster than the subshell writes, the reader blocks until new data arrives.

Cleanup and Fallback Mechanisms

When the command inside the subshell completes, its standard output closes, which signals an End of File (EOF) to the reading process on the other end of the pipe.

Once the outer command finishes reading and exits:

While modern Linux distributions rely on /dev/fd/ for process substitution, systems lacking support for /dev/fd/ cause Bash to fall back on creating temporary named pipes (FIFOs) via the mkfifo() system call in /tmp, which are subsequently unlinked after execution finishes.