sshでLinuxサーバーへリモートログインしているとしよう。一度実行すると完了するまで5時間もかかるバッチを貴方は実行しようとしている。バッチコマンドを実行し、次の日の朝にバッチの結果を見ようと考え、その日は帰宅する。
翌朝出社して、前日帰宅前に実行していたバッチの結果を見ようとしたら、ネットワークの接続が不安定となったようで、sshセッションが切断されてしまっていた。バッチはもちろん最後まで実行されていない。やられた~~~!!なんて言っても、後の祭りだ。
なぜこういうことになるのか?それは、POSIXにてセッションが終了する時にはそこから生じたすべてのプロセスを道連れに終了する(Hang Upする)ことを規定しているからだ。自分でやったことはきちんと自分で始末しましょうという次第。
この辺りの話はProcess groupに詳しい。かいつまんで説明すると、プロセスはプロセスグループに所属する。プロセスグループにはプロセスグループリーダーがおり、プロセスグループリーダーとなるプロセスのPIDがプロセスグループIDとなる。プロセスグループには(おそらく)複数のプロセスが所属する。もちろん、一つだけかもしれないが。なぜプロセスをプロセスグループとしてまとめるのかというと、シグナルを一斉配信したいからだ。逆にいうと、単にシグナル配信の目的のためにグループ化しているだけで、各プロセス間には親子関係が存在しないかもしれない。
更に、プロセスグループはセッションに所属する。一つのセッションに(おそらく)複数のプロセスグループが所属する。もちろん、一つだけかもしれない。
テキスト端末でLinuxにログインすると、カーネルはログインセッションを開始する。この時、セッションリーダーとよばれる単一のプロセスが制御端末(デバイス)とやりとりし、そこから生じる一切のプロセスグループへの端末制御に関するシグナル配信を取り仕切る。プロセスをフォアグラウンドやバックグラウンドに切り替えるというのもあるが、もっとも重要なものは、セッションを閉じたときに所属する全プロセスグループへHang Upシグナルを送信することだ。POSIXではそのように決まっているのだが、Redhat系Linuxのbashのデフォルト設定は少しこれと違う。その点は後ほど。
実際のリモートシェルでは、どのようになっているのか。
# echo $$ 3316 # ps -ejH|grep bash 3316 3316 3316 pts/0 00:00:00 bash #
これは、カレントシェルのPID($$変数)が3316であり、PID=3316、プロセスグループID=3316、そしてセッションIDが3316であるbashプロセスを表示している。割り当てられた制御端末はpts/0。sshでログインすると、ログインシェルそのものが制御端末とやり取りするセッションリーダーとなるようだね。
Redhat系bashのデフォルト設定をshoptコマンドで見てみよう。
# shopt cdable_vars off cdspell off checkhash off checkwinsize on cmdhist on compat31 off dotglob off execfail off expand_aliases on extdebug off extglob off extquote on failglob off force_fignore on gnu_errfmt off histappend off histreedit off histverify off hostcomplete on huponexit off interactive_comments on lithist off login_shell on mailwarn off no_empty_cmd_completion off nocaseglob off nocasematch off nullglob off progcomp on promptvars on restricted_shell off shift_verbose off sourcepath on xpg_echo off #
huponexitがoffとなっている。この設定だと、bashは、ログインセッションを終了する時に、フォアグラウンドプロセスにのみSIGHUPを送信する。だから、バックグラウンドプロセスはsshを閉じたりした後にもそのまま残る。
テストしてみよう。次のスクリプトをtest.shとして保存する。
#! /bin/sh echo "`date +'%Y/%m/%d %H:%M:%S'` ARGS : $*" >> log/test.log echo "`date +'%Y/%m/%d %H:%M:%S'` ARGS : $*" while [ : ] do sleep 5 echo "`date +'%Y/%m/%d %H:%M:%S'` Woke up." >> log/test.log echo "`date +'%Y/%m/%d %H:%M:%S'` Woke up." done
logサブディレクトリはあらかじめ作成しておく。sshでLinuxにログインし、これを実行する。
# echo $$ 6317 # ps -ejH|grep bash 6317 6317 6317 pts/0 00:00:00 bash # # ./test.sh
ログインシェルのPIDは6317。覚えておこう。
別のsshセッションを張り、test.shの情報を見る。
# ps -ejH|grep test.sh 25114 25114 6317 pts/0 00:00:00 test.sh #
test.shのPIDは25114、プロセスグループIDは25114、つまり自分がプロセスグループリーダーだ。セッションIDは6317。これはtest.shを実行したbashシェルのPIDだ。
test.shを実行しているシェルを閉じてしまおう。もう一つのシェルを覗いてみると、
# ps -ejH|grep test.sh #
test.shは居なくなっている。
次に、test.shを実行するときに、バックグラウンドへ追い出してしまう。末尾に"&"を付ければ良い。
# echo $$ 26155 # ps -ejH|grep bash|grep 26155 26155 26155 26155 pts/0 00:00:00 bash # # ./test.sh & [1] 26194 # 2015/04/01 17:09:06 Woke up. 2015/04/01 17:09:11 Woke up. 2015/04/01 17:09:16 Woke up. 2015/04/01 17:09:21 Woke up. 2015/04/01 17:09:26 Woke up. ...
[]の中はジョブIDだ。ジョブID=1でバックグラウンドへ入った。ごらんの通り、バックグラウンドプロセスもきちんと制御端末へ標準出力を出してくれる。bashのセッションIDは26155。別のシェルで覗いてみると、
# ps -ejH|grep test.sh 26194 26194 26155 pts/0 00:00:00 test.sh #
test.shのセッションIDはもちろん、26155となる。test.shを実行したシェルを閉じてしまおう。
# ps -ejH|grep test.sh 26194 26194 26155 ? 00:00:00 test.sh # # ps -ef|grep bash|grep 26155 #
test.shはまだ残っている。bashセッションを終了して閉じたにもかかわらずセッションIDとして"26155"が残っていることに注目しよう。それと、制御端末が"?"となっており、すでにtest.shを制御する端末デバイスが存在しないことを示している。test.shの標準出力等を見ることはもう不可能だ。
だから、Redhat系OSで、標準入出力を必要としないバッチプロセスを、ssh等のログインセッションが切断されても実行し続けたいのであるならば、単純にバックグラウンドプロセスとして実行してしまえば良いことになる。
Redhatは気を利かせたつもりでこういう設定としたのであろうが、非POSIX的な振る舞いなのは如何なものか?shoptコマンドでhuponexit設定を切り替えてしまえば、POSIXコンプライアントとなり、シェルを終了するときにすべてを道連れにして終わってくれるぞ。
# shopt -s huponexit # shopt |grep huponexit huponexit on #
このbash設定変更は、やるならば自己責任でよろしくね。解除設定は"-s"オプションではなく、"-u"オプションが使えるらしい。超簡単!shoptでbashの"秘められた真のチカラ"を開放する 【サンプルあり】に色々と書いてあるよ。
さて、バックグラウンドプロセスとして実行すると、その標準出力・標準エラー出力(バッチならば対話的な標準入力は使わないだろう)は、ログインセッションが終了する時点までしか記録されない。それでは困るという向きもおられよう。
そういう場合には、nohupコマンドを利用する。
# nohup ./test.sh a1 b2 c3
test.shに引数"a1", "b2", "c3"が渡されていることに注目。末尾の"&"が付いていない事にも注目。つまり、これはフォアグラウンドとして実行されている。nohupは引数をきちんとtest.shへ渡してくれる。実行ディレクトリ上に、nohup.outなるファイルが作成されるので、それを覗いてみよう。
# tail -f nohup.out 2015/04/01 17:18:46 ARGS : a1 b2 c3 2015/04/01 17:18:51 Woke up. 2015/04/01 17:18:56 Woke up. 2015/04/01 17:19:01 Woke up. 2015/04/01 17:19:06 Woke up. 2015/04/01 17:19:11 Woke up. 2015/04/01 17:19:16 Woke up. 2015/04/01 17:19:21 Woke up. ...
この状態で、test.shを起動したログインシェルを閉じてしまっても、nohup.outがずっと出力され続けることに注目。これならば、ssh接続が切れたとしても、安心してバッチを実行できるよね。
なお、X Windowのセッションは、端末ベースのセッションとはまた別の概念である。カーネルとは関係なくGUI専用のセッションを管理している。悪しからず。
テストが一式済んだら、test.shプロセスを終了させてしまおう。
# pkill test.sh # ps -ef|grep test.sh root 3217 25125 0 17:27 pts/1 00:00:00 grep test.sh #
お疲れ様でした。