Linuxのシェルジョブ制御:活用方法と応用例

Linuxの対話シェルでは、実行中のパイプラインを「ジョブ」として一時停止し、前面または背後で再開できます。Bashのジョブ制御を理解すると、長めの確認処理を続けながら別の作業をしたり、誤って前面で始めた処理を整理したりできます。ただし、ジョブ番号はそのシェルだけの一時的な識別子で、恒久的なサービス管理やセッション切断対策の代わりにはなりません。

目次

プロセス、パイプライン、ジョブの違い

Bashは一つのパイプラインを一つのジョブとして管理します。パイプラインに複数プロセスが含まれていても、利用者は%1のようなジョブ指定でまとめて前面・背後を切り替えられます。ジョブにはシェル内のジョブ番号が付き、各プロセスにはOSのPIDが付きます。両者は別物なので、[1] 25647と表示されたとき、1はジョブ番号、25647は表示対象となったプロセスのPIDです。

端末には前景プロセスグループという概念があり、通常は前景のジョブだけが端末入力を受け取ります。ジョブ制御は、端末ドライバー、プロセスグループ、Bashが協力して実現します。そのため、対話端末のない非対話シェル、cron、CI、コンテナのエントリポイントなどでは、同じ操作感を前提にできません。

バックグラウンドで開始してjobsで確認する

コマンド行の末尾へ&を付けると、Bashはそのパイプラインを非同期で開始し、次のプロンプトを返します。練習には副作用のないsleepが適しています。開始後はjobsで状態を確認し、-lを付けるとPIDも表示できます。ジョブの完了通知は通常、Bashが次のプロンプトを表示するタイミングで出ます。

sleep 120 &
jobs
jobs -l
jobs -r
jobs -s

jobs -rは実行中、jobs -sは停止中のジョブに絞ります。jobsの+は現在のジョブ、-は直前のジョブです。ジョブ番号は完了後に再利用されるため、過去ログの%1を後から実行してはいけません。対象コマンドと現在のjobs -l表示を都度照合します。

Ctrl+Z、bg、fgで実行場所を切り替える

前景ジョブの実行中に通常Ctrl+Zを押すと、端末は停止要求を送り、Bashへ制御が戻ります。これは終了ではなく一時停止です。停止した処理を背後で再開するにはbg %1、前景で再開するにはfg %1を使います。番号を省略した場合は現在のジョブが対象になるため、複数ジョブがあるときは明示した方が安全です。

# 前景で sleep 120 を実行し、Ctrl+Z で停止した後
jobs -l
bg %1
jobs -l
fg %1

背後へ回したプログラムが端末入力を要求すると、端末設定により停止することがあります。また複数の処理が端末へ出力すると、プロンプトと混ざって読みにくくなります。長い処理は開始時から標準出力と標準エラーを適切なログへ向けるか、ジョブ制御よりも用途に合うバッチ実行・サービス管理機能を選びます。

jobspecを正確に指定する

Bashのジョブ指定は%nでジョブ番号n、%+または%%で現在のジョブ、%-で直前のジョブを表します。コマンド名の接頭辞や部分文字列でも指定できますが、複数に一致すると曖昧としてエラーになります。運用手順では読み違いを避けるため、直前にjobs -lを表示し、数値のjobspecを使うのが分かりやすい方法です。

ジョブ番号はシェルごとに独立しています。別の端末、別のSSH接続、別のtmuxペインでは%1が違うジョブを示します。プロセスをOS全体から調べるpsと、現在のBashが管理するjobsも役割が異なります。別シェルで開始されたプロセスをfgへ取り込むことは通常できません。

waitで完了と終了ステータスを受け取る

背後で開始した処理の完了を待ち、成功・失敗を確実に受け取るにはBash組み込みのwaitを使います。$!は直前に非同期開始したジョブに関係するPIDを保持するため、開始直後に別変数へ保存します。引数なしのwaitは対象範囲が広く、どの処理の結果か分かりにくくなるので、PIDを明示する設計が扱いやすいです。

sleep 3 &
worker_pid=$!
printf 'started pid=%s\n' "$worker_pid"
if wait "$worker_pid"; then
  printf '%s\n' 'worker completed'
else
  worker_status=$?
  printf 'worker failed: status=%s\n' "$worker_status"
fi

wait直後の$?が対象処理の終了ステータスです。別のコマンドを挟むと上書きされるため、失敗分岐の先頭で保存します。Bashには完了した処理を一つずつ受け取るwait -nや、対応する識別子を変数へ格納する-pもありますが、利用可能なオプションは配布版のBashバージョンで確認します。

停止ではなく終了させる場合

不要になった自分のテストジョブは、まず対象をjobs -lで確認し、通常の終了要求を送ります。BashのkillはPIDだけでなくjobspecも受け取れます。強制終了を最初から選ぶと、アプリケーションが後処理や一時ファイル整理を行う機会を失います。重要な処理、共有環境、他利用者のプロセスには、権限と業務影響を確認せず信号を送らないでください。

jobs -l
kill -TERM %1
wait %1
printf 'status=%s\n' "$?"

信号による終了では、終了ステータスが128と信号番号の和として表されることがありますが、移植性やプログラム側の処理も関係します。停止中のジョブは終了通知の扱いが分かりにくい場合があるため、状態を再確認します。名前の部分一致で対象を決めたり、古いPID一覧を流用したりせず、現在のジョブ表とコマンド行を照合することが重要です。

disownとセッション切断の注意点

disownはBashのジョブ表から対象を外します。disown -hは表から削除せず、BashがSIGHUPを受けた際にそのジョブへSIGHUPを送らないよう印を付けます。しかし、これだけでプロセスが端末、標準入出力、SSH接続、親プロセス、ログ保存から完全に独立するとは限りません。ネットワーク切断後も必ず動き続ける保証として扱わないでください。

長時間処理を確実に管理したい場合は、systemdのサービスまたは一時ユニット、ジョブスケジューラー、tmuxやscreen、アプリケーション固有のワーカー管理を検討します。再起動時の扱い、ログ、終了コード、再試行、資源制限、資格情報、監視方法まで設計できます。ジョブ制御は主に現在の対話セッションを操作する機能です。

シェルスクリプトではPIDとwaitを中心にする

非対話スクリプトではジョブ制御が無効であることが一般的で、%1のようなjobspecへ依存すると動作環境によって失敗します。並列処理では、各コマンドを&で開始した直後に$!を配列へ保存し、明示したPIDをwaitする方が意図を記述できます。途中終了時の子プロセス処理、最大並列数、各終了ステータス、ログの分離も合わせて設計します。

Bashのset -mでジョブ制御を有効化できる場面はありますが、対話操作をそのまま自動化する理由にはなりません。パイプラインではプロセスが複数あり、$!や終了ステータスの意味も構成に依存します。重要な自動化は、対象Bashの公式マニュアルに合わせて小さなテスト環境で異常系まで検証します。

トラブル時の確認順序

  1. 現在の端末とBashが目的のセッションかを確認する。
  2. jobs -lで実行中・停止中・完了済みの状態とコマンドを確認する。
  3. 前景へ戻すなら対象jobspecを明示してfgを使う。
  4. 背後で続けるなら端末入力と出力先を確認してからbgを使う。
  5. 完了判定が必要なら開始時に保存したPIDへwaitを使う。
  6. セッションを越えて継続させる要件なら、サービス管理や端末多重化へ移す。

確認チェックリスト

  • ジョブ番号とPIDを区別している
  • 操作前に現在のjobs -lを確認している
  • バックグラウンド処理の入力元と出力先を確認している
  • wait直後の終了ステータスを保存している
  • disownを恒久的なサービス管理の代用にしていない
  • 非対話スクリプトでjobspecへ依存していない

Bashのジョブ制御は、目の前の対話作業を柔軟に整理するための仕組みです。jobsで現在状態を確認し、bgとfgで実行場所を切り替え、完了結果はwaitで受け取ります。セッションをまたぐ処理や本番サービスは別の管理基盤へ任せることで、再現性と監視性を保てます。

公式情報・参考資料

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次