おそらくはそれさえも平凡な日々

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などで登壇して説明したいと思っています。

created at
last modified at

2018-12-19T15:22:27+0900

comments powered by Disqus