shellの-cオプションについて、全てのshellで同じ挙動だと思っていたら、shellごとに結構違う挙動を持っていたことが原因でハマったのでメモ
shellの-cオプションについて
manコマンドで見てみるとこんな感じ。
For sh(dash) ubuntuの場合shはdashという軽量シェルにリンクしています
Read commands from the command_string operand instead of from the standard input. Special 0 will be set from the command_name operand and the positional parameters (\$1, \$2, etc.) set from the remaining argument operands.
For bash
If the -c option is present, then commands are read from the first non-option argument command_string. If there are arguments after the command_string, they are assigned to the positional parameters, starting with \$0.
For zsh
Take the first argument as a command to execute, rather than reading commands from a script or standard input. If any further arguments are given, the first one is assigned to \$0, rather than being used as a positional parameter.
特段挙動に違いはなさそうです、通常shellプログラムがコマンドを標準入力+改行コードで受け取るのではなく、-cで渡される文字列をコマンドとして認識して実行するということが書かれています。
bash -c './sample.sh' と bash sample.shは違う
たまに、混在してしまう人がいるのですが上記は、全く意味合いが違うので整理しておいた方がいいです。
下記は、新しいbashプロセス上でsample.shという実行可能スクリプトファイルを実行しています。そのため、sample.shのコンテンツを評価するのは、この実行結果からはbashであると判断できません shebangにzshやshが指定されていた場合は、もちろんそちらのプログラムでコンテンツが評価されます
$ bash -c './sample.sh'
下記は、bashにsample.shというコマンドが羅列されているファイルを読み込ませ、bashが評価しています。そのため、sample.shに羅列されているコマンドは全てbashで評価されます
$ bash sample.sh
-cで評価される時にshellプログラムに渡される$0が指定できる
manを見ると1つ面白い挙動としてこんなことは書かれていますね
If there are arguments after the command_string, they are assigned to the positional parameters, starting with \$0.
$0には、通常コマンド名(スクリプト名)が自動挿入されるということをみなさん思われていると思いますので下記の実行結果は誰もが予測できるものだと思います。
$ bash -c 'echo $0'
bash
ただ、manの通り command_string(ここでは echo $0) の後に、引数を複数渡すと \$0 から順にpositional argumentに代入されていくそうなのです。なので、上記の呼び出しに追加で引数を渡してあげると、下記のようになります。
$ bash -c 'echo $0' first_argument
first_argument
ユースケースはイマイチ思いつきませんが、まぁ頭の片隅にでも入れておきます。
dash, bash, zshの-cの違い
とりあえず、上述まではshellの-cオプションの簡単な説明として、ここからは本題に入っていきます。
何が違うか、端的にいうとbash, zshは特定の条件下(コマンドが1で出会った場合)で暗黙的にexecコマンドにより、渡されたコマンドを新しいshellプロセスの実行空間でそのまま実行します
bash, zshでコマンドが1行だった場合
command_string が 1行のコマンドだった場合
vagrant@vagrant:~$ echo $$
9842
vagrant@vagrant:~$ bash -c 'sleep 100'
vagrant@vagrant:~$ ps axl | grep 9842 | grep -v 'grep --color=auto 9842'
0 1000 9842 9841 20 0 21308 3936 wait Ss pts/0 0:00 -bash
0 1000 10212 9842 20 0 7292 816 hrtime S+ pts/0 0:00 sleep 100
vagrant@vagrant:~$ zsh -c 'sleep 100'
vagrant@vagrant:~$ ps axl | grep 9842 | grep -v 'grep --color=auto 9842'
0 1000 9842 9841 20 0 21308 3936 wait Ss pts/0 0:00 -bash
0 1000 10287 9842 20 0 7292 656 hrtime S+ pts/0 0:00 sleep 100
bashもzshも現在のloginシェルの子プロセスとしてsleep 100
が実行されています
dashでコマンドが1行だった場合
vagrant@vagrant:~$ echo $$
9842
vagrant@vagrant:~$ sh -c 'sleep 100'
vagrant@vagrant:~$ ps axl | grep 9842 | grep -v 'grep --color=auto 9842'
0 1000 9842 9841 20 0 21308 3936 wait Ss pts/0 0:00 -bash
0 1000 10394 9842 20 0 4508 752 wait S+ pts/0 0:00 sh -c sleep 100
vagrant@vagrant:~$ ps axl | grep 10394 | grep -v 'grep --color=auto 10394'
0 1000 10394 9842 20 0 4508 752 wait S+ pts/0 0:00 sh -c sleep 100
0 1000 10395 10394 20 0 7292 752 hrtime S+ pts/0 0:00 sleep 100
sleep 100は、現在のloginシェルの孫プロセスとして実行されていることがわかります。つまり、dashは単純に新しくdashプログラムを立ち上げインタラクティブシェルと同じように、そこで順順に子プロセスを作成しコマンドを実行している事がわかります。
bashでコマンドが2行以上だった場合
vagrant@vagrant:~$ echo $$
9842
vagrant@vagrant:~$ bash -c 'ls; sleep 100'
a.out devstack ls nova sample.sh t t1 tes test.c tete
vagrant@vagrant:~$ ps axl | grep 9842 | grep -v 'grep --color=auto 9842'
0 1000 9842 9841 20 0 21308 3936 wait Ss pts/0 0:00 -bash
0 1000 10482 9842 20 0 12520 2820 wait S+ pts/0 0:00 bash -c ls; sleep 100
2行以上の場合は、コマンド情報を保持するためにexecですぐさまプロセス空間を受け渡す事はせず(できません)、すべてのコマンドは、ログインシェルからみて、孫プロセスとして実行されます
zshでコマンドが2行以上だった場合
vagrant@vagrant:~$ echo $$
9842
vagrant@vagrant:~$ zsh -c 'sleep 10;sleep 100'
vagrant@vagrant:~$ date
Sun Sep 10 11:45:21 UTC 2017
vagrant@vagrant:~$ ps axl | grep 9842 | grep -v 'grep --color=auto 9842'
0 1000 9842 9841 20 0 21308 3936 wait Ss pts/0 0:00 -bash
0 1000 10806 9842 20 0 26044 3180 sigsus S+ pts/0 0:00 zsh -c sleep 10;sleep 100
vagrant@vagrant:~$ date
Sun Sep 10 11:45:33 UTC 2017
vagrant@vagrant:~$ ps axl | grep 9842 | grep -v 'grep --color=auto 9842'
0 1000 9842 9841 20 0 21308 3936 wait Ss pts/0 0:00 -bash
0 1000 10806 9842 20 0 7292 764 hrtime S+ pts/0 0:00 sleep 100
2行以上の場合は、bash同様コマンド情報を保持するためにexecですぐさまプロセス空間を受け渡す事はせず(できません)、しかし最後のコマンドではexecを使いzshのプロセス空間でコマンドを実行している事がわかります。
dashでコマンドが2行以上だった場合
言わずもがな、1行の時と同じく全コマンドは孫プロセスとして実行されます
-cでexecするかどうか、子か孫かどうかで何が問題に?
ぶっちゃけ、そこらへんの挙動どうでもよくね?って僕も思ってたのですが、実はここの理解をきちんと整理する必要のあるシチュエーションが最近出てきたのです。はい、あれです。docker周りです。
ubuntuイメージでのDockerfileのCMDに文字列を渡すとき
DockerfileのCMDには、2種類の値が渡せます配列と文字列。配列の場合は、shellを通さずに実行されるのですが、文字列の場合はshellのコマンドとして解釈されるので(sh -cの引数として評価)、AppacheやNginxなどの常駐プログラムを実行する時は、注意が必要です。
kill時SIGTERMがCMDに書かれているエントリの実行プロセスに送られる
なぜ注意がいるかというと、docker killした際、dockerデーモン(docker-containerd)は、CMDで実行されたプロセスに対して、安全に終了できるように、SIGTERMシグナルを送ってくれます。なのでそれを受け取って安全に終了プロセスに入れるんですが、もちろん世の中SIGTERMをきちんとハンドルしてくれる行儀のよいソフトウェアばかりでないので、デフォルト10秒待ってもプロセスが死なない場合は、SIGKILLで強制的にプロセスを終了させようとします。
なので、SIGTERMをきちんとハンドルできるようにする事はかなり重要なのです。
shellはsignalの伝搬なんてしない
なので、docker containerを立ち上げる際に、コンテナから見て最上位プロセスがSIGTERMをきちんとハンドルできるようにしないといけないのです。
もちろんshellはsignalを子プロセスに伝搬なんぞしないので、例えばubuntuイメージで下記みたいなCMDを設定すると
CMD "nginx -g 'daemon off'"
文字列が指定されているので、sh -c
の引数として評価され
docker containerでは下記のような形になります。先ほどのセクション通りUbuntuのsh(dash)は、-cで渡されたコマンドをexecしないので、コンテナ内のPID1はshellになります。
pid:1 sh -c "nginx -g 'daemon off;'"
ppid:1 pid:2 nginx: master process nginx -g daemon off;
ppid:2 pid:3 nginx: worker process
この状態で、docker killをすると、pid1のshellにSIGTERMが送られるので当たり前のように無視され、タイムアウト後SIGKILLで強制的に殺されるという結末になってしまいます。
dockerを使い始めたばかりのプロジェクトなどでは、たまにこういうのを見かけるので注意が必要です。結構この手の間違いをしてしまう人多いはずです。また、centosのshはbashのシンボリックリンクになっているので、PID1がnginxになります。手元のOSがCentosで手元のshではちゃんとexecしてくれているように見えるけどコンテナの中は違うとか(ええ、実際そういうのありました)
あとがき
実は、このshellごとに-cの処理の仕方が違う事についてはdockerで問題になってから調べました。
一部コンテナが正常終了しないという状況で調べて見たら、PID1がshやbashになっていることに気づき、調べ始めたところ思いの外、違う挙動をする事がわかりました。
みんな使うshellを統一しようなんてしないので、この手の微妙な違いから生まれるこういう問題は少し厄介だなと・・・・
zshだと、最後のコマンドが自動的にexecされるのでsedなどで実行時configをいじって、最後に常駐プログラムを実行みたいなこともできるのですが、bashでは明示的にしないといけないとか。
当たり前のことですが、きちんと確認することは大事ですね。