maehachi08 Anything Blog

2014年03月19日
【WIP】シェルスクリプトでタイムアウトを実装する

シェルスクリプトでのタイムアウト実装についてのエントリーです。

【その①】sleepコマンドとkillコマンドの組み合わせで実装

特殊変数$$ として保持されるシェルスクリプト実行PIDをn 秒後にkillするという方法です。

n 秒待つところは、sleepで実装します。

ただsleepコマンドを書いてしまうとそれより早く処理が終わった場合でもsleep n秒分待たなくてはいけないので良くありません。

そこで、sleepコマンドとkillコマンドをグルーピングし、バックグラウンドで実行します。
タイムアウト秒以内に処理を完了できた場合は、バッググラウンドで実行された直前のプロセ
スのPIDを保持する特殊変数$! をkillすることでタイムアウト処理を終わらせます。

#!/bin/bash
my_pid=$$
timeout=5

( sleep $timeout ; kill -SIGALRM  $my_pid ) &
timeout_job=$!

trap "echo 'timeout error' ; exit 1" SIGALRM
trap "kill $timeout_job 2> /dev/null" EXIT

count=0
while [ $count -ne 2 ]
do
    date
    sleep 1
    count=$( expr $count + 1 )
done

echo "job done"

上記コードはタイムアウトし、プロセスがkillされるものです。コマンドを実行すると以下のように動作します。

$ ./timeout_sleep.sh
2014年  3月 19日 水曜日 20:22:42 JST
2014年  3月 19日 水曜日 20:22:43 JST
2014年  3月 19日 水曜日 20:22:44 JST
2014年  3月 19日 水曜日 20:22:46 JST
2014年  3月 19日 水曜日 20:22:47 JST
timeout error

while文で$countの比較対象の数字をsleep秒数よりも短くすることで処理を最後まで走らせることが出来ます。

$ ./timeout_sleep.sh
2014年  3月 19日 水曜日 20:22:57 JST
2014年  3月 19日 水曜日 20:22:58 JST
job done

##【その②】timeoutコマンドで実装 ( シェルスクリプト内部で実装 )

timeoutコマンドとは、coreutils-7.1以降のバージョンに含まれる、指定した時間の後、プログラムにシグナルを送るためのコマンドです。

# less coreutils-7.1/ChangeLog

2008-06-02  Pádraig Brady  <P@draigBrady.com>

        new program: timeout
        * AUTHORS: Register as the author.
        * NEWS: Mention this change.
        * README: Add timeout command to list.
        * src/timeout.c: New file.
        * src/kill.c (operand2sig): Move function to its own file,
        now that timeout.c will also use it.
        * src/operand2sig.c (operand2sig): New file, extracted from kill.c.
        * src/operand2sig.h (operand2sig): Declare.
        * src/Makefile.am (EXTRA_PROGRAMS): Add timeout.
        * src/.gitignore: Add timeout binary to list to ignore.
        * doc/coreutils.texi (timeout invocation): Add timeout info.
        (Signal specifications): New section, also referenced by kill.
        * man/Makefile.am (timeout.1): Add dependency.
        * man/timeout.x: New file.
        * po/POTFILES.in: Add timeout.c and operand2sig.c to list to translate.
        * tests/Makefile.am (TESTS): Add the two new tests.
        * tests/misc/help-version: Add support for new timeout command.
        * tests/misc/invalid-opt: Add support for new timeout command.
        * tests/misc/timeout: New file: check basic timeout operation.
        * tests/misc/timeout-parameters: New file: check invalid parameter
        combinations.

coreutilsとは、cat,ls,rm等のUnix系OSにおける基本的なコマンド群のパッケージ、ないし、その開発やメンテナンスを行うGNUプロジェクトの1つのサブプロジェクトです。

今回検証に使用したマシンはCentOS6.5なのでtimeoutコマンドが入っていました。

# cat /etc/redhat-release
CentOS release 6.5 (Final)

# uname -r
2.6.32-431.1.2.0.1.el6.x86_64

# which timeout
/usr/bin/timeout

# rpm -qf /usr/bin/timeout
coreutils-8.4-31.el6.x86_64

# yum info coreutils-8.4-31.el6.x86_64
Installed Packages
Name        : coreutils
Arch        : x86_64
Version     : 8.4
Release     : 31.el6
Size        : 12 M
Repo        : installed
From repo   : anaconda-CentOS-201311272149.x86_64
Summary     : A set of basic GNU tools commonly used in shell scripts
URL         : http://www.gnu.org/software/coreutils/
License     : GPLv3+
Description : These are the GNU core utilities.  This package is the combination of
            : the old GNU fileutils, sh-utils, and textutils packages.

timeoutコマンドの使い方は難しくはありません。

timeout -s < SIGANL> < TIMEOUT NUM > < COMMAND >

送出するシグナルを指定しなかった場合はSIGHUPがデフォルトシグナルとなり、TIMEOUT値にプレフィックスを付けない場合は秒指定となります。詳細は、timeoutコマンドのヘルプで参照できます。

# timeout --help
Usage: timeout [OPTION] NUMBER[SUFFIX] COMMAND [ARG]...
  or:  timeout [OPTION]
Start COMMAND, and kill it if still running after NUMBER seconds.
SUFFIX may be `s' for seconds (the default), `m' for minutes,
`h' for hours or `d' for days.

長いオプションに必須の引数は短いオプションにも必須です.
  -s, --signal=SIGNAL
                   specify the signal to be sent on timeout.
                   SIGNAL may be a name like `HUP' or a number.
                   See `kill -l` for a list of signals
      --help     この使い方を表示して終了
      --version  バージョン情報を表示して終了

If the command times out, then exit with status 124.  Otherwise, exit
with the status of COMMAND.  If no signal is specified, send the TERM
signal upon timeout.  The TERM signal kills any process that does not
block or catch that signal.  For other processes, it may be necessary to
use the KILL (9) signal, since this signal cannot be caught.

Report timeout bugs to bug-coreutils@gnu.org
GNU coreutils home page: <http://www.gnu.org/software/coreutils/>
General help using GNU software: <http://www.gnu.org/gethelp/>
Report timeout translation bugs to <http://translationproject.org/team/>
For complete documentation, run: info coreutils 'timeout invocation'

では、ここでtimeoutコマンドを使ったタイムアウト実装を紹介します。

#!/bin/bash

timeout -s SIGALRM 5 bash -c '{
    trap "echo timeout error ; exit 1" SIGALRM

    count=0
    while [ $count -ne 10 ]
    do
        date
        sleep 1
        count=$( expr $count + 1 )
    done

    echo "job done" 
}'

上記コードはタイムアウトするものです。コマンドを実行すると以下のように動作します。

```bash
# ./timeout.sh
2014年  3月 19日 水曜日 22:37:40 JST
2014年  3月 19日 水曜日 22:37:41 JST
2014年  3月 19日 水曜日 22:37:42 JST
2014年  3月 19日 水曜日 22:37:43 JST
2014年  3月 19日 水曜日 22:37:44 JST
アラームクロック
timeout error

##【その③】timeoutコマンドで実装 ( シェルスクリプト外部で実装 )

先述したとおり、timeoutコマンドの基本的な文法は以下のとおりです。

timeout -s < SIGANL> < TIMEOUT NUM > < COMMAND >

実行対象のスクリプトを上記の< COMMAND >に指定することでタイムアウトを実現することも出来ます。

テストスクリプトは以下のとおりです。

#!/bin/bash
trap "echo timeout error ; exit 1" SIGALRM

count=0
while [ $count -ne 10 ]
do
    date
    sleep 1
    count=$( expr $count + 1 )
done

echo "job done"

このスクリプトを以下のようにして実行します。

# timeout -s SIGALRM 5 ./test.sh
2014年  3月 19日 水曜日 22:50:06 JST
2014年  3月 19日 水曜日 22:50:07 JST
2014年  3月 19日 水曜日 22:50:08 JST
2014年  3月 19日 水曜日 22:50:09 JST
2014年  3月 19日 水曜日 22:50:10 JST
アラームクロック
timeout error

3つのタイムアウト実装を紹介しましたが、他に良い実装方法があれば、このエントリーを更新します。

だからタイトルがWIPなのです(笑)