シェルのジョブ制御に慣れる

# ジョブとは

- ジョブはシェルが管理するプログラムのまとまり
- ジョブはプロセスと関連付けられており、プロセスを抽象化したインタフェースをユーザに提供する

## フォアグラウンドジョブ

- フォアグラウンドジョブはシェル上でユーザの入力を受け付ける形式で実行されるジョブ
- ユーザはフォアグラウンドジョブが完了するまで、その端末デバイス上で他のコマンドを実行できない
- ユーザがインタラクティブに操作する必要がある場合に多く使用される

## バックグラウンドジョブ

- バックグラウンドジョブはシェル上でユーザの入力を受け付けない形式で実行されるジョブ
- 長時間の実行が必要な場合や、ユーザの介入が不要である場合に多く使用される

# ジョブ制御の基本

- ジョブ制御で頻出の`jobs`、`&`、`fg`、`bg`、`Ctrl + z`、`kill`の使い方

```bash
# コマンドの末尾に & を付けるとバックグラウンド実行される
# []内がジョブ番号、その隣の数値がプロセスIDを示す
$ sleep 1000 &
[1] 118792

# バックグラウンドジョブをまとめて複数実行する場合
$ sleep 2000 & sleep 3000 &
[2] 118870
[3] 118871

# ジョブ番号の隣に表示されている + はデフォルトのジョブ、 - は次にデフォルトになるジョブ
# デフォルトのジョブはfgコマンドやbgコマンドで引数を指定せずに実行した場合に対象となる
$ jobs
[1]   Running                 sleep 1000 &
[2]-  Running                 sleep 2000 &
[3]+  Running                 sleep 3000 &

# ジョブ番号1をフォアグラウンド実行に戻す
# その後、 Ctrl + z で一時停止する
$ fg 1
sleep 1000
^Z
[1]+  Stopped                 sleep 1000

# ジョブ番号1が一時停止に変更された
$ jobs
[1]+  Stopped                 sleep 1000
[2]   Running                 sleep 2000 &
[3]-  Running                 sleep 3000 &

# ジョブ番号1をバックグラウンド実行に戻す
$ bg 1
[1]+ sleep 1000 &

# 一時停止からバックグラウンド実行に変更された
$ jobs
[1]   Running                 sleep 1000 &
[2]-  Running                 sleep 2000 &
[3]+  Running                 sleep 3000 &

# ジョブ番号1を終了する
# デフォルトではSIGTERM(シグナル番号15)が送信される
# それでも終了しない場合はSIGKILL(シグナル番号9)を送信することで強制終了させることも可能
$ kill %1
[1]   Terminated              sleep 1000

# シグナルを明示的に指定する場合
$ kill -15 %1
$ kill -SIGTERM %1

# ジョブ番号1が終了された
$ jobs
[2]-  Running                 sleep 2000 &
[3]+  Running                 sleep 3000 &
```

# シェルが管理するジョブの範囲

- シェルは子プロセスはジョブとして管理するが、孫プロセスはジョブとして管理しない

```bash
# シェルからシェルを起動
$ bash

# 新たに起動したシェルからコマンドをバックグラウンド実行
$ sleep 1000 &
[1] 101302

# 新たに起動したシェルからはバックグラウンド実行したコマンドがジョブとして認識されている
$ jobs
[1]+  Running                 sleep 1000 &

# 新たに起動したシェルを一時停止して抜ける
$ suspend
[1]+  Stopped                 bash

# 元のシェルからはバックグラウンド実行したコマンドが見えない
$ jobs
[1]+  Stopped                 bash
```

# シェル終了時の挙動

- シェルの終了時にそのシェルから`SIGHUP`シグナルが送信されるかどうかや、そのシェルから起動されたジョブが受け取った`SIGHUP`シグナルをどう処理するかによって、シェル終了時におけるジョブの挙動は変わる

## 検証環境

- OS(ディストリビューション)は`Ubuntu Server 22.04 LTS`でユーザは`ubuntu`で検証
- ログインシェルでかつ、端末デバイスはttyで検証
- ログインシェルでない場合や、端末デバイスがpts(擬似端末、psedo-terminal)の場合は上手く動作しなかった
    - シェルの終了後も端末デバイスと関連付けられたままになっており、おそらくそれが原因でシェルの終了時に`SIGHUP`シグナルが送信されなかったのだと推測している(未検証)

```bash
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=22.04
DISTRIB_CODENAME=jammy
DISTRIB_DESCRIPTION="Ubuntu 22.04.1 LTS"

$ whoami
ubuntu

$ tty
/dev/tty1
```

## 準備

- `SIGHUP`シグナルを受け取ると終了するシェルスクリプト

```bash:hup.sh
#!/bin/bash

# trapコマンドは特定のシグナルを捕捉した際に、任意の処理を実行できる
# この場合はSIGHUPシグナルを捕捉した際に、echoコマンドとexitコマンドを実行している
trap "echo 'Caught SIGHUP'; exit" HUP

# １秒毎に「Running ...」を出力し続ける
while true; do
    sleep 1
    echo "Running ..."
done
```

- `SIGHUP`シグナルを受け取っても終了しないシェルスクリプト

```bash:nohup.sh
#!/bin/bash

# hup.shと違い、exitしないようになっている
trap "echo 'Caught SIGHUP'" HUP

while true; do
    sleep 1
    echo "Running ..."
done
```

- それぞれのシェルスクリプトの実行権限を付与する

```bash
$ chmod +x ./hup.sh
$ chmod +x ./nohup.sh
```

## パターン１: `SIGHUP`シグナルが送信され、ジョブが終了するケース

```bash
# シェルの終了時にSIGHUPシグナルを送信する
# ちなみにshoptはbash固有のコマンド
$ shopt -s huponexit

# 設定がオンになっていることを確認
$ shopt huponexit
huponexit      	on

# SIGHUPシグナルを受け取ると終了するシェルスクリプトの起動
# > /dev/null で標準出力を破棄し、 2>&1 で標準エラー出力を標準出力にリダイレクトしている
# こうしておかないとバックグラウンドで実行していても出力がシェルに流れる
$ ./hup.sh > /dev/null 2>&1 &
[1] 122400

# ジョブの確認
$ jobs
[1]+  Running                 ./hup.sh > /dev/null 2>&1 &

# シェルから抜ける
$ exit

# シェルの終了時にSIGHUPシグナルが送信され、ジョブが終了している
$ ps 122400
    PID TTY      STAT   TIME COMMAND
```

## パターン２: `SIGHUP`シグナルが送信されないケース

```bash
# シェルの終了時にSIGHUPシグナルを送信しない
$ shopt -u huponexit

# 設定がオフになっていることを確認
$ shopt huponexit
huponexit      	off

# SIGHUPシグナルを受け取ると終了するシェルスクリプトの起動
$ ./hup.sh > /dev/null 2>&1 &
[1] 121852

# ジョブの確認
$ jobs
[1]+  Running                 ./hup.sh > /dev/null 2>&1 &

# シェルから抜ける
$ exit

# シェルの終了時にSIGHUPシグナルが送信されなかったため、ジョブは終了していない
# TTYが ? になっているのはプロセスが端末デバイスから切り離されたため
$ ps 121852
    PID TTY      STAT   TIME COMMAND
 121852 ?        S      0:00 /bin/bash ./hup.sh

# シェルの終了後、親プロセスIDが1になっており、STATもSになっていることから、ゾンビプロセスにならずinitプロセスに回収されたことが分かる
$ ps -o pid,ppid,cmd -p 121852
    PID    PPID CMD
 121852       1 /bin/bash ./hup.sh
```

## パターン３: `SIGHUP`シグナルが送信されたが、ジョブが無視したケース

```bash
# シェルの終了時にSIGHUPシグナルを送信する
$ shopt -s huponexit

# 設定がオンになっていることを確認
$ shopt huponexit
huponexit      	on

# SIGHUPシグナルを受け取っても終了しないシェルスクリプトの起動
$ ./nohup.sh > /dev/null 2>&1 &
[1] 204427

# ジョブの確認
$ jobs
[1]+  Running                 ./nohup.sh > /dev/null 2>&1 &

# シェルから抜ける
$ exit

# シェルの終了時にSIGHUPシグナルが送信されたがジョブが無視したため、ジョブは終了していない
$ ps 204427
    PID TTY      STAT   TIME COMMAND
 204427 ?        S      0:00 /bin/bash ./nohup.sh

# シェルの終了後、親プロセスIDが1になっており、STATもSになっていることから、ゾンビプロセスにならずinitプロセスに回収されたことが分かる
$ ps -o pid,ppid,cmd -p 204427
    PID    PPID CMD
 204427       1 /bin/bash ./nohup.sh
```

# プロセスをシェルから切り離す

- シェルの設定やジョブの実装によって`SIGHUP`シグナルが送信されるのかや、それをどう扱うかというのが異なる
- そこで`nohup`や`disown`を使用することで、ジョブが`SIGHUP`シグナルの影響を受けず、シェルの終了後も動作し続けることを保証することができる

## `nohup`

- nohupはプロセスがSIGHUPシグナルを受信しないようにすることで、シェルが閉じられてもプロセスが継続して実行されるようにする

```bash
# nohupはプロセスの起動時に利用する
$ nohup sleep 1000 &
[1] 96964
nohup: ignoring input and appending output to 'nohup.out'

# ジョブの確認
$ jobs
[1]+  Running                 nohup sleep 1000 &
```

## `disown`

- disownはジョブテーブルからジョブを削除することで、シェルが閉じられてもプロセスが継続して実行されるようにする

```bash
$ sleep 2000 &
[1] 97412

# ジョブの確認
$ jobs
[1]+  Running                 sleep 2000 &

# disownはジョブの起動後に利用する
$ disown %1

# ジョブテーブルから削除された
$ jobs
```