タブを閉じてもログアウトしても実行し続けるジョブにしておく

前回までにバックグラウンドでジョブを複数管理できるようになった。ところで...

  • ターミナルでタブを開く度に、あるいはsshでどこかの端末にログインする度に、
  • シェルが起動して、コマンドを入力・実行・出力する環境を整えてくれる。
  • そして、タブを閉じたり、ログアウトすると、シェルは終了する。
  • と同時に、通常はそのシェル上で実行されていたジョブも終了してしまう。
  • 不要なバックグラウンドジョブを漏れなく終了できるので、多くの場合これで良いのだけど、
  • 稀に、シェルが終了しても処理を続けて欲しい状況もある。
  • 特にsshでログインして時間のかかる処理(ダウンロードとか)をしている時などは、
  • 一旦ログアウトしたいんだけど、処理はそのまま続けて欲しいことが多い。

そんな時は、nohupコマンドを使うと幸せになれる。

環境

$ bash --version
GNU bash, version 3.2.53(1)-release (x86_64-apple-darwin13)
Copyright (C) 2007 Free Software Foundation, Inc.

基本

  • 新規タブを開いて、xclockコマンドで秒針付きの時計を表示してみる。
$ xclock -update 1


  • タブを閉じると(command-W)、xclockも終了した。
  • 再びタブを開いて、xclockコマンドで秒針付きの時計を表示してみる。但し、コマンド先頭にnohudを付加する。
$ nohup xclock -update 1
appending output to nohup.out


  • すると今度はタブを閉じても(command-W)、xclockは終了しない!
タブを閉じても終了しない理由
  • タブを閉じると(command-W)、そのタブ環境下のジョブには一斉にSIGHUPシグナルが送信されるらしい。
  • SIGHUPは、端末の回線が切断したという合図。(端末を操作しても何も反応しなくなる状態=hang upがシグナル名の由来と思われる)
  • SIGHUPを受け取ったジョブは、一般的に終了する。(但し、必ずしも終了する訳ではない。実装によっては別の動作をすることもある)
  • 一方、nohupを付加して起動されたジョブは、SIGHUPを無視するように振る舞う。
  • SIGHUPを無視したジョブはそのまま起動し続けようとする。だから、終了しない。

nohupがなくてもOK

  • シェルの環境にもよるが、実はOSXデフォルトのbashは、ログアウトしてもバックグラウンドジョブをそのまま維持してくれる。
    • 但し、exitあるいはlogoutコマンドでログアウトした場合のみ。(ログアウトが完了するとタブは自動的に閉じる)
    • command-WなどGUI操作でタブを閉じた場合は終了してしまう。

実験

  • 先程のxclockコマンドをバックグラウンドジョブとして実行してみる。
$ xclock -update 1 &
  • そして、exitコマンドで終了してみる。
$ exit
  • ログアウト完了と同時にタブも自動的に閉じる*1のだけど、xclockは表示され続けるのだ!
  • もちろん、秒針もちゃんと動き続けている。

  • 一方、command-Wでタブを閉じた時は、残念ながら、xclockは終了してしまう...。
バックグラウンドジョブが終了しない理由
  • bashの設定項目であるhuponexitがオフ(デフォルト設定)になっていると、
$ shopt huponexit
huponexit      	off
  • ログアウトした時にSIGHUPシグナルは送信されなくなる。
      • 但し、command-WなどGUI操作でタブを閉じた場合は、SIGHUPシグナルは送信されるようだ。
      • exitあるいはlogoutコマンドでログアウトする時と、command-WなどGUI操作でタブを閉じる時で、送信されるシグナルが異なるのだ。

shopt huponexit=offの場合、ログアウトしてもSIGHUPシグナルが送信されない。だからジョブは終了しない。

  • 一方、huponexitがオンになっていると、
$ shopt -s huponexit
$ shopt huponexit
huponexit      	on
  • exitやlogoutでもSIGHUPシグナルは送信されてしまう。
  • つまり、ログアウトするとジョブは終了してしまう...。

nohupでもジョブが終了してしまう場合

  • ところで、nohubを付加しても、ジョブが終了してしまう場合がある。
  • nohupを付加して、xclockをフォアグラウンドジョブとして起動して、その後control-Zで一時停止しておく。
$ nohup xclock -update 1
appending output to nohup.out
^Z
[1]+  Stopped                 nohup xclock -update 1

$ exit
logout
There are stopped jobs.
  • この状態でログアウトしようとしても、stopped状態のジョブが存在するとして、ログアウトできない...。
  • そこでcommand-WなどのGUI操作で強引にタブ閉じてしまうと、xclockは終了してしまうのだ!

stopped状態のジョブは、nohupを付加しても終了してしまう。

nohupしたのにログアウトで終了してしまう理由
  • 実は、タブが閉じる時にジョブに送信されるシグナルは、SIGHUPだけではない。
  • stopped状態のジョブに対しては、SIGCONTとSIGTERMシグナルも送信されるらしい。
  • nohupコマンドが無視するのは、SIGHUPシグナルのみであり、
  • SIGCONTとSIGTERMシグナルについては無視されず、有効となるのだ。

stopped状態のジョブは、SIGTERMによって終了していたのだ。

nohup中の出力

  • nohupを付加した時のジョブが何らかの出力を伴う場合、その出力はnohup.outに追記されていく。
    • 標準出力・標準エラーの両方とも、デフォルトではカレントディレクトリのnohup.outに追記されるのだ。
    • "appending output to nohup.out"はそれを伝えてくれるメッセージだったのだ。
$ nohup date
appending output to nohup.out

$ cat nohup.out
2014年 12月15日 月曜日 15時57分33秒 JST

$ nohup date
appending output to nohup.out

$ cat nohup.out
2014年 12月15日 月曜日 15時57分33秒 JST
2014年 12月15日 月曜日 15時57分51秒 JST
  • もちろん、出力先に任意のファイルを指定することもできる。
$ nohup date > std.out

$ cat std.out
2014年 12月15日 月曜日 16時07分09秒 JST
  • 標準出力と標準エラーを分けることもできる。
$ nohup date -x > std.out 2> err.out

$ cat err.out
date: illegal option -- x
usage: date [-jnu] [-d dst] [-r seconds] [-t west] [-v[+|-]val[ymwdHMS]] ... 
            [-f fmt date | [[[mm]dd]HH]MM[[cc]yy][.ss]] [+format]

複数のコマンドをnohup

各コマンドの前にnohupを付加する方法
$ nohup command 1 & nohup command 2 &
  • command 1とcommand 2は、二つのバックグラウンドジョブとして並列処理される。
sh -c でスクリプトを渡す方法
$ nohup sh -c 'command 1 ; command 2' &
  • 一つのバックグラウンドジョブとして、command 1が完了してから、command 2が処理される。
$ nohup sh -c 'command 1 | command 2' &
  • 一つのバックグラウンドジョブとして、command 1とcommand 2をパイプで連携させる。

コマンド実行後にタブを閉じても終了しないジョブにする

  • nohupを付加しておけば、タブを閉じてもジョブは終了せず、処理は継続される。そのことはよく分かった。
  • nohupさえ付加しておけばすべてうまくいく。とても簡単なことだ。

しかし、それができない...。

  • コマンドを実行してなかなか処理が終わらずに、あとでnohupを付加しておけば良かったと後悔することがしばしば。
  • せっかく途中まで処理が進んでいるのに、nohupのために途中で終了して、最初からやり直すなんて...。

そんな後悔をしないために、disownコマンドを覚えておく!

  • コマンドを実行して、予想外に時間がかかると気付いたら、disownを実行しておくだけでOK。
    • 引数にはジョブを指定する。(複数指定OK)
    • 引数を省略すると、カレントジョブ(%%または%+)が指定されたことになる。
$ xclock -update 1 &
[1] 58303

$ disown
  • disownを実行するだけで、タブを閉じてもログアウトしても、このジョブは終了せずに処理を継続するのだ!
  • ちなみに-aオプションを指定すると、シェル管理配下のすべてのジョブを指定したことになる。
$ disown -a
disownコマンドは何をしているのか?
  • disownしたジョブは、シェルのジョブ管理テーブルから取り除かれるようだ。
    • だから、disownしてしまうとjobsコマンドに表示されなくなる。
    • fg・bgなどのコマンドで操作できなくなる。
  • タブを閉じた時のSIGHUPシグナルは、ジョブ管理テーブルに存在するジョブだけに送信されるらしく、
  • そのため、disownしたジョブには、タブを閉じてもSIGHUPシグナルが送信されない。
  • だからタブを閉じても終了しないのだ。

disownコマンドは、シェルの管理配下のジョブとの関係を断つ!

      • disownには「自分の所有物であると認めない」「縁を切る」「勘当する」といった意味合いがある。
      • その意味のとおり、まさにシェルが指定したジョブと縁を切った状態になるのだ。
disownしたその後
  • 一旦disownしてしまったジョブを、再びシェルの管理配下に組み入れる方法を、自分は知らない。
  • よって、disownしたジョブをやっぱり終了させたくなったら...

プロセスIDを指定してkillコマンドを実行するしかないのだ。

$ xclock -update 1 &
[1] 65595

$ disown

$ kill 65595
  • しかし、タブを閉じてしまったら、プロセスIDさえ分からないかも...

その場合はプロセス名を思い出してkillallコマンドを実行してみる。

$ xclock -update 1 &
[1] 72053

$ xclock -update 1 &
[2] 72158

$ killall xclock
[1]-  Terminated: 15          xclock -update 1
[2]+  Terminated: 15          xclock -update 1
    • killコマンドはプロセスIDを必要とするが、killallコマンドはプロセス名を指定するのだ。
    • 但し、一致するプロセス名が複数あると、そのすべてを終了してしまう...。
  • だからkillallする前に、指定した名前にヒットするプロセスを調べておいた方が良いかもしれない。
$ ps -ax|grep xclock
72581 ttys005    0:00.00 grep xclock
ジョブ管理テーブルに残したままdisownする
  • 以上のように、一旦シェルの管理配下から取り除かれてしまったジョブを操作するのは非常に面倒である。
  • そこで、-hオプションを指定してdisownコマンドを実行しておけば、
  • タブを閉じるまでは、そのシェルでジョブ管理を続けられるのだ!
$ xclock -update 1 &
[1] 74522

$ disown -h

$ jobs
[1]+  Running                 xclock -update 1 &

$ fg
xclock -update 1
^C
      • ジョブ管理を続けながらSIGHUPシグナルのみ無視するので、おそらくnohupコマンドと同等?
      • 結局タブを閉じてしまえばプロセスIDで管理するしかないのだけど、タブを開いている限りジョブ管理を続けられるのだ。


disownコマンドは-hオプションを指定しておいた方が幸せになれそう!

まとめ

  • exitあるいはlogoutでログアウトした場合は、バックグラウンドジョブはそのまま継続される。
$ xclock -update 1 &
$ exit
  • 但し、shopt huponexitがoffの場合(デフォルト設定)
    • `shopt -s huponexit`を実行すると、onになる。
    • `shopt -u huponexit`を実行すると、offになる。
$ shopt huponexit
huponexit      	off
  • タブを閉じてもジョブを継続したい場合は、nohupを付加して実行する。
$ nohup xclock -update 1 &
  • nohupを付加しても、stoppedなジョブは終了してしまう。
$ nohup xclock -update 1
appending output to nohup.out
^Z
[1]+  Stopped                 nohup xclock -update 1
  • 出力を伴うコマンドをnohupすると、カレントディレクトリのnohup.outに出力される。
$ nohup date
appending output to nohup.out

$ cat nohup.out
2014年 12月16日 火曜日 13時44分24秒 JST
  • コマンドを実行してから処理に時間がかかることに気付いたら...
  • control-Z、bg、disown -hによって、タブを閉じても継続するジョブとなる。
  • 特に理由がない限り、disownには-hオプションを指定しておく。
  • そうしておけば、タブを閉じるまではジョブ管理を続けられる。
$ xclock -update 1
^Z
[1]+  Stopped                 xclock -update 1

$ bg
[1]+ xclock -update 1 &

$ disown -h

$ fg
xclock -update 1

タブを閉じた後のプロセスの行方

  • ターミナル.appでタブを開くと、タブごとに対応するシェルが起動した環境になる。
  • そこで何らかのコマンドを実行すると、コマンドはシェルの子プロセスとして実行されることになる。

  • ところで、nohupやdisownされた子プロセスは、シェルが終了しても、処理を継続する。
  • すると、それらの子プロセスは、親なしのプロセスとなってしまう...。どうなってしまうのか?
  • 実はそのような場合、すべてのプロセスの源流であるプロセスID=1のlaunchdが里親となり、面倒を見てくれるのだ!

*1:タブが自動的に閉じる理由は、ターミナル.app >> 環境設定 >> 設定 >> シェル >> シェルの終了時: 「シェルが正常に終了した場合は閉じる」という設定のため。