プロセスを終了してPIDが再利用されないようにする方法

プロセスを終了してPIDが再利用されないようにする方法

たとえば、次のようなシェルスクリプトがあるとします。

longrunningthing &
p=$!
echo Killing longrunningthing on PID $p in 24 hours
sleep 86400
echo Time up!
kill $p

それはうまくいくでしょうか?プロセスが早期に終了し、対応するPIDがリサイクルされた可能性があるという事実に加えて、これは、いくつかの無実の作業が信号キューから爆弾を受けたことを意味します。実際、これは重要かもしれませんが、まだ心配です。長時間実行されているアイテムをハックして自分で終了したり、FSでPIDを維持/削除したりすることは大丈夫ですが、ここでは一般的なケースを考えています。

答え1

次のコマンドを使用することをお勧めしますtimeout(利用可能な場合)。

timeout 86400 cmd

現在(8.23)GNU実装は、少なくともalarm()サブプロセスを待っている間、または同等の機能を使用して動作します。SIGALRMリターンとシャットダウンの間の転送を妨げないようです(効果的にキャンセルwaitpid()timeoutアラーム)。この小さなウィンドウの間、timeoutメッセージはstderrに書き込まれる可能性があります(たとえば、子プロセスがコアをダンプする場合)、競合ウィンドウはさらに広くなります(stderrがパイプ全体の場合は無制限)。

個人的にこの制限を受けることができます(将来のバージョンでは修正される可能性があります)。timeout正しい終了状態を報告し、他の特別な場合(たとえば、起動時にSIGALRMがブロック/無視され、他の信号を処理するなど)を手動で実行するよりも、より良い処理を行うために特別な注意が必要です。

おおよそ次のように書くことができますperl

perl -MPOSIX -e '
  $p = fork();
  die "fork: $!\n" unless defined($p);
  if ($p) {
    $SIG{ALRM} = sub {
      kill "TERM", $p;
      exit 124;
    };
    alarm(86400);
    wait;
    exit (WIFSIGNALED($?) ? WTERMSIG($?)+128 : WEXITSTATUS($?))
  } else {exec @ARGV}' cmd

timelimitコマンドがありますhttp://devel.ringlet.net/sysutils/timelimit/timeoutGNUより数ヶ月早い)。

 timelimit -t 86400 cmd

この方法は同様のメカニズムを使用しますが、子の死を検出するためにハンドラalarm()(停止した子を無視)をインストールします。SIGCHLDまた、実行する前に警告をキャンセルしwaitpid()(保留中の場合は転送をキャンセルしませんが、作成されSIGALRMた方法では問題ありません)、終了します。今後呼び出されますwaitpid()(したがって再利用されたPIDは終了できません)。

ネットワークパイプコマンドがもう1つありますtimelimit。他のすべての方法よりも数十年前のこの方法は代替アプローチをとりますが、停止したコマンドに対しては正しく機能せず、タイムアウト時に終了ステータスを1返します。

あなたの質問に対するより直接的な答えで、次のことができます。

if [ "$(ps -o ppid= -p "$p")" -eq "$$" ]; then
  kill "$p"
fi

つまり、そのプロセスがまだ子プロセスであることを確認してください。同様に、プロセスが終了し、そのpidが他のプロセスで再利用される可能性がある小さな競合期間(psプロセス状態の検索とkill終了の間)があります。

一部のシェル(zsh、、、、 )を使用すると、pidbashmksh代わりに作業仕様を渡すことができます。

cmd &
sleep 86400
kill %
wait "$!" # to retrieve the exit status

これは、1つのバックグラウンドジョブのみを作成する場合にのみ機能します(そうでなければ、正しいジョブ仕様を確実に取得することは必ずしも可能ではありません)。

これが問題の場合は、新しいシェルインスタンスを起動してください。

bash -c '"$@" & sleep 86400; kill %; wait "$!"' sh cmd

これは、子が死んだときにシェルが割り当てリストから割り当てを削除するために機能します。ここでは競合ウィンドウがあってはなりません。これは、シェルが呼び出されたときにkill()SIGCHLDシグナルがまだ処理されておらず、pidを再利用できないか(待機していないため)、すでに処理されており、pidを使用できないためです。ジョブがプロセステーブルから削除されました(killエラーが報告されています)。拡張のために作業テーブルにアクセスする前に、少なくともSIGCHLDをブロックしてから後でbashブロックを解除してください。kill%kill()

死後も保留中のプロセスを防ぐsleepもう1つのオプションは、orの代わりにパイプを使用することです。cmdbashksh93read -tsleep

{
  {
    cmd 4>&1 >&3 3>&- &
    printf '%d\n.' "$!"
  } | {
    read p
    read -t 86400 || kill "$p"
  }
} 3>&1

コマンドにはまだ競合状態があるため、コマンドの終了状態が失われます。また、cmdfd 4を閉じないと仮定します。

次の競合のないソリューションを実装してみてくださいperl

perl -MPOSIX -e '
   $p = fork();
   die "fork: $!\n" unless defined($p);
   if ($p) {
     $SIG{CHLD} = sub {
       $ss = POSIX::SigSet->new(SIGALRM); $oss = POSIX::SigSet->new;
       sigprocmask(SIG_BLOCK, $ss, $oss);
       waitpid($p,WNOHANG);
       exit (WIFSIGNALED($?) ? WTERMSIG($?)+128 : WEXITSTATUS($?))
           unless $? == -1;
       sigprocmask(SIG_UNBLOCK, $oss);
     };
     $SIG{ALRM} = sub {
       kill "TERM", $p;
       exit 124;
     };
     alarm(86400);
     pause while 1;
   } else {exec @ARGV}' cmd args...

(他のタイプのコーナーケースを処理するには改善が必要ですが)

別の競合のないアプローチは、プロセスグループを使用することです。

set -m
((sleep 86400; kill 0) & exec cmd)

ただし、端末デバイスへの入出力が関連している場合は、プロセスグループを使用すると副作用が発生する可能性があります。また、によって作成された他のすべての追加プロセスを終了できるという利点もありますcmd

答え2

一般的に言えばできません。これまでに提供されたすべての答えは、欠陥のある経験的な方法です。 pidを使用して信号を送信するのが安全な場合は1つだけです。これは、ターゲットプロセスがシグナルを送信するプロセスの直接的なサブプロセスであり、親プロセスがまだシグナルを待っていない場合です。この場合、終了しても親プロセスが待つまでpidは保持されます(これは「ゾンビプロセス」です)。私はシェルでこれをきれいにする方法がわかりません。

プロセスを終了するもう1つの安全な方法は、マスターがある疑似端末に設定されているコントロールttyを使用してプロセスを開始することです。その後、端末を介して信号を送信できます(たとえば、ptyに送信したり、SIGTERMptyを介して文字を書くことができます)。SIGQUIT

もう1つの便利なスクリプト方法は、名前付きscreenセッションを使用してscreenセッションにコマンドを送信して終了することです。このプロセスは、スクリーンセッションに従って名前付きパイプまたはUnixソケットを介して発生し、安全な識別名を選択すると自動的に再利用されません。

答え3

  1. プロセスの開始時にプロセスの開始時間を保存します。

    longrunningthing &
    p=$!
    stime=$(TZ=UTC0 ps -p "$p" -o lstart=)
    
    echo "Killing longrunningthing on PID $p in 24 hours"
    sleep 86400
    echo Time up!
    
  2. プロセスを終了する前に停止してください。 (必ずしも必要ではありませんが、競合状態を回避する方法です。プロセスを停止すると、そのpidを再利用することはできません。)

    kill -s STOP "$p"
    
  3. そのPIDを持つプロセスの開始時間が同じであることを確認し、そうであれば終了し、そうでない場合はプロセスを続行します。

    cur=$(TZ=UTC0 ps -p "$p" -o lstart=)
    
    if [ "$cur" = "$stime" ]
    then
        # Okay, we can kill that process
        kill "$p"
    else
        # PID was reused. Better unblock the process!
        echo "long running task already completed!"
        kill -s CONT "$p"
    fi
    

これは、同じPIDを持つプロセスが1つしかない可能性があるために機能します。そして特定のオペレーティングシステムの起動時間。

検査中にプロセスを停止すると、競合状態はあまり問題になりません。明らかにここに問題があります。いくつかのランダムなプロセスが数ミリ秒間停止することがあります。プロセスの種類によっては問題になる場合もありません。


個人的には私は単にPythonを使っていますpsutilPIDの再利用を自動的に処理します。

import time

import psutil

# note: it would be better if you were able to avoid using
#       shell=True here.
proc = psutil.Process('longrunningtask', shell=True)
time.sleep(86400)

# PID reuse handled by the library, no need to worry.
proc.terminate()   # or: proc.kill()

答え4

あなたのlongrunningthing行動をもう少し改善し、デーモンに似ていることを検討してください。例えばpidファイルこれにより、プロセスに対して少なくともある程度制限された制御が可能になります。ラッパーを含む元のバイナリを変更せずにこれを行うには、いくつかの方法があります。たとえば、

  1. バックグラウンドで必要な操作を開始し(オプションの出力リダイレクトを使用)、プロセスのPIDをファイルに書き込み、プロセスが完了するのを待ってから(使用wait)ファイルを削除する簡単なラッパースクリプト。待機中にプロセスが終了した場合(例:

    kill $(cat pidfile)
    

    ラッパーはpidfileが削除されたことを確認するだけです。

  2. 配置するモニタラッパーそれPIDをどこかに置き、送信された信号をキャプチャして応答します。簡単な例:

    #!/bin/bash
    p=0
    trap killit USR1

    killit () {
        printf "USR1 caught, killing %s\n" "$p"
        kill -9 $p
    }

    printf "monitor $$ is waiting\n"
    therealstuff &
    p=%1
    wait $p
    printf "monitor exiting\n"

これで@R..と@StéphaneChazelasが指摘したように、これらの方法は通常、どこかに競合状態があるか、または生成できるプロセスの数に制限を課します。また、子孫を分岐して分離できる場合longrunningthing(元の質問では問題にならない可能性があります)、処理しません。

最近(数年前)、Linuxカーネルの場合、この問題は次の方法でうまく処理できます。cgroup今すぐ冷蔵庫- 私の意見では、これはいくつかの最新のLinux initシステムで使用されているようです。

関連情報