命令替代何时产生的子壳比孤立的同一命令更多



昨天有人建议我在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 管道段不在子壳中运行。
  • 用于命令替代($(...)

  • 用于过程替代(<(...)

    • 通常会创建 2 子壳;在A 简单命令的情况下,@konsolebox提出了一种仅创建 1 的技术:使用exec<(exec ...))预留简单命令。
  • 背景执行(&

组合这些构造将导致多个子壳。

在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

相关内容

  • 没有找到相关文章

最新更新