昨天有人建议我在bash中使用命令替换会导致不必要的子壳产生。建议特定于此用例:
# Extra subshell spawned
foo=$(command; echo $?)
# No extra subshell
command
foo=$?
最好,我可以认为这对于此用例似乎是正确的。但是,试图验证这一点的快速搜索导致了令人困惑和矛盾的建议。似乎流行的智慧表明,所有命令替代的用法都将产生一个子壳。例如:
命令替换扩展到命令的输出。这些命令在子壳中执行,> sTDOUT数据是替代语法扩展到的。(来源)
这似乎很简单,除非您继续进行挖掘,否则您将开始发现建议事实并非如此。
命令替换不一定会调用子壳,在大多数情况下不会。它保证的唯一一件事是越来越多的评估:它只是首先评估替代内部的表达式,然后使用替代结果评估周围的陈述。(来源)
这似乎是合理的,但是是真的吗?这个与子壳有关的问题的答案使我提出man bash
有要注意的:
管道中的每个命令都是作为单独的过程(即在子壳中)执行的。
这使我进入了主要问题。确切的是,什么会导致命令替换产生一个不会产生的子壳,无论如何都不会孤立地执行相同的命令?
请考虑以下情况,并说明哪些案件产生了一个额外的子壳的开销:
# Case #1
command1
var=$(command1)
# Case #2
command1 | command2
var=$(command1 | command2)
# Case #3
command1 | command 2 ; var=$?
var=$(command1 | command2 ; echo $?)
这些对中的每对都会产生相同数量的子壳要执行吗?POSIX与Bash实现有区别吗?是否还有其他情况下使用命令替换会产生一个子壳,其中不隔离运行相同的命令?
更新和警告:
这个答案有一个困难的过去,因为我自信地声称事实并非如此。我相信它具有当前形式的价值,但请帮助我消除其他不准确性(或说服我应该完全删除它)。
我 @kojiro指出我的测试方法有缺陷(我最初使用ps
来寻找儿童过程,但这太慢了,以至于永远太慢,以至于总是太慢检测它们);下面描述了一种新的测试方法。
我最初声称并非所有的bash子壳都在自己的子过程中运行,但事实证明并非如此。
正如@kojiro在他的回答中指出的那样, shells-除了bash-有时避免创建子壳子过程,所以,通常在世界上说话贝壳,一个人不应假定子壳意味着一个子过程。
至于 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 中添加一个额外的子壳 - 以及在 (...)
中包含任何命令。
我相信,但是不确定这些结果是正确的;这是我测试的方式(OS X 10.9.1上的BASH 3.2.51) - 请告诉我此方法是否有缺陷:
- 确保仅运行2个交互式bash shell:一个可以运行命令,另一个用于监视。
- 在第二个外壳中,我用
sudo dtruss -t fork -f -p {pidOfShell1}
在第1个中监视了fork()
调用(-f
对于还需要fork()
呼叫"传递性",即包括子壳本身创建的呼叫)。
)。
)。
在测试命令中仅使用indentin
:
(no-op)(以避免使用其他fork()
调用图片,以调用外部可执行文件);具体:-
:
-
$(:)
-
: | :
-
$(: | :)
-
: | :; :
-
$(: | :; :)
-
仅计数包含非零PID的
dtruss
输出线(因为每个子进程还报告了创建它的fork()
调用,但使用PID 0)。- 从结果数中减去1,因为即使是从交互式壳中运行的,显然涉及至少1
fork()
。 - 最后,假定结果计数代表创建的子壳的数量。
以下是我仍然认为从我的原始帖子中正确的:当bash创建子壳时。
bash在以下情况下创建子壳:
- 用于括号包围的表达式(
(...)
)- 直接在
[[ ... ]]
内,其中括号仅用于逻辑分组。
- 直接在
- 对于管道的每个段(
|
),包括 first 一个- 请注意,每个子壳是根据 content 的 ointer shell的克隆(在过程中,可以从subshells分叉其他子壳(执行命令之前))。
因此,较早的管道段中子壳的修改不会影响较晚的段。
(通过设计,管道中的命令同时启动 - 排序仅通过其连接的stdin/stdout管道进行。) -
bash 4.2+
具有Shell选项lastpipe
(默认情况下),这导致 last 管道段不在子壳中运行。
- 请注意,每个子壳是根据 content 的 ointer shell的克隆(在过程中,可以从subshells分叉其他子壳(执行命令之前))。
用于命令替代(
$(...)
)用于过程替代(
<(...)
)- 通常会创建 2 子壳;在A 简单命令的情况下,@konsolebox提出了一种仅创建 1 的技术:使用
exec
(<(exec ...)
)预留简单命令。
- 通常会创建 2 子壳;在A 简单命令的情况下,@konsolebox提出了一种仅创建 1 的技术:使用
- 背景执行(
&
)
组合这些构造将导致多个子壳。
在bash中,子壳总是在新的过程空间中执行。您可以在BASH 4中相当琐碎的验证,该Bash 4具有$BASHPID
和$$
环境变量:
- $$扩展到外壳的过程ID。在()子壳中,它将扩展到当前外壳的过程ID,而不是子壳。
- bashpid扩展到当前bash过程的过程ID。在某些情况下,这与$$不同,例如不需要重新定位的子壳
实践:
$ 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
大约唯一的情况,当您将 Extry 子壳带到显式子壳时:
$ echo $$-$BASHPID | ( read; echo $REPLY:$$-$BASHPID; )
4671-5118:4671-5119
在这里,管道暗示的子壳明确应用,但不重复。
这与其他一些试图避免fork
-ing的外壳不同。因此,虽然我感觉到js-shell-parse
误导性的论点,但并非所有壳体都始终为所有子壳 fork
。