Как дождаться процесса внука (retval `bash` становится -1 в Perl из-за SIG CHLD)

У меня есть сценарий Perl (фрагмент ниже), который запускается в cron для выполнения системных проверок. Я разделяю дочерний элемент как тайм-аут и получаю его с помощью SIG {CHLD}. Perl выполняет несколько системных вызовов сценариев Bash и проверяет их статус выхода. Один сценарий bash дает сбой примерно в 5% случаев без ошибок. Скрипты Bash существуют с 0, а Perl видит $? как -1 и $! как «Нет дочерних процессов».

Этот сценарий bash проверяет лицензии компилятора, а Intel icc остается после завершения сценария Bash (вывод ps ниже). Я думаю, что зомби icc завершает работу, заставляя Perl использовать обработчик SIG {CHLD}, который сдувает $? статус, прежде чем я смогу его прочитать.

Compile status -1; No child processes

#!/usr/bin/perl
use strict;
use POSIX ':sys_wait_h';

my $GLOBAL_TIMEOUT = 1200;

### Timer to notify if this program hangs
my $timer_pid;
$SIG{CHLD} = sub {
    local ($!, $?);
    while((my $pid = waitpid(-1, WNOHANG)) > 0)
    {
        if($pid == $timer_pid)
        {
            die "Timeout\n";
        }
    }
};

die "Unable to fork\n" unless(defined($timer_pid = fork));
if($timer_pid == 0)  # child
{
    sleep($GLOBAL_TIMEOUT);
    exit;
}
### End Timer

### Compile test
my @compile = `./compile_test.sh 2>&1`;
my $status = $?;
print "Compile status $status; $!\n";
if($status != 0)
{
    print "@compile\n";
}

END  # Timer cleanup
{
    if($timer_pid != 0)
    {
        $SIG{CHLD} = 'IGNORE';
        kill(15, $timer_pid);
    }
}

exit(0);
#!/bin/sh

cc compile_test.c
if [ $? -ne 0 ]; then
    echo "Cray compiler failure"
    exit 1
fi

module swap PrgEnv-cray PrgEnv-intel
cc compile_test.c
if [ $? -ne 0 ]; then
    echo "Intel compiler failure"
    exit 1
fi

wait
ps
exit 0

Ожидание на самом деле не ждет, потому что cc вызывает icc, который создает внук-процесс зомби, который wait (или wait PID) не блокируется. (ожидание `pidof icc`, 31589 в данном случае дает" не дочерний элемент этой оболочки ")

user 31589     1  0 12:47 pts/15   00:00:00 icc

Я просто не знаю, как это исправить в Bash или Perl.

Спасибо Крис


person Chris    schedule 05.06.2019    source источник
comment
Похоже, вы столкнетесь с серьезными проблемами, избегая использования alarm. Есть ли причина не использовать здесь alarm?   -  person mob    schedule 05.06.2019
comment
Ваш обработчик SIGCHLD также получает оболочку, порожденную обратными кавычками, поэтому вызов waitpid, выполняемый обратными кавычками, завершается ошибкой (поскольку дочерний элемент уже был получен).   -  person ikegami    schedule 06.06.2019
comment
У меня есть несколько bash вызовов в реальном Perl-скрипте. Только этот периодически выходит из строя. Только что сегодня заметил, что ICC осталась позади, и ждать не могу.   -  person Chris    schedule 06.06.2019
comment
это не удается - я не понял, что не удалось? Тот факт, что icc остается (что неудобно), или это настоящая ошибка? Обратите внимание, что Статус компиляции -1; Отсутствие дочерних процессов не является ошибкой, так как у вас есть CHLD обработчик и проверка $? после обратных кавычек, которые, возможно, были получены обработчиком (так что единственная ошибка - это оба). Кроме того, из того, что вы показываете, видно, что cc запускает icc и не ждет его ...? (Вы уверены? Для меня это звучит очень странно.)   -  person zdim    schedule 06.06.2019
comment
Обратите внимание: вы не можете действительно проверить wait 31589 (или что-то подобное), поскольку вы не знаете, какой PID дочернего элемента находится в текущем прогоне (он, скорее всего, отличается от того, что было в предыдущих прогонах).   -  person zdim    schedule 06.06.2019
comment
Таким образом, локальный ($?, $!) В SIG {ALRM} не сохраняет значения в жатке. Я думаю, нет возможности сохранить $? из `bash` из-за того, что его затирали?   -  person Chris    schedule 06.06.2019
comment
не сохраняет значения в жатке - я не понимаю: обработчик - это подпрограмма, которая запускается и завершается, а значения в ней теряются. Все, что вы хотите от обработчика сигналов, необходимо назначить глобальным переменным. (Например, у вас может быть хэш, связанный с pid-кодами, и назначить его, как только вы проверите, что он получил нужный вам процесс.) Я не уверен, для чего нужен local $? - если он предназначен для защиты вещей вне обработчика, который выиграл не работает: обработчик получает сигнал о завершившемся дочернем элементе, поэтому обратные кавычки (waitpid, который запускается системой для него) просто ничего не имеют, поэтому -1.   -  person zdim    schedule 06.06.2019


Ответы (3)


Разве это не вариант использования alarm? Выбросьте свой обработчик SIGCHLD и скажите

local $? = -1;
eval {
    local $SIG{ALRM} = sub { die "Timeout\n" };
    alarm($GLOBAL_TIMEOUT);
    @compile = `./compile_test.sh 2>&1`;
    alarm(0);
};

my $status = $?;

вместо.

person mob    schedule 05.06.2019
comment
Сценарий Perl выполняет множество других функций. Я просто вырезал ту часть, которая вышла из строя. Там уже есть SIG ALRM для чего-то еще, но, может быть, вы можете вложить один в другой? SIG CHLD был общим тайм-аутом. - person Chris; 05.06.2019
comment
У вас не может быть вложенных сигналов тревоги, но local $SIG{ALRM} = ... может перезаписывать обработчик SIGALRM до конца области, в которой он был определен. - person mob; 05.06.2019
comment
Рассмотрите возможность Time :: Out для разумного выполнения вложенных сигналов тревоги. - person Grinnz; 06.06.2019
comment
Значение $status в основном мусор. -1 обычно означает использовать $!, но вы не можете сказать, означает ли это, что здесь, а $! все равно был заблокирован alarm(0). - person ikegami; 06.06.2019

Я подумал, что самым быстрым решением было бы добавить сон на секунду или две в конце сценария bash, чтобы дождаться завершения zombie icc. Но это не сработало.

Если бы у меня еще не было SIG ALRM (в реальной программе), я согласен, что лучшим выбором было бы обернуть все это в eval. Даже подумал, что это было бы довольно некрасиво для программы из 500 строк.

Без локального ($?) каждый вызов `system` получает $? = -1. $? В этом случае мне нужно после waitpid, а затем, к сожалению, установлено значение -1 после выхода обработчика sig. Я считаю, что это работает. Новые строки показаны с ###

my $timer_pid;
my $chld_status;    ###
$SIG{CHLD} = sub {
    local($!, $?);
    while((my $pid = waitpid(-1, WNOHANG)) > 0)
    {
        $chld_status = $?;    ###
        if($pid == $timer_pid)
        {
            die "Timeout\n";
        }
    }
};

...
my @compile = `./compile_test.sh 2>&1`;
my $status = ($? == -1) ? $chld_status : $?;    ###
...
person Chris    schedule 07.06.2019

У нас была аналогичная проблема, вот наше решение: утечка дескриптора файла на стороне записи в внучку и read () из него, который будет блокироваться до тех пор, пока он не выйдет.

См. Также: ждите детей и внуков

use Fcntl;

# OCF scripts invoked by Pacemaker will be killed by Pacemaker with
# a SIGKILL if the script exceeds the configured resource timeout. In
# addition to killing the script, Pacemaker also kills all of the children
# invoked by that script. Because it is a kill, the scripts cannot trap
# the signal and clean up; because all of the children are killed as well,
# we cannot simply fork and have the parent wait on the child. In order
# to work around that, we need the child not to have a parent proccess
# of the OCF script---and the only way to do that is to grandchild the
# process. However, we still want the parent to wait for the grandchild
# process to exit so that the OCF script exits when the grandchild is
# done and not before. This is done by leaking the write file descriptor
# from pipe() into the grandchild and then the parent reads the read file
# descriptor, thus blocking until it gets IO or the grandchild exits. Since
# the file descriptor is never written to by the grandchild, the parent
# blocks until the child exits.
sub grandchild_wait_exit
{
    # We use "our" instead of "my" for the write side of the pipe. If
    # we did not, then when the sub exits and $w goes out of scope,
    # the file descriptor will close and the parent will exit.
    pipe(my $r, our $w);

    # Enable leaking the file descriptor into the children
    my $flags = fcntl($w, F_GETFD, 0) or warn $!;
    fcntl($w, F_SETFD, $flags & (~FD_CLOEXEC)) or die "Can't set flags: $!\n";

    # Fork the child
    my $child = fork();
    if ($child) {
        # We are the parent, waitpid for the child and
        # then read to wait for the grandchild.
        close($w);
        waitpid($child, 0);
        <$r>;
        exit;
    }

    # Otherwise we are the child, so close the read side of the pipe.
    close($r);

    # Fork a grandchild, exit the child.
    if (fork()) {
        exit;
    }

    # Turn off leaking of the file descriptor in the grandchild so
    # that no other process can write to the open file descriptor
    # that would prematurely exit the parent.
    $flags = fcntl($w, F_GETFD, 0) or warn $!;
    fcntl($w, F_SETFD, $flags | FD_CLOEXEC) or die "Can't set flags: $!\n";
}

grandchild_wait_exit();

sleep 1;
print getppid() . "\n";
print "$$: gc\n";
sleep 30;
exit;
person KJ7LNW    schedule 07.08.2020
comment
Обратите внимание, что getppid () возвращает 1, потому что родительский внук (первый дочерний элемент родителя) завершился, поэтому внук повторно привязан к процессу инициализации. - person KJ7LNW; 07.08.2020
comment
Значит, это процесс инициализации? init - ›ребенок -› внук - person Snorik; 07.08.2020
comment
parent - ›child -› внук, но когда дочерний элемент уходит, он становится родительским - ›[none], init -› grandchild - person KJ7LNW; 08.08.2020