Подтвердить что ты не робот

Как я должен очистить процессы зависания внуков, когда срабатывает тревога в Perl?

У меня есть параллельная автоматизация script, которая требует вызова многих других скриптов, некоторые из которых зависают, потому что они (некорректно) ждут ввода стандартного ввода или ожидают различные другие вещи, которые не произойдут. Это неважно, потому что я поймаю их alarm. Хитрость заключается в том, чтобы закрыть те, кто висел в процессе внуков, когда ребенок выключается. Я думал, что различные заклинания групп SIGCHLD, ожидания и процесса могут сделать трюк, но все они блокируются, а внуки не получают.

Мое решение, которое работает, просто не похоже, что это правильное решение. Я пока не особо заинтересован в решении Windows, но в конце концов мне это тоже понадобится. Mine работает только для Unix, на данный момент это нормально.

Я написал небольшой script, который забирает число одновременных параллельных дочерних элементов и общее количество вилок:

 $ fork_bomb <parallel jobs> <number of forks>

 $ fork_bomb 8 500

В течение нескольких минут это, вероятно, повлияет на лимит процесса для каждого пользователя. Многие решения, которые я нашел, просто говорят вам увеличить лимит для каждого пользователя, но мне нужно, чтобы он работал около 300 000 раз, так что это не сработает. Точно так же предложения о повторном выполнении и т.д. Для очистки таблицы процессов не нужны мне. Я бы хотел исправить проблему, вместо того, чтобы похлопывать по ней ленту.

Я сканирую таблицу процессов, ищущую дочерние процессы, и завершаю отдельные процессы в обработчике SIGALRM, которые должны умереть, потому что остальная часть реального кода не имеет надежды на успех после этого. Прохождение kludgey через таблицу процессов не мешает мне с точки зрения производительности, но я бы не прочь не делать этого:

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:: ForkManager run_on_finish случается слишком поздно для одного и того же кода очистки: внуки, по-видимому, уже не связаны с дочерний процесс обрабатывается в этой точке.

4b9b3361

Ответ 1

Я несколько раз прочел вопрос, и я думаю, что я получаю то, что вы пытаются сделать. У вас есть элемент управления script. Этот script нерестится дети, чтобы сделать что-то, и эти дети порождают внуков на самом деле делать работу. Проблема в том, что внуки могут быть слишком медленно (ожидание STDIN или что-то еще), и вы хотите их убить. Кроме того, если есть один медленный внук, вы хотите, чтобы весь ребенок умирает (убивая других внуков, если это возможно).

Итак, я попытался реализовать эти два пути. Первым было сделать parent порождает ребенка в новом сеансе UNIX, устанавливает таймер для нескольких секунд и убить весь дочерний сеанс, когда таймер погас. Это сделало родителя ответственным как за ребенка, так и за внуки. Это также не сработало.

Следующая стратегия состояла в том, чтобы заставить родителя порождать ребенка, а затем заставляют ребенка отвечать за управление внуками. Это установить таймер для каждого внука и убить его, если процесс не был завершено по истечении срока действия. Это отлично работает, так что вот код.

Мы будем использовать EV для управления детьми и таймерами, а AnyEvent - для API. (Вы можете попробовать другой цикл событий AnyEvent, например Event или POE. Но я знаю, что 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;

Затем нам нужно написать функцию для запуска детей. Вещи родительские породы называются детьми, а вещи, которые дети spawn называются заданиями.

sub start_child([email protected]) {
    my ($on_success, $on_error, @jobs) = @_;

Аргументы - это обратный вызов, вызываемый при завершении дочернего процесса успешно (то есть его рабочие места также были успешными), обратный вызов, когда ребенок не завершил успешно, а затем список coderef выполняемые задания.

В этой функции нам нужно использовать fork. В родителе мы устанавливаем дочерний элемент наблюдатель для наблюдения за ребенком:

    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);
            }
        });
    }

В ребенке мы фактически запускаем задания. Это связано с немного setup. Тем не менее.

Во-первых, мы забываем о родительских наблюдателях детей, потому что это не делает смысл для ребенка быть информированным о своих братьях и сестрах. (Вилка весело, потому что вы наследуете все родительское состояние, даже если это не имеет никакого смысла.)

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

Нам также нужно знать, когда все задания выполняются, и независимо от того, все они были успешными. Мы используем условную переменную counting определить, когда все вышло. Мы приступаем к запуску, и декремент при выходе, а когда число равно 0, мы знаем, что все сделано.

Я также сохраняю логическое значение, чтобы указать состояние ошибки. Если процесс выходы с ненулевым статусом, ошибка переходит к 1. В противном случае он остается 0. Возможно, вы захотите сохранить больше состояний, чем это:)

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

        $done->begin;

(Мы также начинаем подсчет в 1, так что если есть 0 заданий, наш процесс все еще выходит.)

Теперь нам нужно использовать fork для каждого задания и запускать задание. В родителе мы сделайте несколько вещей. Мы увеличиваем конвар. Мы устанавливаем таймер, чтобы убить ребенок, если он слишком медленный. И мы настраиваем наблюдателя за детьми, поэтому мы можем получать информацию о статусе выхода на работу.

    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

Хорошо, так что ребенок и внук/работа. Теперь нам просто нужно написать родителя, что намного проще.

Как и ребенок, мы собираемся использовать счетчик, чтобы дождаться нашего детей.

# 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, он принимает два обратных вызова, ошибку обратный вызов и успешный обратный вызов. Мы подберем их; Ошибка обратный вызов будет печатать "не в порядке" и уменьшать конвер, а успешный обратный вызов будет печатать "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);

Два из них будут тайм-аут, а два будут успешными. Если вы нажмете enter хотя они все же работают, тогда все они могут преуспеть.

В любом случае, как только они начнутся, нам просто нужно дождаться их Отделка:

$all_done->recv;

say "...done";

exit 0;

И что программа.

Одна вещь, которую мы не делаем, что Parallel:: ForkManager делает "ограничение скорости" наших вилок, так что только дети 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, или вы можете игнорировать эти вещи или что-то еще. Вы добираетесь до решите, а не некоторый автор модуля, который пытается сделать вещи "простыми".

В любом случае, надеюсь, что это поможет.

Ответ 2

Брайан - это немного грубо и не-идиоматично, но один из подходов, который я видел, - это: в любое время вы развиваете, вы:

  • Дайте дочернему процессу первый параметр "-id" для программы, имеющий несколько уникальное значение (для каждого ПИД) - хорошим кандидатом может быть временная метка времени до + миллисекунды + родительский ПИД.

  • Родитель записывает дочерний PID и значение -id в (в идеале, постоянный) реестр вместе с желаемым временем ожидания/завершения.

Затем процесс наблюдения (или конечный дед или отдельный процесс с одним и тем же UID) просто периодически циклически проходит через реестр и проверяет, какие процессы, которые необходимо убить (по времени-убить-времени), все еще висят вокруг (путем сопоставления значения параметра PID и "-id" в реестре с PID и командной строкой в ​​таблице процессов); и отправить сигнал 9 на такой процесс (или быть хорошим и попытаться убить нежно сначала, пытаясь отправить сигнал 2).

Уникальный параметр "-id" , очевидно, предназначен для предотвращения убийства какого-то невиновного процесса, который только что произошел, чтобы повторить использование PID предыдущего процесса по совпадению, что, вероятно, возможно, учитывая масштаб, о котором вы упомянули.

Идея реестра помогает решить проблему "уже disassociated" grand-children, так как вы больше не зависите от системы, чтобы поддерживать связь между родителями и дочерними элементами.

Это своего рода грубая сила, но, поскольку никто еще не ответил, я понял, что, хотя мои 3 цента имеют смысл, ваш путь.

Ответ 3

Мне нужно решить эту же проблему в модуле Я работаю. Я не полностью удовлетворен всеми моими решениями, но то, что обычно работает в Unix, это

  • изменить группу дочерних процессов
  • Восстанавливать внуков по мере необходимости
  • снова измените дочернюю группу процессов (скажем, вернемся к исходному значению)
  • сообщите группе процессов внуков, чтобы убить внуков.

Что-то вроде:

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
}

Это не полностью доказательство дурака. Внуки могут изменять свои группы процессов или сигналы ловушки.

Ничего из этого не будет работать в Windows, конечно, но позвольте сказать, что TASKKILL /F /T - ваш друг.


Обновление: Это решение не обрабатывает (для меня, в любом случае) случай, когда дочерний процесс вызывает system "perl -le '<STDIN>'". Для меня это немедленно приостанавливает процесс и предотвращает запуск SIGALRM и запуск обработчика SIGALRM. Закрывает ли STDIN единственный обходной путь?