26

昨日、bash でコマンド置換を使用すると、不要なサブシェルが生成されることが示唆されました。アドバイスは、このユースケースに固有のものでした:

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

# No extra subshell
command
foo=$?

私が理解できる限り、これはこのユースケースには正しいようです。ただし、これを確認しようとして簡単に検索すると、混乱し矛盾するアドバイスが大量に出てきます。コマンド置換をすべて使用すると、サブシェルが生成されるという一般的な知恵があるようです。例えば:

コマンド置換は、コマンドの出力に展開されます。これらのコマンドはサブシェルで実行され、それらの stdout データは、置換構文が展開されるものです。(ソース

掘り続けない限り、これは十分に単純に思えますが、掘り下げ続けると、そうではないという提案への参照を見つけ始めるでしょう。

コマンド置換は必ずしも subshel​​l を呼び出すとは限らず、ほとんどの場合は呼び出されません。それが保証する唯一のものは順不同の評価です: 最初に置換内の式を評価し、次に置換の結果を使用して周囲のステートメントを評価します。(ソース

これは理にかなっているように思えますが、本当ですか? サブシェル関連の質問に対するこの回答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 の実装に違いはありますか? コマンド置換を使用するとサブシェルが生成され、同じコマンド セットを単独で実行しても生成されない他のケースはありますか?

4

2 に答える 2

17

更新と警告:

この回答には、真実ではないことが判明したことを自信を持って主張したという点で、過去の問題があります。現在の形でも価値があると思いますが、他の不正確な点を取り除くのを手伝ってください (または、完全に削除する必要があることを私に納得させてください)。

@kojiro が私のテスト方法に欠陥があることを指摘した後、この回答を大幅に修正し、ほとんどがps骨抜きにしました (私はもともと子プロセスを探していましたが、常に子プロセスを検出するには遅すぎます)。新しいテスト方法を以下に説明します。

私は当初、すべての bash サブシェルが独自の子プロセスで実行されるわけではないと主張しましたが、それは正しくないことが判明しました。

@kojiro が彼の回答で述べているように、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 シェルのみが実行されていることを確認しました。1 つはコマンドの実行用、もう 1 つは監視用です。
  • 2番目のシェルfork()では、1番目の呼び出しを監視しましたsudo dtruss -t fork -f -p {pidOfShell1}(これは、呼び出しを「推移的に」-f追跡するためにも必要です。つまり、サブシェル自体によって作成されたものを含める必要があります)。fork()
  • :テスト コマンドではビルトイン (no-op)のみを使用しました (fork()外部実行可能ファイルの追加呼び出しで図が混乱するのを避けるため)。具体的には:

    • :
    • $(:)
    • : | :
    • $(: | :)
    • : | :; :
    • $(: | :; :)
  • ゼロ以外の PID を含む出力行のみをカウントdtrussします (各子プロセスは、fork()それを作成した呼び出しも報告しますが、PID は 0 です)。

  • 対話型シェルからビルトインを実行するだけでも、明らかに少なくとも 1 が含まれるため、結果の数値から 1 を引きfork()ます。
  • 最後に、結果のカウントが作成されたサブシェルの数を表すと仮定しました。

以下は、元の投稿からまだ正しいと信じていることです。bashがサブシェルを作成するとき。


bash は、次の状況でサブシェルを作成します。

  • (...)括弧 ( ) で囲まれた式の場合
    • ただし[[ ... ]]、括弧は論理的なグループ化にのみ使用されます。
  • 最初の セグメントを含む、パイプライン ( )のすべてのセグメント|
    • 関連するすべてのサブシェルは、コンテンツに関して元のシェルのクローンであることに注意してください(プロセスに関しては、サブシェルは他のサブシェルから分岐できます (コマンドが実行される))。 したがって、以前のパイプライン セグメントでサブシェルを変更しても、後のパイプライン セグメントには影響しません。 (設計上、パイプライン内のコマンドは同時に起動されます。シーケンスは、接続された stdin/stdout パイプを介してのみ発生します。)

    • bash 4.2+シェル オプションlastpipe(デフォルトでは OFF) があり、最後のパイプライン セグメントがサブシェルで実行されないようにします。
  • コマンド置換用 ( $(...))

  • プロセス置換用 ( <(...))

    • 通常、2 つのサブシェルを作成します。単純なコマンドの場合、 @konsolebox は1のみを作成する手法を思いつきました: 単純なコマンドの前に( ) を追加します。exec<(exec ...)
  • バックグラウンド実行 ( &)

これらの構造を組み合わせると、複数のサブシェルが生成されます。

于 2014-01-24T16:55:12.027 に答える
9

Bash では、サブシェルは常に新しいプロセス空間で実行されます。$BASHPIDこれは、環境変数と$$環境変数を持つ Bash 4 でかなり簡単に確認できます。

  • $$ シェルのプロセス ID に展開されます。() サブシェルでは、サブシェルではなく、現在のシェルのプロセス ID に展開されます。
  • BASHPID 現在の bash プロセスのプロセス ID に展開されます。これは、bash の再初期化を必要としないサブシェルなど、特定の状況下での $$ とは異なります。

実際には:

$ 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

ここでは、パイプによって暗示されたサブシェルが明示的に適用されますが、複製はされません。

これは、 -ingを避けようとforkする他のシェルとは異なります。したがって、誤解を招くような議論が行われているように感じますが、すべてのシェルが常にすべてのサブシェルに対応しjs-shell-parseているわけではないのは事実です。fork

于 2014-01-25T04:49:14.960 に答える