Bashで指定されたタイムアウト後に子プロセスを強制終了する方法は?


178

時々クラッシュする(実際にはハングする)子プロセスを起動するbashスクリプトがあり、明確な理由はありません(ソースが閉じているため、私ができることはあまりありません)。結果として、私はこのプロセスを一定時間起動し、一定時間経過しても正常に戻らない場合は強制終了できるようにしたいと考えています。

bashを使用してそれを達成する簡単堅牢な方法はありますか?

PS:この質問がserverfaultまたはスーパーユーザーに適しているかどうか教えてください。



ここで非常に完全な応答:stackoverflow.com/a/58873049/2635443
Orsiris de Jong

回答:


260

BASH FAQエントリ#68で見られる ように:「コマンドを実行して、N秒後にコマンドを中止(タイムアウト)させるには?」

何かをダウンロードしてもかまわない場合は、timeoutsudo apt-get install timeout)を使用して次のように使用します(ほとんどのシステムにはすでにインストールされていますが、それ以外の場合はsudo apt-get install coreutils

timeout 10 ping www.goooooogle.com

何かをダウンロードしたくない場合は、タイムアウトが内部的に行うことを行います:

( cmdpid=$BASHPID; (sleep 10; kill $cmdpid) & exec ping www.goooooogle.com )

より長いbashコードに対してタイムアウトを実行する場合は、2番目のオプションを次のように使用します。

( cmdpid=$BASHPID; 
    (sleep 10; kill $cmdpid) \
   & while ! ping -w 1 www.goooooogle.com 
     do 
         echo crap; 
     done )

8
他の誰かが私が何をしたのか疑問に思った場合のイグナシオの返答:cmdpid=$BASHPIDは、呼び出し側シェルのpidではなく、によって開始される(最初の)サブシェルを受け取り()ます。(sleep...事は...バックグラウンドで10秒待ってからキラーサブシェルプロセスを立ち上げた後、そのワークロードを実行するために進み、最初のサブシェルを殺すために最初のサブシェル内の第2のサブシェルを呼び出す
jamadagni

17
timeoutGNU coreutilsの一部であるため、すべてのGNUシステムにすでにインストールされているはずです。
2015

1
@Sameer:バージョン8のみ。–
Ignacio Vazquez-Abrams

3
私はそのことを100%確信していませんが、私の知る限り(そして私のマンページが私に言ったことを知っている限り)timeoutは現在、coreutilsの一部です。
benaryorg、

5
このコマンドは「早く終了」しません。常にタイムアウト時にプロセスを強制終了しますが、タイムアウトしなかった状況には対応しません。
hawkeye

28
# Spawn a child process:
(dosmth) & pid=$!
# in the background, sleep for 10 secs then kill that process
(sleep 10 && kill -9 $pid) &

または終了コードも取得するには:

# Spawn a child process:
(dosmth) & pid=$!
# in the background, sleep for 10 secs then kill that process
(sleep 10 && kill -9 $pid) & waiter=$!
# wait on our worker process and return the exitcode
exitcode=$(wait $pid && echo $?)
# kill the waiter subshell, if it still runs
kill -9 $waiter 2>/dev/null
# 0 if we killed the waiter, cause that means the process finished before the waiter
finished_gracefully=$?

8
kill -9プロセスが最初に処理できることを示す信号を試す前に使用しないでください。
追って通知があるまで一時停止。

確かに、私は迅速な修正を行っていましたが、クラッシュしたと彼が言ったので、彼がすぐにプロセスを停止させたいと思っていました
Dan

8
これは実際には非常に悪い解決策です。dosmth2秒で終了し、別のプロセスが古いpidを取得して、新しいpidを強制終了するとどうなりますか?
ヤギを

PIDのリサイクルは、限界に達してラップアラウンドすることで機能します。システムが完全に不安定にならない限り、残りの8秒以内に別のプロセスがPIDを再利用することはほとんどありません。
kittydoor

13
sleep 999&
t=$!
sleep 10
kill $t

過度の待機が発生します。実際のコマンド(sleep 999ここ)が、強制されたスリープ(sleep 10)よりも速く終了することが多い場合はどうなりますか?1分、5分までのチャンスを与えたい場合はどうなりますか?スクリプトにそのようなケースがたくさんあるとどうなるでしょうか:)
it3xl

3

私にもこの質問があり、さらに2つのことが非常に役立つことがわかりました。

  1. bashのSECONDS変数。
  2. コマンド「pgrep」。

だから私はコマンドラインでこのようなものを使います(OSX 10.9):

ping www.goooooogle.com & PING_PID=$(pgrep 'ping'); SECONDS=0; while pgrep -q 'ping'; do sleep 0.2; if [ $SECONDS = 10 ]; then kill $PING_PID; fi; done

これはループなので、CPUをクールに保つために「sleep 0.2」を含めました。;-)

(ところで、pingは悪い例です。組み込みの "-t"(タイムアウト)オプションを使用するだけです。)


1

子のpidを追跡するためのpidファイルがある(または簡単に作成できる)場合、pidファイルのmodtimeをチェックし、必要に応じてプロセスを強制終了/再起動するスクリプトを作成できます。次に、スクリプトをcrontabに配置して、ほぼ必要な期間で実行します。

詳細が必要な場合はお知らせください。それがあなたのニーズに合うように聞こえない場合、新興企業はどうですか?


1

1つの方法は、サブシェルでプログラムを実行し、名前付きパイプを介してサブシェルと通信することです。 readコマンドです。このようにして、実行中のプロセスの終了ステータスを確認し、パイプを介して通信することができます。

以下はyes、3秒後にコマンドをタイムアウトする例です。を使用してプロセスのPIDを取得しますpgrep(おそらくLinuxでのみ機能します)。また、読み取り用にパイプを開くプロセスは、書き込み用にも開かれるまでハングし、その逆も同様であるという点で、パイプの使用にはいくつかの問題があります。したがって、readコマンドがハングするのを防ぐために、バックグラウンドサブシェルで読み取り用にパイプを「くさび」で開いています。(つまり、パイプを読み書き可能にするためのフリーズを防ぐ別の方法、つまりread -t 5 <>finished.pipeLinux以外では機能しない場合もあります。)

rm -f finished.pipe
mkfifo finished.pipe

{ yes >/dev/null; echo finished >finished.pipe ; } &
SUBSHELL=$!

# Get command PID
while : ; do
    PID=$( pgrep -P $SUBSHELL yes )
    test "$PID" = "" || break
    sleep 1
done

# Open pipe for writing
{ exec 4>finished.pipe ; while : ; do sleep 1000; done } &  

read -t 3 FINISHED <finished.pipe

if [ "$FINISHED" = finished ] ; then
  echo 'Subprocess finished'
else
  echo 'Subprocess timed out'
  kill $PID
fi

rm finished.pipe

0

プロセスが既に終了した後にプロセスを強制終了しないようにする試みは次のとおりです。これにより、同じプロセスIDを持つ別のプロセスを強制終了する可能性が低くなります(この種のエラーを完全に回避することはおそらく不可能です)。

run_with_timeout ()
{
  t=$1
  shift

  echo "running \"$*\" with timeout $t"

  (
  # first, run process in background
  (exec sh -c "$*") &
  pid=$!
  echo $pid

  # the timeout shell
  (sleep $t ; echo timeout) &
  waiter=$!
  echo $waiter

  # finally, allow process to end naturally
  wait $pid
  echo $?
  ) \
  | (read pid
     read waiter

     if test $waiter != timeout ; then
       read status
     else
       status=timeout
     fi

     # if we timed out, kill the process
     if test $status = timeout ; then
       kill $pid
       exit 99
     else
       # if the program exited normally, kill the waiting shell
       kill $waiter
       exit $status
     fi
  )
}

likeを使用しますrun_with_timeout 3 sleep 10000。これは実行されますsleep 10000が、3秒後に終了します。

これは、バックグラウンドタイムアウトプロセスを使用して、遅延後に子プロセスを強制終了する他の回答と同じです。これは、Danの拡張回答(https://stackoverflow.com/a/5161274/1351983)とほぼ同じだと思います。ただし、タイムアウトシェルがすでに終了している場合は、タイムアウトシェルは強制終了されません。

このプログラムが終了した後も、実行中のいくつかの「スリープ」プロセスが残っていますが、無害なはずです。

これは、移植性のread -tないシェル機能を使用せず、を使用しないため、他の回答よりも優れたソリューションである可能性がありますpgrep


違いは何(exec sh -c "$*") &sh -c "$*" &?具体的には、なぜ後者ではなく前者を使用するのですか?
ジャスティンC

0

ここに私が提出した3番目の回答があります。これは、シグナル割り込みを処理し、SIGINTを受信したときにバックグラウンドプロセスをクリーンアップします。これは、トップアンサーで使用されている$BASHPIDand execトリックを使用して、プロセスのPID(この場合は呼び出し)を取得します。FIFOを使用して、強制終了とクリーンアップを担当するサブシェルと通信します。(これは私の2番目の回答のパイプに似ていますが、名前付きパイプがあると、シグナルハンドラーもパイプに書き込むことができます。)$$sh

run_with_timeout ()
{
  t=$1 ; shift

  trap cleanup 2

  F=$$.fifo ; rm -f $F ; mkfifo $F

  # first, run main process in background
  "$@" & pid=$!

  # sleeper process to time out
  ( sh -c "echo \$\$ >$F ; exec sleep $t" ; echo timeout >$F ) &
  read sleeper <$F

  # control shell. read from fifo.
  # final input is "finished".  after that
  # we clean up.  we can get a timeout or a
  # signal first.
  ( exec 0<$F
    while : ; do
      read input
      case $input in
        finished)
          test $sleeper != 0 && kill $sleeper
          rm -f $F
          exit 0
          ;;
        timeout)
          test $pid != 0 && kill $pid
          sleeper=0
          ;;
        signal)
          test $pid != 0 && kill $pid
          ;;
      esac
    done
  ) &

  # wait for process to end
  wait $pid
  status=$?
  echo finished >$F
  return $status
}

cleanup ()
{
  echo signal >$$.fifo
}

私はできる限り競合状態を避けようとしました。ただし、削除できなかったエラーの原因の1つは、プロセスがタイムアウトとほぼ同じ時間で終了した場合です。たとえば、run_with_timeout 2 sleep 2またはrun_with_timeout 0 sleep 0。私にとって、後者はエラーを出します:

timeout.sh: line 250: kill: (23248) - No such process

すでに終了しているプロセスを強制終了しようとしているためです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.