什么时候命令替换会产生比单独的相同命令更多的子shell?

Cal*_*leb 24 bash shell optimization subshell command-substitution

昨天有人向我建议在bash中使用命令替换会导致产生不必要的子shell.该建议仅针对此用例:

# Extra subshell spawned
foo=$(command; echo $?)

# No extra subshell
command
foo=$?
Run Code Online (Sandbox Code Playgroud)

我认为这对于这个用例似乎是正确的.但是,快速搜索试图验证这会导致大量令人困惑和矛盾的建议.似乎流行的智慧说所有命令替换的使用都会产生一个子shell.例如:

命令替换扩展为命令的输出.这些命令在子shell中执行,其stdout数据是替换语法扩展的内容.(来源)

这看起来很简单,除非你继续挖掘,在这种情况下你会开始找到建议的参考,但事实并非如此.

命令替换不一定会调用子shell,在大多数情况下也不会.它唯一保证的是乱序评估:它只是首先评估替换中的表达式,然后使用替换结果评估周围的语句.(来源)

这似乎是合理的,但这是真的吗?这个与子shell相关的问题的回答让我觉得有点man bash需要注意:

管道中的每个命令都作为单独的进程执行(即,在子shell中).

这让我想到了主要问题.究竟是什么导致命令替换产生一个副本,这个子shell无论如何都不会被隔离地执行相同的命令?

请考虑以下情况并解释哪些情况会产生额外子shell的开销:

# Case #1
command1
var=$(command1)

# Case #2
command1 | command2
var=$(command1 | command2)

# Case #3
command1 | command 2 ; var=$?
var=$(command1 | command2 ; echo $?)
Run Code Online (Sandbox Code Playgroud)

这些对中的每一对都会产生相同数量的子壳来执行吗?POSIX与bash实现有区别吗?是否有其他情况下使用命令替换会产生一个子shell,在这种情况下运行同一组命令不会?

mkl*_*nt0 15

更新和警告:

这个答案有一个困难的过去,因为我自信地声称事情变得不真实.我相信它目前的形式有价值,但请帮助我消除其他不准确之处(或说服我完全删除它).

在@kojiro指出我的测试方法存在缺陷(我最初用于查找子进程,但这太慢而无法始终检测到它们)之后,我已经对这个答案进行了大幅修改 - 并且大部分已经去掉了ps.下面描述一种新的测试方法.

我最初声称并非所有bash子shell都在他们自己的子进程中运行,但事实证明并非如此.

正如@kojiro在他的回答中所说,有些 shell(除了bash)有时会避免为子shell创建子进程,因此,一般来说,在shell的世界中,人们不应该认为子shell意味着子进程.

至于OP在bash中的情况(假设command{n}实例是简单的命令):

# Case #1
command1         # NO subshell
var=$(command1)  # 1 subshell (command substitution)

# Case #2
command1 | command2         # 2 subshells (1 for each pipeline segment)
var=$(command1 | command2)  # 3 subshells: + 1 for command subst.

# Case #3
command1 | command2 ; var=$?         # 2 subshells (due to the pipeline)
var=$(command1 | command2 ; echo $?) # 3 subshells: + 1 for command subst.;
                                     #   note that the extra command doesn't add 
                                     #   one
Run Code Online (Sandbox Code Playgroud)

看起来像使用命令substitution($(...))总是在bash中添加一个额外的子shell - 就像包含任何命令一样(...).

我相信,但不确定这些结果是否正确 ; 这是我测试的方法(OS X 10.9.1上的bash 3.2.51) - 请告诉我这种方法是否存在缺陷:

  • 确保只运行了2个交互式bash shell:一个用于运行命令,另一个用于监视.
  • 在第二个shell中,我监视了fork()第一个中的调用sudo dtruss -t fork -f -p {pidOfShell1}(-ffork()需要"传递" 跟踪调用,即包括由子shell本身创建的调用).
  • :在测试命令中仅使用内置(无操作)(以避免使用fork()外部可执行文件的额外调用混淆图片); 特别:

    • :
    • $(:)
    • : | :
    • $(: | :)
    • : | :; :
    • $(: | :; :)
  • 仅计算dtruss包含非零PID的输出行(因为每个子进程也报告fork()创建它的调用,但PID为0).

  • 从结果数中减去1,因为即使只是从交互式shell运行内置显然至少涉及1 fork().
  • 最后,假设结果计数表示创建的子壳数.

以下是我仍然认为从我的原始帖子中正确的:当bash创建子shell时.


bash在以下情况下创建子shell:

  • 用括号括起来的表达式((...))
    • 除了直接在里面[[ ... ]],括号仅用于逻辑分组.
  • 对于管道()的每个|,包括一个段
    • 请注意,所涉及的每个子shell都是内容方面的原始 shell 的克隆(在进程方面,子shell可以从其他子shell分叉(执行命令之前)). 因此,早期管道段中的子壳的修改不会影响后面的子壳. (按照设计,管道中的命令会同时启动- 只能通过连接的stdin/stdout管道进行排序.)

    • bash 4.2+具有shell选项lastpipe(默认为OFF),这会导致最后一个管道段不在子shell中运行.
  • 用于命令替换($(...))

  • 进程替换(<(...))

  • 后台执行(&)

组合这些结构将导致多个子shell.

  • 我不认为你的结果是有效的,因为ps不够快,无法捕获你在上面创建的那些子shell.例如,你说*括号括起来的表达式不会在简单命令*的子进程中运行,但是在{0..999999}中运行`for i时尝试连续输出`ps`; 做(:); done`.您不会看到每个新进程,但您会看到一些进程,并且系统经历的PID数量会迅速增加. (2认同)

koj*_*iro 7

在Bash中,子shell总是在新的进程空间中执行.您可以在Bash 4中相当简单地验证这一点,它具有$BASHPID$$环境变量:

  • $$扩展为shell的进程ID.在()子shell中,它扩展为当前shell的进程ID,而不是子shell.
  • BASHPID扩展到当前bash进程的进程ID.这在某些情况下不同于$$,例如不需要重新初始化bash的子shell

在实践中:

$ type echo
echo is a shell builtin
$ echo $$-$BASHPID
4671-4671
$ ( echo $$-$BASHPID )
4671-4929
$ echo $( echo $$-$BASHPID )
4671-4930
$ echo $$-$BASHPID | { read; echo $REPLY:$$-$BASHPID; }
4671-5086:4671-5087
$ var=$(echo $$-$BASHPID ); echo $var
4671-5006
Run Code Online (Sandbox Code Playgroud)

关于shell可以省略额外子shell 的唯一情况是当你管道到一个显式的子shell时:

$ echo $$-$BASHPID | ( read; echo $REPLY:$$-$BASHPID; )
4671-5118:4671-5119
Run Code Online (Sandbox Code Playgroud)

这里,管道隐含的子shell明确应用,但不重复.

这从一些变化是非常努力,以避免其他炮弹fork-ing.因此,虽然我觉得这个论点js-shell-parse具有误导性,但事实并非所有的贝壳总是fork适用于所有的子壳.