0

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

2
  • 1
    In bash, a subshell is always a separate process. Not so in other shells like ksh93. So you're pretty much wasting your time if you're expecting an example for Bash 5.0.17.
    – user313992
    Nov 11, 2021 at 22:12
  • In ksh93, here's a very simple example of subshell without subprocess: a=3; (a=12); echo $a. The reverse is also true: at least on Linux, one could implement multiple processes within a single subshell environment (i.e. cd could be an external command, running in a separate process, and still change the cwd of its parent). But AFAIK no mainstream shell is making use of that.
    – user313992
    Nov 11, 2021 at 22:19

2 Answers 2

4

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.

2
  • 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.
    – The Quark
    Nov 12, 2021 at 22:28
  • Yes, sorry, was meant to be $n. Edited now. Nov 12, 2021 at 22:29
1

I believe you are misinterpreting "subshell" and "subprocess" (aka child process) and the bash man page doesn't really help much in clearing up the confusion.

A subshell is loosely what you get when the current shell calls fork(). Fork creates subprocesses; so the subshell is a subprocess.

The bash man page says that bash "executes commands in a subshell" but what isn't clear from this is that to run external processes (like ps), it then calls exec() in the subshell, replacing the running bash subshell with the new command's executable. In some cases, like when () is used in bash, the subshell is started by ( and exits at ), and commands in between may be started in their own subshells/subprocesses.

The only way that subshell and child process are not the same is that the child process may or may not (anymore) be another instance of the same executable.

2
  • I assumed the POSIX interpretation of subshell ("A shell execution environment, distinguished from the main or current shell execution environment.") and child process ("A new process created (by fork(), posix_spawn(), or posix_spawnp()) by a given process."). But if bash systematically ties one to the other, then indeed it makes little sense to try and distinguish them there.
    – The Quark
    Nov 12, 2021 at 22:12
  • From the ksh93 answer, I'd say the difference between subshell and child process is merely an implementation detail, and for nearly all shells, it isn't different.
    – user10489
    Nov 12, 2021 at 22:59

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .