何をいまさら当たり前の事を・・・と思われるだろう。
$ nohup long_run_batch.sh &
SSHからログアウト後も実行を続けたいバッチジョブを、"&"を付けてバックグラウンドジョブとしてnohupから起動するのは定番中の定番である。
しかし、「nohupを使わなくても実行を続けることが出来る」やり方があったり、さらには「nohupを付けてもログアウト時に終了してしまう」パターンがあるとしたらどうだろう?
そして、ある日あなたの後輩や同僚がこれらについてあなたに質問してきたら、あなたはどう答えるだろうか?
「Web上で検索したら見つかったのでそれに従ってる」
と答えてお茶を濁すだろうか?
それとも、
「OK, いい質問だ。それはシェルが終了時にSIGHUPをだね・・・」
のように理路整然とした華麗な語り口で受け答えるべきだろうか?
「お茶を濁せればそれでよい」と答えた方、あるいは「よし、じゃぁ一緒にBashのソースコードやプロセス終了時のカーネルのソースを追ってみようか。おっとその前にSUSv3をベースに端末制御とSIGHUPについて復習だ・・・」と颯爽とリードできる人はこの先読む必要は無い。
ここから先はそれなりに長丁場になる。しかし、舞台裏を解き明かすことで「nohupのNGパターン」や「nohupを使わなくてもOKなパターン」のWHYについて説明できるようになるだろう。・・・なってくれると、いいなぁ。いや、なってほしいです。・・・なれなかったとしたら、説明が下手だったということでmsakamoot-sfに責任転嫁してください。
内容的に分量や密度は高めとなる。NetBSD1.6やCentOS5.xでの検証でサンプルコードや動作結果などの分量が多いので、そこは読み飛ばしていただいてもかまわない。
では、しばしお付き合い願います。
改めてタイトルを確認すると、
なぜnohupをバックグランドジョブとして起動するのが定番なのか?
というWHY?になっている。バックグラウンドではなくフォアグラウンドジョブとして起動し、
$ nohup long_run_batch.sh (フォアグラウンド)
この状態でputtyなどの端末エミュレータを終了させてしまってもバッチジョブは残り、実行を続ける。
なのになぜ、わざわざnohupをバックグランドジョブとして起動するのが定番になっているのか?あるいはそうした方がよい理由はあるのか?
まずはnohupの中身を確認すべきだが、それについては既に以下の記事で確認している。
実は今回のWHY?の発端は、nohupが直接の原因ではない。上の記事でnohup単体の謎解きは終えている。
その後、「念のためセッションやプロセスグループ、擬似端末についてAPUEを確認しておこう」と思いAPUEをぱらぱらめくっていくうちに、「あれ?バックグラウンドジョブってSIGHUPを受信することは有り得ないんじゃない?」という疑念が立ち上がり、そこで初めて「じゃぁなんでnohupをバックグランドジョブとして起動するのが定番なんだ?」という疑問が表れた。それが今回のWHY?の発端である。
詳細は後ほど解説するとして、まずは冒頭で述べた
この2パターンを紹介する。本記事を最後まで読んでいただければ、この2パターンの挙動を完璧に説明できるように・・・なってくれると嬉しいです。
実験用のサンプル, hello.sh:
#!/bin/sh i=1 while : do echo "Hello : $i" sleep 1 i=`expr $i + 1` done
何の変哲も無いシェルスクリプトだが、これだけでもnohup無しで、バックグランドでジョブを続けられる。
手順:
$ ./hello.sh > out.txt & [1] 219 $ jobs [1]+ Running ./hello.sh >out.txt & $ exit →端末終了
別の端末エミュレータを立ち上げてpsコマンドを実行すれば、"./hello.sh"とそのwhileループで起動されたsleepプロセスが残っている事を確認できる。また out.txt を見てみると処理は続行し更新が続けられていることも確認できる。
プラットフォームや端末エミュレータの組み合わせによっては、"exit"後、端末エミュレータ側の画面が真っ白のまま終了しない場合がある。その場合は、端末エミュレータ側を強制終了させる。Linux(CentOS5.x)とputtyの組み合わせでこの現象に遭遇した。NetBSD1.6の場合はputty側も"exit"後にすぐ終了した。
「うっかり"&"をつけずにフォアグラウンドで起動してしまったジョブを、"^Z"でバックグラウンドにしてからログアウトする」手順を踏むと、たとえnohupを付けていても終了してしまう。うっかりやってしまい、「アレ?」となった人もいるだろう。自分もしょっちゅうやってました。
$ nohup ./hello.sh sending output to nohup.out ^Z [1]+ Stopped nohup ./hello.sh $ jobs [1]+ Stopped nohup ./hello.sh $ exit
別の端末エミュレータを立ち上げてpsコマンドを実行すれば、"./hello.sh"やsleepプロセスともに終了したためプロセス一覧に表示されない。また out.txt の更新も止まっている。
上記2パターンのように、バックグランドジョブの継続にはnohupが必須というわけではなく、nohupを使えば全く問題が無いわけでもない。したがってこれらの挙動を正確に把握するにはnohup単体の仕組みだけではなく、端末制御やセッション、プロセスグループ、SIGHUPなど周辺要素について調査する必要がある。
nohupはSIGHUPのシグナルハンドラをSIG_IGNに設定、つまり「受信しても無視」する設定にして、コマンドラインで指定されたジョブを実行する。逆に言えばnohupはSIGTERM, SIGQUITなど他のシグナルハンドラをデフォルト設定のままにしている。たとえnohupで起動していたとしても、それらデフォルトでプロセス終了となるシグナルを受信すれば当然、プロセスは終了する。
つまりnohupでも終了してしまうパターンでは、それらSIGHUP以外のプロセス終了シグナルを受信したために終了した可能性が考えられる。ではそのシグナルは何なのか?誰が、いつ送信したのか?SIGHUPとそれ以外のシグナル、どちらを送信するのか判断する基準は?
本記事では、それらの疑問についても回答する予定である。
と問われれば、
「ログインシェルがSIGHUP送ってるんじゃないの?」
と思われる方もいるだろう。実は自分も、この記事を書くまではそう思ってました。
半分正解で、半分不正解。
ログインシェルがSIGHUPを送る場合・送らない場合の両方が有り、さらに端末のデバイスドライバやカーネルが送る場合もある。
それらの詳細については以降の記事で解説するが、実際、ややこしい。実験・検証しているときも、しょっちゅう間違えて時間をとられてしまった。
「誰がSIGHUPを送ったの?」
記事のネタとしてはそれなりだが、実際のお仕事や運用中に一々考えるのは面倒である。
身も蓋も無い言い方だが、使う側が何も知らず、考えなくてもバックグラウンドジョブを継続できるnohupは、やはりスゴイのだ。
本格的な調査に進む前に、前提知識、実験・検証環境について説明する。
最低限、以下の前提知識や技能があることを前提とする。
サンプルコードで主に使うのは次のシステムコールとCライブラリ関数になる。必要に応じてmanpageを参照のこと。
open(), read(), write(), close() signal(), sigaction() fork(), exec()シリーズ perror() printf(), fprintf(), fflush(),
セッション・プロセスグループ・端末制御系はサンプルコードではほとんど使わない。解説はするので、必要であればmanpageを参照のこと。
サンプルコードは掲載しているが、そのコンパイル方法についてはSUIDが必要な場合を除き、省略している。基本的にはmakeコマンドのデフォルトルールに基づき
$ make (ソースファイル名から".c"を除去した実行ファイル名)
または
$ cc -o 実行ファイル名 ソースファイル名 $ gcc -o 実行ファイル名 ソースファイル名
でコンパイルしている。
NetBSD 1.6 および CentOS 5.x を使っている。古いNetBSD 1.6 を使っている理由は、 C言語系/「デーモン君のソース探検」読書メモ で使った環境を引き続き使用しているためである。いずれもVMware仮想マシンとして実行している。
VMwareホスト:
CPU : Intel CORE i3 (2 core) RAM : 4GB OS : Windows7 (32bit), Japanese VMware : 7.1.2 build-301548
今回使用したクライアント側の端末エミュレータ:「PuTTY 0.60 ごった煮版 2007年8月6日版」
NetBSD 1.6:
NetBSD 1.6 i386 GENERIC kernel (32bit) $ bash --version GNU bash, version 2.05.0(1)-release (i386--netbsdelf) Copyright 2000 Free Software Foundation, Inc. $ cc --version 2.95.3
CentOS 5.x:
Linux kernel : 2.6.18-92.1.22.el5 (SMP, i686) (32bit) $ bash --version GNU bash, version 3.2.25(1)-release (i686-redhat-linux-gnu) Copyright (C) 2005 Free Software Foundation, Inc. $ gcc --version gcc (GCC) 4.1.2 20080704 (Red Hat 4.1.2-48) Copyright (C) 2006 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
参考資料の詳細については記事の最後に一覧を載せている。
セッションやプロセスグループ、擬似端末についてAPUEを確認したことが今回のWHY?の発端である。
前提知識の再確認も兼ねて、本記事に特に関連するトピックに絞って、ごく簡単に要点をまとめていく。
単調で退屈な文章が続くが、暫くご辛抱願いたい。
参考:
セッションは制御端末の有無で二種類に分けることができる。
参考:
セッションの新規作成でポイントとなる箇所を示す。
fork()した子プロセス側でsetsid()を呼ぶ。これにより新しいセッションが開始される。
参考:
プロセスグループの新規作成でポイントとなる箇所を示す。
子プロセスと親プロセスの両方でsetpgid()を呼ぶ。なぜ両方で呼ぶ必要があるのかは、APUE参照。
これにより、プロセスグループが(まだ存在していなければ)新規作成され、子プロセスがそのプロセスグループの最初のプロセスであれば子プロセスはグループリーダーになる。
もしパイプなどで複数のプロセスがひとつのプロセスグループとして起動した場合は、シェルの実装にも依存するが、新しいプロセスグループを作成した後、そのプロセスグループに所属するようにfork()を繰り返していく。以下の図ではシェルからfork()したプロセスがさらにfork()していく例を示している。シェル側でfork()とsetpgid()を繰り返す場合もあるだろう。
参考:
昔は"ダム端末"(dumb-terminal)と呼ばれるディスプレイ・キーボードのセットがあり、複数のダム端末が1つのマシンに接続される事で複数人による並行作業が行われていたらしい。
時代が少し進むと、ダイアルアップ回線で通信するモデム経由でログインできるようになったらしい。
2010年現在でも1つのマシンに0または複数のディスプレイ・キーボードが接続される場合があるが、複数人による同時ログインを提供するためのものではなく、マルチディスプレイ環境や、キーボード・モニタ切り替え器を間に挟んで複数マシンで共有する目的が殆どである。リモート接続する場合はtelnetやSSHなどを使う。
マシンに接続されたディスプレイ・キーボードのセットを「コンソール」と呼んでみる。
コンソールからのログインの舞台裏をまとめる。
プラットフォームにより多少の差異はあるかもしれないが、大筋としては上記の流れになる。
参考:
"login"プロンプト表示時点:
$ ps ax -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND 0 0 0 67ae00 30001 ?? DKs [swapper] 1 0 1 bf9880 30001 ?? Is init ... 190 1 190 c8cd40 190 ttyE0 Is+ /usr/libexec/getty Pc console 191 1 191 c8cc80 191 ttyE1 Is+ /usr/libexec/getty Pc ttyE1 192 1 192 c8cd00 192 ttyE2 Is+ /usr/libexec/getty Pc ttyE2 193 1 193 c8ccc0 193 ttyE3 Is+ /usr/libexec/getty Pc ttyE3
四つのgettyが起動している。
ユーザー名入力→"Password:" プロンプト表示の時点:
$ ps ax -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND 0 0 0 67ae00 30001 ?? DKs [swapper] 1 0 1 bf9880 30001 ?? Is init ... 190 1 190 c8cd40 190 ttyE0 S<s+ login -p -- msakamoto 191 1 191 c8cc80 191 ttyE1 Is+ /usr/libexec/getty Pc ttyE1 192 1 192 c8cd00 192 ttyE2 Is+ /usr/libexec/getty Pc ttyE2 193 1 193 c8ccc0 193 ttyE3 Is+ /usr/libexec/getty Pc ttyE3
PID:190が "getty" から "login" に変化している。exec()されたことが確認できる。
パスワード入力→ログインシェル起動後:
$ ps ax -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND 0 0 0 67ae00 30001 ?? DKs [swapper] 1 0 1 bf9880 30001 ?? Ss init ... 365 1 365 c6a740 365 ttyE0 Ss+ -bash 191 1 191 c8cc80 191 ttyE1 Is+ /usr/libexec/getty Pc ttyE1 192 1 192 c8cd00 192 ttyE2 Is+ /usr/libexec/getty Pc ttyE2 193 1 193 c8ccc0 193 ttyE3 Is+ /usr/libexec/getty Pc ttyE3
"ttyE0"で"bash"が起動されたことが確認できる。ただしPIDが変化しているため、fork()後にexec()された可能性がある。またPID:190が消えてしまっており、fork()後の親プロセスは終了している可能性が高い。
ログアウト後:
$ ps ax -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND 0 0 0 67ae00 30001 ?? DKs [swapper] 1 0 1 bf9880 30001 ?? Ss init ... 491 1 491 c97240 491 ttyE0 Ss+ /usr/libexec/getty Pc console 191 1 191 c8cc80 191 ttyE1 Is+ /usr/libexec/getty Pc ttyE1 192 1 192 c8cd00 192 ttyE2 Is+ /usr/libexec/getty Pc ttyE2 193 1 193 c8ccc0 193 ttyE3 Is+ /usr/libexec/getty Pc ttyE3
新しいPIDでgettyが起動されている。おそらくinit側でログインシェルの終了を検出し、自動的にgettyを再起動しているのだろう。
"login"プロンプト表示時点:
$ ps ax -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TPGID TT STAT COMMAND 1 0 1 1 -1 ? Ss init [3] ... 2612 1 2612 2612 2612 tty1 Ss+ /sbin/mingetty tty1 2613 1 2613 2613 2613 tty2 Ss+ /sbin/mingetty tty2 2616 1 2616 2616 2616 tty3 Ss+ /sbin/mingetty tty3 2625 1 2625 2625 2625 tty4 Ss+ /sbin/mingetty tty4 2626 1 2626 2626 2626 tty5 Ss+ /sbin/mingetty tty5 2627 1 2627 2627 2627 tty6 Ss+ /sbin/mingetty tty6
mingettyが6つ起動している。
ユーザー名入力→"Password:" プロンプト表示の時点:
$ ps ax -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TPGID TT STAT COMMAND 1 0 1 1 -1 ? Ss init [3] ... 2612 1 2612 2612 2612 tty1 Ss+ /bin/login -- 2613 1 2613 2613 2613 tty2 Ss+ /sbin/mingetty tty2 2616 1 2616 2616 2616 tty3 Ss+ /sbin/mingetty tty3 2625 1 2625 2625 2625 tty4 Ss+ /sbin/mingetty tty4 2626 1 2626 2626 2626 tty5 Ss+ /sbin/mingetty tty5 2627 1 2627 2627 2627 tty6 Ss+ /sbin/mingetty tty6
PID:2612が "mingetty" から "login" に変化している。exec()されたことが確認できる。
パスワード入力→ログインシェル起動後:
$ ps ax -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TPGID TT STAT COMMAND 1 0 1 1 -1 ? Ss init [3] ... 2612 1 2612 2612 -1 ? Ss login -- msakamoto 15179 2612 15179 15179 15179 tty1 Ss+ -bash 2613 1 2613 2613 2613 tty2 Ss+ /sbin/mingetty tty2 2616 1 2616 2616 2616 tty3 Ss+ /sbin/mingetty tty3 2625 1 2625 2625 2625 tty4 Ss+ /sbin/mingetty tty4 2626 1 2626 2626 2626 tty5 Ss+ /sbin/mingetty tty5 2627 1 2627 2627 2627 tty6 Ss+ /sbin/mingetty tty6
ログインシェルが別プロセスとして起動されている。起動した"login"プロセス自体は残っている。
ログアウト後:
$ ps ax -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TPGID TT STAT COMMAND 1 0 1 1 -1 ? Ss init [3] ... 15332 1 15332 15332 15332 tty1 Ss+ /sbin/mingetty tty1 2613 1 2613 2613 2613 tty2 Ss+ /sbin/mingetty tty2 2616 1 2616 2616 2616 tty3 Ss+ /sbin/mingetty tty3 2625 1 2625 2625 2625 tty4 Ss+ /sbin/mingetty tty4 2626 1 2626 2626 2626 tty5 Ss+ /sbin/mingetty tty5 2627 1 2627 2627 2627 tty6 Ss+ /sbin/mingetty tty6
ログインシェル・"login"両方とも終了し、"mingetty"が新しいプロセスで起動された。
telnetやSSHなどのリモートログインでは、最終的に起動されるログインシェルに対して仮想的な端末を提供するため、「擬似端末」(Pseudo Terminal)という機能を使用している。リモートログイン以外でも、scriptコマンドで擬似端末が活用されている。
擬似端末は"master"側と"slave"側の二つのデバイスを提供する。最初は"slave"側のデバイスは利用できない。"master"側のデバイスをopen()すると、対応する"slave"側のデバイスが利用可能となる。"master"側と"slave"側は双方向パイプのようにread/writeを中継する。双方向パイプと異なるのは、端末ならではの機能を利用できる点にある。これにより、単なるread/writeだけでなく、CANONICALモードやECHOモードを初めとする各種端末設定、バッファリングが可能となり、"slave"側を使うプロセスはあたかも実際の端末が接続されたかのように利用することができる。
リモート端末からの入出力がsocket/pipe/fileなどのファイル記述子として利用できる段階をスタート地点として、擬似端末を使ってログインシェルに対して仮想的な端末を提供し、リモート端末からの入出力を接続する流れを紹介する。
これらの処理はデバイスファイルの操作を含むため、実効(Effective)ユーザーIDがrootになっている必要がある。
次は子プロセス側の処理が中心となる。
この段階で、擬似端末を使った入出力の基本が整った。リモートログインの場合なら、続けて子プロセス側のユーザーIDやグループID、環境変数やカレントディレクトリを調整後、ログインシェルをexec()する。
参考:
SysVにおけるDaemon作成時の制御端末の注意点:
端末の入出力は、その端末を制御端末とするセッションのフォアグラウンドプロセスグループに接続される。
端末からSIGTSTP(^Z)が送信された場合、シェルのジョブコントロール機能によりフォアグラウンド・バックグラウンドが切り替わる。
ログインシェルがバックグラウンドで、他にバックグラウンドのプロセスグループが存在しない場合:
シェルがtcsetpgrp()を呼び、端末に新しいフォアグラウンドプロセスグループを通知する。
以降はシェルがフォアグラウンドプロセスグループに切り替わり、端末の入出力を引き受ける:
一旦シェルがフォアグラウンドに切り替わった後、別のバックグラウンドジョブをフォアグラウンドジョブに切り替える場合:
対象のバックグラウンドジョブがSTOP状態になっていれば、SIGCONTを送信し再開させる。また、tcsetpgrp()を呼び出し新しいフォアグラウンドプロセスを通知する。
以降は切り替わったフォアグラウンドジョブが端末の入出力を引き受ける:
参考:
ダム端末やモデム経由でのログインでは、モデムの回線またはダム端末の物理的な切断が"hangup"の合図だったらしい。
擬似端末の場合、"master"側デバイスのファイル記述子が全てclose()されたのを"hangup"の合図として、端末のデバイスドライバがセッションリーダーのプロセスにSIGHUPを送信する。
フォアグラウンド・バックグラウンドの区別ではなく、「セッションリーダー」に対して送信される点に注意する。
したがって、ログインシェルから起動されたフォアグラウンドプロセスが実行中であったとしても、バックグラウンドに切り替わっているログインシェルのプロセスにSIGHUPが送信される。
ログインシェルに限らず、対話的に動作するプログラムのほとんどはSIGHUPを受信したら終了処理を開始する。またSIGHUPのデフォルト処理はプロセス終了となっている。デーモンプロセスの場合は終了処理ではなく、設定ファイルやログファイルの再読み込みといった処理が慣例となっている。
SIGHUPの受信にかかわらず、プロセスが終了するとき、そのプロセスがセッションリーダーだった場合、カーネルからそのセッションのフォアグラウンドプロセスグループに対してSIGHUPが送信される。
セッションリーダ終了時のSIGHUPについて、NetBSD1.6のカーネルを追ったときのメモがあるので、参考にどうぞ:
参考:
単調で退屈な復習だったが、ここまでまとめなおした段階で、自分は以下の疑問を抱いた。
セッションリーダーであるプロセスが終了するとき、カーネルがSIGHUPを送信する対象はそのセッションのフォアグラウンドジョブであり、バックグラウンドジョブは放置されている。つまりバックグラウンドジョブは基本的にはSIGHUPを受信することは無いのではないか?
さらに、そもそもSIGHUPを受信しないのであればnohupをわざわざバックグラウンドで起動する意味は無い。
ここでようやく、タイトルにも掲げた「なぜnohupをバックグランドジョブとして起動するのが定番なのか?」という疑問が生じる。
端末デバイスやカーネルは、おそらくAPUEの通りに動いているととりあえず信用してみる。となると、残る構成要素であるログインシェルが怪しい。
・・・と、記事を書いている今だからすんなりと目星をつけてますが、調査の段階ではそこまで分析できていない状態でいろいろサンプルコードを作ってはputtyを強制終了したり、ログアウトしたりと組み合わせを弄ってました。それでもなかなかシグナルの発生パターンの切り分けができず、そこでようやく「あ、もしかしてログインシェルが何かしてる?」と気づいた次第。
最初はCentOS5.x上で弄ってたのですが、これに気づいたあとはカーネルを含めてソースを読める環境を整えていたNetBSD1.6に移り、次の順序で徐々に標的を追い詰めていきました。
これらの確認作業の詳細や、それに使用したサンプルコードの解説は長くなるので後回しにし、"WHY?"に対する"BECAUSE"を先にまとめる。
なお、以降の記事ではログインシェル = Bashとして話を進めていく。
他のシェルでも同じ動作が成立するかは確認していない。使用するシェルのmanページを調べるか、"nohup シェル名"でWeb検索してみてほしい。
まずセッションリーダであるbash終了時、カーネルが放置するバックグランドジョブはbash側でSIGHUP/SIGTERM/SIGCONTを適宜組み合わせて送信し、終了させている。もちろん後述するようにシグナルを送信せず放置し、結果としてジョブを継続させる場合もある。
bashが終了する流れは、大きく次の二通りがある。
また"exit"や"logout"による終了時にSIGHUPの送信有無を設定することもできる。huponexitオプションがbashでは提供されている。
# 現在設定の確認 $ shopt huponexit huponexit off # ONに設定 $ shopt -s huponexit # OFFに設定 $ shopt -u huponexit
さらに"disown"シェルコマンドを使うことで、SIGHUPの送信対象から除外するための内部的な印を、ジョブに対して設定することもできる。以降、この印の有無を実際のbashソースコード上でのフラグ名である「"J_NOHUP"の有無」として表記する。
実際の調査ではbashのソースコードをgrepして流れを掴んでいった。ポイントとなるソースコードは後ほど紹介する。
ここでは、調査結果をまとめたシグナル送信のパターン表を先に解説する。
"huponexit" ON:
SIGHUP | SIGCONT | SIGTERM | |
RUNNING状態 + J_NOHUP有 | - | - | - |
RUNNING状態 + J_NOHUP無 | o | - | - |
STOP状態 + J_NOHUP有 | - | o | o |
STOP状態 + J_NOHUP無 | o | o | o |
"huponexit" OFF:
SIGHUP | SIGCONT | SIGTERM | |
RUNNING状態 + J_NOHUP有 | - | - | - |
RUNNING状態 + J_NOHUP無 | - | - | - |
STOP状態 + J_NOHUP有 | - | o | o |
STOP状態 + J_NOHUP無 | - | o | o |
SIGHUP | SIGCONT | SIGTERM | |
RUNNING状態 + J_NOHUP有 | - | - | - |
RUNNING状態 + J_NOHUP無 | o | - | - |
STOP状態 + J_NOHUP有 | - | o | o |
STOP状態 + J_NOHUP無 | o | o | o |
複数回送信される可能性があっても一つの"o"にまとめている。
よく見ると、シグナル送信パターン1の"huponexit" ONのケースはシグナル送信パターン2と同じである。
この表を使うことで、本記事の冒頭で紹介した2つのパターンを説明できるようになる。
このパターンはnohup無しでバックグランド実行し、"exit"でbashを終了している。また"huponexit"はデフォルト=OFFの状態となっている。jobsコマンドの結果ではバックグランドジョブは"Running"となっている。disownは呼んでいないのでJ_NOHUPは無し。
以上より、「シグナル送信パターン1」の"huponexit" OFF, 「RUNNING状態 + J_NOHUP無」の行を見ればよい:
SIGHUP | SIGCONT | SIGTERM | |
RUNNING状態 + J_NOHUP無 | - | - | - |
このように、SIGHUP/SIGCONT/SIGTERMのいずれも送信されない。またバックグラウンドジョブなので、カーネルからSIGHUPが送信されることも無い。これがnohupを使わなくても実行を続けられる理由である。
このパターンでは、"exit"によるbash終了時、nohupで起動したジョブはバックグラウンドで"Stopped"になっている。その他の条件は上記と同じなので、「シグナル送信パターン1」の"huponexit" OFF, 「STOP状態 + J_NOHUP無」の行を見ればよい:
SIGHUP | SIGCONT | SIGTERM | |
STOP状態 + J_NOHUP無 | - | o | o |
SIGCONTとSIGTERMが送信される。前述の通りnohupではSIGHUP以外のシグナルハンドラはデフォルトのままジョブを起動するため、SIGTERMによるデフォルト動作、すなわちプロセス終了となる。これがnohupを使ったとしても、ログアウト時にジョブも一緒に終了してしまった理由である。
前半のまとめとして、nohupをバックグランドジョブとして起動する必要があるパターン = SIGHUPが送信されるパターンを解説する。
"huponexit"がデフォルトのOFFのままだとすれば、このパターンは二つに絞られる。「シグナル送信パターン2」、つまりモデムhangupや端末エミュレータの終了などによりセッションリーダであるbashがSIGHUPを受信したときの、J_NOHUP無の行である:
SIGHUP | SIGCONT | SIGTERM | |
RUNNING状態 + J_NOHUP無 | o | - | - |
STOP状態 + J_NOHUP無 | o | o | o |
nohupではSIGTERMを防げないので "STOP状態" を除外すれば、残るは「RUNNING状態 + J_NOHUP無」の行となる。
hello.shを使って確認してみる。まずnohupを使わない場合:
$ ./hello.sh > out.txt & [1] 465 $ jobs [1]+ Running ./hello.sh >out.txt & $
ここで端末エミュレータ側を終了させてみる。puttyウインドウの「x」ボタンをクリックして終了させてみた。
別端末で確認してみると、終了すると同時にout.txtの更新も止まり、psコマンドからもhello.shやsleepプロセスが消えたことを確認できた。SIGHUP受信により終了したものと思われる。
次にnohupを使う場合、こちらはジョブが残り、実行継続されると予想される:
$ nohup ./hello.sh & [1] 512 $ sending output to nohup.out # nohupからの出力 $ jobs [1]+ Running nohup ./hello.sh & $
ここで上と同様、端末エミュレータ側を終了させる。別端末で確認してみると、hello.shの実行は継続しておりnohup.outの出力も更新されている。SIGHUPを受信しても、nohupにより無視され、ジョブの実行が継続されることを確認できた。
以上で本記事の前半が終わる。後半はNetBSD1.6およびCentOS5.x上で、サンプルコードを使った擬似端末のSIGHUP確認やBashのソースコードの確認、nohupの動作パターンの追加確認などを詳しく紹介していく。
最後に「いともたやすく行われるえげつないnohup」と題しnohupを使うときの注意点をまとめ、そして参考資料の一覧を載せて本記事は終わる。
NetBSD1.6とCentOS5.xの二種類のプラットフォーム上で、それぞれで同じようなサンプルコードと動作確認を行う過程を紹介していくため、また長丁場となる。時間が惜しい方やサンプルコードと実験結果などの詳細までは興味が無い方などは、「いともたやすく行われるえげつないnohup」まで読み飛ばしてもらっても構わない。
それでは小休憩の後、サンプルコードと実験・動作確認の詳細を解説する。
NetBSD1.6上で以下の実験をしていく。
擬似端末を使ってみる。特に、擬似端末のmaster側のファイル記述子を全てcloseすると、slave側擬似端末の制御プロセスにSIGHUPが送信される動作に注目する。NetBSD側の close(2) manpageには記載されていないが、SUSv3のclose(2)の解説ではこの挙動が載っている。
mypty.c:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <fcntl.h> #include <signal.h> #include <termios.h> #include <sys/ioctl.h> /* for CentOS5.x */ /* #include <pty.h> */ /* for NetBSD1.6 */ #include <util.h> pid_t pty_fork(int *ptrfdm, char *slave_name, struct termios *slave_termios, struct winsize *slave_winsize) { int fdm, fds; pid_t pid; char pts_name[20]; if (-1 == openpty(&fdm, &fds, slave_name, slave_termios, slave_winsize)) { perror("openpty()"); exit(1); } if (-1 == (pid = fork())) { perror("fork()"); exit(1); } else if (0 == pid) { /* child */ /* create new session, child becomes session leader, * new process group leader, has no control terminal yet. */ if (-1 == setsid()) { perror("setsid()"); exit(1); } /* open pty slave device 1st, then child becomes * control process. */ if (-1 == open(slave_name, O_RDWR)) { perror("open(slave_name)"); exit(1); } /* close unused pty master device */ close(fdm); /* set control terminal */ if (-1 == ioctl(fds, TIOCSCTTY, (char*)0)) { perror("ioctl(fds, TIOCSCTTY)"); exit(1); } if (NULL != slave_termios) { if (-1 == tcsetattr(fds, TCSANOW, slave_termios)) { perror("tcsetattr(slave_termios)"); exit(1); } } if (NULL != slave_winsize) { if (-1 == ioctl(fds, TIOCSWINSZ, slave_winsize)) { perror("ioctl(TIOCSWINSZ)"); exit(1); } } /* stdin/out/err fileno duplication */ if (STDIN_FILENO != dup2(fds, STDIN_FILENO)) { perror("dup2(STDIN_FILENO)"); exit(1); } if (STDOUT_FILENO != dup2(fds, STDOUT_FILENO)) { perror("dup2(STDOUT_FILENO)"); exit(1); } if (STDERR_FILENO != dup2(fds, STDERR_FILENO)) { perror("dup2(STDERR_FILENO)"); exit(1); } close(fds); return 0; } else { /* parent */ *ptrfdm = fdm; return pid; } } #define BUFSIZE 512 static volatile sig_atomic_t sigcaught; static void sig_term(int signo) { sigcaught = 1; } void loop(int ptym) { pid_t child; int nread; char buf[BUFSIZE]; struct sigaction sa; sa.sa_handler = sig_term; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; #ifdef SA_INTERRUPT sa.sa_flags |= SA_INTERRUPT; #endif if (-1 == (child = fork())) { perror("fork() for loop"); exit(1); } else if (0 == child) { /* child copies stdin to ptym */ for (;;) { if (-1 == (nread = read(STDIN_FILENO, buf, sizeof(buf)))) { perror("read(stdin)"); exit(1); } else if (0 == nread) { fprintf(stderr, "child(read stdin, write ptym) detect EOF from stdin.\n"); break; /* EOF */ } if (nread != write(ptym, buf, nread)) { perror("write(ptym)"); exit(1); } } fprintf(stderr, "send SIGTERM to parent...\n"); kill(getppid(), SIGTERM); close(ptym); fprintf(stderr, "PID[%d] closed ptym, sleeping(10)...\n", getpid()); sleep(10); fprintf(stderr, "PID[%d] awaken, terminates.\n", getpid()); exit(0); } if (-1 == sigaction(SIGTERM, &sa, NULL)) { perror("sigaction(SIGTERM)"); exit(1); } for (;;) { errno = 0; if (0 >= (nread = read(ptym, buf, sizeof(buf)))) { /* error or signal or EOF */ break; } if (nread != write(STDOUT_FILENO, buf, nread)) { perror("write(stdout)"); exit(1); } } if (0 == nread) { fprintf(stderr, "parent(read ptym, write stdin) detect EOF from ptym.\n"); } if (errno) { perror("read(ptym)"); } if (0 == sigcaught) { fprintf(stderr, "send SIGTERM to child(%d)...\n", child); /* error or EOF, tell child termination */ kill(child, SIGTERM); } else { fprintf(stderr, "child sent SIGTERM, maybe terminated.\n"); } close(ptym); fprintf(stderr, "PID[%d] closed ptym, sleeping(10)...\n", getpid()); sleep(10); fprintf(stderr, "PID[%d] awaken, terminates.\n", getpid()); } int main(int argc, char *argv[]) { int fdm; pid_t pid; char slave_name[20]; struct termios orig_termios; struct winsize size; if (2 > argc) { fprintf(stderr, "usage: %s commands...\n", argv[0]); return 1; } if (-1 == tcgetattr(STDIN_FILENO, &orig_termios)) { perror("tcgetattr()"); exit(1); } if (-1 == ioctl(STDIN_FILENO, TIOCGWINSZ, &size)) { perror("ioctl(TIOCGWINSZ)"); exit(1); } pid = pty_fork(&fdm, slave_name, &orig_termios, &size); if (0 == pid) { if (-1 == execvp(argv[1], &argv[1])) { perror("execvp()"); exit(1); } } fprintf(stderr, "slave name = %s\n", slave_name); loop(fdm); fprintf(stderr, "done\n"); return 0; }
コンパイル : openpty()を使うので所有者をrootにしてset-user-idをセット + "-lutil"をコンパイルオプションに追加
$ su # cc -o mypty mypty.c -lutil && chmod +s ./mypty
mycat_detectHUP.c:
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/stat.h> #include <unistd.h> #include <signal.h> #include <errno.h> void sig_hup(int signo) { fprintf(stderr, "detect SIGHUP(%d)\n", signo); fflush(stderr); } int main(int argc, char *argv[]) { char buf[200]; struct sigaction sa; int fderr; /* mypty.cから直接起動され、slave側の擬似端末の制御プロセスとなることを想定している。 * よって、標準エラー出力のシェルによるリダイレクトが使えない。 * このため、いったんSTDERR_FILENOをclose()し、自分で標準エラー出力用のファイルをopen()する。 */ close(STDERR_FILENO); if (-1 == (fderr = open("/tmp/mycat_detectHUP", O_CREAT | O_TRUNC | O_WRONLY, S_IRWXU))) { perror("open(log)"); exit(1); } sa.sa_handler = sig_hup; sigemptyset(&sa.sa_mask); /* fgets()がSIGHUP受信で割り込みされるように、 * sa_flags=0(もし定義されていればSA_INTERRUPTを設定) * でシグナルハンドラをインストールする。 */ sa.sa_flags = 0; #ifdef SA_INTERRUPT sa.sa_flags |= SA_INTERRUPT; #endif sigaction(SIGHUP, &sa, NULL); while (NULL != fgets(buf, sizeof(buf), stdin)) { fprintf(stdout, "PID[%d] : %s\n", getpid(), buf); fflush(stdout); } if (feof(stdin)) { fprintf(stderr, "stdin detect EOF\n"); } else { perror("fgets(stdin)"); } return 0; }
$ ./mypty ./mycat_detectHUP slave name = /dev/ttyp4 abc # <- input from keyboard + RETURN abc # echo back in parent terminal PID[1215] : abc # output from slave def # <- input from keyboard + RETURN def # echo back in parent terminal PID[1215] : def # output from slave
psコマンドによる現在状況の確認:
$ ps ax -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND 347 346 347 c8c000 1214 ttyp1 Ss -bash 1214 347 1214 c8c000 1214 ttyp1 S+ ./mypty ./mycat_detectHUP 1216 1214 1214 c8c000 1214 ttyp1 S+ ./mypty ./mycat_detectHUP 1215 1214 1215 cd2d00 1215 ttyp4 Ss+ ./mycat_detectHUP
全体図:
[putty on Windows] + | (TCP/IP) + [sshd] + | + [/dev/ptyp1] : master [/dev/ttyp1] : slave + | + [-bash] -> fork(),exec() | v fork() [mypty(1)] ----->[mypty(2)] + + | | +----------------+ | + [/dev/ptyp4] : master [/dev/ttyp4] : slave + | + [mycat_detectHUP]
中心部分:
write(STDOUT_FILENO) read(ptym) +<--- [mypty(1)] <---+ | | [/dev/ttyp1]---+ +---[/dev/ptyp4]=[/dev/ttyp4]<--+ | | | +---> [mypty(2)] --->+ [mycat_detectHUP]<--+ read(STDIN_FILENO) write(ptym)
このようにmaster/slave間のread/writeが確認できたら、EOFを入力し、master側のファイル記述子をclose()させる。
(input ^D , no echo back) child(read stdin, write ptym) detect EOF from stdin. send SIGTERM to parent... read(ptym): Interrupted system call child sent SIGTERM, maybe terminated. PID[1214] closed ptym, sleeping(10)... PID[1216] closed ptym, sleeping(10)...
この時点でのps:
$ ps ax -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND 347 346 347 c8c000 1214 ttyp1 Ss -bash 1214 347 1214 c8c000 1214 ttyp1 S+ ./mypty ./mycat_detectHUP 1216 1214 1214 c8c000 1214 ttyp1 S+ ./mypty ./mycat_detectHUP 1215 1214 1215 cd2d00 30001 ttyp4 ZW (mycat_detectHUP)
mycat_detectHUPがゾンビ状態になっている。
ファイルに保存された標準エラー出力を確認してみる:
$ cat /tmp/mycat_detectHUP detect SIGHUP(1) fgets(stdin): Interrupted system call $
SIGHUPの受信を確認できた。
10秒後、myptyがsleep()から復帰し、終了する。
PID[1214] awaken, terminates. PID[1216] awaken, terminates. done
Bashバージョン(NetBSD 1.6 インストールCD付属のpkgより)
$ bash --version GNU bash, version 2.05.0(1)-release (i386--netbsdelf) Copyright 2000 Free Software Foundation, Inc.
Bashの二種類の終了方法:
exit_shell() (shell.c) -> hangup_all_jobs() (jobs.c) : 対話シェルかつログインシェルかつshopt huponexitがセットの場合 -> killpg(<PGID>, SIGHUP) : 対象:シェルの内部フラグでJ_NOHUPが未設定のジョブ -> killpg(<PGID>, SIGCONT) : 対象:STOP状態のジョブ -> end_job_control() (jobs.c) : 非サブシェル -> terminate_stopped_jobs() (jobs.c) : 対話シェルの場合 -> killpg(<PGID>, SIGTERM) : 対象:STOP状態のジョブ -> killpg(<PGID>, SIGCONT) : 対象:同上
"huponexit"有 ("shopt -s huponexit"):
SIGHUP | SIGCONT | SIGTERM | 実験による確認 | |
RUNNING状態 + J_NOHUP有 | - | - | - | |
RUNNING状態 + J_NOHUP無 | o | - | - | |
STOP状態 + J_NOHUP有 | - | o | o | |
STOP状態 + J_NOHUP無 | o | o | o |
"huponexit"無 ("shopt -u huponexit"):
SIGHUP | SIGCONT | SIGTERM | 実験による確認 | |
RUNNING状態 + J_NOHUP有 | - | - | - | |
RUNNING状態 + J_NOHUP無 | - | - | - | 実験1 |
STOP状態 + J_NOHUP有 | - | o | o | |
STOP状態 + J_NOHUP無 | - | o | o | 実験2 |
SIGHUP : termination_unwind_protect() (sig.c) -> hangup_all_jobs() (jobs.c) : 対話シェルかつSIGHUPの場合 -> killpg(<PGID>, SIGHUP) : 対象:シェルの内部フラグでJ_NOHUPが未設定のジョブ -> killpg(<PGID>, SIGCONT) : 対象:STOP状態のジョブ -> end_job_control() (jobs.c) -> terminate_stopped_jobs() (jobs.c) : 対話シェルの場合 -> killpg(<PGID>, SIGTERM) : 対象:STOP状態のジョブ -> killpg(<PGID>, SIGCONT) : 対象:同上
SIGHUP | SIGCONT | SIGTERM | 実験による確認 | |
RUNNING状態 + J_NOHUP有 | - | - | - | |
RUNNING状態 + J_NOHUP無 | o | - | - | 実験3,4 |
STOP状態 + J_NOHUP有 | - | o | o | |
STOP状態 + J_NOHUP無 | o | o | o | 実験5 |
exit_shell() (shell.c) :
/* Exit the shell with status S. */ void exit_shell (int s) { /* 省略 */ #if defined (JOB_CONTROL) /* If the user has run `shopt -s huponexit', hangup all jobs when we exit an interactive login shell. ksh does this unconditionally. */ if (interactive_shell && login_shell && hup_on_exit) hangup_all_jobs (); /* If this shell is interactive, terminate all stopped jobs and restore the original terminal process group. Don't do this if we're in a subshell and calling exit_shell after, for example, a failed word expansion. */ if (subshell_environment == 0) end_job_control (); #endif /* JOB_CONTROL */ /* 省略 */ }
end_job_control(), terminate_stopped_jobs(), hangup_all_jobs() (jobs.c) :
/* If this shell is interactive, terminate all stopped jobs and restore the original terminal process group. This is done before the `exec' builtin calls shell_execve. */ void end_job_control () { if (interactive_shell) /* XXX - should it be interactive? */ { terminate_stopped_jobs (); /* 省略 */ } /* 省略 */ } /* Cause all stopped jobs to exit. */ void terminate_stopped_jobs () { register int i; for (i = 0; i < job_slots; i++) { if (jobs[i] && STOPPED (i)) { killpg (jobs[i]->pgrp, SIGTERM); killpg (jobs[i]->pgrp, SIGCONT); } } } /* Cause all jobs, running or stopped, to receive a hangup signal. If a job is marked J_NOHUP, don't send the SIGHUP. */ void hangup_all_jobs () { register int i; for (i = 0; i < job_slots; i++) { if (jobs[i]) { if ((jobs[i]->flags & J_NOHUP) == 0) killpg (jobs[i]->pgrp, SIGHUP); if (STOPPED (i)) killpg (jobs[i]->pgrp, SIGCONT); } } }
termination_unwind_protect() (sig.c):
sighandler termination_unwind_protect (int sig) { /* (省略) */ #if defined (JOB_CONTROL) if (interactive && sig == SIGHUP) hangup_all_jobs (); end_job_control (); #endif /* JOB_CONTROL */ /* (省略) */ }
ジョブとして動かすサンプルプログラム(細かいお作法やエラー処理は無視)
detectHUPCONTTERM.c:
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <signal.h> #include <errno.h> void sig_hup(int signo) { fprintf(stderr, "detect SIGHUP(%d)\n", signo); fflush(stderr); } void sig_cont(int signo) { fprintf(stderr, "detect SIGCONT(%d)\n", signo); fflush(stderr); } void sig_term(int signo) { fprintf(stderr, "detect SIGTERM(%d)\n", signo); fflush(stderr); } int main(int argc, char *argv[]) { int i; signal(SIGHUP, sig_hup); signal(SIGCONT, sig_cont); signal(SIGTERM, sig_term); for (i=0;;i++) { fprintf(stdout, "stdout:PID[%d],PPID[%d],PGID[%d] i = %d\n", getpid(), getppid(), getpgrp(), i); fflush(stdout); fprintf(stderr, "stderr:PID[%d],PPID[%d],PGID[%d] i = %d\n", getpid(), getppid(), getpgrp(), i); fflush(stderr); sleep(1); } return 0; }
putty + SSH接続:
$ ./detectHUPCONTTERM 1>out.txt 2>err.txt & [1] 1056 $ exit
結果:シグナル未検出
$ ps x -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND 1054 1053 1054 c8c680 1054 ttyp4 Ss+ -bash 1056 1054 1056 c8c680 1054 ttyp4 S ./detectHUPCONTTERM (detectHUPCONTTER)
→
PID PPID PGID SESS TGPID TTY STAT COMMAND 1056 1 1056 c8c680 30001 ttyp4 S ./detectHUPCONTTERM (detectHUPCONTTER)
putty + SSH接続:
$ ./detectHUPCONTTERM 1>out.txt 2>err.txt ^Z [1]+ Stopped ./detectHUPCONTTERM >out.txt 2>err.txt $ exit logout There are stopped jobs. $ exit
結果:SIGCONT, SIGTERM 検出
$ ps x -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND 1096 1095 1096 cd2080 1096 ttyp4 Ss+ -bash 1104 1096 1104 cd2080 1096 ttyp4 T ./detectHUPCONTTERM (detectHUPCONTTER)
→
PID PPID PGID SESS TGPID TTY STAT COMMAND 1104 1 1104 cd2080 30001 ttyp4 S ./detectHUPCONTTERM (detectHUPCONTTER)
putty + SSH接続:
$ ./detectHUPCONTTERM 1>out.txt 2>err.txt
この後、putty側を終了。
結果:SIGHUP検出
$ ps x -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND 1009 1008 1009 cafcc0 1032 ttyp4 Ss -bash 1032 1009 1032 cafcc0 1032 ttyp4 S+ ./detectHUPCONTTERM (detectHUPCONTTER)
→
PID PPID PGID SESS TGPID TTY STAT COMMAND 1032 1 1032 cafcc0 30001 ttyp4 S ./detectHUPCONTTERM (detectHUPCONTTER)
putty + SSH接続:
$ ./detectHUPCONTTERM 1>out.txt 2>err.txt & [1] 1045 $
この後、putty側を終了。
結果:SIGHUP検出
$ ps x -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND 1042 1041 1042 cbd7c0 1042 ttyp4 Ss+ -bash 1045 1042 1045 cbd7c0 1042 ttyp4 S ./detectHUPCONTTERM (detectHUPCONTTER)
→
PID PPID PGID SESS TGPID TTY STAT COMMAND 1045 1 1045 cbd7c0 30001 ttyp4 S ./detectHUPCONTTERM (detectHUPCONTTER)
putty + SSH接続:
$ ./detectHUPCONTTERM 1>out.txt 2>err.txt ^Z [1]+ Stopped ./detectHUPCONTTERM >out.txt 2>err.txt $ jobs [1]+ Stopped ./detectHUPCONTTERM >out.txt 2>err.txt $
この後、putty側を終了。
結果:SIGHUP, SIGCONT x 2, SIGTERM検出
$ ps x -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND 1067 1066 1067 cd2e40 1067 ttyp4 Ss+ -bash 1069 1067 1069 cd2e40 1067 ttyp4 T ./detectHUPCONTTERM (detectHUPCONTTER)
→
PID PPID PGID SESS TGPID TTY STAT COMMAND 1069 1 1069 cd2e40 30001 ttyp4 S ./detectHUPCONTTERM (detectHUPCONTTER)
ここまでの擬似端末およびBashの知識を元に、nohupの挙動を再確認してみる。
まずnohupはSIGHUPをSIG_IGNに設定し、コマンドラインで指定されたプログラムとそのコマンドラインオプションを"sh -c"に続けてexec()する。SIG_IGNを設定されたシグナルはexec()後も引き継がれるため、プログラム側でSIGHUPのシグナルハンドラを再設定する必要は無い。
nohupはSIGHUPにSIG_IGNを設定する。SIGHUP以外のシグナルハンドラは操作しない。起動されるプログラム側でシグナルハンドラを設定しなければ、たとえばSIGTERMを受信したらデフォルトの処理としてプロセスは終了する。
では、nohupで起動したジョブがSIGTERMを受信するケースはどのようなケースか?
これまでのBashの調査で、SIGHUPにせよexitコマンドからにせよ、Bashは終了時に、STOP状態のジョブに対してSIGTERMを送信することが判明している。
ではnohupで起動したジョブがSTOP状態になるのはどのようなケースか?
この2パターンにおいて、Bash終了時にnohupで起動されたジョブも終了すると予想される。
そこで、まずnohupの一般的な使い方について実験し、続いて上記2パターンについて実験してみる。
また、nohupで起動した時点ではRUNNNINGだが、端末が閉じられた後、標準入力に対してread()するとどうなるか確認する。
標準出力・標準エラー出力に対して1秒間隔でメッセージを出力する簡単なサンプルプログラムを作成し、確認する。
nohuptest.c:
#include <stdio.h> #include <unistd.h> int main(int argc, char *argv[]) { int i = 0; for (;;i++) { fprintf(stdout, "stdout, i = %d\n", i); fflush(stdout); fprintf(stderr, "stderr, i = %d\n", i); fflush(stderr); sleep(1); } return 0; }
まずnohup経由でフォアグラウンドジョブとして起動し、端末エミュレータを終了した後もジョブが続行されるか確認する。
$ nohup ./nohuptest sending output to nohup.out
ここで端末エミュレータ(putty)をcloseする。
close前:
$ ps x -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND 204 203 204 c97f80 214 ttyp1 Ss -bash 214 204 214 c97f80 214 ttyp1 S+ ./nohuptest
close後:
$ ps x -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND 214 1 214 c97f80 30001 ttyp1 S ./nohuptest
ジョブが続行されるのを確認できた。
次はnohupをバックグランドジョブとして起動し、bashからlogoutした後もジョブが続行されるか確認する。
$ nohup ./nohuptest & [1] 230 $ sending output to nohup.out # <<< output from nohup $ jobs [1]+ Running nohup ./nohuptest & $ exit
nohup後:
$ ps x -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND 225 224 225 c977c0 225 ttyp1 Ss+ -bash 230 225 230 c977c0 225 ttyp1 S ./nohuptest
exitによるbash終了後:
$ ps x -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND 230 1 230 c977c0 30001 ttyp1 S ./nohuptest
ジョブが続行されるのを確認できた。
次はnohupをバックグランドジョブとして起動し、端末エミュレータを終了した後もジョブが続行されるか確認する。
$ nohup ./nohuptest & [1] 243 $ sending output to nohup.out # <<< output from nohup $ jobs [1]+ Running nohup ./nohuptest & $
ここで端末エミュレータ(putty)をcloseする。
nohup後:
$ ps x -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND 239 238 239 c97800 239 ttyp1 Ss+ -bash 243 239 243 c97800 239 ttyp1 S ./nohuptest
端末close後:
$ ps x -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND 243 1 243 c97800 30001 ttyp1 S ./nohuptest
ジョブが続行されるのを確認できた。
ここまででnohupの一般的な使い方である、端末終了後もジョブが続行するパターンを確認できた。
続けて、nohupで起動したとしても、端末終了時にジョブも終了してしまうパターンを確認していく。
detectHUPCONTTERM サンプルプログラムを使う。
$ nohup ./detectHUPCONTTERM 1>out.txt 2>err.txt ^Z [1]+ Stopped nohup ./detectHUPCONTTERM >out.txt 2>err.txt $ exit logout There are stopped jobs. $ exit
nohupにより起動した時点でのps:
$ ps x -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND 1297 1296 1297 cd2b80 1303 ttyp1 Ss -bash 1303 1297 1303 cd2b80 1303 ttyp1 S+ ./detectHUPCONTTERM (detectHUPCONTTER)
Ctrl-Z(SIGTSTP)によりSTOP状態にした時点でのps:
$ ps x -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND 1297 1296 1297 cd2b80 1297 ttyp1 Ss+ -bash 1303 1297 1303 cd2b80 1297 ttyp1 T ./detectHUPCONTTERM (detectHUPCONTTER)
exit後のps:
$ ps x -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND 1303 1 1303 cd2b80 30001 ttyp1 S ./detectHUPCONTTERM (detectHUPCONTTER)
detectHUPCONTTERMはSIGHUP/SIGCONT/SIGTERMを受信しても終了しないようにプログラムされている。
これら三つのシグナルを受信すると標準エラー出力にメッセージを出力するので、リダイレクトにより保存されたerr.txtを確認してみる。
$ cat out.txt ... stdout:PID[1303],PPID[1297],PGID[1303] i = 42 stdout:PID[1303],PPID[1297],PGID[1303] i = 43 stdout:PID[1303],PPID[1],PGID[1303] i = 44 stdout:PID[1303],PPID[1],PGID[1303] i = 45 ... $ cat err.txt ... stderr:PID[1303],PPID[1297],PGID[1303] i = 41 stderr:PID[1303],PPID[1297],PGID[1303] i = 42 detect SIGCONT(19) detect SIGTERM(15) stderr:PID[1303],PPID[1297],PGID[1303] i = 43 stderr:PID[1303],PPID[1],PGID[1303] i = 44 stderr:PID[1303],PPID[1],PGID[1303] i = 45 ...
SIGCONT, SIGTERM を受信したことを確認できた。
もしnohupにより起動されたプログラムがこれらのシグナルハンドラをカスタマイズしていなければ、SIGTERMによりプロセスは終了する。
mycat_detectHUP.cをSIGTERMに対応させたmycat_detectTERM.cを使う。
mycat_detectTERM.c:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #include <errno.h> void sig_term(int signo) { fprintf(stderr, "detect SIGTERM(%d)\n", signo); fflush(stderr); } int main(int argc, char *argv[]) { char buf[200]; int nread; struct sigaction sa; sa.sa_handler = sig_term; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; #ifdef SA_INTERRUPT sa.sa_flags |= SA_INTERRUPT; #endif /*signal(SIGTERM, sig_term);*/ sigaction(SIGTERM, &sa, NULL); for (;;) { if (-1 == (nread = read(STDIN_FILENO, buf, sizeof(buf)))) { perror("read()"); exit(1); } if (0 == nread) { break; } if (nread != write(STDOUT_FILENO, buf, nread)) { perror("write()"); exit(1); } } return 0; }
$ nohup ./mycat_detectTERM 2>err.txt & [1] 1387 $ jobs [1]+ Stopped nohup ./mycat_detectTERM 2>err.txt $ exit
nohupで起動した時点でのps:
$ ps x -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND 1376 1375 1376 cd2b80 1376 ttyp1 Ss+ -bash 1387 1376 1387 cd2b80 1376 ttyp1 T ./mycat_detectTERM
exitした時点でのps:
$ ps x -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND (ttyp1のプロセスは存在しない)
標準エラー出力のリダイレクト先であるerr.txtを確認してみる:
$ cat err.txt sending output to nohup.out detect SIGTERM(15) read(): Interrupted system call $
SIGTERM を受信したことを確認できた。
また、SIGTERM受信によりread()がEINTR(Interrupted system call)で終了している。
予想:端末がcloseされる時点ではRUNNINGなのでSIGTERMは送信されない。よって端末close後もプロセスは存在するが、標準入力をread()しようとすると既にclose()されているためEOFが返される。
1分間sleep()後、標準入力をfgets()で読み込む delay_read.c:
#include <stdio.h> #include <unistd.h> #include <errno.h> int main(int argc, char *argv[]) { char buf[200]; fprintf(stderr, "after 60 seconds, read() and write()...\n"); sleep(60); fprintf(stderr, "60 seconds passed, now read() and write():"); errno = 0; while (NULL != fgets(buf, sizeof(buf), stdin)) { fprintf(stdout, "PID[%d] : %s\n", getpid(), buf); fflush(stdout); } if (feof(stdin)) { fprintf(stderr, "stdin detect EOF\n"); } else { perror("fgets(stdin)"); } return 0; }
実行してみる:
$ nohup ./delay_read 1>out.txt 2>err.txt & [1] 1462 $ jobs [1]+ Running nohup ./delay_read >out.txt 2>err.txt & $ exit
nohup起動時点:
$ ps x -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND 1437 1436 1437 c9a040 1437 ttyp1 Ss+ -bash 1462 1437 1462 c9a040 1437 ttyp1 S ./delay_read
exit後、60秒経過前:
$ ps x -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND 1462 1 1462 c9a040 30001 ttyp1 S ./delay_read
60秒経過後のps:
$ ps x -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TGPID TTY STAT COMMAND (ttyp1のプロセスは存在しない)
out.txt, err.txtを確認する:
$ wc -c out.txt 0 out.txt $ cat err.txt after 60 seconds, read() and write()... 60 seconds passed, now read() and write():stdin detect EOF ^^^^^^^^^^^^^^^^ $
EOFが返されることを確認できた。
NetBSD1.6と同様に、CentOS5.x上で以下の実験をしていく。
mypty.cはNetBSD1.6と同様。ただし、
#include <util.h>
を
#include <pty.h>
に変更してコンパイル。
$ su # cc -o mypty mypty.c -lutil && chmod +s ./mypty
mycat_detectHUP.cもNetBSD1.6と同様。変更点無し。
"master"側close時のSIGHUP送信を確認する。
$ ./mypty ./mycat_detectHUP slave name = /dev/pts/3 abc # <- input from keyboard + RETURN abc # echo back in parent terminal PID[15481] : abc # output from slave def # <- input from keyboard + RETURN def # echo back in parent terminal PID[15481] : def # output from slave
psコマンドによる現在状況の確認:
$ ps ax -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TPGID TT STAT COMMAND 3287 3286 3287 3287 3910 pts/2 Ss -bash 3910 3287 3910 3287 3910 pts/2 S+ ./mypty ./mycat_detectHUP 3911 3910 3911 3911 3911 pts/3 Ss+ ./mycat_detectHUP 3912 3910 3910 3287 3910 pts/2 S+ ./mypty ./mycat_detectHUP
全体図はNetBSD1.6と同様なため省略。Linuxの場合はslave側デバイスが"/dev/pts/N"というデバイスファイル名になる。
master/slave間のread/writeが確認できたら、EOFを入力し、master側のファイル記述子をclose()させる。
(input ^D , no echo back) child(read stdin, write ptym) detect EOF from stdin. send SIGTERM to parent... read(ptym): Interrupted system call child sent SIGTERM, maybe terminated. PID[3910] closed ptym, sleeping(10)... PID[3912] closed ptym, sleeping(10)...
この時点でのps:
$ ps ax -o pid,ppid,pgid,sess,tpgid,tty,stat,command PID PPID PGID SESS TPGID TT STAT COMMAND 3287 3286 3287 3287 3910 pts/2 Ss -bash 3910 3287 3910 3287 3910 pts/2 S+ ./mypty ./mycat_detectHUP 3911 3910 3911 3911 -1 ? Zs [mycat_detectHUP] <defunct> 3912 3910 3910 3287 3910 pts/2 S+ ./mypty ./mycat_detectHUP
mycat_detectHUPがゾンビ状態になっている。
ファイルに保存された標準エラー出力を確認してみる:(Linuxの場合はrootユーザーで/tmp/mycat_detectHUPが作成されている)
# cat /tmp/mycat_detectHUP detect SIGHUP(1) stdin detect EOF #
SIGHUPの受信を確認できた。
10秒後、myptyがsleep()から復帰し、終了する。
PID[3910] awaken, terminates. PID[3912] awaken, terminates. done
Bashバージョン
$ bash --version GNU bash, version 3.2.25(1)-release (i686-redhat-linux-gnu) Copyright (C) 2005 Free Software Foundation, Inc.
Bashの二種類の終了方法:
exit_shell() (shell.c) -> hangup_all_jobs() (jobs.c) : 対話シェルかつログインシェルかつshopt huponexitがセットの場合 -> killpg(<PGID>, SIGHUP) : 対象:シェルの内部フラグでJ_NOHUPが未設定のジョブ -> killpg(<PGID>, SIGCONT) : 対象:STOP状態のジョブでJ_NOHUPが未設定のジョブ -> end_job_control() (jobs.c) : 非サブシェル -> terminate_stopped_jobs() (jobs.c) : 対話シェルの場合 -> killpg(<PGID>, SIGTERM) : 対象:STOP状態のジョブ -> killpg(<PGID>, SIGCONT) : 対象:同上
"huponexit"有 ("shopt -s huponexit"):
SIGHUP | SIGCONT | SIGTERM | 実験による確認 | |
RUNNING状態 + J_NOHUP有 | - | - | - | |
RUNNING状態 + J_NOHUP無 | o | - | - | |
STOP状態 + J_NOHUP有 | - | o | o | |
STOP状態 + J_NOHUP無 | o | o | o |
"huponexit"無 ("shopt -u huponexit"):
SIGHUP | SIGCONT | SIGTERM | 実験による確認 | |
RUNNING状態 + J_NOHUP有 | - | - | - | |
RUNNING状態 + J_NOHUP無 | - | - | - | 実験1 |
STOP状態 + J_NOHUP有 | - | o | o | |
STOP状態 + J_NOHUP無 | - | o | o | 実験2 |
SIGHUP : termsig_sighandler() -> termsig_handler() (sig.c) -> hangup_all_jobs() (jobs.c) : 対話シェルかつSIGHUPの場合 -> killpg(<PGID>, SIGHUP) : 対象:シェルの内部フラグでJ_NOHUPが未設定のジョブ -> killpg(<PGID>, SIGCONT) : 対象:STOP状態のジョブでJ_NOHUPが未設定のジョブ -> end_job_control() (jobs.c) -> terminate_stopped_jobs() (jobs.c) : 対話シェルの場合 -> killpg(<PGID>, SIGTERM) : 対象:STOP状態のジョブ -> killpg(<PGID>, SIGCONT) : 対象:同上
SIGHUP | SIGCONT | SIGTERM | 実験による確認 | |
RUNNING状態 + J_NOHUP有 | - | - | - | |
RUNNING状態 + J_NOHUP無 | o | ? | - | 実験3,4 |
STOP状態 + J_NOHUP有 | - | o | o | |
STOP状態 + J_NOHUP無 | o | o | o | 実験5 |
"RUNNING状態 + J_NOHUP無"のSIGCONTについては実験4で説明する。
諸事情によりバイナリと異なるバージョンのソースコードを載せる。
・・・いえ、その、決してSRPM入れて展開するのが面倒くさかったからとかじゃありませんよ?
ソースコードのバージョンは bash-3.2.48 。(ログインシェルとして使っているのはCentOSのRPM, 3.2.25)
NetBSD1.6のbash-2.x.xから殆ど変わっていない。hangup_all_jobs()中でのループで、J_NOHUPの条件だけがわずかに変更されている。
exit_shell() (shell.c) :
/* Exit the shell with status S. */ void exit_shell (int s) { /* 省略 */ #if defined (JOB_CONTROL) /* If the user has run `shopt -s huponexit', hangup all jobs when we exit an interactive login shell. ksh does this unconditionally. */ if (interactive_shell && login_shell && hup_on_exit) hangup_all_jobs (); /* If this shell is interactive, terminate all stopped jobs and restore the original terminal process group. Don't do this if we're in a subshell and calling exit_shell after, for example, a failed word expansion. */ if (subshell_environment == 0) end_job_control (); #endif /* JOB_CONTROL */ /* 省略 */ }
end_job_control(), terminate_stopped_jobs(), hangup_all_jobs() (jobs.c) :
void end_job_control () { if (interactive_shell) /* XXX - should it be interactive? */ { terminate_stopped_jobs (); /* 省略 */ } /* 省略 */ } /* Cause all stopped jobs to exit. */ void terminate_stopped_jobs () { register int i; for (i = 0; i < job_slots; i++) { if (jobs[i] && STOPPED (i)) { killpg (jobs[i]->pgrp, SIGTERM); killpg (jobs[i]->pgrp, SIGCONT); } } } /* Cause all jobs, running or stopped, to receive a hangup signal. If a job is marked J_NOHUP, don't send the SIGHUP. */ void hangup_all_jobs () { register int i; for (i = 0; i < job_slots; i++) { if (jobs[i]) { if (jobs[i]->flags & J_NOHUP) continue; killpg (jobs[i]->pgrp, SIGHUP); if (STOPPED (i)) killpg (jobs[i]->pgrp, SIGCONT); } } }
termsig_sighandler() , termsig_handler() (sig.c):
sighandler termsig_sighandler (sig) int sig; { terminating_signal = sig; if (terminate_immediately) { terminate_immediately = 0; termsig_handler (sig); } SIGRETURN (0); } void termsig_handler (int sig) { /* (省略) */ #if defined (JOB_CONTROL) if (interactive && sig == SIGHUP) hangup_all_jobs (); end_job_control (); #endif /* JOB_CONTROL */ /* (省略) */ }
detectHUPCONTTERM.c : NetBSD1.6と同じ。変更点無し。
各実験については結果として検出したシグナルのみ記す。シェル上での作業やpsコマンドの出力は省略する。
・実験1:"exit"でBash終了時のRUNNNING状態バックグラウンドジョブ(J_NOHUP無, huponexit未設定)
→結果:シグナル未検出
・実験2:"exit"でBash終了時のSTOP状態バックグラウンドジョブ(J_NOHUP無, huponexit未設定)
→結果:SIGCONT, SIGTERM 検出
・実験3:SIGHUPでBash終了時のRUNNNING状態フォアグラウンドジョブ(J_NOHUP無, huponexit未設定)
→結果:SIGHUP, SIGCONT, SIGTERM 検出
・実験4:SIGHUPでBash終了時のRUNNNING状態バックグラウンドジョブ(J_NOHUP無, huponexit未設定)
→結果:SIGHUP, SIGCONT検出
ざっとソースを読んだ限りではSIGCONTが送信されるとは思えないのだが、それはソース読解が浅いせいかもしれない。想像以上にシグナルの送受信が発生し、何度かbashとジョブの間でステータスの変化やそれにともなうシグナルのやりとり、bashの内部情報の変更などが発生しているのかもしれない。
・実験5:SIGHUPでBash終了時のSTOP状態バックグラウンドジョブ(J_NOHUP無, huponexit未設定)
→結果:SIGCONT, SIGTERM検出
NetBSD1.6と同様、nohupの使い方やnohupを使っても終了してしまうパターンを実験してみる。
nohuptest.c:NetBSD1.6と同じ。変更無し。
・nohupの一般的な使い方:RUNNING状態でフォアグランドジョブ続行
結果:ジョブが続行されるのを確認できた。
・nohupの一般的な使い方:RUNNING状態でバックグランドジョブ続行その1
結果:ジョブが続行されるのを確認できた。
・nohupの一般的な使い方:RUNNING状態でバックグランドジョブ続行その2
結果:ジョブが続行されるのを確認できた。
ここまででnohupの一般的な使い方である、端末終了後もジョブが続行するパターンを確認できた。
続けて、nohupで起動したとしても、端末終了時にジョブも終了してしまうパターンを確認していく。
・nohupで起動しても終了するパターン1:ジョブをCtrl+Z(SIGTSTP)でSTOP状態にしてlogout
結果:SIGTERM, SIGCONT受信を確認できた。
・nohupで起動しても終了するパターン2:バックグランドジョブで標準入力(=擬似端末のslave側)をread()
mycat_detectTERM.c:NetBSD1.6と同じ。変更無し。
$ nohup ./mycat_detectTERM 2>err & [1] 12704 $ [1]+ Exit 1 nohup ./mycat_detectTERM 2> err $ jobs $ $ which nohup /usr/bin/nohup $ rpm -qf /usr/bin/nohup coreutils-5.97-14.el5
CentOS5.xの場合、即座に終了してしまった。straceでnohupが呼んでいるシステムコールを追跡したところ、標準入力をclose()してからexec()していることが分かった。標準エラー出力のリダイレクト先に保存されたperror()メッセージも、read()でEBADFが発生したことを示している。
$ cat err nohup: appending output to `nohup.out' read(): Bad file descriptor
・nohupで起動した時点ではRUNNNINGだが、端末が閉じられた後、標準入力に対してread()するとどうなるか
delay_read.c:NetBSD1.6と同じ、変更点無し。
結果:端末がcloseされる時点ではRUNNINGなのでSIGTERMは送信されない。よって端末close後もプロセスは存在するが、標準入力をread()しようとすると最初からclose()されている(exec()の時点でclose()されている)ためEBADFになる。
まとめ
反省
総括として、nohupを使うときの注意事項をまとめておく。
上記3点に注意すれば、"nohupを使ったのに端末終了orログアウトでジョブが終了してしまった"と戸惑うことは無いだろう。
ログインシェルによっては、終了時のシグナル送信の条件やシグナルの種類が異なるかもしれないので、気になる場合はmanページ等で確認しておくと良いだろう。
また、nohupを使わなくともバックグランドジョブを続行することは可能である。ただし上記nohup時の注意点に加え、nohup無しなのでSIGHUPについても注意を払う必要がある。
ログインシェル終了時のシグナル送信条件やパターンについて細かく把握するのは非常に手間がかかる。
nohupを挟むことで少なくともSIGHUPについて頭を悩ませる必要は無くなる。"「誰がSIGHUP送ったの?」"の項でも書いたが、使う側が何も知らず、考えなくてもバックグラウンドジョブを(それなりに)継続できるnohupは、やはりスゴイのだ。
他、UNIXの規格と便利なman検索:
nohupの使い方:
SSHとバックグラウンドジョブ, nohupについて:
擬似端末全般:
"/dev/console"とか"/dev/ttyN"の話は本記事では全力でスルーさせて貰いましたww
いずれその辺も調べてみたいっすね・・・。
擬似端末(Pseudo Terminal) Master側close()時のSIGHUP送信:
以下、後書き。
Webアプリケーション全盛の昨今、Unixの端末の仕組みについて知らなくても殆ど困らないのが現実です。
知ってるから、勉強したからといってお給料が上がるわけでもありません。実際、自分も未だに自宅警備員ですし。
いやほんと、こーゆーの調べたり記事にしたりすることでお給料もらえるところあったら、就職したいですわー。
話を戻して。
ただまぁ、Unixの端末の仕組みというのも、知らない人間から見れば結構ミステリアス。オカルトですな。
今回は全く取り上げていませんが、というかそこまで調べ始めたら話がまとまらないのでお手上げというか戦略的撤退として全力でスルーさせてもらった"/dev/console"なども結構黒魔術めいてます。キーボードに打ち込んだ文字が、いったいカーネル内部をどのように伝播してディスプレイに表示されるのか、気になりだすと夜も眠れません。
・・・うそです。眠れます。本当に眠れなくなったら、心療内科とか行った方が良いですね。不眠症です。
で、"nohup"コマンドというのは自分の中でかなり長い間、「喉に刺さった魚の小骨」だったんです。気にならないっちゃーならないんだけど、意識しちゃうと暫くそこから離れられない・・・って感じです。
そこにようやく、ピリオドを打てたので「あー、すっきりした」ってところです。
こうして調べてみますと、Unixというのが本当に、小さなパーツ、独立した仕様が絡み合って蠢いているそれなりに混沌とした"システム"であることがよく分かります。ホント、POSIXとかSUS策定した人たち、マジすげー。
Webアプリケーション全盛の昨今、・・・って同じ書き出しですが、とにかく昨今は、そうした混沌としたシステムの裏側を覗く必要性は殆ど無いと思います。セキュリティ系や大規模なインフラ、金融、通信系は別かもしれませんが。
つーかそれ以前に、アプリの仕様だのプロジェクトマネジメントだのチーム内の人間関係だのがカオスなんだから、それに加えてもう一つカオスな代物の裏側まで覗きたくねーよ、お腹一杯だぁ。って感じですね。
たとえ裏側を覗こうとしても、たとえば今回のようにnohup一つとっても、nohup単体だけでは不十分で、ログインシェルであるとかシグナルであるとか擬似端末であるとか、いろいろな要素が絡み合っていて、それら全てを、「ほどほど」だけじゃ不十分でSUSであるとか実際のソースコードであるとか、そこまで探索して初めて、全体の「絵」が描けるようになる・・・んだと思います。シグナルにしたって、プロセス間で送受信してる様子を「ビジュアルに」追えるわけではありませんし。
何よりも「地図」が無い。それが困ってます。
個別要素の地図はあるんですよ。擬似端末に絞った記事とか、nohup単体のmanページとか。
ところがいざ、裏側の全体像に目を凝らそうとすると、地図をつなぎ合わせる横串がどこにあるのか、それがさっぱり見えてこないんです。
たとえてみれば、地図はあるんですけど、全ページ切り離されてばらばらです。おまけに全体地図が欠落してる。そんな感じです。
自分のオツムが悪いだけですかね・・・。あるいは、調査不足とか。あ、APUEは結構助かります。といってもUnix全般を扱ってるため、Linuxなどプラットフォーム特有の問題までは書いてありません。
今回、この後書きを書くまで実に5日間、ほぼフルに使いましたが、半分以上がLinux上での悪戦苦闘で占められています。なんというか、なかなか実験結果が安定しないんですよね・・・。
ともあれ、自宅警備員ならではの社会貢献として・・・言い訳ですよ、ハイ。自分で書いてて虚しくなった。
とにかく、とりあえずnohup周辺の地図を自分なりにつなぎあわせてみました。
今回つなぎ合わせた地図が、自分同様、裏舞台が気になって夜も眠れなくなっちゃうけどまぁちゃっかり眠ってる、そんな読者の、何かしらの糧になれば幸いです。
コメント