ジョブとは
- ジョブはシェルが管理するプログラムのまとまり
- ジョブはプロセスと関連付けられており、プロセスを抽象化したインタフェースをユーザに提供する
フォアグラウンドジョブ
- フォアグラウンドジョブはシェル上でユーザの入力を受け付ける形式で実行されるジョブ
- ユーザはフォアグラウンドジョブが完了するまで、その端末デバイス上で他のコマンドを実行できない
- ユーザがインタラクティブに操作する必要がある場合に多く使用される
バックグラウンドジョブ
- バックグラウンドジョブはシェル上でユーザの入力を受け付けない形式で実行されるジョブ
- 長時間の実行が必要な場合や、ユーザの介入が不要である場合に多く使用される
ジョブ制御の基本
- ジョブ制御で頻出の
jobs
、&
、fg
、bg
、Ctrl + z
、kill
の使い方
# コマンドの末尾に & を付けるとバックグラウンド実行される
# []内がジョブ番号、その隣の数値がプロセス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
# 新たに起動したシェルからコマンドをバックグラウンド実行
$ 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
シグナルが送信されなかったのだと推測している(未検証)
- シェルの終了後も端末デバイスと関連付けられたままになっており、おそらくそれが原因でシェルの終了時に
$ 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
シグナルを受け取ると終了するシェルスクリプト
hup.sh
#!/bin/bash
# trapコマンドは特定のシグナルを捕捉した際に、任意の処理を実行できる
# この場合はSIGHUPシグナルを捕捉した際に、echoコマンドとexitコマンドを実行している
trap "echo 'Caught SIGHUP'; exit" HUP
# 1秒毎に「Running ...」を出力し続ける
while true; do
sleep 1
echo "Running ..."
done
-
SIGHUP
シグナルを受け取っても終了しないシェルスクリプト
nohup.sh
#!/bin/bash
# hup.shと違い、exitしないようになっている
trap "echo 'Caught SIGHUP'" HUP
while true; do
sleep 1
echo "Running ..."
done
- それぞれのシェルスクリプトの実行権限を付与する
$ chmod +x ./hup.sh
$ chmod +x ./nohup.sh
パターン1: SIGHUP
シグナルが送信され、ジョブが終了するケース
# シェルの終了時に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
パターン2: SIGHUP
シグナルが送信されないケース
# シェルの終了時に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
パターン3: SIGHUP
シグナルが送信されたが、ジョブが無視したケース
# シェルの終了時に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シグナルを受信しないようにすることで、シェルが閉じられてもプロセスが継続して実行されるようにする
# nohupはプロセスの起動時に利用する
$ nohup sleep 1000 &
[1] 96964
nohup: ignoring input and appending output to 'nohup.out'
# ジョブの確認
$ jobs
[1]+ Running nohup sleep 1000 &
disown
- disownはジョブテーブルからジョブを削除することで、シェルが閉じられてもプロセスが継続して実行されるようにする
$ sleep 2000 &
[1] 97412
# ジョブの確認
$ jobs
[1]+ Running sleep 2000 &
# disownはジョブの起動後に利用する
$ disown %1
# ジョブテーブルから削除された
$ jobs