26

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

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

# No extra subshell
command
foo=$?

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

命令替换扩展到命令的输出。这些命令在 subshel​​l 中执行,它们的标准输出数据是替换语法扩展的内容。(来源

这似乎很简单,除非您继续挖掘,在这种情况下,您将开始找到对事实并非如此的建议的引用。

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

这似乎很合理,但这是真的吗?This answer to a subshel​​l related question提示我man bash注意以下事项:

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

这让我想到了主要问题。究竟什么会导致命令替换产生一个无论如何都不会产生的子shell来单独执行相同的命令?

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

# Case #1
command1
var=$(command1)

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

# Case #3
command1 | command 2 ; var=$?
var=$(command1 | command2 ; echo $?)

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

4

2 回答 2

17

更新和警告

这个答案有一个麻烦的过去,因为我自信地声称事实证明不是真的。我相信它以目前的形式具有价值,但请帮助我消除其他不准确之处(或说服我应该完全删除它)。

在@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 中添加一个额外的子外壳- 就像在(...).

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

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

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

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

以下是我在原始帖子中仍然认为正确的内容:when bash 创建 subshel​​ls。


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

  • (...)用括号 ( ) 包围的表达式
    • 除了直接 inside [[ ... ]],括号仅用于逻辑分组。
  • 对于管道的 一段 ( |),包括第一段
    • 请注意,所涉及的每个子shell 在内容方面都是原始shell的克隆(就过程而言,子shell 可以从其他子shell 派生(执行命令之前))。 因此,早期管道段中子壳的修改不会影响后面的子壳。 (根据设计,管道中的命令是同时启动的——排序只通过它们连接的标准输入/标准输出管道发生。)

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

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

    • 通常创建2个子壳;在简单命令的情况下,@konsolebox 提出了一种仅创建1的技术:在简单命令前加上exec( <(exec ...))。
  • 后台执行 ( &)

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

于 2014-01-24T16:55:12.027 回答
9

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

  • $$ 扩展为 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

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

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

在这里,管道隐含的子shell 被显式应用,但不重复。

这与其他一些极力避免fork-ing的 shell 不同。因此,虽然我觉得这个论点js-shell-parse具有误导性,但确实并非所有的 shell 总是fork适用于所有的子 shell。

于 2014-01-25T04:49:14.960 回答