サーバー運用をしていると、ネットワーク遅延やデッドロックでコマンドがハングアップするケースは珍しくありません。特に cron から定期実行しているスクリプトは、一度ハングすると次の実行が積み上がり、リソースを食い潰す事態になります。
この記事では、timeout コマンドを使って制限時間付きでコマンドを実行する方法を解説します。
シェルスクリプトでのタイムアウト処理、終了コードの読み方、バックグラウンドプロセスへの対処、実務でよく使う組み合わせパターンまでカバーします。
この記事のポイント
・timeout コマンドで指定秒数を超えたプロセスを自動終了できる
・終了コード 124 で「タイムアウトによる終了」を判定できる
・--kill-after で SIGTERM の後に SIGKILL を送るダブル保険が設定できる
・パイプライン全体には bash -c でラップしてタイムアウトをかける
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
timeoutコマンドとは|なぜ必要なのか
サーバー運用で「コマンドがハングアップする」ケースには主に4つの原因があります。・NFS マウント先やリモートサーバーがネットワーク経路の問題で応答しなくなり、rsync・scp が固まる
・curl や wget で
--max-time を指定しないと、リモートホストが無応答のまま永遠に接続を保持する・DB クライアントが接続プール枯渇でブロックされ、スクリプトが停止したまま動かない
・デッドロックが発生したプロセスがいつまでもロックを解放しない
このような状況に対処するのが
timeout コマンドです。指定した秒数が経過してもコマンドが終了しなかった場合、シグナルを送って強制終了します。
特に危険なのが、cron から定期実行しているジョブのハングアップです。私が運用しているサーバーでも、NFS マウント先のストレージ障害でバックアップスクリプトが翌朝まで動き続け、プロセスが大量に積み上がった経験があります。
# タイムアウトなしのcronスクリプトが積み上がった例 # ps aux | grep backup.sh | grep -v grep root 1482 0.0 0.0 14532 1908 ? S Jan10 0:00 bash /usr/local/bin/backup.sh root 3241 0.0 0.0 14532 1912 ? S Jan10 0:00 bash /usr/local/bin/backup.sh root 5109 0.0 0.0 14532 1904 ? S Jan10 0:00 bash /usr/local/bin/backup.sh root 6873 0.0 0.0 14532 1908 ? S Jan11 0:00 bash /usr/local/bin/backup.sh root 8244 0.0 0.0 14532 1916 ? S Jan11 0:00 bash /usr/local/bin/backup.sh # ... 以下19プロセス続く
timeout コマンドを使ったタイムアウト設計です。その後、全バッチジョブに timeout を標準導入しました。実行環境:RHEL 9.4 / Ubuntu 24.04 LTS(coreutils パッケージに含まれる)
基本的な使い方
1. 書式と基本動作を確認する
書式は非常にシンプルです。# 基本書式 timeout 秒数 コマンド [引数...] # 例: 5秒以内に終わらなければ終了する $ timeout 5 sleep 10 $ echo $? 124
sleep 10 は 5 秒で強制終了され、終了コード 124 が返ります。終了コードの種類をまとめると以下のとおりです。
・0:コマンドが制限時間内に正常終了した
・124:タイムアウトが発生し SIGTERM を送信した
・125:timeout コマンド自体のエラー(引数不正等)
・126:コマンドの実行権限なし
・127:コマンドが見つからない
・128以上:コマンドがシグナルで終了した(128 + シグナル番号)
スクリプト内では「終了コード 124 = タイムアウトによる終了」として判定します。
if 文の直前に他のコマンドを挟むと終了コードが上書きされるため、必ず変数に退避してから判定してください。2. 時間単位を指定する
秒数の代わりに単位付きで指定できます。# 30秒(単位なしの数値は秒) timeout 30 コマンド # 2分 timeout 2m コマンド # 1時間 timeout 1h コマンド # 90分(分単位で指定) timeout 90m コマンド # 1.5秒(小数指定も可) timeout 1.5 コマンド
s(秒)・m(分)・h(時間)・d(日)です。3. 送信するシグナルを変更する
デフォルトではSIGTERM(番号15)が送信されます。プロセスによっては SIGTERM を無視するものがあるため、
-s オプションでシグナルを指定できます。# SIGKILL(強制終了)を送信する timeout -s SIGKILL 10 コマンド # シグナル番号で指定(9=SIGKILL) timeout -s 9 10 コマンド
4. パイプライン全体にタイムアウトをかける
timeout コマンド | コマンド と書くと、タイムアウトは最初のコマンドにしかかかりません。パイプライン全体や複数コマンドのシーケンスにタイムアウトをかけたい場合は、
bash -c でラップします。# パイプライン全体に10秒のタイムアウト timeout 10 bash -c 'cat /var/log/app.log | grep "ERROR" | wc -l' # 複数コマンドのシーケンスに60秒のタイムアウト timeout 60 bash -c ' mysqldump -u root --single-transaction mydb > /tmp/dump.sql && gzip /tmp/dump.sql '
応用・実務Tips
1. --kill-after でダブル保険をかける
SIGTERM を送ってもプロセスが終了しないケースがあります。--kill-after(短縮形 -k)を使うと、SIGTERM の後に指定秒数待って、まだ動いていれば SIGKILL を送ります。# 30秒でSIGTERM → さらに10秒待ってSIGKILL timeout --kill-after=10 30 コマンド # 短縮形 timeout -k 10 30 コマンド
128 + 9 = 137 になります。本番スクリプトでは SIGTERM(124)と SIGKILL(137)の両方を処理するのが安全です。timeout --kill-after=10 120 rsync -av /data/ user@backup-server:/backup/ exit_code=$? case "${exit_code}" in 0) echo "バックアップ完了" ;; 124) echo "WARN: タイムアウト(120秒)で終了しました" >&2 ;; 137) echo "WARN: タイムアウト後にSIGKILLで強制終了しました" >&2 ;; *) echo "ERROR: rsyncが失敗しました(exit: ${exit_code})" >&2; exit 1 ;; esac
2. シェルスクリプトでタイムアウト判定する
#!/bin/bash # 外部APIを30秒以内に呼び出す例 timeout 30 curl -s https://api.example.com/status -o /tmp/api_result.json EXIT_CODE=$? if [ $EXIT_CODE -eq 124 ]; then echo "ERROR: APIへの接続がタイムアウトしました" >&2 exit 1 elif [ $EXIT_CODE -ne 0 ]; then echo "ERROR: curl が失敗しました(終了コード: ${EXIT_CODE})" >&2 exit 1 fi echo "API応答を取得しました" cat /tmp/api_result.json
$? を変数に保存するのがポイントです。毎回タイムアウト判定を書くのが冗長に感じる場合は、再利用可能な関数にまとめるのが効果的です。
#!/bin/bash # タイムアウト付きコマンド実行関数 # 使い方: run_with_timeout 秒数 コマンド [引数...] run_with_timeout() { local timeout_sec="$1" shift timeout --kill-after=10 "${timeout_sec}" "$@" local exit_code=$? if [ "${exit_code}" -eq 124 ] || [ "${exit_code}" -eq 137 ]; then echo "ERROR: タイムアウトしました(${timeout_sec}秒): $*" >&2 return 124 fi return "${exit_code}" } # 使用例(各コマンドに個別のタイムアウトを指定できる) run_with_timeout 10 curl -sf "https://api.example.com/health" || { echo "ヘルスチェック失敗"; exit 1; } run_with_timeout 120 rsync -av /var/data/ user@backup-server:/backup/ || { echo "バックアップ失敗"; exit 1; } run_with_timeout 30 mysqldump -u root mydb > /tmp/dump.sql || { echo "ダンプ失敗"; exit 1; }
3. cron ジョブのハングアップ対策
cron で定期実行しているジョブがハングアップすると、次の実行が重複して起動されます。timeout を組み合わせることでジョブの最大実行時間を保証できます。
方法1:crontab エントリ側で timeout を追加する(シンプル)
# /etc/cron.d/backup-job の設定例 # バックアップスクリプトを最大115分でタイムアウトさせる 0 2 * * * root timeout -k 60 115m /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
crontab エントリを変更できない環境や、スクリプト側で自己制御したい場合に有効です。
#!/bin/bash # スクリプト自身の実行時間を制限する # 環境変数でガードして無限ループを防ぐ if [ -z "${_TIMEOUT_GUARD:-}" ]; then export _TIMEOUT_GUARD=1 exec timeout --kill-after=30 3600 "$0" "$@" fi # --- 以下、実際の処理 --- set -euo pipefail LOG_FILE="/var/log/backup.log" echo "[$(date '+%Y-%m-%d %H:%M:%S')] バックアップ開始" | tee -a "${LOG_FILE}" rsync -av /var/data/ user@backup-server:/backup/ >> "${LOG_FILE}" 2>&1 echo "[$(date '+%Y-%m-%d %H:%M:%S')] バックアップ完了" | tee -a "${LOG_FILE}"
exec を使うことです。exec はカレントプロセスを置き換えるため、timeout の監視プロセスが実際のスクリプト処理の親プロセスになります。環境変数 _TIMEOUT_GUARD のガードで、ラップされた後は再度ラップしない制御を入れてください。なお、Apache のタイムアウト設定は
timeout コマンドではなく httpd.conf の Timeout ディレクティブで行います。詳しくはApache タイムアウト設定の詳細を参照してください。
4. バックグラウンドプロセスへの待機タイムアウト
timeout はフォアグラウンドで実行したコマンドにしかタイムアウトをかけられません。バックグラウンドプロセスの完了を待ちながらタイムアウトをかけるには、PID を取得して kill と wait を組み合わせます。#!/bin/bash TIMEOUT_SEC=60 # バックグラウンドで処理を起動してPIDを取得 long_process_command & BG_PID=$! # 最大TIMEOUT_SEC秒間、1秒ずつ生存確認する elapsed=0 while kill -0 "${BG_PID}" 2>/dev/null; do if [ "${elapsed}" -ge "${TIMEOUT_SEC}" ]; then echo "WARN: タイムアウト(${TIMEOUT_SEC}秒)。プロセス ${BG_PID} を終了します" >&2 kill -TERM "${BG_PID}" 2>/dev/null sleep 5 kill -KILL "${BG_PID}" 2>/dev/null wait "${BG_PID}" 2>/dev/null exit 124 fi sleep 1 elapsed=$(( elapsed + 1 )) done # バックグラウンドプロセスの終了ステータスを取得 wait "${BG_PID}" echo "処理完了(${elapsed}秒)"
kill -0 PID はシグナルを送らずプロセスの生存確認だけ行います。プロセスが生きていれば終了コードが 0、死んでいれば 0 以外になります。5. 対話型コマンドに使う場合の注意
パスワードを求める SSH やsudo のような対話型コマンドは、timeout ではなく SSH の
ConnectTimeout オプションなど専用の設定を使うのが適切です。# SSH接続のタイムアウトは -o ConnectTimeout が正しい ssh -o ConnectTimeout=10 user@192.0.2.10 "コマンド" # コマンド全体に上限をかけたい場合は timeout との組み合わせも有効 timeout 60 ssh -o ConnectTimeout=10 user@192.0.2.10 "バッチ処理.sh"
トラブルシュート|よくある問題と対処法
「タイムアウトしているはずなのにプロセスが残る」
SIGTERM を無視するプロセス(trap "" SIGTERM 等)や、子プロセスを生成して制御が分散している場合に起きます。--kill-after を必ず併用してください。SIGKILL はトラップ不可で必ずプロセスを終了させます。それでも残る場合は、
setsid でプロセスグループリーダーにしてから timeout を実行すると効果的です。# setsid でプロセスグループをまとめて timeout にかける timeout --kill-after=5 30 setsid コマンド # プロセスが残っているか確認(ss / lsof でポートを確認するのも有効) ss -tlnp | grep ポート番号
「パイプ内のコマンドに timeout が効かない」
timeout 10 cmd1 | cmd2 は cmd1 だけにタイムアウトがかかります。パイプライン全体にかけるには timeout 10 bash -c 'cmd1 | cmd2' とラップしてください。「timeout: failed to run command: No such file or directory」
コマンド名のスペルミス、またはコマンドが PATH に含まれていないことが原因です。# コマンドが存在するか確認する which コマンド名 type コマンド名 # 絶対パスで指定する timeout 30 /usr/local/bin/myscript.sh
「古いシステムで timeout コマンドが見つからない」
timeout は coreutils 7.0 以降(2009年リリース)に含まれます。RHEL 6 以前など古い環境では未インストールの場合があります。
# インストール確認(RHEL/Rocky 系) rpm -q coreutils # インストール確認(Ubuntu/Debian 系) dpkg -l coreutils # バージョン確認 timeout --version
「SIGKILL で終了した後にロックファイルが残る」
SIGKILL はtrap を経由せず強制終了するため、trap cleanup SIGTERM EXIT で設定したクリーンアップ関数が動きません。次回起動時にロックファイルの PID が生きているか確認する設計で対応します。# 起動時にロックファイルのPIDが生きているか確認するパターン LOCK_FILE="/var/run/backup.pid" if [ -f "${LOCK_FILE}" ]; then OLD_PID=$(cat "${LOCK_FILE}") if kill -0 "${OLD_PID}" 2>/dev/null; then echo "ERROR: 前回の実行が残っています(PID: ${OLD_PID})" >&2 exit 1 fi echo "WARN: 古いロックファイルを削除します(PID: ${OLD_PID})" >&2 rm -f "${LOCK_FILE}" fi echo $$ > "${LOCK_FILE}" trap 'rm -f "${LOCK_FILE}"' EXIT SIGTERM
本記事のまとめ
| やりたいこと | コマンド・設計パターン |
|---|---|
| 30秒でタイムアウトさせる | timeout 30 コマンド |
| 2分でタイムアウトさせる | timeout 2m コマンド |
| SIGTERM後10秒でSIGKILL | timeout --kill-after=10 30 コマンド |
| 終了コードでタイムアウト判定 | EXIT=$?; [ $EXIT -eq 124 ] |
| シグナル種別を指定する | timeout -s SIGKILL 10 コマンド |
| パイプライン全体にタイムアウト | timeout 秒数 bash -c 'cmd1 | cmd2' |
| バックグラウンドプロセスにタイムアウト | PIDを取得し kill -0 ループ + kill -TERM を使う |
| SSH接続タイムアウト(推奨) | ssh -o ConnectTimeout=10 user@host |
timeout コマンドをシェルスクリプトや cron ジョブに組み込んでおくことで、ハングアップが起きても影響を最小限に抑えられます。
シグナル処理の詳細についてはLinux 基本コマンドの解説もあわせて参考にしてください。
シェルスクリプトの書き方や、サーバー運用の「型」が身についていますか?
timeout コマンドひとつとっても、終了コードの読み方や --kill-after の使い所を知らなければ、ハングアップが起きたときに対処できません。
ネットの切れ端の情報をコピペするだけでなく、現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、『Linuxサーバー構築入門マニュアル(図解60P)』を完全無料でプレゼントしています。
「独学の時間がもったいない」「プロから直接、現場の技術を最短で学びたい」という本気の方には、2日で実務レベルのスキルが身につく【初心者向けハンズオンセミナー】も開催しています。
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 次のページへ:inotifywaitコマンドでファイル変更をリアルタイム監視する方法|スクリプト自動化と実務活用も
- 前のページへ:trapコマンドでbashスクリプトのシグナルを捕捉・処理する方法|一時ファイル削除やエラー終了処理の実践例も
- この記事の属するカテゴリ:Linuxtips・シェルスクリプトへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます