#navi_header|技術| 何をいまさら当たり前の事を・・・と思われるだろう。 $ nohup long_run_batch.sh & SSHからログアウト後も実行を続けたいバッチジョブを、"&"を付けてバックグラウンドジョブとしてnohupから起動するのは定番中の定番である。 しかし、「nohupを使わなくても実行を続けることが出来る」やり方があったり、さらには ''「nohupを付けてもログアウト時に終了してしまう」'' パターンがあるとしたらどうだろう? そして、ある日あなたの後輩や同僚がこれらについてあなたに質問してきたら、あなたはどう答えるだろうか? 「Web上で検索したら見つかったのでそれに従ってる」 と答えてお茶を濁すだろうか? それとも、 「OK, いい質問だ。それはシェルが終了時にSIGHUPをだね・・・」 のように理路整然とした華麗な語り口で受け答えるべきだろうか? 「お茶を濁せればそれでよい」と答えた方、あるいは「よし、じゃぁ一緒にBashのソースコードやプロセス終了時のカーネルのソースを追ってみようか。おっとその前にSUSv3をベースに端末制御とSIGHUPについて復習だ・・・」と颯爽とリードできる人はこの先読む必要は無い。 ---- #more|| ここから先はそれなりに長丁場になる。しかし、舞台裏を解き明かすことで「nohupのNGパターン」や「nohupを使わなくてもOKなパターン」のWHYについて説明できるようになるだろう。・・・なってくれると、いいなぁ。いや、なってほしいです。・・・なれなかったとしたら、説明が下手だったということでmsakamoot-sfに責任転嫁してください。 内容的に分量や密度は高めとなる。NetBSD1.6やCentOS5.xでの検証でサンプルコードや動作結果などの分量が多いので、そこは読み飛ばしていただいてもかまわない。 では、しばしお付き合い願います。 ---- #outline|| ---- * WHY?の発端 改めてタイトルを確認すると、 なぜnohupをバックグランドジョブとして起動するのが定番なのか? というWHY?になっている。バックグラウンドではなくフォアグラウンドジョブとして起動し、 $ nohup long_run_batch.sh (フォアグラウンド) この状態でputtyなどの端末エミュレータを終了させてしまってもバッチジョブは残り、実行を続ける。 なのになぜ、わざわざnohupをバックグランドジョブとして起動するのが定番になっているのか?あるいはそうした方がよい理由はあるのか? まずはnohupの中身を確認すべきだが、それについては既に以下の記事で確認している。 - C言語系/「デーモン君のソース探検」読書メモ/A07, nohup(1) -- http://www.glamenv-septzen.net/view/847 実は今回のWHY?の発端は、nohupが直接の原因ではない。上の記事でnohup単体の謎解きは終えている。 その後、「念のためセッションやプロセスグループ、擬似端末についてAPUEを確認しておこう」と思いAPUEをぱらぱらめくっていくうちに、「あれ?バックグラウンドジョブってSIGHUPを受信することは有り得ないんじゃない?」という疑念が立ち上がり、そこで初めて「じゃぁなんでnohupをバックグランドジョブとして起動するのが定番なんだ?」という疑問が表れた。それが今回のWHY?の発端である。 詳細は後ほど解説するとして、まずは冒頭で述べた - nohupを使わなくても実行を続けることが出来るパターン - nohupを付けてもログアウト時に終了してしまうパターン この2パターンを紹介する。本記事を最後まで読んでいただければ、この2パターンの挙動を完璧に説明できるように・・・なってくれると嬉しいです。 ** nohupを使わなくても実行を続けることが出来るパターン 実験用のサンプル, hello.sh: #pre||> #!/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"後にすぐ終了した。 ** nohupを付けてもログアウト時に終了してしまうパターン 「うっかり"&"をつけずにフォアグラウンドで起動してしまったジョブを、"^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 の更新も止まっている。 ** nohupの仕組みだけでは説明しきれない 上記2パターンのように、バックグランドジョブの継続にはnohupが必須というわけではなく、nohupを使えば全く問題が無いわけでもない。したがってこれらの挙動を正確に把握するにはnohup単体の仕組みだけではなく、端末制御やセッション、プロセスグループ、SIGHUPなど周辺要素について調査する必要がある。 nohupはSIGHUPのシグナルハンドラをSIG_IGNに設定、つまり「受信しても無視」する設定にして、コマンドラインで指定されたジョブを実行する。逆に言えばnohupはSIGTERM, SIGQUITなど他のシグナルハンドラをデフォルト設定のままにしている。たとえnohupで起動していたとしても、それらデフォルトでプロセス終了となるシグナルを受信すれば当然、プロセスは終了する。 つまりnohupでも終了してしまうパターンでは、それらSIGHUP以外のプロセス終了シグナルを受信したために終了した可能性が考えられる。ではそのシグナルは何なのか?誰が、いつ送信したのか?SIGHUPとそれ以外のシグナル、どちらを送信するのか判断する基準は? 本記事では、それらの疑問についても回答する予定である。 ** 「誰がSIGHUP送ったの?」(「誰がこまどり殺したの?」) と問われれば、 「ログインシェルがSIGHUP送ってるんじゃないの?」 と思われる方もいるだろう。実は自分も、この記事を書くまではそう思ってました。 半分正解で、半分不正解。 ログインシェルがSIGHUPを送る場合・送らない場合の両方が有り、さらに端末のデバイスドライバやカーネルが送る場合もある。 それらの詳細については以降の記事で解説するが、実際、ややこしい。実験・検証しているときも、しょっちゅう間違えて時間をとられてしまった。 「誰がSIGHUPを送ったの?」 記事のネタとしてはそれなりだが、実際のお仕事や運用中に一々考えるのは面倒である。 身も蓋も無い言い方だが、 ''使う側が何も知らず、考えなくてもバックグラウンドジョブを継続できるnohupは、やはりスゴイ'' のだ。 ** 本記事を読むときの前提知識、実験・検証環境について 本格的な調査に進む前に、前提知識、実験・検証環境について説明する。 *** 前提知識 最低限、以下の前提知識や技能があることを前提とする。 - UnixでのC言語と標準Cライブラリを使ったプログラミング・コンパイル経験 - Unixのシステムコール:分野としては File I/O, Signal, Process, Error Handlingの4つ - Bashシェルの使い方、ジョブ制御 サンプルコードで主に使うのは次のシステムコールと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 を使っている理由は、 [[546]] で使った環境を引き続き使用しているためである。いずれも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日版」 - putty -- http://www.chiark.greenend.org.uk/~sgtatham/putty/ - PuTTY ごった煮版 -- http://yebisuya.dip.jp/Software/PuTTY/ 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" : "Advanced Programming in the UNIX Environment Second Edition" : "AUP" : "Advanced UNIX Programming Second Edition" : "SUSv3" : Single UNIX ® Specification, Version 3 参考資料の詳細については記事の最後に一覧を載せている。 * 退屈な復習から生まれたWHY?の発端 セッションやプロセスグループ、擬似端末についてAPUEを確認したことが今回のWHY?の発端である。 前提知識の再確認も兼ねて、本記事に特に関連するトピックに絞って、ごく簡単に要点をまとめていく。 単調で退屈な文章が続くが、暫くご辛抱願いたい。 ** セッション・プロセスグループの全体像 - 1つのプロセスは1つのプロセスグループに所属する。 - 1つのプロセスグループには1つまたはそれ以上のプロセスが所属する。 - 1つのプロセスグループにはグループリーダーとなるプロセスが0 or 1つ存在する。 - 1つのプロセスグループは1つのセッションに所属する。 - 1つのセッションには1つまたはそれ以上のプロセスグループが所属する。 - 1つのセッションにはセッションリーダーとなるプロセスが1つ存在する。 &image(yb://images/jobcontrols_pty_nohups/01_overview.png) 参考: - APUE : p269, "9.4 Process Groups" - APUE : p270, "9.5 Sessions" ** セッションと制御端末(Controlling Terminal) セッションは制御端末の有無で二種類に分けることができる。 - 制御端末を持たないセッション(initやinitから起動されたデーモンプロセスなど) - 制御端末を持つセッション(ログインシェルやログインシェルから起動されたプロセスなど) -- 端末からの入出力を受け付けるプログラムはこちらのセッションに所属することになる。 &image(yb://images/jobcontrols_pty_nohups/02_session_types.png) 参考: - APUE : p272, "9.6 Controlling Terminal" ** セッションの新規作成 セッションの新規作成でポイントとなる箇所を示す。 初期状態: &image(yb://images/jobcontrols_pty_nohups/03_new_sess_1.png) まずfork()で子プロセスを作成する。 &image(yb://images/jobcontrols_pty_nohups/03_new_sess_2.png) fork()した子プロセス側でsetsid()を呼ぶ。これにより新しいセッションが開始される。 + setsid()を呼んだプロセスは新しいセッションのセッションリーダーとなる。 + 同プロセスは新しいプロセスグループのグループリーダーとなる。新しいプロセスグループのIDは同プロセスのIDと等しくなる。 + 新しいセッションはまだ制御端末を持っていない。親プロセスの所属するセッションが制御端末を持っていたとしても、引き継がれない。(ログインシェル上からデーモンを起動したときのイメージ) &image(yb://images/jobcontrols_pty_nohups/03_new_sess_3.png) 参考: - APUE : p270, "9.5 Sessions" ** プロセスグループの新規作成 プロセスグループの新規作成でポイントとなる箇所を示す。 初期状態: &image(yb://images/jobcontrols_pty_nohups/04_new_grp_1.png) まずfork()で子プロセスを作成する。 &image(yb://images/jobcontrols_pty_nohups/04_new_grp_2.png) 子プロセスと親プロセスの両方でsetpgid()を呼ぶ。なぜ両方で呼ぶ必要があるのかは、APUE参照。 これにより、プロセスグループが(まだ存在していなければ)新規作成され、子プロセスがそのプロセスグループの最初のプロセスであれば子プロセスはグループリーダーになる。 もしパイプなどで複数のプロセスがひとつのプロセスグループとして起動した場合は、シェルの実装にも依存するが、新しいプロセスグループを作成した後、そのプロセスグループに所属するようにfork()を繰り返していく。以下の図ではシェルからfork()したプロセスがさらにfork()していく例を示している。シェル側でfork()とsetpgid()を繰り返す場合もあるだろう。 &image(yb://images/jobcontrols_pty_nohups/04_new_grp_3.png) 参考: - APUE : p269, "9.4 Process Groups" - APUE : p278, "9.9 Shell Execution of Programs" ** コンソールログインの舞台裏 昔は"ダム端末"(dumb-terminal)と呼ばれるディスプレイ・キーボードのセットがあり、複数のダム端末が1つのマシンに接続される事で複数人による並行作業が行われていたらしい。 時代が少し進むと、ダイアルアップ回線で通信するモデム経由でログインできるようになったらしい。 2010年現在でも1つのマシンに0または複数のディスプレイ・キーボードが接続される場合があるが、複数人による同時ログインを提供するためのものではなく、マルチディスプレイ環境や、キーボード・モニタ切り替え器を間に挟んで複数マシンで共有する目的が殆どである。リモート接続する場合はtelnetやSSHなどを使う。 マシンに接続されたディスプレイ・キーボードのセットを「コンソール」と呼んでみる。 コンソールからのログインの舞台裏をまとめる。 + システム起動時にinitプロセスがコンソール入力を受け付けるプロセスを起動する。 ++ NetBSD1.6の場合は "/usr/libexec/getty", CentOS5.x の場合は "/sbin/mingetty" がコンソール入力を受け付けるプログラム。以降、まとめて "getty" と表記する。 + どの端末デバイスをopen()し、いくつ起動するのかはinitプロセス用の設定ファイルに書かれている。 ++ NetBSD1.6の場合は "/etc/ttys", CentOS5.x の場合は "/etc/inittab" に書かれている。 + gettyはinitとは別にそれぞれ独立したセッションを持ち、セッションリーダーとして起動する。 + gettyはログインプロンプトをそれぞれの端末に出力し、ログインユーザー名の入力を待つ。 &image(yb://images/jobcontrols_pty_nohups/05_tty_login_1.png) + 端末からログインユーザー名が入力されると、gettyは"login"プログラムをexec()する。 + "login"プログラムはパスワード入力プロンプトを表示し、端末へのechoをOFFにし、パスワードの入力を待つ。 + パスワードが入力されると"login"プログラムは認証処理を行い、有効なユーザー名とパスワードであればログイン処理に進む。ログイン処理の概要は以下の通り。 ++ カレントディレクトリをユーザーのホームディレクトリに変更 ++ 端末デバイスのデバイスファイルをchown() ++ 同デバイスファイルのアクセス権限をユーザーがread/writeできるよう変更 ++ プロセスのグループIDを変更 ++ 環境変数を調整する。"HOME", "SHELL", "USER", "PATH"など ++ プロセスのユーザーIDを変更 ++ 最後にユーザーのログインシェルをexec()で実行 プラットフォームにより多少の差異はあるかもしれないが、大筋としては上記の流れになる。 &image(yb://images/jobcontrols_pty_nohups/05_tty_login_2.png) 参考: - APUE : p261, "9.2 Terminal Logins" *** NetBSD1.6でのコンソールログイン例 "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 セッションリーダーであるプロセスが終了するとき、カーネルがSIGHUPを送信する対象はそのセッションのフォアグラウンドジョブであり、バックグラウンドジョブは放置されている。つまりバックグラウンドジョブは基本的にはSIGHUPを受信することは無いのではないか? さらに、そもそもSIGHUPを受信しないのであればnohupをわざわざバックグラウンドで起動する意味は無い。 ここでようやく、タイトルにも掲げた ''「なぜnohupをバックグランドジョブとして起動するのが定番なのか?」'' という疑問が生じる。 ||< 端末デバイスやカーネルは、おそらくAPUEの通りに動いているととりあえず信用してみる。となると、残る構成要素であるログインシェルが怪しい。 ・・・と、記事を書いている今だからすんなりと目星をつけてますが、調査の段階ではそこまで分析できていない状態でいろいろサンプルコードを作ってはputtyを強制終了したり、ログアウトしたりと組み合わせを弄ってました。それでもなかなかシグナルの発生パターンの切り分けができず、そこでようやく「あ、もしかしてログインシェルが何かしてる?」と気づいた次第。 最初はCentOS5.x上で弄ってたのですが、これに気づいたあとはカーネルを含めてソースを読める環境を整えていたNetBSD1.6に移り、次の順序で徐々に標的を追い詰めていきました。 + 簡単なサンプルコードで擬似端末の動作確認 + 端末デバイスドライバがSIGHUPをセッションリーダーに送信する挙動の確認 + Bashのソースを読んで、 ''SIGTERMとSIGCONTを送信するケースを確認'' + ここまでの確認に基づいた「ログインシェル終了時のシグナル送信パターン」をnohupで確認 これらの確認作業の詳細や、それに使用したサンプルコードの解説は長くなるので後回しにし、"WHY?"に対する"BECAUSE"を先にまとめる。 なお、 ''以降の記事ではログインシェル = Bashとして話を進めていく。'' 他のシェルでも同じ動作が成立するかは確認していない。使用するシェルのmanページを調べるか、"nohup シェル名"でWeb検索してみてほしい。 ** BECAUSE : ログインシェル(今回はBash)が、終了時にSIGHUP/SIGTERM/SIGCONTをバックグランドジョブに送信する「場合がある」から。 まずセッションリーダであるbash終了時、カーネルが放置するバックグランドジョブはbash側でSIGHUP/SIGTERM/SIGCONTを適宜組み合わせて送信し、終了させている。もちろん後述するようにシグナルを送信せず放置し、結果としてジョブを継続させる場合もある。 bashが終了する流れは、大きく次の二通りがある。 + コマンドラインで"exit"や"logout"コマンドを入力されて終了する流れ + 端末デバイスドライバがセッションリーダでもあるbashにSIGHUPを送信し、終了する流れ ++ 原因1:モデム回線のhangup ++ 原因2:端末エミュレータの終了などで擬似端末の"master"側のファイル記述子が全てcloseされる また"exit"や"logout"による終了時にSIGHUPの送信有無を設定することもできる。huponexitオプションがbashでは提供されている。 # 現在設定の確認 $ shopt huponexit huponexit off # ONに設定 $ shopt -s huponexit # OFFに設定 $ shopt -u huponexit さらに"disown"シェルコマンドを使うことで、SIGHUPの送信対象から除外するための内部的な印を、ジョブに対して設定することもできる。以降、この印の有無を実際のbashソースコード上でのフラグ名である「"J_NOHUP"の有無」として表記する。 実際の調査ではbashのソースコードをgrepして流れを掴んでいった。ポイントとなるソースコードは後ほど紹介する。 ここでは、調査結果をまとめたシグナル送信のパターン表を先に解説する。 : シグナル送信パターン1:"exit" or "logout" による終了時 :#block||> "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 | ||< : シグナル送信パターン2:SIGHUP受信による終了時 :#block||> | | 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を使わなくても実行を続けることが出来るパターン」の解説 このパターンは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を使わなくても実行を続けられる理由である。 *** 「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が送信されるパターン 前半のまとめとして、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」まで読み飛ばしてもらっても構わない。 ---- それでは小休憩の後、サンプルコードと実験・動作確認の詳細を解説する。 * NetBSD 1.6 における擬似端末, Bash, nohupの確認 NetBSD1.6上で以下の実験をしていく。 + 擬似端末の使い方を簡単なサンプルで確認 + Bash終了時のシグナル送信の組み合わせを実際のソースコードを読んで確認 + Bash終了時のシグナル送信とnohupの組み合わせを再確認 ** 擬似端末を使ったサンプル 擬似端末を使ってみる。特に、擬似端末のmaster側のファイル記述子を全てcloseすると、slave側擬似端末の制御プロセスにSIGHUPが送信される動作に注目する。NetBSD側の close(2) manpageには記載されていないが、SUSv3のclose(2)の解説ではこの挙動が載っている。 *** NetBSD版 擬似端末実験用サンプルコード mypty.c - 参考:APUE 19.3 Opening Pseudo-Terminal Devices - NetBSD1.6提供のoptnpty()(libutil)を使用 - プログラム簡素化のため端末属性は変更しない。 - EOF(^D)入力後 master 側のファイル記述子を全てclose()し、10秒間sleep()する。この間にslave側擬似端末の制御プロセスがSIGHUPを受信したか確認できるようにしている。 mypty.c: #code|c|> #include #include #include #include #include #include #include #include /* for CentOS5.x */ /* #include */ /* for NetBSD1.6 */ #include 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 *** NetBSD版 slave側擬似端末の制御プロセス用サンプルコード mycat_detectHUP.c - 標準C関数のfgets()で標準入力を読み、fprintf()で標準出力に自分のPIDを付けて出力する。 - SIGHUPを受信すると標準エラー出力にメッセージを出力する。 mycat_detectHUP.c: #code|c|> #include #include #include #include #include #include #include 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; } ||< *** サンプルコードによるSIGHUPの確認 #pre||> $ ./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 全体図: #pre||> [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] ||< 中心部分: #pre||> 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がジョブに送信するシグナル 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" or "logout" シェルコマンドを実行する - SIGHUPを受信する *** "exit" or "logout" シェルコマンドを実行して終了するときにBashが送信するシグナル exit_shell() (shell.c) -> hangup_all_jobs() (jobs.c) : 対話シェルかつログインシェルかつshopt huponexitがセットの場合 -> killpg(, SIGHUP) : 対象:シェルの内部フラグでJ_NOHUPが未設定のジョブ -> killpg(, SIGCONT) : 対象:STOP状態のジョブ -> end_job_control() (jobs.c) : 非サブシェル -> terminate_stopped_jobs() (jobs.c) : 対話シェルの場合 -> killpg(, SIGTERM) : 対象:STOP状態のジョブ -> killpg(, 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を受信して終了するときにBashが送信するシグナル + bashの初期処理で initialize_terminating_signals() (sig.c) がSIGHUPのシグナルハンドラにtermination_unwind_protect()を設定 + (シェル上での作業開始 ... 終了) + クライアントの終了:端末エミュレータ側でのネットワークコネクションまたはウインドウのclose + サーバー側の終了処理開始 → 擬似端末のマスター側のファイル記述子が全てclose + 擬似端末のターミナルドライバが制御プロセス(=bash)へSIGHUPを送信 SIGHUP : termination_unwind_protect() (sig.c) -> hangup_all_jobs() (jobs.c) : 対話シェルかつSIGHUPの場合 -> killpg(, SIGHUP) : 対象:シェルの内部フラグでJ_NOHUPが未設定のジョブ -> killpg(, SIGCONT) : 対象:STOP状態のジョブ -> end_job_control() (jobs.c) -> terminate_stopped_jobs() (jobs.c) : 対話シェルの場合 -> killpg(, SIGTERM) : 対象:STOP状態のジョブ -> killpg(, 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) : #code|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) : #code|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): #code|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 ジョブとして動かすサンプルプログラム(細かいお作法やエラー処理は無視) detectHUPCONTTERM.c: #code|c|> #include #include #include #include #include #include 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; } ||< *** 実験1:"exit"でBash終了時のRUNNNING状態バックグラウンドジョブ(J_NOHUP無, huponexit未設定) 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) *** 実験2:"exit"でBash終了時のSTOP状態バックグラウンドジョブ(J_NOHUP無, huponexit未設定) 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) *** 実験3:SIGHUPでBash終了時のRUNNNING状態フォアグラウンドジョブ(J_NOHUP無, huponexit未設定) 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) *** 実験4:SIGHUPでBash終了時のRUNNNING状態バックグラウンドジョブ(J_NOHUP無, huponexit未設定) 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) *** 実験5:SIGHUPでBash終了時のSTOP状態バックグラウンドジョブ(J_NOHUP無, huponexit未設定) 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) ** nohupの確認 ここまでの擬似端末および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状態になるのはどのようなケースか? + フォアグラウンドで起動した後、Ctrl+Z(SIGTSTP)によりSTOP状態のバックグランドジョブになる + 標準入力をread()するプロセスをバックグランドジョブとして実行する。 ++ 制御端末に対してread/writeできるのはフォアグラウンドジョブだけ。 ++ バックグラウンドジョブが制御端末に対してread/writeしようとすると、制御端末のターミナルドライバはバックグラウンドジョブに対してSIGTTINを送信する。 ++ シェルはwait()系で子プロセスに送られたSIGTTINを検知し、バックグラウンドジョブをSTOPする。 この2パターンにおいて、Bash終了時にnohupで起動されたジョブも終了すると予想される。 そこで、まずnohupの一般的な使い方について実験し、続いて上記2パターンについて実験してみる。 また、nohupで起動した時点ではRUNNNINGだが、端末が閉じられた後、標準入力に対してread()するとどうなるか確認する。 *** nohupの一般的な使い方:RUNNING状態でバックグランドジョブ続行 標準出力・標準エラー出力に対して1秒間隔でメッセージを出力する簡単なサンプルプログラムを作成し、確認する。 nohuptest.c: #code|c|> #include #include 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で起動したとしても、端末終了時にジョブも終了してしまうパターンを確認していく。 *** nohupで起動しても終了するパターン1:ジョブをCtrl+Z(SIGTSTP)でSTOP状態にしてlogout 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を確認してみる。 #pre||> $ 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によりプロセスは終了する。 *** nohupで起動しても終了するパターン2:バックグランドジョブで標準入力(=擬似端末のslave側)をread() mycat_detectHUP.cをSIGTERMに対応させたmycat_detectTERM.cを使う。 mycat_detectTERM.c: #code|c|> #include #include #include #include #include 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)で終了している。 *** nohupで起動した時点ではRUNNNINGだが、端末が閉じられた後、標準入力に対してread()するとどうなるか 予想:端末がcloseされる時点ではRUNNINGなのでSIGTERMは送信されない。よって端末close後もプロセスは存在するが、標準入力をread()しようとすると既にclose()されているためEOFが返される。 1分間sleep()後、標準入力をfgets()で読み込む delay_read.c: #code|c|> #include #include #include 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が返されることを確認できた。'' * Linux(CentOS5.x) における擬似端末, Bash, nohupの確認 NetBSD1.6と同様に、CentOS5.x上で以下の実験をしていく。 + 擬似端末の使い方を簡単なサンプルで確認 + Bash終了時のシグナル送信の組み合わせを実際のソースコードを読んで確認 + Bash終了時のシグナル送信とnohupの組み合わせを再確認 ** 擬似端末を使ったサンプル mypty.cはNetBSD1.6と同様。ただし、 #include を #include に変更してコンパイル。 $ su # cc -o mypty mypty.c -lutil && chmod +s ./mypty mycat_detectHUP.cもNetBSD1.6と同様。変更点無し。 "master"側close時のSIGHUP送信を確認する。 #pre||> $ ./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] 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バージョン $ bash --version GNU bash, version 3.2.25(1)-release (i686-redhat-linux-gnu) Copyright (C) 2005 Free Software Foundation, Inc. Bashの二種類の終了方法: - "exit" or "logout" シェルコマンドを実行する - SIGHUPを受信する *** "exit" or "logout" シェルコマンドを実行して終了するときにBashが送信するシグナル exit_shell() (shell.c) -> hangup_all_jobs() (jobs.c) : 対話シェルかつログインシェルかつshopt huponexitがセットの場合 -> killpg(, SIGHUP) : 対象:シェルの内部フラグでJ_NOHUPが未設定のジョブ -> killpg(, SIGCONT) : 対象:STOP状態のジョブでJ_NOHUPが未設定のジョブ -> end_job_control() (jobs.c) : 非サブシェル -> terminate_stopped_jobs() (jobs.c) : 対話シェルの場合 -> killpg(, SIGTERM) : 対象:STOP状態のジョブ -> killpg(, 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を受信して終了するときにBashが送信するシグナル + bashの初期処理で initialize_terminating_signals() (sig.c) がSIGHUPのシグナルハンドラにtermsig_sighandler()を設定 + (シェル上での作業開始 ... 終了) + (シェル上での作業開始 ... 終了) + クライアントの終了:端末エミュレータ側でのネットワークコネクションまたはウインドウのclose + サーバー側の終了処理開始 → 擬似端末のマスター側のファイル記述子が全てclose + 擬似端末のターミナルドライバが制御プロセス(=bash)へSIGHUPを送信 SIGHUP : termsig_sighandler() -> termsig_handler() (sig.c) -> hangup_all_jobs() (jobs.c) : 対話シェルかつSIGHUPの場合 -> killpg(, SIGHUP) : 対象:シェルの内部フラグでJ_NOHUPが未設定のジョブ -> killpg(, SIGCONT) : 対象:STOP状態のジョブでJ_NOHUPが未設定のジョブ -> end_job_control() (jobs.c) -> terminate_stopped_jobs() (jobs.c) : 対話シェルの場合 -> killpg(, SIGTERM) : 対象:STOP状態のジョブ -> killpg(, 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) : #code|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) : #code|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): #code|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, 実験1 - 5 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検出 ** nohupの確認 NetBSD1.6と同様、nohupの使い方やnohupを使っても終了してしまうパターンを実験してみる。 nohuptest.c:NetBSD1.6と同じ。変更無し。 ・nohupの一般的な使い方:RUNNING状態でフォアグランドジョブ続行 + nohup経由でフォアグラウンドジョブとして起動 + 端末エミュレータを終了した後もジョブが続行されるか確認する。 結果:ジョブが続行されるのを確認できた。 ・nohupの一般的な使い方:RUNNING状態でバックグランドジョブ続行その1 + nohup経由でバックグラウンドジョブとして起動(RUNNING) + exitコマンドでログアウト後もジョブが続行されるか確認する。 結果:ジョブが続行されるのを確認できた。 ・nohupの一般的な使い方:RUNNING状態でバックグランドジョブ続行その2 + nohup経由でバックグラウンドジョブとして起動(RUNNING) + 端末エミュレータを終了した後もジョブが続行されるか確認する。 結果:ジョブが続行されるのを確認できた。 ここまでで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になる。 ** 実験・検証のまとめと反省 まとめ - bash終了時の流れと、RUNNING/STOPPEDジョブへのシグナル送信の組み合わせ表を作成し、NetBSD1.6ではその通りに、CentOS5.xではほぼその通りに動作することが確認できた。 - カーネル側では放置しているバックグラウンドジョブに対しても、bash側で終了時にSIGTERM/SIGCONTを送信する事を確認できた。 反省 - CentOS5.xでの実験結果が安定しなかった。環境によっては、1回では本記事と同じ結果にはならないかもしれない。 -- シグナルハンドラでfprintf()などシグナルセーフでない関数を呼んでいたり、bashとジョブの間で、何度かシグナルがやりとりされ、その結果bash内部のジョブ状態が変更されるなどして、今回取り上げたソース以外の場所でいろいろと処理が走っているのかもしれない。 -- 使用しているBashと全く同じソースを用意できなかったので、その影響もあるかもしれない。SRPMのバージョンや、内部で適用しているpatchの影響もあるかもしれない。 * まとめ:「いともたやすく行われるえげつないnohup」 総括として、nohupを使うときの注意事項をまとめておく。 + nohupで防げるのはSIGHUPだけ、SIGTERM/SIGCONTに注意すること。 + バックグラウンドジョブをSTOPPED状態にして端末終了orログアウトするとSIGTERMが送信される点に注意すること。 + nohupで続行させるバックグラウンドジョブの中で、標準入力をreadしないようにすること。 上記3点に注意すれば、"nohupを使ったのに端末終了orログアウトでジョブが終了してしまった"と戸惑うことは無いだろう。 ログインシェルによっては、終了時のシグナル送信の条件やシグナルの種類が異なるかもしれないので、気になる場合はmanページ等で確認しておくと良いだろう。 また、nohupを使わなくともバックグランドジョブを続行することは可能である。ただし上記nohup時の注意点に加え、nohup無しなのでSIGHUPについても注意を払う必要がある。 ログインシェル終了時のシグナル送信条件やパターンについて細かく把握するのは非常に手間がかかる。 nohupを挟むことで少なくともSIGHUPについて頭を悩ませる必要は無くなる。"「誰がSIGHUP送ったの?」"の項でも書いたが、使う側が何も知らず、考えなくてもバックグラウンドジョブを(それなりに)継続できるnohupは、やはりスゴイのだ。 * 参考資料 #amazon||> ||< - "Advanced Programming in the UNIX Envrionment Second Edition" -- http://www.apuebook.com/ #amazon||> ||< - "Advanced UNIX Programming" -- http://basepath.com/aup/ 他、UNIXの規格と便利なman検索: - Single UNIX ® Specification, Version 3 (The Open Group Base Specifications Issue 6, IEEE Std 1003.1, 2004 Edition) -- http://www.unix.org/version3/online.html - FreeBSD Man Pages (他のBSDやLinuxディストリビューションのmanページまで検索できるのでスゴイ便利) -- http://www.freebsd.org/cgi/man.cgi - NetBSD1.6, CentOS 5.x 付属のmanpage nohupの使い方: - nohup - Wikipedia, the free encyclopedia -- http://en.wikipedia.org/wiki/Nohup - ログアウトしてもバックグラウンド ジョブを継続する方法 -- http://www.codereading.com/notebook/moin.cgi/IgnoreTheHangupSignal - 【 nohup 】 ログアウトした後もコマンドを実行し続ける - Linuxコマンド集:ITpro -- http://itpro.nikkeibp.co.jp/article/COLUMN/20060227/230850/ - nohup ハングアップに反応しないようにしてコマンドを実行する - UNIXコマンド辞典:CodeZine(コードジン) -- http://codezine.jp/unixdic/w/nohup/ - UNIXの部屋 コマンド検索:nohup (*BSD/Linux) -- http://x68000.q-e-d.net/~68user/unix/pickup?nohup - [vine-users:066116] nohupは不要? -- http://search.luky.org/vine-users.6/msg06116.html - Do background processes get a SIGHUP when logging off? - Server Fault -- http://serverfault.com/questions/117152/do-background-processes-get-a-sighup-when-logging-off SSHとバックグラウンドジョブ, nohupについて: - SSH Frequently Asked Questions -- http://www.snailbook.com/faq/background-jobs.auto.html - SSH -- http://en.wikipedia.org/wiki/Secure_Shell - The SSH Protocol -- http://www.snailbook.com/protocols.html 擬似端末全般: - Pseudo terminal - Wikipedia, the free encyclopedia -- http://en.wikipedia.org/wiki/Pseudo_terminal - 擬似端末と制御端末(さらにちょっと追記) - ソースコード備忘録 -- http://d.hatena.ne.jp/yuki_rinrin/20090725/1248521786 - LinuxControllingTTY  アクセンスのおまけ -- http://omake.accense.com/wiki/LinuxControllingTTY - Ctrl-D の話 - ひげぽん OSとか作っちゃうかMona- -- http://d.hatena.ne.jp/higepon/20080602/1212385423 "/dev/console"とか"/dev/ttyN"の話は本記事では全力でスルーさせて貰いましたww いずれその辺も調べてみたいっすね・・・。 擬似端末(Pseudo Terminal) Master側close()時のSIGHUP送信: - Linux-Kernel Archive: pty master close and SIGHUP -- http://lkml.indiana.edu/hypermail/linux/kernel/0407.2/0084.html - LKML: Albert Cahalan: pty master close and SIGHUP -- http://lkml.org/lkml/2004/7/16/86 - Unix & Linux: Child process and pseudo-terminals - programming.itags.org -- http://programming.itags.org/unix-linux-programming/79368/ - close(2) -- close a file descriptor (man pages section 2: System Calls) - Sun Microsystems -- http://docs.sun.com/app/docs/doc/816-5167/close-2?l=en&n=1&a=view ---- 以下、後書き。 Webアプリケーション全盛の昨今、Unixの端末の仕組みについて知らなくても殆ど困らないのが現実です。 知ってるから、勉強したからといってお給料が上がるわけでもありません。実際、自分も未だに自宅警備員ですし。 いやほんと、こーゆーの調べたり記事にしたりすることでお給料もらえるところあったら、就職したいですわー。 話を戻して。 ただまぁ、Unixの端末の仕組みというのも、知らない人間から見れば結構ミステリアス。オカルトですな。 今回は全く取り上げていませんが、というかそこまで調べ始めたら話がまとまらないのでお手上げというか戦略的撤退として全力でスルーさせてもらった"/dev/console"なども結構黒魔術めいてます。キーボードに打ち込んだ文字が、いったいカーネル内部をどのように伝播してディスプレイに表示されるのか、気になりだすと夜も眠れません。 ・・・うそです。眠れます。本当に眠れなくなったら、心療内科とか行った方が良いですね。不眠症です。 で、"nohup"コマンドというのは自分の中でかなり長い間、「喉に刺さった魚の小骨」だったんです。気にならないっちゃーならないんだけど、意識しちゃうと暫くそこから離れられない・・・って感じです。 そこにようやく、ピリオドを打てたので「あー、すっきりした」ってところです。 こうして調べてみますと、Unixというのが本当に、小さなパーツ、独立した仕様が絡み合って蠢いているそれなりに混沌とした"システム"であることがよく分かります。ホント、POSIXとかSUS策定した人たち、マジすげー。 Webアプリケーション全盛の昨今、・・・って同じ書き出しですが、とにかく昨今は、そうした混沌としたシステムの裏側を覗く必要性は殆ど無いと思います。セキュリティ系や大規模なインフラ、金融、通信系は別かもしれませんが。 つーかそれ以前に、アプリの仕様だのプロジェクトマネジメントだのチーム内の人間関係だのがカオスなんだから、それに加えてもう一つカオスな代物の裏側まで覗きたくねーよ、お腹一杯だぁ。って感じですね。 たとえ裏側を覗こうとしても、たとえば今回のようにnohup一つとっても、nohup単体だけでは不十分で、ログインシェルであるとかシグナルであるとか擬似端末であるとか、いろいろな要素が絡み合っていて、それら全てを、「ほどほど」だけじゃ不十分でSUSであるとか実際のソースコードであるとか、そこまで探索して初めて、全体の「絵」が描けるようになる・・・んだと思います。シグナルにしたって、プロセス間で送受信してる様子を「ビジュアルに」追えるわけではありませんし。 何よりも「地図」が無い。それが困ってます。 個別要素の地図はあるんですよ。擬似端末に絞った記事とか、nohup単体のmanページとか。 ところがいざ、裏側の全体像に目を凝らそうとすると、地図をつなぎ合わせる横串がどこにあるのか、それがさっぱり見えてこないんです。 たとえてみれば、地図はあるんですけど、全ページ切り離されてばらばらです。おまけに全体地図が欠落してる。そんな感じです。 自分のオツムが悪いだけですかね・・・。あるいは、調査不足とか。あ、APUEは結構助かります。といってもUnix全般を扱ってるため、Linuxなどプラットフォーム特有の問題までは書いてありません。 今回、この後書きを書くまで実に5日間、ほぼフルに使いましたが、半分以上がLinux上での悪戦苦闘で占められています。なんというか、なかなか実験結果が安定しないんですよね・・・。 ともあれ、自宅警備員ならではの社会貢献として・・・言い訳ですよ、ハイ。自分で書いてて虚しくなった。 とにかく、とりあえずnohup周辺の地図を自分なりにつなぎあわせてみました。 今回つなぎ合わせた地図が、自分同様、裏舞台が気になって夜も眠れなくなっちゃうけどまぁちゃっかり眠ってる、そんな読者の、何かしらの糧になれば幸いです。 #navi_footer|技術|