5

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?

haccks
  • 203
  • 4
  • 10

1 Answers1

11

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.

Michael Homer
  • 76,565
  • Generally, the most optimised shell when it comes to saving processes would be ksh93 and the least optimised bash with other shells sitting somewhere it between. – Stéphane Chazelas Mar 13 '18 at 21:08
  • anything run in the background requires spawning a new process: so does (sleep 2) &. But it looks same as the case (sleep 2). Even (sleep 2, echo) &1 looks same as (sleep 2). – haccks Mar 13 '18 at 21:17
  • Yes, the only difference between those is whether the parent waits for the child to finish or not. – Michael Homer Mar 13 '18 at 21:20
  • @MichaelHomer; In both cases, (sleep 2) & and { sleep 2; } &, parent doesn't wait for the child to finish as per the behavior of &. – haccks Mar 13 '18 at 21:23
  • Then pstree for both of (sleep 2) & and { sleep 2; } & should be same, shouldn't be? Because (sleep 2) & will also spawn a new new process in the background. – haccks Mar 14 '18 at 03:12
  • 1
    (sleep 2) was already a separate process, with one optimised away (first half of the answer), and the only difference the & makes is not waiting for it to finish; { sleep 2; } & becomes a Bash process, which then spawns sleep 2 (second half of the answer). They are not the same. – Michael Homer Mar 14 '18 at 03:24
  • The first half of the last comment is the key point. It is interesting to know that & makes parent shell not to wait in first case while in second case it spawn a new process along with making parent shell not to wait.

    So can I say in general that if a command is already running in a subshell then & only makes not waiting for it to finish otherwise it's job is to 1) execute the command in a subshell and 2) make parent shell not to wait ?

    – haccks Mar 14 '18 at 05:51
  • Awesome! That's interesting. Where could I find such details? I would be grateful if you can give some resources. And I would also request you to add this behaviour of & explicitly in the answer for future reader (most of the reader do not read comments). – haccks Mar 14 '18 at 05:57
  • I don’t think the last-command part is documented - it’s not a feature, just an optimisation, so they’re free to change the behaviour. The rest is in the answer and links. – Michael Homer Mar 14 '18 at 06:00
  • I forgot to tell you. Links to opengroup you provided in first two para is not opening on my system. – haccks Mar 14 '18 at 06:30
  • It won't run the last command. The optimisation is to skip fork before exec if the command to run is simple. If it is not simple, like multiple commands, all of them gets subprocesses. See https://git.savannah.gnu.org/cgit/bash.git/tree/shell.c?id=7de27456f6494f5f9c11ea1c19024d0024f31112#n1370 (At least for bash -c) – Gert van den Berg Apr 03 '18 at 14:07