nohupコマンドとは、端末を閉じたときに届くSIGHUP(ハングアップシグナル)を無視する設定でコマンドを起動し、ログアウト後も処理を続けさせるコマンドです。 末尾に & を付けてバックグラウンド実行と組み合わせ、nohup ./batch.sh > batch.log 2>&1 & の形で使うのが基本形です。

この記事では、「& だけ」「nohup」「disown」「setsid」「tmux」の5つで、端末を閉じた後にプロセスが残るかを Ubuntu 24.04 で実際に試した結果を比較表にしました。あわせて jobs・fg・bg・Ctrl+Z によるジョブ操作の流れ、nohup を付け忘れたときの対処、nohup.out の出力先変更、PID を保存した止め方、huponexit の設定、systemd との使い分けまでを、コピーして使えるコマンドと出力つきでまとめます。

検証環境は次のとおりです。

項目 バージョン
OS Ubuntu 24.04.4 LTS
シェル GNU bash 5.2.21
nohup GNU coreutils 9.4
tmux 3.4
setsid util-linux(Ubuntu 24.04 標準)

nohupとバックグラウンド実行の早見表

まず、やりたいことから書き方を引ける一覧です。

やりたいこと コマンド 補足
バックグラウンドで実行する コマンド & 端末を閉じると終了する
ログアウト後も続ける(基本形) nohup コマンド > out.log 2>&1 & 出力先を自分で決める形
出力先を指定しない nohup コマンド & カレントディレクトリの nohup.out に追記
出力を捨てる nohup コマンド > /dev/null 2>&1 & ログが不要なとき
PID を保存する echo $! > app.pid 起動直後に実行する
PID で止める kill "$(cat app.pid)" まず SIGTERM で止める
ジョブ一覧を見る jobs / jobs -l -l は PID つき
実行中のコマンドを一時停止 Ctrl+Z 状態が Stopped になる
停止中のジョブを裏で再開 bg %1 番号省略時はカレントジョブ(+)
ジョブを手前に戻す fg %1 Ctrl+C で止められるようになる
ジョブを止める kill %1 % を付けるとジョブ番号の指定
nohup を付け忘れた Ctrl+Z → bg → disown -h %1 起動済みのジョブを SIGHUP から外す
ジョブ表から外す disown %1 jobs に出なくなる
新しいセッションで起動する setsid コマンド > out.log 2>&1 端末から切り離して起動
画面ごと残す tmux new -s work 後から tmux attach -t work で戻れる
複数コマンドをまとめる nohup bash -c 'a && b' > out.log 2>&1 & nohup が受け取るのは1コマンド
exit 時にジョブへ SIGHUP を送る shopt -s huponexit ログインシェルで有効

次に、この記事の中心になる「端末を閉じたあと、プロセスは残るか」の比較表です。tmux で作った疑似端末の中で各方法を使って sleep を起動し、端末(tmux のウィンドウ)を閉じる操作と、exit で抜ける操作をそれぞれ行って確認しました。

起動方法 端末を閉じる(SIGHUP が届く) exit で抜ける 端末への出力を続けた場合
sleep 1000(フォアグラウンド) 終了 (入力待ちにならない) ―
sleep 1000 & 終了 残る ―
nohup sleep 1000 & 残る 残る nohup.out などファイルへ書かれる
sleep 1000 & disown 残る 残る 次の出力で Input/output error
sleep 1000 & disown -h 残る 残る 次の出力で Input/output error
setsid sleep 1000 残る 残る 次の出力で Input/output error
(sleep 1000 &) 残る 残る 次の出力で Input/output error
setsid nohup sleep 1000 > /dev/null 2>&1 & 残る 残る ファイルへ書かれる
shopt -s huponexit + sleep 1000 &(ログインシェル) 終了 終了 ―
tmux セッション内で sleep 1000 残る デタッチなら残る tmux の画面にそのまま残る

ポイントは2つです。「& だけ」は exit で抜ければ残りますが、端末を閉じると終了します。そして disown や setsid は SIGHUP からは守れますが、出力先が端末のままだと次の出力でエラーになるため、リダイレクトとセットで使います。 以降の章で、この表の根拠となる実行結果を順に示します。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「nohupコマンドの使い方|バックグラウンド実行・jobs・fg・bg」

バックグラウンド実行の基本(&・jobs・fg・bg・Ctrl+Z)

nohup を理解する前提として、シェルのジョブ制御を押さえます。ジョブとは、シェルが管理している「1行のコマンド(パイプでつないだ場合はそのまとまり)」のことです。

& を付けてバックグラウンドで実行する

コマンドの末尾に & を付けると、シェルはコマンドの終了を待たずにプロンプトを返します。

$ sleep 300 &
[1] 6590
$ sleep 400 &
[2] 6596

[1] がジョブ番号、6590 がPID(プロセスID)です。ジョブ番号はそのシェルの中だけで通じる番号、PID はシステム全体で通じる番号という違いがあります。

jobs でジョブの一覧を見る

$ jobs
[1]-  Running                 sleep 300 &
[2]+  Running                 sleep 400 &
$ jobs -l
[1]-  6590 Running                 sleep 300 &
[2]+  6596 Running                 sleep 400 &
表示 意味
[1] ジョブ番号。%1 で指定する
+ カレントジョブ。fg や bg で番号を省略したときの対象
- 1つ前のジョブ。%- で指定できる
Running 実行中
Stopped 一時停止中(Ctrl+Z または SIGSTOP)
Done 正常に完了した
Terminated シグナルで終了した

jobs の主なオプションは次のとおりです。

オプション 内容
jobs -l PID も表示する
jobs -p PID だけを表示する
jobs -r 実行中のジョブだけ
jobs -s 停止中のジョブだけ

Ctrl+Z で一時停止し、bg で裏に回す

フォアグラウンドで動かしたコマンドを Ctrl+Z で一時停止すると、ジョブとして登録されます。

$ sleep 500
^Z
[3]+  Stopped                 sleep 500
$ jobs
[1]   Running                 sleep 300 &
[2]-  Running                 sleep 400 &
[3]+  Stopped                 sleep 500
$ bg %3
[3]+ sleep 500 &
$ jobs
[1]   Running                 sleep 300 &
[2]-  Running                 sleep 400 &
[3]+  Running                 sleep 500 &

Ctrl+Z の直後は Stopped(止まったまま)です。 bg を実行してはじめてバックグラウンドで再開されます。「Ctrl+Z を押したのに処理が進まない」というときは、jobs で Stopped になっていないかを確認すると原因がすぐわかります。

fg で手前に戻し、Ctrl+C で止める

$ fg %1
sleep 300
^C

fg %1 でジョブ1がフォアグラウンドに戻り、Ctrl+C(SIGINT)で止められるようになります。

kill %1 でジョブを止める

$ kill %2
$ jobs
[2]-  Terminated              sleep 400
[3]+  Running                 sleep 500 &
$ jobs
[3]+  Running                 sleep 500 &

kill %2 の % はジョブ番号を表す記号です。 kill 2 と書くと PID 2 のプロセスを指してしまうため、ジョブを止めるときは % を付けます。終了の通知(Terminated)は1回表示されると、次の jobs からは消えます。

kill -STOP %3 で一時停止(Ctrl+Z と同じ状態)、kill -CONT %3 または bg %3 で再開もできます。

$ kill -STOP %3
[3]+  Stopped                 sleep 500
$ kill %3; fg %3
bash: fg: job has terminated
[3]+  Terminated              sleep 500

停止中のジョブに kill を送ると、bash は SIGTERM のあとに SIGCONT も送ってジョブを終了させます。上の例では終了済みのジョブを fg しようとしたため、job has terminated と表示されました。

ジョブ指定(jobspec)の書き方

書き方 指すもの
%1 ジョブ番号1
%% または %+ カレントジョブ
%- 1つ前のジョブ
%sleep コマンドが sleep で始まるジョブ
%?400 コマンド文字列に 400 を含むジョブ

ジョブ操作の流れを図で見る

stateDiagram-v2
    [*] --> フォアグラウンド: コマンド
    [*] --> バックグラウンド: コマンド &
    フォアグラウンド --> 停止中: Ctrl+Z
    停止中 --> バックグラウンド: bg
    停止中 --> フォアグラウンド: fg
    バックグラウンド --> フォアグラウンド: fg
    フォアグラウンド --> [*]: Ctrl+C
    バックグラウンド --> [*]: kill %番号
    バックグラウンド --> ジョブ表の外: disown

なぜ端末を閉じるとプロセスが終わるのか

端末を閉じると、カーネルはその端末を制御端末にしているセッションのリーダー(通常はシェル)へ SIGHUP を送ります。bash は SIGHUP を受け取ると、終了する前に自分のジョブ表にある全ジョブへ SIGHUP を転送します。 SIGHUP を受けたプロセスは、特に設定がなければ終了します。これが「& だけでは端末を閉じると終わる」理由です。

この仕組みから、プロセスを残す方法は次の3系統に整理できます。

系統 方法 仕組み
SIGHUP を無視する nohup プロセス側で SIGHUP を「無視」に設定してから起動する
シェルが SIGHUP を送らないようにする disown、disown -h、(コマンド &) ジョブ表から外す、または送信対象から外す
端末と切り離す setsid、tmux、systemd 別のセッション・別の端末・別の管理者のもとで動かす

nohup が SIGHUP を無視に設定していることは、/proc/PID/status の SigIgn(無視しているシグナルのビット列)で確認できます。

$ sleep 801 & A=$!
$ nohup sleep 802 > /dev/null 2>&1 & B=$!
$ grep SigIgn /proc/$A/status /proc/$B/status
/proc/7192/status:SigIgn:   0000000000000000
/proc/7193/status:SigIgn:   0000000000000001

末尾の 1 が SIGHUP(シグナル番号1)のビットです。両方に SIGHUP を直接送ると、nohup を付けたほうだけが残りました。

$ kill -HUP $A $B
/bin/bash: line 14:  7192 Hangup                  sleep 801
$ ps -o pid,stat,args -p $A,$B
  PID STAT COMMAND
 7193 S    sleep 802

端末を閉じた後に残るかを試す(& / nohup / disown / setsid / tmux)

冒頭の比較表のもとになった検証手順と結果です。手元でも同じ手順で再現できます。

検証の方法

tmux で検証専用のサーバーを立て、ウィンドウ1つを「1つの端末」とみなします。各ウィンドウの bash でコマンドを起動したあと、tmux kill-window でウィンドウを閉じます。これは端末エミュレーターのウィンドウを閉じたり、SSH 接続が切れたりしたときと同じく、シェルに SIGHUP が届く操作です。

# 検証用の tmux サーバー(ソケット名 t151)を用意
tmux -L t151 -f /dev/null new-session -d -s base 'sleep 100000'

# 例: 「& だけ」を試す
tmux -L t151 new-window -d -n a 'bash --norc --noprofile'
tmux -L t151 send-keys -t a 'sleep 7101 &' Enter
sleep 1
tmux -L t151 kill-window -t a        # 端末を閉じる
sleep 1
ps -eo ppid,tty,args | grep 'sleep 7101$' | grep -v grep || echo '終了'

exit で抜ける場合は、kill-window の代わりに send-keys -t a 'exit' Enter でシェルを終了させました。

結果1:端末を閉じた場合(SIGHUP が届く)

== mode=close
& のみ: 終了
nohup &: 残る      1 ?        sleep 7102
& disown: 残る      1 ?        sleep 7103
setsid: 残る      1 ?        sleep 7104
huponexit+&: 終了
& disown -h: 残る      1 ?        sleep 7106
(cmd &): 残る      1 ?        sleep 7107
setsid nohup: 残る      1 ?        sleep 7108
foreground: 終了

残ったプロセスは、親プロセス(PPID)が 1 に、端末(TTY)が ? に変わっています。 親のシェルが終了したため PID 1 に引き取られ、制御端末もなくなった状態です。

結果2:exit で抜けた場合(SIGHUP は届かない)

== mode=exit
& のみ: 残る      1 ?        sleep 7201
nohup &: 残る      1 ?        sleep 7202
& disown: 残る      1 ?        sleep 7203
setsid: 残る      1 ?        sleep 7204
huponexit+&: 残る      1 ?        sleep 7205
& disown -h: 残る      1 ?        sleep 7206
(cmd &): 残る      1 ?        sleep 7207
setsid nohup: 残る      1 ?        sleep 7208

exit や Ctrl+D でシェルを終了した場合、bash は既定ではジョブに SIGHUP を送りません。そのため「& だけ」でも残りました。 「nohup を付けなくても残った」という経験と、「付けなかったら消えた」という経験の両方があるのは、この終了方法の違いによるものです。SSH の接続が回線の都合で切れた場合は「端末を閉じる」側の動きになるため、長時間の処理には nohup などを付けておくと安心です。

なお、この結果の huponexit+& は非ログインシェルで試したため残っています。ログインシェルでの結果は後述の「huponexit の設定」の章で示します。

結果3:端末に出力し続けるプロセスの場合

sleep は何も出力しませんが、実際のバッチやサーバーは標準出力にログを書きます。そこで、1秒ごとに標準出力へ1行書き、同時に別ファイルへ記録を残す Python スクリプトで試しました。

# out.py(Python 3.11)
import sys, time
n = int(sys.argv[1])
log = open(f"/tmp/t151_out_{n}.log", "w")
try:
    for i in range(1, 9):
        print("line", i, flush=True)          # 標準出力(端末)へ
        log.write(f"wrote line {i}\n"); log.flush()
        time.sleep(1)
    log.write("finished\n")
except Exception as e:
    log.write(f"stopped: {e!r}\n")
log.close()

起動から約2秒後に端末を閉じた結果です。

起動方法 記録ファイルの内容 結果
python3 out.py 1 & disown wrote line 2 のあと stopped: OSError(5, 'Input/output error') 出力時に停止
setsid python3 out.py 2 wrote line 2 のあと stopped: OSError(5, 'Input/output error') 出力時に停止
nohup python3 out.py 3 > o3.txt 2>&1 & wrote line 8、finished 最後まで完了
python3 out.py 4 > o4.txt 2>&1 & disown wrote line 8、finished 最後まで完了

disown や setsid で SIGHUP を避けても、標準出力が閉じた端末を向いたままだと、次に書き込んだ時点で Input/output error(EIO)になります。 nohup は標準出力が端末のとき自動でファイルへ切り替えてくれるため、この点でも扱いやすい方法です。disown や setsid を使うときは、> out.log 2>&1 を付けて出力先をファイルにしておくと最後まで動きます。

結果4:tmux の中で動かした場合

tmux のセッション内で sleep 7401 を動かし、tmux クライアントを表示していた外側の端末を閉じました。

 6006     1 ?        tmux -L t151in -f /dev/null new -s work
 6149  6008 pts/14   sleep 7401
work: 1 windows (created Fri Oct  2 10:20:36 2026)

sleep 7401 は tmux サーバーが持つ疑似端末 pts/14 につながったまま動き続け、セッション work も残っています。tmux は「プロセスが端末ごと残る」ため、出力先を変えなくても画面の内容がそのまま保たれ、tmux attach -t work で続きから操作できます。 tmux の詳しい操作は、当サイトの「tmuxの使い方|キーバインド早見表とコピペ用.tmux.conf」で扱っています。

nohupコマンドの使い方

書式とオプション

nohup コマンド [引数...]

GNU coreutils の nohup が持つオプションは --help と --version の2つだけです。動作は次の表のとおりで、覚えるのは「何が自動で行われるか」です。

状況 nohup が自動で行うこと 表示されるメッセージ
標準入力が端末 読み取れないファイルに切り替える(入力を無視) ignoring input
標準出力が端末 nohup.out に追記する appending output to 'nohup.out'
標準エラー出力が端末 標準出力と同じ先へ向ける redirecting stderr to stdout
nohup.out に書けない $HOME/nohup.out に追記する appending output to '/home/ユーザー/nohup.out'

4行目の $HOME/nohup.out への切り替えは、root で検証したため手元では再現できず、GNU coreutils の公式ドキュメントで確認した内容です。

また、nohup 自体はコマンドをバックグラウンドにしません。 プロンプトをすぐ返してほしいときは、自分で & を付けます。

基本:nohup コマンド &

検証用に、1秒ごとに1行出力するスクリプトを用意します。

#!/bin/bash
# tick.sh:1秒ごとに1行出力するテスト用スクリプト
i=0
while true; do
  i=$((i+1))
  echo "tick $i $(date +%T)"
  sleep 1
done
$ chmod +x tick.sh
$ nohup ./tick.sh &
[1] 6816
$ nohup: ignoring input and appending output to 'nohup.out'
$ cat nohup.out
tick 1 10:21:11
tick 2 10:21:12
tick 3 10:21:13
tick 4 10:21:14
$ kill %1
[1]+  Terminated              nohup ./tick.sh

nohup: ignoring input and appending output to 'nohup.out' はエラーではなく案内のメッセージです。プロンプトのあとに割り込んで表示されるため、入力待ちのように見えますが、Enter を押せば通常のプロンプトに戻ります。

nohup.out の出力先を変える(> と 2>&1)

出力先を自分で決めるには、リダイレクトを書きます。

$ nohup ./tick.sh > tick.log 2>&1 &
[1] 6889
$ tail -n 2 tick.log
tick 2 10:21:17
tick 3 10:21:18
書き方 標準出力 標準エラー出力 nohup.out
nohup cmd & nohup.out nohup.out 作られる
nohup cmd > out.log & out.log out.log(自動) 作られない
nohup cmd > out.log 2>&1 & out.log out.log 作られない
nohup cmd > out.log 2> err.log & out.log err.log 作られない
nohup cmd >> out.log 2>&1 & out.log に追記 out.log に追記 作られない
nohup cmd > /dev/null 2>&1 & 捨てる 捨てる 作られない

2>&1 は「標準エラー出力(2)を、標準出力(1)と同じ先へ向ける」という意味です。順序が大切で、> out.log 2>&1 と、ファイルの指定を先に書きます。 2>&1 > out.log の順にすると、標準エラー出力は切り替え前の出力先(端末)を向いたままになります。

2>&1 を省略した場合の動きも確認しました。

$ nohup ./tick.sh > tick.log &
[1] 7052
$ nohup: ignoring input and redirecting stderr to stdout

標準エラー出力が端末のままだったため、nohup が自動で標準出力と同じ先へ向けています。結果は同じですが、2>&1 を明示しておくとメッセージが出ず、スクリプトを読む人にも意図が伝わります。

ログが大きくなる処理では、日付をファイル名に入れておくと整理しやすくなります。

nohup ./batch.sh > "batch_$(date +%Y%m%d_%H%M%S).log" 2>&1 &

パイプや複数コマンドと組み合わせる

nohup が SIGHUP から守るのは、直後に書いた1つのコマンドです。&& やパイプでつないだ全体を守りたいときは、bash -c でまとめます。

$ nohup bash -c 'echo step1 && sleep 1 && echo step2' > chain.log 2>&1 &
$ cat chain.log
step1
step2

パイプの左側だけに nohup を付けた場合、標準出力はパイプに流れるため nohup.out は作られません。

$ nohup ./tick.sh | head -n 2
nohup: ignoring input and redirecting stderr to stdout
tick 1 10:21:26
tick 2 10:21:27

シェル関数・エイリアスには bash -c を使う

nohup は外部コマンドとして実行ファイルを探すため、シェル関数やエイリアス、cd のような組み込みコマンドはそのままでは起動できません。

$ hello() { echo hi; }
$ nohup hello
nohup: failed to run command 'hello': No such file or directory
$ echo "exit=$?"
exit=127

関数を使いたいときは、スクリプトファイルにするか、nohup bash -c '...' の中に処理を書きます。

nohup の終了ステータス

終了ステータス 意味
125 nohup 自体の問題(引数の誤りなど)
126 コマンドは見つかったが起動できなかった
127 コマンドが見つからなかった
それ以外 起動したコマンドの終了ステータス

引数なしで実行すると次のように表示されます。

$ nohup
nohup: missing operand
Try 'nohup --help' for more information.

nohupを付け忘れたとき(Ctrl+Z → bg → disown)

すでにフォアグラウンドで動かしてしまった処理も、止めずにあとから SIGHUP の対象から外せます。 手順は3ステップです。

$ sleep 600            # nohup も & も付けずに起動してしまった
^Z                     # 1. Ctrl+Z で一時停止
[1]+  Stopped                 sleep 600
$ bg                   # 2. バックグラウンドで再開
[1]+ sleep 600 &
$ jobs -l
[1]+  6763 Running                 sleep 600 &
$ disown -h %1         # 3. SIGHUP を送らない印を付ける
$ jobs
[1]+  Running                 sleep 600 &

disown -h はジョブ表に残したまま「シェルが SIGHUP を受けても、このジョブには転送しない」という印を付けます。jobs で引き続き確認でき、fg で戻すこともできます。

-h なしの disown %1 は、ジョブ表から取り除きます。

$ disown %1
$ jobs
$ ps -o pid,ppid,stat,tty,args -C sleep | grep 'sleep 600'
 6763  6575 S    pts/13   sleep 600

jobs には出なくなりましたが、プロセスは動き続けています。以降は PID(ここでは 6763)で管理します。

書き方 動き
disown カレントジョブをジョブ表から外す
disown %1 ジョブ1をジョブ表から外す
disown -h %1 ジョブ表に残したまま、SIGHUP の転送対象から外す
disown -a すべてのジョブを外す
disown -r 実行中のジョブだけを外す
disown -ah すべてのジョブに SIGHUP を転送しない印を付ける

付け忘れ対応で気をつけたいのは出力先です。 先ほどの検証のとおり、端末に出力し続ける処理は、端末を閉じたあとの最初の出力で Input/output error になります。出力の多い処理を確実に最後まで動かしたいときは、可能であれば一度止めて nohup ... > out.log 2>&1 & で起動し直すか、次回から tmux の中で動かす方法が向いています。

nohup と disown の違いを整理します。

比較 nohup disown
種類 外部コマンド(coreutils) bash の組み込みコマンド
使うタイミング 起動するとき 起動したあと
仕組み プロセスが SIGHUP を無視する シェルが SIGHUP を転送しない
出力の切り替え 端末なら自動でファイルへ 行わない(自分でリダイレクト)
kill -HUP PID を直接送ったとき 残る 終了する
使えるシェル どのシェルでも bash、zsh など

setsidで端末から切り離して起動する

setsid は、コマンドを新しいセッションで起動します。セッションとは、1つの端末にひもづくプロセスのまとまりのことです。新しいセッションには制御端末がないため、元の端末が閉じても SIGHUP は届きません。

$ setsid sleep 803
$ ps -o pid,ppid,pgid,sid,tty,args -C sleep
  PID  PPID  PGID   SID TT       COMMAND
 7209     1  7209  7209 ?        sleep 803

PID・PGID(プロセスグループID)・SID(セッションID)がすべて同じ値になり、TT が ?(制御端末なし)、PPID が 1 になっています。対話的なシェルから実行すると、& を付けなくてもすぐにプロンプトが返ります。

Ubuntu 24.04 の setsid --help に載っているオプションは次のとおりです。

オプション 内容
-c, --ctty 現在の端末を制御端末に設定する
-f, --fork 常に fork する
-w, --wait コマンドの終了を待ち、同じ終了ステータスを返す

出力のあるコマンドでは、nohup と同じくリダイレクトを付けます。

setsid ./batch.sh > batch.log 2>&1 < /dev/null

setsid はジョブ表に入らないため jobs には出ません。PID は pgrep -f batch.sh などで調べます。

プロセスの止め方(PIDを保存する)

バックグラウンドに回した処理は、起動直後に PID を保存しておくと、あとから迷わず止められます。 $! は「直前にバックグラウンドで起動したプロセスの PID」を表す特殊変数です。

$ nohup ./tick.sh > tick.log 2>&1 &
[1] 6889
$ echo $! > tick.pid; cat tick.pid
6889
$ kill "$(cat tick.pid)"
[1]+  Terminated              nohup ./tick.sh > tick.log 2>&1

止め方の一覧

状況 コマンド
同じシェルから止める kill %1
PID ファイルから止める kill "$(cat tick.pid)"
PID を調べて止める pgrep -af tick.sh で確認してから kill PID
子プロセスごと止める kill -- -PGID(プロセスグループを指定)
SIGTERM で止まらない 数秒待ってから kill -KILL PID
動いているか確認する kill -0 PID(シグナルは送らず、存在だけ確認)

kill は既定で SIGTERM(終了の依頼)を送ります。プログラム側で後始末ができるため、まず SIGTERM を使い、止まらないときに SIGKILL へ進む順番がおすすめです。シグナルの種類や ps・pgrep の詳しい使い方は、「Linuxプロセス確認と停止|ps・top・kill逆引きコマンド一覧」で解説しています。

シェルスクリプトを止めると子プロセスが残ることがある

tick.sh のようなシェルスクリプトを PID で止めると、その瞬間に実行中だった子プロセス(ここでは sleep 1)は別プロセスのため、少しの間残ります。

$ nohup ./tick.sh > /dev/null 2>&1 & P=$!
$ ps -o pid,ppid,pgid,args --ppid $P -p $P
  PID  PPID  PGID COMMAND
 7580  7493  7580 /bin/bash ./tick.sh
 7609  7580  7580 sleep 1
$ kill $P
$ ps -o pid,ppid,args -C sleep | grep 'sleep 1$'
 7609     1 sleep 1

sleep 1 の親が PID 1 に変わって残っています。今回は1秒で終わりますが、時間のかかる子プロセスを持つスクリプトでは、プロセスグループごと止めると確実です。上の例では PGID が 7580 なので、kill -- -7580 と PGID にマイナスを付けて指定します(対話的なシェルから & で起動した場合、ジョブごとにプロセスグループが作られます)。

コピーして使える起動・停止スクリプト

start・stop・status をまとめたスクリプトです。CMD などの3行を書き換えて使えます。

#!/bin/bash
# ctl.sh  使い方: ./ctl.sh start|stop|status
set -u
CMD="./tick.sh"          # 動かしたいコマンド
PIDFILE="./tick.pid"     # PID の保存先
LOGFILE="./tick.log"     # ログの保存先

is_running() {
  [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null
}

case "${1:-}" in
  start)
    if is_running; then echo "already running (PID $(cat "$PIDFILE"))"; exit 0; fi
    nohup $CMD >> "$LOGFILE" 2>&1 < /dev/null &
    echo $! > "$PIDFILE"
    echo "started (PID $!)"
    ;;
  stop)
    if ! is_running; then echo "not running"; rm -f "$PIDFILE"; exit 0; fi
    PID="$(cat "$PIDFILE")"
    kill "$PID"                         # まず SIGTERM
    for _ in 1 2 3 4 5; do
      kill -0 "$PID" 2>/dev/null || break
      sleep 1
    done
    kill -0 "$PID" 2>/dev/null && kill -KILL "$PID"   # 残っていたら SIGKILL
    rm -f "$PIDFILE"
    echo "stopped (PID $PID)"
    ;;
  status)
    if is_running; then echo "running (PID $(cat "$PIDFILE"))"; else echo "stopped"; fi
    ;;
  *)
    echo "usage: $0 start|stop|status"; exit 2 ;;
esac

実行結果です(PID は NNNN に置き換えています)。

$ ./ctl.sh status
stopped
$ ./ctl.sh start
started (PID NNNN)
$ ./ctl.sh start
already running (PID NNNN)
$ ./ctl.sh status
running (PID NNNN)
$ tail -n 2 tick.log
tick 2 10:22:09
tick 3 10:22:10
$ ./ctl.sh stop
stopped (PID NNNN)
$ ./ctl.sh status
stopped

< /dev/null は標準入力を空にする指定です。SSH 越しに起動するとき、標準入力・標準出力・標準エラー出力の3つすべてを端末以外へ向けておくと、接続を閉じるときに待たされにくくなります。

huponexitの設定(exit時にジョブを終了させる)

huponexit は bash のシェルオプションで、対話的なログインシェルが終了するとき、すべてのジョブに SIGHUP を送る設定です。既定はオフです。

$ shopt -p huponexit
shopt -u huponexit        # -u はオフを表す
$ shopt -s huponexit      # オンにする
$ shopt -u huponexit      # オフに戻す

ログインシェル(bash -l)で、オンとオフの両方を試しました。

設定 操作 sleep ... & の結果
huponexit オン(ログインシェル) exit 終了
huponexit オフ(ログインシェル) exit 残る
huponexit オン(非ログインシェル) exit 残る

huponexit が効くのはログインシェルだけです。 SSH でログインした直後のシェルはログインシェルですが、その中で起動した bash や、デスクトップ環境の端末エミュレーターで開いたシェルは非ログインシェルであることが多く、設定しても動きは変わりません。

こうしたい 設定
ログアウト時にバックグラウンドの処理を残したくない ~/.bash_profile などに shopt -s huponexit
特定の処理だけは残したい その処理を nohup で起動する、または disown する
既定の動きのままでよい 何も設定しない

huponexit をオンにしていても、nohup で起動したプロセスは SIGHUP を無視するため残ります。

nohup・tmux・systemdの使い分け

ここまでの方法は「自分のシェルから起動した処理を、ログアウト後も続ける」ためのものです。処理の性質によっては、tmux や systemd のほうが向いています。

観点 nohup / disown / setsid tmux systemd-run systemd サービス
向いている用途 1回きりの長いバッチ 画面を見ながら進める対話的な作業 1回きりの処理をログつきで管理 常時動かすサーバー
途中経過の確認 ログファイルを tail -f 画面にそのまま残る journalctl -u ユニット名 journalctl -u ユニット名
あとから操作する できない(シグナルのみ) できる(attach) systemctl stop など systemctl 一式
異常終了時の再起動 なし なし 設定次第 Restart= で自動
OS 再起動後 起動しない 起動しない 起動しない enable で自動起動
準備 不要 tmux の導入 systemd が動く環境 unit ファイルの作成

判断の目安は次のとおりです。

状況 選ぶもの
数時間かかる集計やデータ移行を一度だけ流す nohup ... > log 2>&1 &
起動したあとで nohup を付け忘れたと気づいた Ctrl+Z → bg → disown -h
進み具合を画面で見ながら、途中で入力もしたい tmux
1回きりだが、ログやリソース制限を systemd で管理したい systemd-run
再起動後も自動で立ち上がってほしい systemd サービス
落ちたら自動で立ち上げ直してほしい systemd サービス(Restart=on-failure)

systemd-run で一時的なユニットとして動かす

systemd-run は、unit ファイルを作らずにコマンドを systemd の管理下で動かすコマンドです。次の書き方は systemd の公式マニュアル(systemd-run(1))で確認した内容です。今回の検証環境はコンテナで PID 1 が systemd ではなく、systemd-run を実行すると Failed to connect to bus: No medium found と表示されたため、この節のコマンドは手元では実行していません。

# ユーザー権限で一時ユニットとして起動
systemd-run --user --unit=nightly-batch ./batch.sh

# 状態とログの確認、停止
systemctl --user status nightly-batch
journalctl --user -u nightly-batch -f
systemctl --user stop nightly-batch

標準出力は journal に記録されるため、リダイレクトを書かなくてもログが残ります。ログの絞り込み方は「journalctlの使い方|目的別コマンド逆引き一覧と絞り込み例」が参考になります。

systemd サービスにする

常時動かすプログラムは、unit ファイルを書いてサービスとして登録すると、起動・停止・自動再起動・ログの管理を systemd に任せられます。雛形と各設定項目の意味は「systemdサービス登録|本番用unitファイル雛形と設定項目の意味」にまとめています。

ログアウト時にプロセスが終了する設定(KillUserProcesses)

systemd を使う環境では、/etc/systemd/logind.conf の KillUserProcesses= という設定があります。公式マニュアル(logind.conf(5))によると、これを yes にすると、ユーザーがログアウトしたときにそのセッションのプロセスがすべて終了し、nohup や tmux で起動したものも対象になります。 既定値は no です。

nohup を付けたのにログアウト後に消えている場合は、この設定を確認します。yes の環境で処理を残したいときは、loginctl enable-linger ユーザー名 でユーザーの常駐を有効にしたうえで systemd-run --user を使う方法が公式マニュアルで案内されています(この設定も手元では未実行です)。

つまずきやすい点と対処

症状 原因 対処
nohup を付けたのに端末を閉じたら止まった & を付けずフォアグラウンドで動かし、Ctrl+C で止めた 末尾に & を付ける
nohup: ignoring input ... のあと固まったように見える 案内メッセージがプロンプトのあとに表示されただけ Enter を押す
nohup.out が大きくなり続ける 出力先を指定していない > app.log 2>&1 で指定する。不要なら /dev/null
failed to run command ... No such file or directory パスの誤り、または関数・エイリアスを指定した ./ を付ける、bash -c '...' で包む
failed to run command ... Permission denied 実行権限がない chmod +x を付ける、または nohup bash script.sh &
disown したのに途中で止まった 閉じた端末へ出力して Input/output error 起動時に > out.log 2>&1 を付ける
Ctrl+Z のあと処理が進まない Stopped のまま bg で再開する
exit すると There are stopped jobs. と出る 停止中のジョブが残っているという確認 jobs で確認し、bg で再開するか kill で止める。もう一度 exit すると終了する
SSH を exit しても接続が閉じない バックグラウンドの処理が端末の入出力を握っている > log 2>&1 < /dev/null を付けて起動する
nohup を付けたのにログアウトで消える KillUserProcesses=yes の環境 systemd-run --user と linger を検討する
sudo と組み合わせるとパスワード入力で止まる バックグラウンドでは入力できない 先に sudo -v で認証してから nohup sudo cmd &

実行権限については「chmodの755と644の意味|用途別パーミッション早見表」もあわせて参照してください。

直す前と後

# 直す前:出力先が端末のまま。端末を閉じると終了する
./batch.sh &

# 直した後:SIGHUP を無視し、出力と PID を残す
nohup ./batch.sh > batch.log 2>&1 < /dev/null &
echo $! > batch.pid
# 直す前:&& の後ろは nohup の対象外
nohup ./step1.sh && ./step2.sh &

# 直した後:全体を1つのコマンドにまとめる
nohup bash -c './step1.sh && ./step2.sh' > steps.log 2>&1 &

起動前のチェックリスト

確認すること 確認方法
出力先を決めたか > xxx.log 2>&1 を付けた
ログを置く場所に空きがあるか df -h .
あとで止められるか echo $! > xxx.pid を実行した
動いているか jobs -l、tail -f xxx.log
入力待ちになる処理ではないか 対話的な確認があるコマンドは -y などのオプションを付ける
再起動後も必要な処理か 必要なら systemd サービスを選ぶ

よくある質問

Q. nohup と & は何が違いますか? A. & は「バックグラウンドで実行する(プロンプトをすぐ返す)」ための記号で、nohup は「SIGHUP を無視して起動する」ためのコマンドです。役割が別なので、nohup コマンド & と両方を付けて使います。今回の検証では、& だけのプロセスは端末を閉じると終了し、nohup を付けたプロセスは残りました。

Q. nohup.out を作らないようにするにはどうすればよいですか? A. 標準出力を自分でリダイレクトすると作られません。ログを残すなら nohup コマンド > app.log 2>&1 &、不要なら nohup コマンド > /dev/null 2>&1 & と書きます。nohup.out が作られるのは、標準出力が端末を向いているときだけです。

Q. nohup で起動したプロセスを止めるにはどうすればよいですか? A. 同じシェルなら jobs で番号を確認して kill %1、別のシェルやログインし直したあとなら pgrep -af コマンド名 で PID を調べて kill PID で止めます。起動直後に echo $! > app.pid で PID を保存しておくと、kill "$(cat app.pid)" で止められます。

Q. nohup と tmux はどちらを使えばよいですか? A. 起動したら終わるまで触らないバッチには nohup、途中経過を画面で見たり入力したりする作業には tmux が向いています。常時動かすサーバーや、OS の再起動後も自動で立ち上げたい処理は systemd サービスにすると管理しやすくなります。

まとめ

  • & だけのバックグラウンド実行は、exit で抜ければ残り、端末を閉じると SIGHUP で終了する
  • 基本形は nohup コマンド > out.log 2>&1 &。出力先を指定すれば nohup.out は作られない
  • 起動直後に echo $! > app.pid で PID を保存し、kill "$(cat app.pid)" で止める
  • 付け忘れたら Ctrl+Z → bg → disown -h %1。出力先が端末のままの処理は、次の出力で Input/output error になる点に注意する
  • disown と setsid は出力を切り替えないため、> out.log 2>&1 とセットで使う
  • huponexit はログインシェルの exit 時にジョブへ SIGHUP を送る設定で、既定はオフ
  • 1回きりのバッチは nohup、画面を残したい作業は tmux、常時動かすものは systemd サービスを選ぶ

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「nohupコマンドの使い方|バックグラウンド実行・jobs・fg・bg」

株式会社RJCでは、nohupやジョブ制御といったLinuxの基礎を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?