HUPシグナルとnohupとdisownとバック/フォアグラウンドジョブの理解

人に教えるには自分が完全に理解していなければ、ということで調査・検証した結果。

## 動機

* シェルからバックグラウンドで処理を行うには？
  * `nohup コマンド実行 &` を使う。
  * コマンド実行 => Ctrl-Z => `disown` を使う。

定番は前者だが、両者の違いは何なのか？
disownとは一体何をするのか？

この機会に調べてみた。

## 結果のまとめ

長くなったので最初にまとめを。しかしまとめも長い。
（後自分が読む時に一番下にスクロールしたくないので…）

#### ジョブテーブル

* シェルはセッションに紐づくジョブテーブルを持っている。
  * バックグラウンドジョブを登録している。
  * (フォアグラウンドジョブは未確認。ソースを読むしかないか)
* `jobsビルトインコマンド` で一覧を参照可能。
  * **ここに見える限り、シェルのexit時にSIGHUPを送る。**
  * この一覧にない場合、exit時にSIGHUPは送らない。
  * **シェルオプションでnohupを指定した場合、ジョブテーブルに見えてもSIGHUPを送らない。**
  * ジョブテーブルから消すには、disownを使う。
  * fgやbgでフォアグラウンド、バックグラウンド化できる。(monitorオプション有効時）

#### nohupコマンド

* **SIGHUPを無視した状態でプロセスを起動する。**
* シェルは終了時にSIGHUPを送るかもしれないが、届いても無視する。`kill -SIGHUP`で直接送っても無視。
* **disownしない限りジョブテーブルに残る。** fg, bg, disown, Ctrl-Zなどを受け付ける。
* フォアグラウンドジョブに付与した場合でも、SIGHUPを受け付けずに継続する。
* (標準出力/エラー出力が端末の場合にnohup.outに出したりする)
* (尚、nohupを付けて実行すると、trapビルトインコマンドでもキャッチできない)

#### disownコマンド

* **ジョブテーブルから外れる。**
  * fg、bgコマンドを受け付けず、jobsで参照できない。
  * シェルの終了時にSIGHUPを送らない。
  * SIGHUPを`kill -SIGHUP`などで直接送ると受け付ける。（nohupしない限り）
* (フォアグラウンドジョブ実行状態で自分にdisownは打てないので、対バックグラウンド専用)

#### シェルのhup/nohupオプション

* **シェルの終了処理でジョブテーブルのジョブに対してSIGHUPを送るかどうか。**
  * disownでジョブテーブルから外れている場合はこれに関係なく送らない。
* **フォアグラウンドジョブでもhupオプションがセットされていれば、シェルの終了時にSIGHUPを送る。**
  * 端末が切れた場合にフォアグラウンドジョブにはカーネルからもSIGHUPが送られる。上記と合わせて２回のケースがある。
  * 逆に言えば、シェルオプションでnohupにしても、フォアグラウンドのジョブ実行中のまま端末を切るとカーネルがSIGHUPを送ってくる。

#### シェルのmonitorオプション

* bg, fg %ジョブ番号で制御できるかどうか。Ctrl-Zで停止できるかどうか。
    * ジョブテーブルに存在しても、monitorオプションを消すと受け付けない。
* **有効/無効に関わらずジョブテーブルは存在する。**
* 有効/無効に関わらずdisownは指定できる。
* **SIGHUPを送るかどうかには一切関係なかった。**



nohupとdisownは、SIGHUPを送られても無視するか、
シェルのexit時にHUPを送らないようにするかの違いと、
ジョブコントロールコマンドが効くかどうかの2点が主なものだった。
シェルオプションでnohupにしても、
フォアグラウンド実行中で端末を切るとカーネルからSIGHUPが飛ぶというスペシャルケースがある。

**nohupコマンドを前置して実行すればどちらのケースも防げる。nohup最強。**

以下検証の説明。

## バックグラウンド処理

バックグラウンド処理の実行。主に以下2つ。

1. コマンド実行時に&を付与する
2. コマンド実行後にCtrl-Zでsuspendさせて、jobsで確認し、bg %{ジョブ番号}を使う。
（zshなら、&!及び&|でも可能。）

## 途中で終了しないバックグラウンド処理

バックグラウンドでジョブを実行する場合、そのシェルをexitした後も動作継続して欲しいことが多い。
しかし、exitした後（もしくはexit時）に幾つかの原因で動作が意図せず終了することがある。

これを避けるため、nohup コマンド &で実行するのが定番となっている。

## バックグラウンドジョブが意図せず終了するケース

主にはSIGHUPが投げられるケース。詳しくはどのようなケースなのか。

http://www.glamenv-septzen.net/view/854

こちらに大変詳しい解説をされている方がいらっしゃいました。
本当に参考になりました。ありがとうございます。

私の理解でざっくり予期しない終了ケースを挙げると、

1. シェルをexitする。
  * **シェルの終了処理** で、バックグラウンドジョブにSIGHUPを送信。
2. ターミナルセッションが切れる。
  * 端末のデバイスドライバがセッションリーダープロセス（この場合ログインシェル）にSIGHUPを送信。
  * セッションリーダーのプロセスが終了する時、そのセッションリーダーの**フォアグラウンドジョブにカーネルからSIGHUPが送信される。**
  * ログインシェルの終了処理で、 **バックグラウンドジョブに対してSIGHUPを投げる。**

ターミナルセッションを切った場合にはフォアグラウンドもバックグラウンドもSIGHUPを受けることになる。
尚、STOP状態の場合にはシェルはSIGCONT/SIGTERMを投げるようです。
（が、こちらのケースは上記の解説がメインに扱っていらっしゃいますのでここでは割愛）

## SIGHUPを回避する

幾つか方法がある。

* disown

ジョブテーブルから削除する。
disown %{ジョブ番号}で指定。
ジョブの番号はjobsコマンドで出力される一覧で参照するとよい。

ジョブテーブルから削除されると、
fg、bg、disown などジョブ番号を指定するコマンドでの制御ができなくなる。

本当にSIGHUPされないかどうかは実験する。


* nohupコマンド

nohupはビルトインではなく、外部コマンド(/usr/bin/nohup)。
ハングアップしないようにしてコマンドを起動する。

(ビルトインになっているシェルもある？)

* シェルの終了処理でSIGHUPを送らないオプション

zshならば、

```zsh
setopt hup
setopt nohup
```

bashでは、

```bash
shopt -s huponexit  # set
shopt -u huponexit  # unset
````

これならば、nohupやdisownを使わなくても、
シェルのexit時にSIGHUPが送られることはない。

しかし、フォアグラウンドジョブ実行中にターミナルを切ると、
nohupオプションをセットしていてもカーネルからSIGHUPが飛ぶはず。

## 実験

ジョブテーブルから削除した場合にSIGHUPが送られるのかどうか、
シェルオプションでnohupにした場合の挙動など、
幾つか実験した。
Ubuntu14.04で、bash-4.3.11及びzsh-5.0.2の両方を使用。

シェルのhupオプション（終了時にSIGHUPを送る）状態
と、nohupオプション状態で、それぞれ以下を試す。

* コマンド & 実行 => シェルをexitする。 （ &実行 ）
* コマンド実行 => Ctrl-Z => bg => disown => シェルをexitする。 （ disown実行 ）
* nohup コマンド & => シェルをexitする。 （nohup 実行）
* nohup ＋ フォアグラウンドジョブ => 端末を切る。 (nohup フォア実行)
* フォアグラウンドジョブ => 端末を切る。 （フォア実行）

その後、SIGHUPを受け取ったかどうかを見る。
バックグラウンド実行の場合、可能であればjobsを打った結果（ジョブテーブル）
また、fg, bgを打ってみる。

* 実行スクリプト

適当にSIGHUPをキャッチして終了するものを。

```sh
#!/usr/bin/env bash

function catch_hungup(){
    date >> ./sig.log
    echo "SIGHUP recieved." >> ./sig.log
    exit 99
}

trap "catch_hungup" SIGHUP

for N in {01..30}
do
    date
    sleep 10
done
```

## 実験結果

* シェルオプションhup状態。

| ケース           | SIGHUPで終了したか |
|:-----------------|:-------------------|
| & 実行           | 終了               |
| disown 実行      | 終了しない         |
| nohup & 実行     | 終了しない         |
| フォア実行       | 終了               |
| nohup フォア実行 | 終了しない         |

* シェルオプションnohup状態

| ケース           | SIGHUPで終了したか |
|:-----------------|:-------------------|
| & 実行           | 終了しない         |
| disown 実行      | 終了しない         |
| nohup & 実行     | 終了しない         |
| フォア実行       | 終了               |
| nohup フォア実行 | 終了しない         |


disown実行やnohupを付けてバックグラウンドジョブを実行すればSIGHUPを無視できる。
フォアグラウンドのまま端末を切るとnohup付与で実行していなければSIGHUPが送信される。

#### SIGHUP以外に確認した項目

* disownすると、jobsでは見えなくなる。fg, bgも効かなくなる。
disown後もプロセスは起動端末に紐付いたまま（psコマンドのTTYの欄は変化なし）で、
今回のスクリプトだとdateコマンドの結果が出続けた。

* nohup コマンド &
nohupしてもjobsで見えるし、fg、bgも効いた。
この後さらにdisownも可能で、disownするとjobsの一覧から消えてfg, bgが効かなくなる。

* フォアグラウンドジョブ実行中のまま端末を切る

シェルオプションがhupの場合は２回、
シェルオプションがnohupの場合は１回のSIGHUPを受信した。
（2回受信は確実ではなく、今回の例ではsig.logに1回又はたまに２回分のログが出る。
dateコマンドの出力だけ２回という時もある)

シェルの終了処理でフォアグラウンドプロセスにSIGHUPしている。
（ジョブテーブルにフォアグラウンドも登録されるからなのか、それとも別の管理なのかは未調査）
シェル以外（カーネル）から送られていることも確認できた。

## monitorオプションとジョブコントロール

少しそれるが、bashやzshには「ジョブコントロールを行うかどうか」というmonitorオプションがある。
ということは、これを無効にすれば常にdisown状態でジョブテーブルから見えないのか？

気になったのでやってみた。

zshでは、

```zsh
setopt monitor
setopt nomonitor
```

bashでは、

```bash
set -m  # (monitorモード on)
set +m  # (monitorモード off)
```

デフォルトではmonitorモードなので、
nomonitorにセットして実行する。(hupオプションもセット)

* コマンド実行 => Ctrl-Z => bg => disown

...Ctrl-Zが効かなかった。
SIGTSTPが無効にされている？

なぜかzshではCtrl-Cで中断できたが、bashではそれすら不可能だった。
SIGINTに対する扱いの違いか。

* & を付けてバックグラウンド実行

fg, bgは効かない。
しかしjobsで参照できるし、
その後disownも効く。

また、exitするとSIGHUPされた。

他のケースも試したが大きな違いはfg/bgとCtrl-Zの挙動くらい。

ややこしいのだが、monitorオプションによるジョブコントロールは、
ジョブテーブルとdisownには関係なく、
SIGHUPの判断にも関わらない。

## 所感

合わせてnohupがどのようにSIGHUPを無視しているかを見た。

* http://www.glamenv-septzen.net/view/847
* またしてもこちらの解説が大変詳しく、参考になりました。

disownもnohupもこれでクリアになった。。。
が、普段気軽に使ってるくせに意外と長かった。。。。。

（何か間違い等ご指摘ありましたらコメントいただければ幸いです）