12

私は、他の多くのスクリプトを呼び出す必要がある並列化された自動化スクリプトを持っています。そのうちのいくつかは、(誤って) 標準入力を待機したり、発生しない他のさまざまなことを待機したりするためにハングします。警戒して捕まえるから大したことじゃない。秘訣は、子プロセスがシャットダウンしたときにハングした孫プロセスをシャットダウンすることです。、待機、および処理グループのさまざまな呪文でSIGCHLDうまくいくと思いましたが、それらはすべてブロックされ、孫は刈り取られません。

うまくいく私の解決策は、それが正しい解決策であるようには思えません。Windows ソリューションには今のところ特に関心はありませんが、最終的には Windows ソリューションも必要になるでしょう。私のものは Unix でのみ動作しますが、今のところは問題ありません。

同時に実行する並列の子の数とフォークの総数を取得する小さなスクリプトを作成しました。

 $ fork_bomb <parallel jobs> <number of forks>

 $ fork_bomb 8 500

これはおそらく、数分以内にユーザーごとのプロセス制限に達します。私が見つけた多くの解決策は、ユーザーごとのプロセス制限を増やすように指示しているだけですが、これを約 300,000 回実行する必要があるため、うまくいきません。同様に、プロセステーブルをクリアするための再実行などの提案は、私が必要としているものではありません。ダクトテープを叩くのではなく、実際に問題を修正したいと思います。

プロセス テーブルをクロールして子プロセスを探し、ハングしたプロセスをハンドラーで個別にシャットダウンしますSIGALRM。実際のコードの残りの部分はその後成功する見込みがないため、ハンドラーを停止する必要があります。プロセス テーブルをざっくりとクロールすることは、パフォーマンスの観点からは気になりませんが、実行しなくてもかまいません。

use Parallel::ForkManager;
use Proc::ProcessTable;

my $pm = Parallel::ForkManager->new( $ARGV[0] );

my $alarm_sub = sub {
        kill 9,
            map  { $_->{pid} }
            grep { $_->{ppid} == $$ }
            @{ Proc::ProcessTable->new->table }; 

        die "Alarm rang for $$!\n";
        };

foreach ( 0 .. $ARGV[1] ) 
    {
    print ".";
    print "\n" unless $count++ % 50;

    my $pid = $pm->start and next; 

    local $SIG{ALRM} = $alarm_sub;

    eval {
        alarm( 2 );
        system "$^X -le '<STDIN>'"; # this will hang
        alarm( 0 );
        };

    $pm->finish;
    }

プロセスを使い果たしたい場合は、killを取り出します。

プロセスグループを設定するとうまくいくので、すべてをまとめて殺すことができると思いましたが、それはブロックします:

my $alarm_sub = sub {
        kill 9, -$$;    # blocks here
        die "Alarm rang for $$!\n";
        };

foreach ( 0 .. $ARGV[1] ) 
    {
    print ".";
    print "\n" unless $count++ % 50;

    my $pid = $pm->start and next; 
    setpgrp(0, 0);

    local $SIG{ALRM} = $alarm_sub;

    eval {
        alarm( 2 );
        system "$^X -le '<STDIN>'"; # this will hang
        alarm( 0 );
        };

    $pm->finish;
    }

POSIXの同じことsetsidも機能しませんでした。実際にはこれをデーモン化していないため、実際には別の方法で問題が発生したと思います。

不思議なことに、Parallel::ForkManagerrun_on_finish発生は同じクリーンアップ コードでは遅すぎます。その時点で、孫プロセスは明らかに子プロセスから切り離されているようです。

4

3 に答える 3

8

私は質問を数回読みました、そして私はあなたがやろうとしていることをある程度理解していると思います。制御スクリプトがあります。このスクリプトは、子を生成していくつかのことを実行し、これらの子は孫を生成して実際に作業を実行します。問題は、孫が遅すぎる可能性があり(STDINなどを待っている)、あなたが孫を殺したいということです。さらに、遅い孫が1人いる場合は、子供全体を死なせます(可能であれば、他の孫を殺します)。

そこで、この2つの方法を実装してみました。1つ目は、親が新しいUNIXセッションで子を生成し、タイマーを数秒に設定し、タイマーが切れたときに子セッション全体を強制終了することでした。これにより、親は子供と孫の両方に責任を持つようになりました。また、正しく機能しませんでした。

次の戦略は、親に子をスポーンさせてから、子に孫の管理を任せることでした。孫ごとにタイマーを設定し、有効期限までにプロセスが終了していなかった場合はタイマーを強制終了します。これはうまく機能するので、ここにコードがあります。

EVを使用して子とタイマーを管理し、AnyEventをAPIに使用します。(EventやPOEなどの別のAnyEventイベントループを試すことができます。ただし、EVは、ループに監視するように指示する前に、子が終了する状態を正しく処理することを知っています。これにより、他のループが脆弱である煩わしい競合状態が排除されます。)

#!/usr/bin/env perl

use strict;
use warnings;
use feature ':5.10';

use AnyEvent;
use EV; # you need EV for the best child-handling abilities

チャイルドウォッチャーを追跡する必要があります。

# active child watchers
my %children;

次に、子を起動する関数を作成する必要があります。親がスポーンするものは子と呼ばれ、子がスポーンするものはジョブと呼ばれます。

sub start_child($$@) {
    my ($on_success, $on_error, @jobs) = @_;

引数は、子が正常に完了したときに呼び出されるコールバック(つまり、そのジョブも成功したことを意味します)、子が正常に完了しなかったときに呼び出されるコールバック、および実行するcoderefジョブのリストです。

この関数では、フォークする必要があります。親では、子を監視するために子ウォッチャーを設定します。

    if(my $pid = fork){ # parent
        # monitor the child process, inform our callback of error or success
        say "$$: Starting child process $pid";
        $children{$pid} = AnyEvent->child( pid => $pid, cb => sub {
            my ($pid, $status) = @_;
            delete $children{$pid};

            say "$$: Child $pid exited with status $status";
            if($status == 0){
                $on_success->($pid);
            }
            else {
                $on_error->($pid);
            }
        });
    }

子供の中で、私たちは実際に仕事をします。ただし、これには少しセットアップが必要です。

まず、親の子ウォッチャーを忘れます。これは、子がその兄弟の退出について通知されることは意味がないためです。(フォークは楽しいです。それがまったく意味をなさない場合でも、親の状態をすべて継承するからです。)

    else { # child
        # kill the inherited child watchers
        %children = ();
        my %timers;

また、すべての仕事がいつ完了したか、そしてそれらがすべて成功したかどうかを知る必要があります。カウント条件変数を使用して、すべてがいつ終了したかを判別します。起動時にインクリメントし、終了時にデクリメントします。カウントが0の場合、すべてが完了したことがわかります。

また、エラー状態を示すためにブール値を保持します。プロセスがゼロ以外のステータスで終了する場合、エラーは1になります。それ以外の場合、プロセスは0のままです。これよりも多くの状態を保持することをお勧めします:)

        # then start the kids
        my $done = AnyEvent->condvar;
        my $error = 0;

        $done->begin;

(また、カウントを1から開始して、ジョブが0の場合でも、プロセスが終了するようにします。)

次に、ジョブごとにフォークして、ジョブを実行する必要があります。親では、いくつかのことを行います。condvarをインクリメントします。遅すぎる場合に子供を殺すためにタイマーを設定しました。また、子ウォッチャーを設定して、ジョブの終了ステータスを通知できるようにします。

    for my $job (@jobs) {
            if(my $pid = fork){
                say "[c] $$: starting job $job in $pid";
                $done->begin;

                # this is the timer that will kill the slow children
                $timers{$pid} = AnyEvent->timer( after => 3, interval => 0, cb => sub {
                    delete $timers{$pid};

                    say "[c] $$: Killing $pid: too slow";
                    kill 9, $pid;
                });

                # this monitors the children and cancels the timer if
                # it exits soon enough
                $children{$pid} = AnyEvent->child( pid => $pid, cb => sub {
                    my ($pid, $status) = @_;
                    delete $timers{$pid};
                    delete $children{$pid};

                    say "[c] [j] $$: job $pid exited with status $status";
                    $error ||= ($status != 0);
                    $done->end;
                });
            }

タイマーは状態を保持するため、タイマーの使用はアラームよりも少し簡単です。各タイマーはどのプロセスを強制終了するかを知っており、プロセスが正常に終了したときにタイマーを簡単にキャンセルできます。ハッシュからタイマーを削除するだけです。

それが(子の)親です。(子の、または仕事の)子は本当に単純です:

            else {
                # run kid
                $job->();
                exit 0; # just in case
            }

必要に応じて、ここでstdinを閉じることもできます。

ここで、すべてのプロセスが生成された後、condvarを待機して、すべてのプロセスが終了するのを待ちます。イベントループは子とタイマーを監視し、私たちのために正しいことをします:

        } # this is the end of the for @jobs loop
        $done->end;

        # block until all children have exited
        $done->recv;

次に、すべての子が終了したら、次のようなクリーンアップ作業を実行できます。

        if($error){
            say "[c] $$: One of your children died.";
            exit 1;
        }
        else {
            say "[c] $$: All jobs completed successfully.";
            exit 0;
        }
    } # end of "else { # child"
} # end of start_child

OK、それが子供と孫/仕事です。ここで、親を作成する必要があります。これははるかに簡単です。

子供のように、カウント条件を使用して子供を待ちます。

# main program
my $all_done = AnyEvent->condvar;

やるべき仕事が必要です。これは常に成功するものであり、returnキーを押すと成功しますが、タイマーで強制終了するだけでは失敗します。

my $good_grandchild = sub {
    exit 0;
};

my $bad_grandchild = sub {
    my $line = <STDIN>;
    exit 0;
};

したがって、子ジョブを開始する必要があります。の先頭に戻る方法を覚えている場合はstart_child、エラーコールバックと成功コールバックの2つのコールバックが必要です。それらを設定します。エラーコールバックは「notok」を出力してcondvarをデクリメントし、成功コールバックは「ok」を出力して同じことを行います。とてもシンプルです。

my $ok  = sub { $all_done->end; say "$$: $_[0] ok" };
my $nok = sub { $all_done->end; say "$$: $_[0] not ok" };

次に、さらに多くの孫の仕事をしているたくさんの子供たちを始めることができます:

say "starting...";

$all_done->begin for 1..4;
start_child $ok, $nok, ($good_grandchild, $good_grandchild, $good_grandchild);
start_child $ok, $nok, ($good_grandchild, $good_grandchild, $bad_grandchild);
start_child $ok, $nok, ($bad_grandchild, $bad_grandchild, $bad_grandchild);
start_child $ok, $nok, ($good_grandchild, $good_grandchild, $good_grandchild, $good_grandchild);

それらのうちの2つはタイムアウトになり、2つは成功します。ただし、実行中にEnterキーを押すと、すべて成功する可能性があります。

とにかく、それらが開始されたら、それらが終了するのを待つ必要があります。

$all_done->recv;

say "...done";

exit 0;

そしてそれがプログラムです。

Parallel :: ForkManagerが行っていないことの1つはn、一度に子だけが実行されるようにフォークを「レート制限」することです。ただし、これは手動で実装するのは非常に簡単です。

 use Coro;
 use AnyEvent::Subprocess; # better abstraction than manually
                           # forking and making watchers
 use Coro::Semaphore;

 my $job = AnyEvent::Subprocess->new(
    on_completion => sub {}, # replace later
    code          => sub { the child process };
 )

 my $rate_limit = Coro::Semaphore->new(3); # 3 procs at a time

 my @coros = map { async {
     my $guard = $rate_limit->guard;
     $job->clone( on_completion => Coro::rouse_cb )->run($_);
     Coro::rouse_wait;
 }} ({ args => 'for first job' }, { args => 'for second job' }, ... );

 # this waits for all jobs to complete
 my @results = map { $_->join } @coros;

ここでの利点は、子が実行されている間に他のことを実行できることですasync。ブロッキング結合を実行する前に、より多くのスレッドを生成するだけです。また、AnyEvent :: Subprocessを使用して、子をより詳細に制御できます。子をPtyで実行し、stdinにフィードし(Expectのように)、そのstdinとstdoutとstderrをキャプチャするか、無視することができます。それらのもの、または何でも。物事を「シンプル」にしようとしているモジュール作成者ではなく、あなたが決めることができます。

とにかく、これが役立つことを願っています。

于 2010-05-16T00:37:03.847 に答える
1

ブライアン-それは少し粗雑で慣用的ではありませんが、私が取ったアプローチの1つはこれです:あなたがフォークするときはいつでも、あなた:

  1. 子プロセスに、プログラムの最初の「-id」ダミーパラメータを、ある程度一意の(PIDごとの)値で指定します。適切な候補は、最大ミリ秒のタイムスタンプ+親のPIDです。

  2. 親は、子PIDと-id値を、必要なタイムアウト/キル時間とともに(理想的には永続的な)レジストリに記録します。

次に、ウォッチャープロセス(最終的な祖父母または同じUIDを持つ別のプロセス)にレジストリを定期的に循環させ、(キルタイムごとに)強制終了する必要のあるプロセスがまだハングしていることを確認します(照合することにより)プロセステーブルにPIDとコマンドラインがあるレジストリのPIDと「-id」パラメータ値の両方。そして、そのようなプロセスにシグナル9を送信します(または、シグナル2を送信しようとして、最初に優しく殺してみてください)。

一意の「-id」パラメータは、前のプロセスのPIDを偶然に再利用したばかりの無実のプロセスを強制終了することを防ぐことを目的としています。

レジストリのアイデアは、親子の関連付けを維持するためにシステムに依存しなくなったため、「すでに関連付けが解除された」孫の問題に役立ちます。

これは一種のブルートフォースですが、まだ誰も答えていないので、3セントの価値のあるアイデアをあなたのやり方で考えます。

于 2010-05-16T00:20:33.407 に答える
0

私が取り組んでいるモジュールでこの同じ問題を解決する必要があります。私もすべてのソリューションに完全に満足しているわけではありませんが、Unix で一般的に機能するのは

  1. 子のプロセス グループを変更する
  2. 必要に応じて孫をスポーンする
  3. 子のプロセス グループを再度変更します (たとえば、元の値に戻します)。
  4. 孫のプロセスグループに孫を殺すように合図する

何かのようなもの:

use Time::HiRes qw(sleep);

sub be_sleepy { sleep 2 ** (5 * rand()) }
$SIGINT = 2;

for (0 .. $ARGV[1]) {
    print ".";
    print "\n" unless ++$count % 50;
    if (fork() == 0) {   
        # a child process
        # $ORIGINAL_PGRP and $NEW_PGRP should be global or package or object level vars
        $ORIGINAL_PGRP = getpgrp(0);
        setpgrp(0, $$);
        $NEW_PGRP = getpgrp(0);

        local $SIG{ALRM} = sub {
            kill_grandchildren();
            die "$$ timed out\n";
        };

        eval {
            alarm 2;
            while (rand() < 0.5) {
                if (fork() == 0) {
                    be_sleepy();
                }
            }
            be_sleepy();
            alarm 0;
            kill_grandchildren();
        };

        exit 0;
    }
}

sub kill_grandchildren {
    setpgrp(0, $ORIGINAL_PGRP);
    kill -$SIGINT, $NEW_PGRP;   # or  kill $SIGINT, -$NEW_PGRP
}

これは完全にばかげた証拠ではありません。孫がプロセス グループを変更したり、シグナルをトラップしたりする可能性があります。

TASKKILL /F /Tもちろん、これはどれも Windows では機能しませんが、それがあなたの友達だとだけ言っておきましょう。


更新:このソリューションは、(とにかく、私にとっては) 子プロセスが呼び出すケースを処理しませんsystem "perl -le '<STDIN>'"。私の場合、これによりプロセスがすぐに中断され、SIGALRM が起動されなくなり、SIGALRM ハンドラが実行されなくなります。閉じるSTDINことが唯一の回避策ですか?

于 2010-05-17T20:52:41.263 に答える