昨天有人向我建议在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,在这种情况下单独运行同一组命令会不会?
答案 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) - 请告诉我这种方法是否有缺陷:
fork()
监控了第一个sudo dtruss -t fork -f -p {pidOfShell1}
次呼叫(-f
是必要的,还可以“传递”跟踪fork()
个呼叫,即包括由子壳本身创建的那些)。在测试命令中仅使用内置:
(无操作)(以避免使用外部可执行文件的额外fork()
调用混淆图片);具体是:
:
$(:)
: | :
$(: | :)
: | :; :
$(: | :; :)
仅计算包含非零PID的dtruss
输出行(因为每个子进程也报告创建它的fork()
调用,但PID 0)。
fork()
。以下是我在原帖中仍然认为正确的内容:当bash创建子shell时。
bash会在以下情况下创建子广告:
(...)
)
[[ ... ]]
内,其中括号仅用于逻辑分组。|
),包括第一个
bash 4.2+
有shell选项lastpipe
(默认情况下为OFF),这会导致 last 管道段无法在子shell中运行。用于命令替换($(...)
)
进程替换(<(...)
)
exec
(<(exec ...)
)之前添加简单命令。 &
)组合这些结构将产生多个子shell。
答案 1 :(得分:7)
在Bash中,子shell总是在新的进程空间中执行。您可以在Bash 4中相当简单地验证这一点,Bash 4中包含$BASHPID
和$$
个环境变量:
实践中:
$ 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
。