So, I was given this one line script:
echo test | cat | grep test
Could you please explain to me how exactly that would work given the following system calls: pipe(), fork(), exec() and dup2()?
I am looking for an general overview here and mainly the sequence of operations. What I know so far is that the shell will fork using fork() and the script's code will replace the shell's one by using the exec(). But what about pipe and dup2? How do they fall in place?
Thanks in advance.
First consider a simpler example, such as:
What we want is to execute
echo
in a separate process, arranging for its standard output to be diverted into the standard input of the process executingcat
. Ideally this diversion, once setup, would require no further intervention by the shell — the shell would just calmly wait for both processes to exit.The mechanism to achieve that is called the "pipe". It is an interprocess communication device implemented in the kernel and exported to the user-space. Once created by a Unix program, a pipe has the appearance of a pair of file descriptors with the peculiar property that, if you write into one of them, you can read the same data from the other. This is not very useful within the same process, but keep in mind that file descriptors, including but not limited to pipes, are inherited across
fork()
and even accrossexec()
. This makes pipe an easy to set up and reasonably efficient IPC mechanism.The shell creates the pipe, and now owns a set of file descriptors belonging to the pipe, one for reading and one for writing. These file descriptors are inherited by both forked subprocesses. Now only if
echo
were writing to the pipe's write-end descriptor instead of to its actual standard output, and ifcat
were reading from the pipe's read-end descriptor instead of from its standard input, everything would work. But they don't, and this is wheredup2
comes into play.dup2
duplicates a file descriptor as another file descriptor, automatically closing the new descriptor beforehand. For example,dup2(1, 15)
will close file descriptor 1 (by convention used for the standard output), and reopen it as a copy of file descriptor 15 — meaning that writing to the standard output will in fact be equivalent to writing to file descriptor 15. The same applies to reading:dup2(0, 8)
will make reading from file descriptor 0 (the standard input) equivalent to reading from file descriptor 8. If we proceed to close the original file descriptor, the open file (or a pipe) will have been effectively moved from the original descriptor to the new one, much like sci-fi teleports that work by first duplicating a piece of matter at a remote location and then disintegrating the original.If you're still following the theory, the order of operations performed by the shell should now be clear:
The shell creates a pipe and then
fork
two processes, both of which will inherit the pipe file descriptors,r
andw
.In the subprocess about to execute
echo
, the shell callsdup2(1, w); close(w)
beforeexec
in order to redirect the standard output to the write end of the pipe.In the subprocess about to execute
cat
, the shell callsdup2(0, r); close(r)
in order to redirect the standard input to the read end of the pipe.After forking, the main shell process must itself close both ends of the pipe. One reason is to free up resources associated with the pipe once subprocesses exit. The other is to allow
cat
to actually terminate — a pipe's reader will receive EOF only after all copies of the write end of the pipe are closed. In steps above, we did close the child's redundant copy of the write end, the file descriptor 15, right after its duplication to 1. But the file descriptor 15 must also exist in the parent, because it was inherited under that number, and can only be closed by the parent. Failing to do that leavescat
's standard input never reporting EOF, and itscat
process hanging as a consequence.This mechanism is easily generalized it to three or more processes connected by pipes. In case of three processes, the pipes need to arrange that
echo
's output writes tocat
's input, andcat
's output writes togrep
's input. This requires two calls topipe()
, three calls tofork()
, four calls todup2()
andclose
(one forecho
andgrep
and two forcat
), three calls toexec()
, and four additional calls toclose()
(two for each pipe).