bashとPOSIXセッション

  • 投稿日:
  • by
  • カテゴリ:

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
#

お疲れ様でした。