Does the shell fork when I use built-in commands?

You're right in that normally, to redirect a command, all the shell has to do is fork a child process, do the redirection in the child only (the parent file descriptors are not affected) and then execute the command in there (while the parent just waits for the child).

For builtin commands (or compound commands, or functions...), there is no fork nor exec. Yet after a redirected builtin command terminates, the file descriptors are back to how they were before.

To do that, the shell just saves a copy of the redirected file descriptor to another file descriptor before doing the redirection, marks that file descriptor with the O_CLOEXEC flag (in case the builtin ends up executing a command like eval or command would), and when the builtin returns, the shell restores the file descriptor.

You can see that if you run for instance:

strace sh -c 'echo test > /dev/null; :'

You'll see (only relevant entries included):

  • open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3: opens the file to redirect to.
  • fcntl(1, F_DUPFD, 10) = 10: duplicate the original stdout to the first available fd >= 10 (here 10).
  • fcntl(10, F_SETFD, FD_CLOEXEC) = 0: set the O_CLOEXEC flag on it
  • dup2(3, 1) = 1: make the redirected file stdout
  • close(3) = 0: no longer needed
  • write(1, "test\n", 5) = 5: echo runs and writes "test\n" on its stdout (now redirected to /dev/null).
  • dup2(10, 1) = 1: restore stdout
  • close(10) = 0: close fd 10 no longer needed.

Note that in the Bourne shell, redirecting a compound command did cause a fork (as in a=0; { a=1; echo "$a"; } >&2; echo "$a" would give you 1 then 0). To work around that, you actually had to do the above by hand:

Instead of

while cmd1; do cmd2; i=`expr "$i" + 1`; done > file

You had to do:

exec 3>&1 > file
while cmd1 3>&-; do cmd2 3>&-; i=`expr "$i" + 1 3>&-`; done
exec >&3 3>&-

(with a 3>&- for each command as that fd doesn't have the O_CLOEXEC flag).

And in early versions of the Bourne shell, you could not redirect builtins. On a Unix V7 on an emulated pdp11:

$ eval a=1 > /tmp/x
illegal io
$ read a < /etc/passwd
illegal io

Output redirection

File descriptor 1 represents stdout, the standard output stream. When output redirection is used in type type > abc.txt, the shell opens the file abc.txt for writing and the file descriptor 1 is modified so that it points to the open file instead of the terminal device.

However, this redirection only applies to the current command being executed so this does not imply that the command executes in a forked process (or subshell).

Persistent redirection

If you wanted the redirection to persist, you could use the exec shell builtin to modify the file descriptors, e.g., to redirect standard output for successive commands, run the following command.

exec >abc.txt

Be careful running this as your shell session will be hard to use if all command output is being redirected to a file instead of your terminal device. You can restore the stdout file descriptor to the terminal output device by redirecting it to the same device pointed to by stderr (file descriptor 2):

exec >&2

Related resources

For more details, see:

  • the section on Redirections from the Bash manual
  • Input And Output from Greg’s Wiki
  • Redirection tutorial from Bash-hackers Wiki
  • the Redirection Wikipedia article
  • the file descriptor Wikipedia article