I just recently learned that "subshell" is not the same as "child shell process" (see for example What is the exact difference between a "subshell" and a "child process"? and the POSIX definitions of subshell and child process).
To convince myself of this, I am looking for a command that illustrates (proves) that a subshell is created without a child-shell being spawned.
For now, everything I tried seemed to spawn a child-shell whenever a subshell is created:
$ echo $BASHPID; (pwd; cd ..; echo $BASHPID; pwd); pwd # `( ...)` executed in a subshell
# and in a child-shell process
$ >&2 ps | ps # Theoretically executed in two subshells and apparently without child-shells
# but I cannot be sure due to the outcome of the next example
$ $ >&2 echo $BASHPID | ps # `ps` doesn't display a child-shell for the execution of `echo`
953790 # but `echo $BASHPID` shows a new process that is necessarily
PID TTY TIME CMD # a child-shell since echo is a built-in
948538 pts/2 00:00:00 bash
953791 pts/2 00:00:00 ps
I am looking for a way to demonstrate that having a subshell doesn't necessarily imply having a child-shell...
Bash 5.0.17
In the bash shell, subshells are implemented by forking a child process, so you won't see a case of a subshell not running in a child process in that shell.
ksh93 is the only shell that I know that skips the forking when possible for subshells (an optimisation that is still quite buggy and that the successive people that have tried to maintain it after AT&T disbanded the team that had written it have considered removing).
If you do for instance:
strace ksh93 -c 'pwd; (cd /; umask 0; pwd; exit 2); pwd'
You'll see ksh93 not forking any process but do something like this instead:
openat(AT_FDCWD, ".", O_RDONLY|O_PATH) = 3
fcntl(3, F_DUPFD, 10) = 10
close(3) = 0
fcntl(10, F_SETFD, FD_CLOEXEC) = 0
[...]
Which saves the current directory on fd 10. Then:
chdir("/") = 0
umask(000) = 002
Which changes the current directory and umask in the subshell. And upon termination of the subshell (the exit 2
not calling the _exit()
system call):
fchdir(10) = 0
close(10) = 0
To restore the current working directory and:
umask(002) = 000
To restore the umask.
Some shells like FreeBSD's sh
can skip the fork in very specific cases, like in:
var=$(printf %04d "$n")
(here with a printf
builtin, and no change to the environment is being done in there).
In a pipeline, all components have to run concurrently, so they have to run in separate processes, even in ksh93.
In bash
, they all run in child processes. In AT&T ksh or zsh, or with bash -O lastpipe
(when non-interactive), the rightmost one doesn't (of course, you still need to fork a child process to run external commands such as ps
).
You don't see an extra bash
process in ps >&2 | ps
or (ps)
because ps
is executed directly in that child process, which before executing ps
was bash interpreting the pipeline component: the subshell. For instance, in:
n=0; /bin/true "$((n=1))" | /bin/echo "$((n=2))"; echo "$n"
You'll see 2
and 0
in bash, and 2
and 2
in zsh/ksh93. /bin/true
and /bin/echo
are executed in child processes, /bin/true
directly in the subshell process that had done n=1
earlier, same in bash for /bin/echo
(and n=2
), but in zsh
/ksh
/bash -O lastpipe
, the n=2
was done in the main shell process, and a child only forked to execute that external utility, just like when you run /bin/echo "$((n=2))"
not as part of a pipeline.
In bash
(contrary to zsh
/ksh
), you do see an extra bash
process in (: anything; ps)
, the optimisation is only done if the subshell has only one external command, you'd need to use exec
to do that optimisation by hand there: (: anything; exec ps)
.
Same goes for { ps; } | cat
.
echo "$$"
did you mean echo "$n"
? In bash -0 lastpipe
I still see 2
ad 0
(assuming echo "$n"
), even when using echo
instead of/bin/echo
. But also in man bash
it only says that "[with lastpipe
] the last element of a pipeline may be run by the shell process", so it could also be expected to have the same outcome here even with lastpipe
. The effect of exec
is quite illustrative I find. — Nov 12, 2021 at 22:28 $n
. Edited now. — Nov 12, 2021 at 22:29 External links referenced by this document: