31 votes

wait3 (waitpid alias) retourne -1 avec errno errno réglé sur ECHILD alors qu'il ne devrait pas

Le contexte est ce Redis question. Nous avons un wait3() appel qui attend l'AOF réécriture de l'enfant pour créer la nouvelle AOF version sur le disque. Lorsque l'enfant est fait, le parent est informé par wait3() afin de remplacer l'ancienne AOF avec le nouveau.

Toutefois, dans le contexte du problème ci-dessus, l'utilisateur nous a informés au sujet d'un bug. J'ai un peu modifié la mise en œuvre de Redis 3.0 afin de clairement journal lorsqu' wait3() retourné -1 au lieu de s'écraser en raison de cette condition inattendue. Donc, c'est ce qui se passe, apparemment:

  1. wait3() est appelée lorsque nous avons en attendant les enfants à attendre.
  2. l' SIGCHLD doit être réglé à l' SIG_DFL, il n'y a pas de code de ce signal à tous dans le Redis, c'est le comportement par défaut.
  3. Lors de la première AOF réécriture qui se passe, wait3() réussi fonctionne comme prévu.
  4. À partir de la deuxième AOF réécriture (le deuxième enfant de sa création) wait3() commence à retourner -1.

Autant que je sache, il n'est pas possible dans le code actuel, que nous appelons wait3() alors qu'il n'y a pas dans l'attente des enfants, depuis quand l'AOF enfant est créé, nous avons mis en server.aof_child_pid de la valeur du pid, et nous le réinitialiser seulement après la réussite de l' wait3() appel.

Donc, wait3() devrait avoir aucune raison d'échouer avec -1 ECHILD, mais il le fait, alors, probablement, le zombie enfant n'est pas créé, pour des raisons imprévues.

Hypothèse 1: Il est possible que Linux lors de certaines bizarre conditions aura pour effet de rejeter le zombie enfant, par exemple en raison de la pression de la mémoire? N'a pas l'air raisonnable depuis le zombie vient de métadonnées attachées à elle, mais qui sait.

Notez que nous appelons wait3() avec WNOHANG. Et étant donné que l' SIGCHLD est définie à l' SIG_DFL par défaut, la seule condition qui doit conduire à l'échec et le retour de -1 ECHLD devrait être pas de zombie disponible pour les renseignements.

Hypothèse 2: Autre chose qui pourrait arriver, mais il n'y a pas d'explication si cela arrive, c'est qu'après le premier enfant meurt, l' SIGCHLD gestionnaire d' SIG_IGN, provoquant wait3() de retour -1 ECHLD.

Hypothèse 3: Est-il possible de supprimer le zombie enfants à l'extérieur? Peut-être que cet utilisateur a une sorte de script qui supprime zombie processus en arrière-plan de sorte que l'information n'est plus disponible pour l' wait3()? À ma connaissance, il devrait ne jamais être possible de supprimer le zombie si le parent ne l'attendez pas (avec waitpid ou de la manipulation du signal) et si l' SIGCHLD n'est pas ignorée, mais peut-être il ya certains Linux de manière spécifique.

Hypothèse 4: Il y a effectivement quelques bug dans le Redis code, de sorte que nous avons avec succès wait3() l'enfant la première fois sans correctement la réinitialisation de l'état, et plus tard nous appelons wait3() encore et encore, mais il n'y a plus de zombies, elle retourne -1. En analysant le code, il semble impossible, mais peut-être que je me trompe.

Une autre chose importante: nous n'avons jamais observé dans le passé. Ne se produit que dans ce système Linux apparemment.

Mise à JOUR: Yossi Gottlieb proposé que l' SIGCHLD est reçu par un autre thread dans le Redis processus pour une raison quelconque (ne se fait pas normalement, uniquement sur ce système). Nous avons déjà masque SIGALRM en bio.c threads, peut-être que nous pourrions essayer de masquage SIGCHLD de I/O fils.

Annexe: une sélection de pièces de Redis code

Où wait3() est appelée:

/* Check if a background saving or AOF rewrite in progress terminated. */
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) {
    int statloc;
    pid_t pid;

    if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
        int exitcode = WEXITSTATUS(statloc);
        int bysignal = 0;

        if (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc);

        if (pid == -1) {
            redisLog(LOG_WARNING,"wait3() returned an error: %s. "
                "rdb_child_pid = %d, aof_child_pid = %d",
                strerror(errno),
                (int) server.rdb_child_pid,
                (int) server.aof_child_pid);
        } else if (pid == server.rdb_child_pid) {
            backgroundSaveDoneHandler(exitcode,bysignal);
        } else if (pid == server.aof_child_pid) {
            backgroundRewriteDoneHandler(exitcode,bysignal);
        } else {
            redisLog(REDIS_WARNING,
                "Warning, detected child with unmatched pid: %ld",
                (long)pid);
        }
        updateDictResizePolicy();
    }
} else {

Les parties sélectionnées d' backgroundRewriteDoneHandler:

void backgroundRewriteDoneHandler(int exitcode, int bysignal) {
    if (!bysignal && exitcode == 0) {
        int newfd, oldfd;
        char tmpfile[256];
        long long now = ustime();
        mstime_t latency;

        redisLog(REDIS_NOTICE,
            "Background AOF rewrite terminated with success");

        ... more code to handle the rewrite, never calls return ...

    } else if (!bysignal && exitcode != 0) {
        server.aof_lastbgrewrite_status = REDIS_ERR;

        redisLog(REDIS_WARNING,
            "Background AOF rewrite terminated with error");
    } else {
        server.aof_lastbgrewrite_status = REDIS_ERR;

        redisLog(REDIS_WARNING,
            "Background AOF rewrite terminated by signal %d", bysignal);
    }

cleanup:
    aofClosePipes();
    aofRewriteBufferReset();
    aofRemoveTempFile(server.aof_child_pid);
    server.aof_child_pid = -1;
    server.aof_rewrite_time_last = time(NULL)-server.aof_rewrite_time_start;
    server.aof_rewrite_time_start = -1;
    /* Schedule a new rewrite if we are waiting for it to switch the AOF ON. */
    if (server.aof_state == REDIS_AOF_WAIT_REWRITE)
        server.aof_rewrite_scheduled = 1;
}

Comme vous pouvez le voir tous les chemins de code doit exécuter l' cleanup code de réinitialisation server.aof_child_pid à -1.

Les erreurs consignées par le Redis lors de l'émission

21353:C 29 Nov 04:00:29.957 * AOF réécriture: 8 MO de mémoire utilisée par copy-on-write

27848:M 29 Nov 04:00:30.133 ^@ wait3() a renvoyé une erreur: Pas de processus enfants. rdb_child_pid = -1, aof_child_pid = 21353

Comme vous pouvez le voir aof_child_pid n'est pas -1.

6voto

abligh Points 15586

TLDR: vous êtes actuellement en s'appuyant sur un quelconque comportement de l' signal(2); utilisation sigaction (soigneusement) à la place.

Tout d'abord, SIGCHLD est étrange. À partir de la page de manuel pour sigaction;

POSIX.1-1990 refusé réglage de l'action pour l' SIGCHLD de SIG_IGN. POSIX.1-2001 permet cette possibilité, de sorte que le fait d'ignorer SIGCHLD peut être utilisé pour empêcher la création de zombies (voir wait(2)). Néanmoins, l'historique de la BSD et System V comportements d'ignorer SIGCHLD différents, de sorte que la seule à être totalement portable méthode de veiller à ce que résilié les enfants ne deviennent pas des zombies est de prendre de l' SIGCHLD de signal et d'effectuer une wait(2) ou similaire.

Et voici le peu d' wait(2) s'à la page de manuel:

POSIX.1-2001 indique que si la répartition des SIGCHLD est définie à l' SIG_IGN ou SA_NOCLDWAIT indicateur est défini pour SIGCHLD (voir sigaction(2)), alors les enfants qui se terminent ne deviennent pas des zombies et un appel à l' wait() ou waitpid() bloquera jusqu'à ce que tous les enfants ont terminé, et puis échoue avec errno à l' ECHILD. (L'original de la norme POSIX gauche le comportement de paramètre SIGCHLD de SIG_IGN non spécifié. Notez que même si le défaut de disposition d' SIGCHLD est "ignorer", la définition explicite de la disposition à l' SIG_IGN résultats dans le traitement différent de zombie processus d'enfants.) Linux 2.6 est conforme à cette spécification. Cependant, Linux 2.4 (et versions antérieures) ne le fait pas: si un wait() ou waitpid() appel tout en SIGCHLD est ignoré, l'appel se comporte exactement comme s' SIGCHLD n'ont pas été ignorés, qui est, à l'appel des blocs jusqu'à ce que le prochain enfant prend fin et ensuite renvoie l'ID de processus et l'état de l'enfant.

Remarque la conséquence, c'est que si le signal est de la manipulation se comporte comme SIG_IGN est définie, alors (sous Linux 2.6+), vous verrez le comportement que vous voyez - à-d. wait() sera de retour -1 et ECHLD parce que l'enfant aura été automatiquement récolté.

Deuxièmement, traitement du signal avec pthreads (je pense que vous utilisez ici) est notoirement difficile. La façon dont il est censé travailler (je suis sûr que vous le savez, est que le processus dirigé les signaux envoyés à l'arbitraire d'un thread dans le processus qui a le signal démasqué. Mais alors que les threads ont leur propre masque de signal, il y a un processus d'action à l'échelle de gestionnaire.

Mettre ces deux choses ensemble, je pense que vous exécutez sur un problème que j'ai rencontré avant. J'ai eu des problèmes pour obtenir de l' SIGCHLD de la manipulation d'un travail avec signal() (ce qui est assez juste que c'était obsolète avant pthreads), qui a été fixé par le déplacement d' sigaction et soigneusement réglage par thread masques de signal. Ma conclusion, à l'époque était que la bibliothèque C était émulation (avec sigaction) ce que je disais à faire avec signal(), mais a été prise en fauché par pthreads.

Notez que vous êtes actuellement en s'appuyant sur un quelconque comportement. À partir de la page de manuel de signal(2):

Les effets de l' signal() dans un processus multithread ne sont pas spécifiés.

Voici ce que je vous recommande de faire:

  1. Déplacer sigaction() et pthread_sigmask(). Définir explicitement le traitement de tous les signaux qui vous intéressent (même si vous pensez que c'est la valeur par défaut), même lorsque la mise à SIG_IGN ou SIG_DFL. - Je bloquer les signaux alors que je le fais (éventuellement surabondance de l'attention, mais j'ai copié l'exemple de quelque part).

Voici ce que je suis en train de faire (à peu près):

sigset_t set;
struct sigaction sa;

/* block all signals */
sigfillset (&set);
pthread_sigmask (SIG_BLOCK, &set, NULL);

/* Set up the structure to specify the new action. */
memset (&sa, 0, sizeof (struct sigaction));
sa.sa_handler = handlesignal;        /* signal handler for INT, TERM, HUP, USR1, USR2 */
sigemptyset (&sa.sa_mask);
sa.sa_flags = 0;
sigaction (SIGINT, &sa, NULL);
sigaction (SIGTERM, &sa, NULL);
sigaction (SIGHUP, &sa, NULL);
sigaction (SIGUSR1, &sa, NULL);
sigaction (SIGUSR2, &sa, NULL);

sa.sa_handler = SIG_IGN;
sigemptyset (&sa.sa_mask);
sa.sa_flags = 0;
sigaction (SIGPIPE, &sa, NULL);     /* I don't care about SIGPIPE */

sa.sa_handler = SIG_DFL;
sigemptyset (&sa.sa_mask);
sa.sa_flags = 0;
sigaction (SIGCHLD, &sa, NULL);     /* I want SIGCHLD to be handled by SIG_DFL */

pthread_sigmask (SIG_UNBLOCK, &set, NULL);
  1. Dans la mesure du possible à toutes vos gestionnaires de signaux et de masques etc. avant tout pthread des opérations. Si possible, ne pas changer les gestionnaires de signaux et de masques (vous pourriez avoir besoin de le faire avant et après fork() des appels).

  2. Si vous avez besoin d'un gestionnaire de signal pour SIGCHLD (plutôt que de miser sur SIG_DFL), si possible le laisser être reçu par n'importe quel thread, et d'utiliser l'auto-pipe méthode ou similaire pour alerter le programme principal.

  3. Si vous devez avoir des threads qui ne/n'est pas gérer certains signaux, essayez de vous limiter à l' pthread_sigmask dans le thread plutôt que d' sig* des appels.

  4. Juste au cas où vous exécutez la tête la première dans le prochain numéro j'ai couru dans, de s'assurer que, après vous avez fork()'d, vous définissez à nouveau le traitement de signal à partir de zéro (de l'enfant) plutôt que de compter sur ce que vous ne pourriez hériter du processus parent. Si il y a une chose pire que les signaux mixés avec pthread, c'est les signaux mixés avec pthread avec fork().

Remarque je ne peux pas expliquer exactement entièrement pourquoi variation (1) des œuvres, mais il a fixé ce qui ressemble à une très similaires problème pour moi et, après tout, en s'appuyant sur quelque chose qui était 'non spécifié' auparavant. Il est le plus proche de votre "hypothèse 2", mais je pense que c'est vraiment incomplet de l'émulation de l'héritage signal de fonctions (plus précisément à l'émulation précédemment racé comportement de l' signal() qui est ce qui l'a causé d'être remplacé par sigaction() , en premier lieu - mais c'est juste une supposition).

D'ailleurs, je vous suggère d'utiliser wait4() , ou (comme vous ne l'utilisez pas rusage) waitpid() plutôt que d' wait3(), de sorte que vous pouvez spécifier un PID à attendre. Si vous avez quelque chose d'autre qui génère les enfants (j'ai eu une bibliothèque de le faire), vous pourriez vous retrouver en attente pour quelque chose de mal. Cela dit, je ne pense pas que c'est ce qui se passe ici.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X