実行したコマンドが、いつまで待っても返ってこない。
ネットワーク越しの処理や定期実行のスクリプトを書いていると、この「終わらない」に必ず一度はぶつかります。
ぶら下がったままのプロセスは後続の処理を止め、ジョブそのものを詰まらせる原因になるのが厄介なところ。
そこで出番になるのが、GNU coreutils に含まれる timeout コマンドです。
制限時間を渡して実行するだけで、超えた瞬間にシグナルを送って打ち切ってくれます。
本記事の実行例は、GNU coreutils 8.32 のサンドボックス環境で実際に動かし、出力をそのまま貼りました。
timeout は「時間で打ち切る」ための道具
timeout は、指定した秒数だけコマンドを走らせ、超えたらシグナルを送って止めるだけの小さなツールです。
タイムアウト処理をスクリプト側で自前実装しようとすると、バックグラウンド実行とPID管理とスリープ監視を書く羽目になります。
その面倒を丸ごと肩代わりしてくれるのが timeout の価値。
書式は驚くほど単純で、時間とコマンドを並べるだけです。
timeout 5 sleep 30
これで、最大5秒だけ sleep 30 を走らせ、超えたら止めます。
既存のコマンドの前に足すだけで済むので、シェルスクリプトへの後付けが効くのもありがたい点。
時間の指定にはサフィックスが使える
数字だけを書いた場合、単位は秒として扱われます。
サフィックスを付ければ、分や時間でも指定できました。
timeout 90 ./job.sh # 90秒
timeout 1.5s true # 1.5秒(小数もOK)
timeout 2m ./batch.sh # 2分
timeout 1h ./nightly.sh # 1時間
s(秒)/m(分)/h(時)/d(日)の4つが使えます。
小数が通るのは地味に便利で、0.5 のような短い上限もそのまま書けました。
逆に「30秒」を 30m と書き間違えると30分待たされるので、サフィックスの打ち間違いだけは注意が要ります。
終了ステータスで「時間切れ」を見分ける
打ち切ったのか、それともコマンドが自力で失敗したのか。
これを区別する鍵が終了ステータス124です。
$ timeout 2 sleep 5
$ echo $?
124
$ timeout 2 sleep 1
$ echo $?
0
時間切れなら124、時間内に終わればコマンド自身の終了ステータスがそのまま返ってきました。
125はtimeout自身の失敗、126は実行できない、127はコマンドが見つからないという割り当てになっています。
手元でも次のとおり再現できました。
$ timeout 2 /nonexistent-cmd
timeout: failed to run command '/nonexistent-cmd': No such file or directory
$ echo $?
127
$ timeout 2 /etc
timeout: failed to run command '/etc': Permission denied
$ echo $?
126
スクリプト側では「124なら時間切れ」と分岐を書いておけば、リトライと本当のエラーを切り分けられます。
SIGTERM を無視されると、待ち続けてしまう
ここが timeout のいちばんの落とし穴でした。
timeout が既定で送るのはSIGTERMで、受け取った側が無視するようにできている場合、プロセスは死にません。
検証用に、SIGTERM を握りつぶすスクリプトを用意します。
#!/bin/bash
trap '' TERM
sleep 30
これを2秒の制限で実行した結果が次のとおり。
$ time timeout 2 ./ignore.sh
real 0m30.002s
$ echo $?
124
2秒で打ち切ったつもりが、実際には30秒まるごと待たされました。
終了ステータスは124なので、ログだけ見ると「ちゃんと2秒で止まった」と誤読しかねません。
対策が -k オプションで、SIGTERM のあと指定秒だけ待って、それでも死ななければSIGKILLを送るという追い討ちの設定です。
$ time timeout -k 1 2 ./ignore.sh
real 0m3.001s
$ echo $?
137
2秒+1秒の計3秒で終わり、SIGKILLを示す137が返りました。
止まらないと困る場面では -k をセットで書く、と決めておくのが安全です。
送るシグナルと終了ステータスは変えられる
-s を使えば、送るシグナルそのものを差し替えられます。
$ timeout -s KILL 1 sleep 5
$ echo $?
137
もうひとつ覚えておきたいのが --preserve-status。
124の代わりに「シグナルで終わった」ことを示すステータスを返すオプションです。
$ timeout --preserve-status 2 sleep 5
$ echo $?
143
143は128+15、つまりSIGTERMで終了したという意味になります。
呼び出し元がシグナル由来の終了を前提に組まれているときは、こちらのほうが素直につながりました。
ただし124という分かりやすい目印は消えるので、切り替えるなら受け側の分岐もあわせて直す必要があります。
パイプの中では124が出ないことがある
実測していて気付いた挙動を、もうひとつ。
timeout をパイプの左側に置くと、下流が先に閉じたときは時間切れになりません。
$ timeout 2 yes | head -3
y
y
y
$ echo ${PIPESTATUS[0]}
141
141は128+13で、SIGPIPE を受けて終わったことを示す値です。
この場合は timeout が働いたのではなく、head が読み終えて下流を閉じただけ。
パイプの中で124を期待して分岐を書くと、思ったとおりに動かないわけです。
判定したいなら $? ではなく PIPESTATUS を見る、と覚えておくと事故が減ります。
最後に
timeout は、覚えることが少ないわりに効き目の大きいコマンドでした。
まず「時間とコマンドを並べるだけ」という最小の使い方を押さえる。
次に124という目印を知り、SIGTERM を無視されうるという前提で -k を添える。
この3つだけでも、終わらない処理に振り回される場面はかなり減らせます。
詳しい仕様は GNU Coreutils マニュアル と timeout(1) man page を参照してください。
以上です。











コメントを残す