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

时间:2014-01-24 11:05:57

标签: bash shell optimization subshell command-substitution

昨天有人向我建议在bash中使用命令替换会导致产生不必要的子shell。该建议特定于this use case

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

# No extra subshell
command
foo=$?

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

  

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

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

  

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

这看似合理,但这是真的吗? This answer与子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 $?)

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

2 个答案:

答案 0 :(得分:13)

更新并提出警告

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

在@kojiro指出我的测试方法存在缺陷(我最初使用ps寻找子进程)之后,我已经进行了大量修改 - 并且大部分内容 - 这个答案,但那是总是检测到它们太慢了;下面介绍一种新的测试方法。

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

正如@kojiro在他的回答中所述,一些 shell - 除了bash之外 - 有时会避免为子shell创建子进程,因此,通常在世界上说话shell,不应该假设子shell意味着子进程。

至于 bash 中的OP案例(假设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

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

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

  • 确保只运行了两个交互式bash shell:一个用于运行命令,另一个用于监视。
  • 在第二个shell中,我使用fork()监控了第一个sudo dtruss -t fork -f -p {pidOfShell1}次呼叫(-f是必要的,还可以“传递”跟踪fork()个呼叫,即包括由子壳本身创建的那些)。
  • 在测试命令中仅使用内置:(无操作)(以避免使用外部可执行文件的额外fork()调用混淆图片);具体是:

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

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

以下是我在原帖中仍然认为正确的内容:当bash创建子shell时。


bash会在以下情况下创建子广告:

  • 表示括号括起来的表达式((...)
    • 除了直接在[[ ... ]]内,其中括号仅用于逻辑分组。
  • for 管道的每个段(|),包括第一个
    • 请注意,所涉及的每个子shell都是内容原始 shell的克隆(在流程方面,子shell可以从其他子shell(在执行命令之前)) 因此,对早期管道段中的子壳的修改不会影响以后的子壳 (按照设计,管道中的命令同时启动 - 只能通过连接的stdin / stdout管道进行排序。)
    • bash 4.2+有shell选项lastpipe(默认情况下为OFF),这会导致 last 管道段无法在子shell中运行。
  • 用于命令替换($(...)

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

  • 后台执行(&

组合这些结构将产生多个子shell。

答案 1 :(得分:7)

在Bash中,子shell总是在新的进程空间中执行。您可以在Bash 4中相当简单地验证这一点,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

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

$ echo $$-$BASHPID | ( read; echo $REPLY:$$-$BASHPID; )
4671-5118:4671-5119

这里,显式应用了管道隐含的子shell,但没有重复。

这与某些other shells that try very hard to avoid fork-ing不同。因此,虽然我觉得js-shell-parse中的论点具有误导性,但对于所有子壳而言,并非所有炮弹都是fork