Bash Subshell – Why Subshell Doesn’t Spawn New Child Process in Background

bashcommandsubshell

After running the command

{ sleep 5; } &  

output of ps is (output 1)

 PID TTY           TIME CMD
  972 ttys000    0:00.27 -bash
 2556 ttys000    0:00.00 -bash
 2557 ttys000    0:00.00 sleep 5   

while for

( sleep 5 ) &    

out of ps is (output 2)

PID TTY           TIME CMD
 972 ttys000    0:00.28 -bash
2566 ttys000    0:00.00 sleep 5  

() causes a subshell environment and I expect "output 1" for this case as it results in forking a child process while I expects "output 2" for { sleep 5; } & as it executes in the current shell. This might look a silly question but I really do not understand this behavior.
What I am missing here?

Best Answer

Bash will run the last or only command of a subshell with exec if it figures it can do so safely, as an optimisation. You can validate this clearly with pstree:

$ pstree $$
bash---pstree
$ ( pstree $$ )
bash---pstree
$ ( pstree $$ ; echo)
bash---bash---pstree

$ 

That is why your ( sleep 5 ) shows up as just the sleep command, and no intermediate shell. In the last example above, the echo forces the shell to do something after pstree completes, so there's a real shell around to use; in the middle case, the spawned shell just immediately execs pstree, so it looks the same as the first case (which is standard fork-exec). Many shells do this.


On the other hand, anything run in the background requires spawning a new process: fundamentally, that's what "background" is. Commands in braces do ordinarily run in the current shell, but that can't happen if they're to run in the background. So that the parent shell can keep doing whatever it's doing while the background commands run, a new shell has to be created to run the whole { ... } in. Thus

$ { pstree $$ ; }
bash---pstree
$ { pstree $$ ; } &
bash---bash---pstree

In this case, there's no difference whether there's another trailing command or not. Bash documents this behaviour for &:

If a command is terminated by the control operator ‘&’, the shell executes the command asynchronously in a subshell. This is known as executing the command in the background. The shell does not wait for the command to finish, and the return status is 0 (true).

You can also see this happen by backgrounding a built-in command like read:

$ read &
$ pstree $$
[1] 32394
[1]+  Stopped                 read
$ pstree $$
bash-+-bash
     `-pstree

My shell now has two children: a bash running read, and the pstree command printing that output.


It's true that the shell could make a further optimisation and apply it to backgrounded { ... } as it does to parenthesised subshells, but it doesn't. It's possible some other shells do, but I haven't found one in quick testing. It's a pretty rare case and formally different, and there are some odd corner cases.