sh -cで呼び出したコマンドがbashだと孫プロセスにならないことがある
前提として、/bin/sh
は、デフォルトでは、RHEL系の場合bashシェル、Debian系の場合dashシェルへのsymlinkになっています。この2つのシェルの挙動は細かいところで結構異なります。そもそもの思想として、dashシェルはPOSIX互換を目指す軽量なシェルであり、bashは拡張された高機能なシェル。なのでbash前提で書かれたシェルスクリプトがdashでは動かない、みたいなことはよくあります。そういう感じで困ることがままありますが今回もそういう話。
例えば % sh -c "sleep 100"
のようなコマンドを実行した場合、呼び出し元の子プロセスが sh
になり、その更に子プロセスが sleep
になると直感的には思うでしょう。つまり以下のような具合。
.
\_ sh -c sleep 100
\_ sleep 100
しかし、 sh
の実体が bash
である場合なんとそうならず、sleep
が直の子プロセスになります。
.
\_ sleep 100
dashシェルでは期待通り、 sleep
は呼び出し元の孫プロセスになります。
どうやら、bashは sh -c
に渡された文字列が、単独コマンドとして実行できる場合は、exec
してくれるような挙動となるようです。この場合 exec sleep 100
してくれる。実際bashのソースコードを見に行くと以下の記述が見られます。
Define ONESHOT if you want sh -c 'command' to avoid forking to execute 'command' whenever possible. This is a big efficiency improvement.
http://git.savannah.gnu.org/cgit/bash.git/tree/config-top.h?h=bash-4.4#n39
「可能な限りforkなしでコマンドを実行する」と書かれています。単独コマンドじゃない場合、例えば、 sh -c "sleep 10; sleep 5;"
だと、当然ですがexecすることはありません。
これはなかなかbashは攻めた挙動で少しやりすぎなのではないかとも感じます。
この挙動が嬉しい場合
ただ、確かにこの挙動が嬉しい局面もあります。例えば、プログラムからシェル経由でコマンドを呼び出したときに、呼び出した子プロセスにシグナルを送ってそのコマンドを停止させようとしても、通常のdashの挙動だと、呼び出し元の子プロセスは sh -c
になるため、孫プロセスのコマンドにまではシグナルが届きません。逆にbashの挙動だと、子プロセスが直接実行コマンドになるため、そのコマンドを停止させられます。
mackerel-agentの場合
実は、mackerel-agentがそれで困ったことがありました。mackerel-agentはプラグインの実行をコマンド呼び出しでおこなっていますが、その多くはシェル経由で実行されています。また、プラグインの実行時間が長くなった場合にシグナルを送ってタイムアウトさせる機構も持っています。
しかし、前述の通り、素朴に子プロセスにシグナルを送っても、プラグイン実行が孫プロセスになっている場合にはプラグインを直接停止させることができません。昔は素朴に、子プロセスにシグナルを送っていたため、bash環境ではタイムアウトが動くのに、dash環境ではタイムアウトが動かないという問題がありました。
現在は、プラグイン実行時にプロセスグループを作り、プロセスグループに対してシグナルを送ることでその問題を解決しています。具体的には、コマンドタイムアウト用のライブラリである、github.com/Songmu/timeout に以下の変更を加えたことでそれが可能になりました。
https://github.com/Songmu/timeout/pull/6
github.com/Songmu/timeout はだいぶ良いライブラリで、内部的にも面白いことをやっているので、そのうち解説記事を書くなり、Go Conferenceなどで登壇して説明したいと思っています。