2
votes

Comment attendre le processus des petits-enfants (`bash` retval devient -1 en Perl en raison de SIG CHLD)

J'ai un script Perl (extrait ci-dessous) qui s'exécute dans cron pour effectuer des vérifications du système. Je lance un enfant comme un timeout et le récolte avec SIG {CHLD}. Perl effectue plusieurs appels système de scripts Bash et vérifie leur état de sortie. Un script bash échoue environ 5% du temps sans erreur. Les scripts Bash existent avec 0 et Perl voit $? comme -1 et $! comme "Aucun processus enfant".

Ce script bash teste les licences du compilateur, et Intel icc est laissé une fois le script Bash terminé (sortie ps ci-dessous). Je pense que le zombie icc se termine, forçant Perl à entrer dans le gestionnaire SIG {CHLD}, ce qui souffle le $? état avant que je puisse le lire.

user 31589     1  0 12:47 pts/15   00:00:00 icc
#!/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
#!/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);

L'attente n'attend pas vraiment car cc appelle icc qui crée un processus de petit-fils zombie qui attend (ou attend le PID) ne bloque pas. (attendez `pidof icc`, 31589 dans ce cas, donne" pas un enfant de ce shell ")

Compile status -1; No child processes

Je ne sais tout simplement pas comment résoudre ce problème dans Bash ou Perl.

Merci, Chris


7 commentaires

Il semble que vous allez vous donner beaucoup de mal pour éviter d'utiliser alarm . Y a-t-il une raison de ne pas utiliser alarm ici?


Votre gestionnaire SIGCHLD récupère également le shell généré par les backticks, donc l'appel waitpid effectué par les backticks échoue (puisque l'enfant a déjà été récolté).


J'ai plusieurs appels bash dans le vrai script Perl. Seul celui-ci échoue périodiquement. Je viens de remarquer aujourd'hui que l'icc a laissé derrière lui, que «attendre» ne peut pas attraper.


" celui-ci échoue " - Je n'ai pas obtenu ce qui échoue? Le fait que icc reste (ce qui est gênant), ou y a-t-il une erreur réelle? Notez que " État de compilation -1; Aucun processus enfant " n'est pas une erreur puisque vous avez un gestionnaire CHLD et vérifiez $ ? après les backticks, qui peuvent avoir été récupérés par le gestionnaire (donc la seule erreur est de faire les deux). De plus, d'après ce que vous montrez, il apparaît que cc démarre icc et ne l'attend pas ...? (Êtes-vous sûr? Cela me semble vraiment étrange.)


Notez que vous ne pouvez pas vraiment vérifier wait 31589 (ou autre) car vous ne savez pas quel PID d'un enfant est dans l'exécution actuelle (il est très probablement différent de ce qu'il était dans les exécutions précédentes ).


Ainsi, le local ($?, $!) Dans SIG {ALRM} ne garde pas les valeurs dans le moissonneur. Je suppose qu'il n'y a aucun moyen de garder $? de `bash` d'être écrasé?


" ne garde pas les valeurs dans le moissonneur " - Je ne comprends pas: le gestionnaire est un sous-marin qui s'exécute et sort et les valeurs y sont perdues. Tout ce que vous voulez d'un gestionnaire de signaux doit être affecté à des variables globales. (Vous pouvez avoir un hachage clé par pids, par exemple, et lui attribuer une fois que vous avez vérifié qu'il a récolté un processus que vous voulez.) Je ne sais pas à quoi sert local $? - si c'est destiné à protéger des éléments en dehors du gestionnaire qui ne fonctionneront pas: le gestionnaire récupère le signal concernant l'enfant sorti de sorte que les backticks (waitpid qui est exécuté par le système pour lui) n'ont rien, donc -1.


3 Réponses :


1
votes

N'est-ce pas un cas d'utilisation pour alarm ? Jetez votre gestionnaire SIGCHLD et dites à la place

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

my $status = $?;

.


4 commentaires

Le script Perl contient des tonnes d'autres éléments. Je viens de couper la partie qui échoue. Il y a déjà un SIG ALRM là-dedans pour autre chose, mais peut-être pouvez-vous en avoir un imbriqué dans un autre? Le SIG CHLD était un timeout global.


Vous ne pouvez pas avoir d'alarmes imbriquées, mais un $ SIG local {ALRM} = ... peut écraser le gestionnaire SIGALRM jusqu'à la fin de la portée dans laquelle il a été défini.


Pensez à Time :: Out pour faire des alarmes imbriquées judicieusement.


La valeur de $ status est principalement garbage. -1 signifie normalement utiliser $! , mais vous ne pouvez pas dire si cela signifie que ici, et $! a été écrasé par < code> alarm (0) quand même.



1
votes

Je pensais que la solution la plus rapide serait d'ajouter un sommeil d'une seconde ou deux en bas du script bash pour attendre la fin de l'icc zombie. Mais cela n'a pas fonctionné.

Si je n'avais pas déjà un SIG ALRM (dans le vrai programme), je suis d'accord que le meilleur choix serait d'envelopper le tout dans une évaluation. Même pensé que ce serait assez moche pour un programme de 500 lignes.

Sans le local ($?), chaque appel `système` obtient $? = -1. Le $? J'ai besoin dans ce cas après waitpid, puis malheureusement mis à -1 après la sortie du gestionnaire de sig. Donc je trouve que cela fonctionne. Nouvelles lignes affichées avec ###

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 : $?;    ###
...


0 commentaires

1
votes

Nous avons eu un problème similaire, voici notre solution: faites glisser un descripteur de fichier côté écriture dans le petit-enfant et lisez () à partir de celui-ci qui bloquera jusqu'à sa sortie.

Voir aussi: wait pour les enfants et petits-enfants

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;

p >


3 commentaires

Notez que getppid () renvoie "1" parce que le parent du petit-enfant (le premier enfant du parent) est sorti et que le petit-enfant est re-parenté au processus "init".


Le processus "init" est-il donc celui-ci? init -> enfant -> petit-enfant


parent -> enfant -> petit-enfant, mais quand l'enfant sort, il devient parent -> [aucun], init -> petit-enfant