`fork`、子プロセス、および「サブシェル」について

`fork`、子プロセス、および「サブシェル」について

この投稿は基本的に以前の質問私の。

その質問への回答から、私は「サブシェル」の概念全体を理解していないだけでなく、より一般的には、fork-ing と子プロセスの関係を理解し​​ていないことに気付きました。

以前は、プロセスがXを実行するとfork新しいプロセスYが作成され、その親は ですXが、その質問の答えによると、

[a] サブシェルは完全に新しいプロセスではなく、既存のプロセスのフォークです。

ここでの含意は、「フォーク」は「完全に新しいプロセス」ではない(または、その結果として新しいプロセスは生まれない)」ということです。

私は今、非常に混乱しています。実際のところ、混乱しすぎていて、自分の混乱を直接解消するための一貫した質問を組み立てることができません。

しかし、間接的に啓蒙につながるかもしれない質問をすることはできます。

によるとzshall(1)$ZDOTDIR/.zshenvの新しいインスタンスがzsh起動するたびに がソース化されるため、 の「完全に新しい [zsh] プロセス」の作成につながるコマンドはすべて無限後退を引き起こします。一方、ファイル$ZDOTDIR/.zshenvに次の行のいずれかを含めると、$ZDOTDIR/.zshenvない無限後退につながる:

echo $(date; printenv; echo $$) > /dev/null    #1
(date; printenv; echo $$)                      #2

上記のメカニズムによって無限後退を誘発するために私が見つけた唯一の方法は、ファイルに次の1 の$ZDOTDIR/.zshenvような行を含めることでした。

$SHELL -c 'date; printenv; echo $$'            #3

私の質問は次のとおりです:

  1. #1上記の とでマークされたコマンド#2と でマークされた#3コマンドの違いは、この動作の違いから何を説明するのでしょうか?

  2. で作成されるシェルが「サブシェル」#1#2呼ばれる場合、 によって生成されるシェルのようなものは何と#3呼ばれますか?

  3. 上記の経験的/逸話的な発見を、Unix プロセスの「理論」(適切な言葉が見つからないため) の観点から合理化 (および一般化) することは可能でしょうか?

最後の質問の動機は、事前に(つまり、実験に頼らずに) に含まれている場合、どのようなコマンドが無限後退につながりますか$ZDOTDIR/.zshenv?


1上記のさまざまな例で使用した特定のコマンドのシーケンスはdate; printenv; echo $$それほど重要ではありません。これらのコマンドの出力は、私の「実験」の結果を解釈するのに役立つ可能性があります。(ただし、これらのシーケンスを複数のコマンドで構成したかったのは、前述の理由からです。ここ

答え1

zshall(1)によれば、$ZDOTDIR/.zshenvはzshの新しいインスタンスが起動するたびに読み込まれる。

ここで「開始」という言葉に焦点を当てると、物事がより良く進むでしょう。の効果はfork()別のプロセスを作成することですそれはまさに現在のプロセスがすでに始まっているところから始まります既存のプロセスを複製していますが、唯一の違いは の戻り値ですfork。ドキュメントでは、「開始」は最初からプログラムに入ることを意味します。

例 3 では が実行され$SHELL -c 'date; printenv; echo $$'、最初から完全に新しいプロセスが開始されます。通常の起動動作が実行されます。たとえば、別のシェルに切り替えて を実行することでこれを説明できます。bash -c ' ... 'の代わりにを実行しますzsh -c ' ... '。ここで を使用することに特別なことはありません$SHELL

例 #1 と #2 はサブシェルを実行します。シェルはfork自身を実行し、その子プロセス内でコマンドを実行し、子プロセスが終了すると自身の実行を続行します。


質問 1 に対する答えは上記のとおりです。例 3 は最初から完全に新しいシェルを実行し、他の 2 つはサブシェルを実行します。起動動作には の読み込みが含まれます.zshenv

彼らがこの動作を特に指摘する理由は、おそらくあなたの混乱の原因となっているのですが、このファイルは(他のファイルとは異なり)対話型シェルと非対話型シェルの両方で読み込まれるからです。


質問2について:

#1 と #2 で作成されるシェルが「サブシェル」と呼ばれる場合、#3 で生成されるもののようなものは何と呼ばれますか?

名前が必要な場合は、「子シェル」と呼ぶこともできますが、実際には何の意味もありません。同じシェル、別のシェル、または など、シェルから開始する他のプロセスと何ら変わりはありませんcat


質問3について:

上記の経験的/逸話的な発見を、Unix プロセスの「理論」(適切な言葉が見つからないため) の観点から合理化 (および一般化) することは可能でしょうか?

fork新しい PID を持つ新しいプロセスを作成し、このプロセスが中断したところから並行して実行を開始します。exec現在実行中のコードを、どこかからロードされた新しいプログラムに置き換え、最初から実行します。新しいプログラムを生成するときは、最初にfork自分自身を起動し、次にexecそのプログラムを子プロセスで起動します。これが、シェルの内外を問わず、どこにでも適用されるプロセスの基本理論です。

サブシェルはforkであり、実行するすべての非組み込みコマンドは と の両方につながりforkますexec


$$親シェルのPIDに展開されることに注意してくださいPOSIX互換シェルではなので、期待する出力が得られない可能性があります。また、zsh はサブシェルの実行を積極的に最適化し、通常はexec最後のコマンドを実行するか、サブシェルがなくてもすべてのコマンドが安全な場合はサブシェルをまったく生成しないことにも注意してください。

直感をテストするのに役立つコマンドの 1 つは次のとおりです。

strace -e trace=process -f $SHELL -c ' ... '

これにより、新しいシェルで実行するコマンドのすべてのプロセス関連イベント (他のイベントはなし) が標準エラーに出力されます...。新しいプロセスで実行されるものと実行されないもの、および発生する場所を確認できますexec

もう一つの役に立つかもしれないコマンドは でpstree -h、これは現在のプロセスの親プロセスのツリーを出力して強調表示します。出力で何層の深さか確認できます。

答え2

マニュアルで のコマンドが.zshenv「ソース化」されていると書かれている場合、それは、それらを実行しているシェル内で実行されることを意味します。 の呼び出しは発生しないfork()ため、サブシェルは生成されません。 3 番目の例では、 を呼び出して を呼び出して を呼び出してサブシェルを明示的に実行しfork()、無限に再帰します。これで、最初の質問に (少なくとも部分的に) 答えられると思います。

  1. コマンド 1 と 2 では何も「作成」されていないため、何も呼び出されません。これらのコマンドは、ソース シェルのコンテキスト内で実行されます。

  2. 一般化は、シェル ルーチンまたはプログラムの「呼び出し」と、シェル ルーチンまたはプログラムの「ソース化」の違いです。後者は通常、シェル コマンド/スクリプトにのみ適用され、外部プログラムには適用されません。シェル スクリプトの「ソース化」は、通常、または. <scriptname>ではなくを介して行われます。ソース化ディレクティブの先頭の「ドット スペース」シーケンスに注意してください。ソース化は を使用して呼び出すこともできます。この場合、コマンドはシェル内部です。./<scriptname>/full/path/to/scriptsource <scriptname>source

答え3

forkすべてがうまくいったと仮定すると、 は 2 回戻ります。1 回目は親プロセス (元のプロセス ID を持つ) で戻り、もう 1 回目は新しい子プロセス (プロセス ID は異なりますが、それ以外は親プロセスと多くの共通点を持つ) で戻ります。この時点で、子はexec(3)何かを行うことができ、そのプロセスに「新しい」バイナリがロードされますが、子はそうする必要はなく、親プロセスによってすでにロードされている他のコード (たとえば、zsh 関数) を実行することができます。したがって、fork「完全に新しい」がシステム コールによってロードされた何かを意味すると解釈すると、 は「完全に新しい」プロセスになる場合とならない場合がありますexec(3)

どのコマンドが無限後退を引き起こすかを事前に推測するのは難しい。フォーク呼び出しフォークの場合(別名「フォーク爆弾」)のほかに、コマンドを単純な関数ラッパーで囲むのも簡単な方法である。

function ssh() {
   ssh -o UseRoaming=no "$@"
}

おそらく次のように書くべきでしょう

function ssh() {
  =ssh -o UseRoaming=no "$@"
}

または、関数が関数を呼び出し、その関数が関数を呼び出すcommand ssh ...という無限の関数呼び出しを回避するには...関数呼び出しは ZSH プロセスの内部で行われるため、これはまったく関係ありませんが、その単一の ZSH プロセスによって何らかの制限に達するまで、無限に発生します。sshsshfork

straceは、いつものように、任意のコマンド (特にここforkとおそらくいくつかのexec呼び出し) に関係するシステム コールを正確に明らかにするのに役立ちます。シェルは、シェルが内部で何を実行しているか (関数呼び出しなど) を示す または同様のものを使用してデバッグできます-x。詳細については、Stevens の「Advanced Programming in the Unix Env​​ironment」に、新しいプロセスの作成と処理に関連する章がいくつかあります。

関連情報