シェルからプロセスが開始されると、なぜプロセスを実行する前にシェル自体がフォークするのですか?
たとえば、ユーザーがgrep blabla foo
を入力すると、シェルは子シェルなしでgrepでexec()
を呼び出すことができないのはなぜですか?
また、シェルがGUIターミナルエミュレーター内で自分自身をフォークするときに、別のターミナルエミュレーターを起動しますか? (pts/13
開始pts/14
など)
exec
ファミリーメソッドを呼び出すと、新しいプロセスは作成されず、代わりにexec
が現在のプロセスメモリや命令セットなどを実行するプロセスに置き換えます。
例として、execを使用してgrep
を実行します。 bash
はプロセスです(独立したメモリ、アドレススペースがあります)。 exec(grep)
を呼び出すと、execは現在のプロセスのメモリ、アドレス空間、命令セットなどをgrep's
データで置き換えます。つまり、bash
プロセスはもう存在しません。その結果、grep
コマンドを完了した後にターミナルに戻ることはできません。これが、execファミリのメソッドが返らない理由です。 execの後にコードを実行することはできません。それは到達不能です。
pts
に従って、自分で確認してください。シェルで実行します
echo $$
あなたのプロセスID(PID)を知るために、例えば
echo $$
29296
次に、たとえばsleep 60
を実行してから、別のターミナルで
(0)samsung-romano:~% ps -edao pid,ppid,tty,command | grep 29296 | grep -v grep
29296 2343 pts/11 zsh
29499 29296 pts/11 sleep 60
そのため、一般に、プロセスに関連付けられた同じttyがあります。 (これはあなたのsleep
であることに注意してください。親としてシェルがあるためです)。
TL; DR:これは、新しいプロセスを作成し、対話型シェルで制御を維持するための最適な方法であるため
この質問の特定の部分に答えるために、grep blabla foo
がexec()
を介して親で直接呼び出される場合、親は存在するように捕捉し、すべてのリソースを含むそのPIDがgrep blabla foo
に引き継がれます。
ただし、exec()
とfork()
については一般的に話しましょう。このような動作の主な理由は、fork()/exec()
がUnix/Linuxで新しいプロセスを作成する標準的な方法であり、これがbash固有のものではないためです。この方法は最初から導入されており、当時の既存のオペレーティングシステムの同じ方法の影響を受けていました。 goldilocks's answer 関連する質問について、言い換えると、新しいプロセスを作成するためのfork()
は、リソースを割り当てる限りカーネルが行う作業が少なく、多くのプロパティ(ファイル記述子など) 、環境など)-すべてを親プロセスから継承できます(この場合はbash
から)。
第二に、対話型シェルに関する限り、フォークせずに外部コマンドを実行することはできません。ディスク上に存在する実行可能ファイル(/bin/df -h
など)を起動するには、exec()
などのexecve()
ファミリー関数の1つを呼び出す必要があります。これは、親を新しいプロセスに置き換え、そのPIDと既存のファイルを引き継ぎます記述子などインタラクティブシェルの場合、コントロールをユーザーに戻し、親のインタラクティブシェルに引き継がせます。したがって、最善の方法は、fork()
を介してサブプロセスを作成し、そのプロセスをexecve()
を介して引き継ぐことです。したがって、対話型のシェルPID 1156は、PID 1157でfork()
を介して子を生成し、execve("/bin/df",["df","-h"],&environment)
を呼び出します。これにより、/bin/df -h
がPID 1157で実行されます。
df | grep
などの2つ以上のコマンド間にパイプを作成する必要がある場合、2つのファイル記述子(pipe()
syscallからのパイプの読み取りと書き込み)を作成する方法が必要です。新しいプロセスはそれらを継承します。新しいプロセスをフォークし、dup2()
呼び出しを介してパイプの書き込み終了をそのstdout
別名fd 1にコピーすることで完了しました(書き込み終了がfd 4の場合、dup2(4,1)
を実行します)。 df
を生成するexec()
が発生すると、子プロセスはstdout
について何も考えず、出力が実際にパイプになることを(積極的にチェックしない限り)意識せずに書き込みます。 grep
にも同じプロセスが発生しますが、fork()
を使用して、grep
をdup(3,0)
で生成する前に、fd 3およびexec()
でパイプの読み取り終了を取得します。この間、親プロセスはまだ存在し、パイプラインが完了すると制御を取り戻すのを待っています。
組み込みコマンドの場合、一般的にシェルはsource
コマンドを除き、fork()
を実行しません。サブシェルにはfork()
が必要です。
要するに、これは必要かつ有用なメカニズムです。
現在、これは 非対話型シェルでは異なる (bash -c '<simple command>'
など)です。 fork()/exec()
は多くのコマンドを処理する必要がある最適な方法ですが、コマンドが1つしかない場合はリソースの無駄です。 StéphaneChazelas from this post を引用するには:
フォークは、CPU時間、メモリ、割り当てられたファイル記述子の点で高価です...終了する前に別のプロセスを待機するだけのシェルプロセスがあると、リソースが無駄になります。また、コマンドを実行する別のプロセスの終了ステータスを正しく報告することが難しくなります(たとえば、プロセスが強制終了された場合)。
したがって、多くのシェル(bash
だけでなく)はexec()
を使用して、その単一の単純なコマンドでbash -c ''
を引き継ぐことができます。上記の理由から、シェルスクリプトでパイプラインを最小限に抑えることをお勧めします。多くの場合、初心者は次のようなことを行うことができます。
cat /etc/passwd | cut -d ':' -f 6 | grep '/home'
もちろん、これはfork()
3プロセスになります。これは簡単な例ですが、ギガバイトの範囲の大きなファイルを考えてみましょう。 1つのプロセスでより効率的になります。
awk -F':' '$6~"/home"{print $6}' /etc/passwd
リソースの浪費は、実際にはサービス拒否攻撃の一種である可能性があり、特に fork bombs は、パイプラインで自分自身を呼び出すシェル関数を介して作成されます。現在、これは systemdのcgroups でプロセスの最大数を制限することで軽減されています。Ubuntuもバージョン15.04以降で使用しています。
もちろん、それは分岐が単に悪いことを意味するものではありません。前に説明したように、これはまだ便利なメカニズムですが、より少ないプロセスで連続してリソースを減らしてパフォーマンスを向上できる場合は、可能であればfork()
を避ける必要があります。
Bashプロンプトで発行する各コマンド(例:grep)について、実際には新しいプロセスを開始し、実行後にbashプロンプトに戻るつもりです。
Shellプロセス(bash)がexec()を呼び出してgrepを実行すると、Shellプロセスはgrepに置き換えられます。 Grepは正常に機能しますが、実行後、bashプロセスが既に置き換えられているため、コントロールをシェルに戻すことはできません。
このため、bashはfork()を呼び出しますが、これは現在のプロセスを置き換えません。