プロセスがシェルから開始されると、なぜプロセスを実行する前にシェル自体がフォークするのですか?
たとえば、ユーザーがを入力したときに、シェルが子シェルなしでgrepをgrep blabla foo
呼び出すことができないのはなぜexec()
ですか?
また、シェルがGUIターミナルエミュレーター内で自分自身をフォークするときに、別のターミナルエミュレーターを起動しますか?(pts/13
開始などpts/14
)
プロセスがシェルから開始されると、なぜプロセスを実行する前にシェル自体がフォークするのですか?
たとえば、ユーザーがを入力したときに、シェルが子シェルなしでgrepをgrep blabla foo
呼び出すことができないのはなぜexec()
ですか?
また、シェルがGUIターミナルエミュレーター内で自分自身をフォークするときに、別のターミナルエミュレーターを起動しますか?(pts/13
開始などpts/14
)
回答:
exec
ファミリメソッドを呼び出すと、新しいプロセスは作成されず、代わりexec
に現在のプロセスメモリや命令セットなどが実行するプロセスに置き換えられます。
例として、grep
execを使用して実行したいとします。bash
プロセス(個別のメモリ、アドレス空間を持っています)です。を呼び出すとexec(grep)
、execは現在のプロセスのメモリ、アドレス空間、命令セットなどをgrep's
データで置き換えます。つまり、bash
プロセスはもう存在しません。その結果、grep
コマンドの完了後にターミナルに戻ることはできません。これが、execファミリのメソッドが返らない理由です。execの後にコードを実行することはできません。それは到達不能です。
exec grep blabla foo
。もちろん、この特定のケースでは、それはあまり便利ではありません(grepが終了するとすぐにターミナルウィンドウが閉じます)が、時々便利です(たとえば、sshを介して別のシェルを起動する場合など) / sudo / screen、および元のものに戻るつもりはありません、またはこれを実行しているシェルプロセスが複数のコマンドを実行することを決して意図していないサブシェルである場合)。
bash -c 'grep foo bar'
にはexecを呼び出すことで、bashが自動的に行う最適化の形式があります
に従って、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の答えを多少言い換えると、fork()
新しいプロセスを作成するのは、リソースを割り当てる限りカーネルの作業が少なく、多くのプロパティ(ファイル記述子、環境など)-すべてができるため、簡単です親プロセス(この場合はbash
)から継承されます。
第二に、対話型シェルに関する限り、フォークせずに外部コマンドを実行することはできません。ディスク上に存在する実行可能ファイル(たとえば、/bin/df -h
)を起動するには、親などを新しいプロセスに置き換え、そのPIDや既存のファイル記述子などを引き継ぐexec()
など、ファミリー関数の1つを呼び出す必要がexecve()
あります。対話型シェルの場合、コントロールをユーザーに戻し、親の対話型シェルに引き継がせます。したがって、最善の方法は、経由fork()
でサブプロセスを作成し、そのプロセスを経由で引き継ぐことexecve()
です。対話型シェルPID 1156を経由して子供を生むだろうので、fork()
PID 1157で、その後、呼び出しexecve("/bin/df",["df","-h"],&environment)
可能おり、/bin/df -h
シェルは唯一の出口と復帰制御に、それまでのプロセスを待つ必要がある今、PID 1157で実行します。
たとえばdf | grep
、2つ以上のコマンド間にパイプを作成する必要がある場合は、2つのファイル記述子(pipe()
syscall からのパイプの読み取りと書き込み)を作成し、何らかの方法で2つの新しいプロセスがそれらを継承できるようにする必要があります。新しいプロセスをフォークし、dup2()
呼び出しを介してパイプの書き込み終了をstdout
別名のfd 1にコピーしました(書き込み終了がfd 4の場合、実行しますdup2(4,1)
)。ときexec()
卵には、df
子プロセスがそのの何も考えていないだろうが起こるstdout
その出力が実際にパイプを行くこと(それ積極的にチェックしていない限り)意識せずに、それへの書き込みを。同じプロセスが、たまたまgrep
、私たちを除いてfork()
、FD 3とし、パイプの読み出し側を取るdup(3,0)
産卵前grep
にexec()
。この間、親プロセスはまだ存在し、パイプラインが完了したら制御を取り戻すのを待っています。
組み込みコマンドの場合、一般的にシェルはコマンドfork()
を除いてそうではありませんsource
。サブシェルが必要ですfork()
。
要するに、これは必要かつ有用なメカニズムです。
現在、これはなどの非対話型シェルでは異なりbash -c '<simple command>'
ます。fork()/exec()
多くのコマンドを処理する必要がある最適な方法であるにもかかわらず、コマンドが1つしかない場合はリソースの無駄です。この投稿からステファン・チャゼラスを引用するには:
フォークは、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
リソースの浪費は、実際にはサービス拒否攻撃の一種である可能性があります。特に、フォーク爆弾は、パイプラインで自身を呼び出すシェル関数を介して作成されます。現在、これはsystemd上のcgroupsのプロセスの最大数を制限することで緩和されています。Ubuntuもバージョン15.04以降で使用しています。
もちろん、それは分岐が単に悪いことを意味するものではありません。前に説明したように、これはまだ有用なメカニズムですが、より少ないプロセスで連続してリソースを減らしてパフォーマンスを向上fork()
できる場合は、可能な限り避ける必要があります。