3

I know that grouping commands(command-list) creates a subshell environment, and each listed command is executed in that subshell. But if I execute a simple command in the grouping command, (use the ps command to output the processes), then no subshell process is output. But if I tried to execute a list of commands (compound command) in the grouping command, then a subshell process is output. Why does it produce such a result?

  • A test of executing a simple command (only a ps command) in a grouping command:
    [root@localhost ~]# (ps -f)
    
    with the following output:
    UID         PID   PPID  C STIME TTY          TIME CMD
    root       1625   1623  0 13:49 pts/0    00:00:00 -bash
    root       1670   1625  0 15:05 pts/0    00:00:00 ps -f
    
  • Another test of executing a compound command(a list of commands) in a grouping command:
    [root@localhost ~]# (ps -f;cd)
    
    with the following output:
    UID         PID   PPID  C STIME TTY          TIME CMD
    root       1625   1623  0 13:49 pts/0    00:00:00 -bash
    root       1671   1625  0 15:05 pts/0    00:00:00 -bash
    root       1672   1671  0 15:05 pts/0    00:00:00 ps -f
    

I tested a lot of other commands (compound commands and simple commands), but the results are the same. I guess even if I execute a simple command in a grouping command, bash should fork a subshell process, otherwise it can't execute the command. But why can't I see it?

agc
  • 7,973
  • 2
  • 29
  • 50
Linke
  • 336
  • 1
  • 10
  • Use `(echo $BASH_SUBSHELL)` to print subshell level and use `echo $BASHPID` (bash ver > 4) to print current process id of `bash` – anubhava Jun 16 '19 at 07:31
  • To the basics I recommend this book: [Advanced Programming in the UNIX Environment](https://www.amazon.de/Programming-Environment-Addison-Wesley-Professional-Computing/dp/0321637739) – Cyrus Jun 16 '19 at 07:36
  • Can you give me a clear explanation, thank you – Linke Jun 16 '19 at 07:38
  • Interestingly you don't see a subshell `bash` process when running `(ps -f & cd)` either. This makes me think it might be a timing issue. – l0b0 Jun 16 '19 at 09:56
  • More minimal demo of this curiosity: `ps -o ppid=$(pidof ps)`, then with grouping `(ps -o ppid=$(pidof ps))`, then with grouping and an added `:` do-nothing command `(ps -o ppid=$(pidof ps) ; :)`. The first two output the same two numbers, the last outputs the same two and one more. – agc Jun 16 '19 at 15:01
  • As Kamil Cuk and PSkocik said, it may be due to the optimization of bash that these unintended consequences are caused. – Linke Jun 16 '19 at 18:10

2 Answers2

4

Bash optimizes the execution. It detects that only one command is inside the ( ) group and calls fork + exec instead of fork + fork + exec. That's why you see one bash process less in the list of processes. It is easier to detect when using command that take more time ( sleep 5 ) to eliminate timing. Also, you may want to read this thread on unix.stackexchange.

I think the optimization is done somewhere inside execute_cmd.c in execute_in_subshell() function (arrows > added by me):

 /* If this is a simple command, tell execute_disk_command that it
     might be able to get away without forking and simply exec.
>>>> This means things like ( sleep 10 ) will only cause one fork
     If we're timing the command or inverting its return value, however,
     we cannot do this optimization. */

and in execute_disk_command() function we can also read:

/* If we can get away without forking and there are no pipes to deal with,
   don't bother to fork, just directly exec the command. */
KamilCuk
  • 120,984
  • 8
  • 59
  • 111
2

It looks like an optimization and dash appears to be doing it too:

Running

bash -c '( sleep 3)' & sleep 0.2 && ps #or with dash

as does, more robustly:

strace -f -e trace=clone dash -c '(/bin/sleep)' 2>&1 |grep clone # 1 clone

shows that the subshell is skipped, but if there's post work to be done in the subshell after the child, the subshell is created:

strace -f -e trace=clone dash -c '(/bin/sleep; echo done)' 2>&1 |grep clone #2 clones

Zsh and ksh are taking it even one step further and for (when they see it's the last command in the script):

strace -f -e trace=clone ksh -c '(/bin/sleep; echo done)' 2>&1 |grep clone # 0 clones

they don't fork (=clone) at all, execing directly in the shell process.

Petr Skocik
  • 58,047
  • 6
  • 95
  • 142