Как мне очистить зависшие процессы внуков, когда тревога срабатывает в Perl? - PullRequest
12 голосов
/ 15 мая 2010

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

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

Ответы [ 3 ]

8 голосов
/ 16 мая 2010

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

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

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

Мы будем использовать EV для управления детьми и таймерами, а AnyEvent для API. (Вы можете попробовать другой цикл событий AnyEvent, такой как Event или POE. Но я знаю, что ЭВ правильно справляется с условием, когда ребенок выходит прежде чем вы скажете петле контролировать его, что устраняет надоедливую гонку условия, к которым уязвимы другие циклы.)

#!/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
            }

Вы также можете закрыть здесь стандартный ввод, если хотите.

Теперь, после того, как все процессы были созданы, мы ждем их все выход, ожидая на 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;

Нам нужно кое-что сделать. Вот тот, который всегда успешен, и тот, который будет успешным, если вы нажмете возврат, но потерпит неудачу, если вы просто пусть его убьет таймер:

my $good_grandchild = sub {
    exit 0;
};

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

Итак, нам просто нужно начать работу с детьми. Если вы помните способ к началу start_child, требуется два обратных вызова, ошибка обратный вызов и обратный вызов успеха. Мы настроим их; Ошибка обратный вызов выведет «не в порядке» и уменьшит condvar, а Успешный обратный вызов выведет «ок» и сделает тоже самое Очень просто.

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

У двух из них истечет время ожидания, а у двух все получится. Если вы нажмете ввод хотя, пока они бегут, все они могут преуспеть.

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

$all_done->recv;

say "...done";

exit 0;

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

Единственное, чего не делает Parallel :: ForkManager, это «ограничить скорость» наших вилок, чтобы только 1071 ребенок бегал с время. Это довольно легко реализовать вручную, хотя:

 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 и кормить это стандартный ввод (как с Expect), и вы можете захватить его стандартный ввод и стандартный вывод и stderr, или вы можете игнорировать эти вещи, или что угодно. Вы получаете к решите, а не какой-нибудь автор модуля, который пытается сделать вещи "простыми".

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

1 голос
/ 16 мая 2010

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

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

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

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

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

Идея реестра помогает решить проблему "уже разобщенных" внуков, поскольку вы больше не зависите от системы, которая поддерживает связь между родителями и детьми.

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

0 голосов
/ 18 мая 2010

Я должен решить эту проблему в модуле Я работал над . Я не полностью удовлетворен всеми моими решениями, но в 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
}

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

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


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

...