I think you're getting mixed up between shell syntax for pipelines vs. the underlying Unix systems programming. A pipe / FIFO is a type of file that isn't stored on disk, but instead passed data from a writer to a reader through a buffer in the kernel.
A pipe/FIFO works the same whether the writer and reader got connected by making system calls like open("/path/to/named_pipe", O_WRONLY);, or with a pipe(2) to create a new anonymous pipe and return open file descriptors to both the read and write sides.
fstat(2) on the pipe file descriptor will give you sb.st_mode & S_IFMT == S_IFIFO either way.
When you run foo | bar:
- The shell forks like normal for any non-builtin command
- Then makes a
pipe(2) system call to get two file descriptors: the input and output of an anonymous pipe. - Then it forks again.
- The child (where
fork() returned 0) - closes the read side of the pipe (leaving the write fd open)
- and redirects
stdout to the write fd with dup2(pipefd[1], 1) - then does
execve("/usr/bin/foo", ...)
- The parent (where
fork() returned the non-0 child PID) - closes the write side of the pipe (leaving the read fd open)
- and redirects
stdin from the read fd with dup2(pipefd[0], 0) - then does
execve("/usr/bin/bar", ...)
You get into a very similar situation if you run foo > named_pipe & bar < named_pipe.
A named pipe is a rendezvous for processes to establish pipes between each other.
The situation is similar to anonymous tmp files vs. files with names. You can open("/path/to/dir", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR); to create a temporary file with no name (O_TMPFILE), as if you'd opened "/path/to/dir/tmpfile" with O_CREAT and then unlinked it, leaving you with a file descriptor to a deleted file.
Using linkat, you can even link that anonymous file into the filesystem, giving it a name, if it was created with O_TMPFILE. (You can't do this on Linux for files that you created with a name then deleted, though.)