Journal wait4: attendre la fin d’un ou plusieurs processus quelconques

Posté par  (site web personnel) . Licence CC By‑SA.
Étiquettes :
30
21
mar.
2013

Il y a quelques semaines, srb nous présentait waitend, un script Python permettant de lancer une commande à la fin de l’exécution d’un processus.

Inspiré par quelques-uns des commentaires du journal sus-cité, j’ai commis wait4, un programme en C permettant de faire sensiblement la même chose mais en utilisant Netlink.

wait4 s’utilise comme la commande standard wait : il prend un ou plusieurs PID en paramètres et rend la main lorsque tous les processus correspondants sont terminés. La différence avec wait est que les processus à attendre ne doivent pas nécessairement descendre du shell courant (ils peuvent même appartenir à un autre utilisateur).

Une option --timeout permet de ne pas attendre plus d’un certain laps de temps.

L’utilisation de Netlink apporte deux avantages :

  • il n’y a pas besoin de « polling », c’est le noyau qui prévient le programme que le processus cible s’est terminé ;
  • on peut récupérer le code de retour du processus, qui est alors renvoyé comme code de retour de wait4 lui-même.

(Netlink apporte aussi un inconvénient : le programme doit être setuid root, parce que seul un processus privilégié peut lier une socket à une adresse de type AF_NETLINK ; mais les privilèges sont abandonnés sitôt la liaison établie.)

Une méthode alternative est fournie pour les systèmes où Netlink n’est pas disponible : dans ce cas, wait4 utilise kill(2) pour vérifier régulièrement (toutes les secondes, délai non-paramétrable pour l’instant) l’existence du ou des processus cible(s).

Le code (sous GPL3 ou plus) :

  • # intéressant

    Posté par  . Évalué à 10.

    Intéressant !

    Quelques remarques :

    Ca ne fonctionnait pas sur ma bécanne, voilà un patch :

    --- src/wait4pid.c.orig 2013-03-21 14:20:12.000000000 +0100
    +++ src/wait4pid.c      2013-03-21 14:33:13.129854021 +0100
    @@ -43,6 +43,8 @@
         } *payload;
         char buf[NLMSG_SPACE(sizeof(struct payload_t))];
    
    +    memset(buf, 0, sizeof(buf));
    +
         hdr = (struct nlmsghdr *)buf;
         hdr->nlmsg_type = NLMSG_DONE;
         hdr->nlmsg_flags = NLM_F_REQUEST;
    @@ -52,6 +54,7 @@
         payload = (struct payload_t *) NLMSG_DATA(hdr);
         payload->msg.id.idx = CN_IDX_PROC;
         payload->msg.id.val = CN_VAL_PROC;
    +    payload->msg.len = sizeof(payload->op);
         payload->op = enable ? PROC_CN_MCAST_LISTEN : PROC_CN_MCAST_IGNORE;
    
         return send(sock, hdr, NLMSG_SPACE(sizeof(*payload)), 0);
    
    

    Il y a un problème dans le cas où netlink n'est pas supporté et on passe le PID d'un process auquel on ne peut pas envoyer de signal: EPERM est retourné par kill(), et wait4 ne va jamais retourner.

    static void
    on_alarm(int sig)
    {
        exit(EXIT_SUCCESS);
    }
    
    

    Un signal handler doit être async-signal safe, en gros réentrant.
    Et exit() ne l'est pas, contrairement à _exit(), notamment puisqu'il appelle les callbacks enregistrés avec atexit().

    Là tu n'as pas vraiment de risque de deadlock/crash normalement, mais écrire du code exécutant dans le contexte d'un signal handler est subtil, par exemple :
    ```
    static void
    wait4pid_close(void)
    {
    if ( listener_set ) {
    set_proc_listen(sock, 0);
    listener_set = 0;
    }

    if ( sock ) {
        close(sock);
        sock = 0;
    }
    
    

    }
    ```

    Les variables listener_set et sock devraient être volatile (elles pourraient aussi être static).

    La façon propre de faire serait d'utiliser select() sur ton socket, pour sortir lorsque le timeout est atteint.

    Enfin c'est un détail.

    Sinon :

    ec = payload->evt.event_data.exit.exit_code / 256;

    Tu peux utiliser WEXITSTATUS().

    Dans set_proc_listen, tu ne retry pas sur EINTR (peu probable).

    • [^] # Re: intéressant

      Posté par  (site web personnel) . Évalué à 1.

      Merci pour les remarques et le patch.

      Il y a un problème dans le cas où netlink n'est pas supporté et on passe le PID d'un process auquel on ne peut pas envoyer de signal: EPERM est retourné par kill(), et wait4 ne va jamais retourner.

      C’est le comportement voulu : si kill(2) renvoie EPERM, c’est que le processus cible existe (on ne peut pas lui envoyer de signal, mais peu importe, savoir que le processus existe est la seule chose qui nous intéresse), et qu’il faut donc continuer à l’attendre. Ce n’est que lorsque le processus n’existe pas ou plus (soit lorsque kill(2) échoue avec errno == ESRCH) que l’on peut retourner.

      Je note les remarques concernant le gestionnaire de signal.

      Sinon :

      ec = payload->evt.event_data.exit.exit_code / 256;

      Tu peux utiliser WEXITSTATUS().

      Ah, merci ! Ça me dérangeait de devoir utiliser une constante magique.

      • [^] # Re: intéressant

        Posté par  . Évalué à 4.

        C’est le comportement voulu

        Oula je plane, en effet.

        Sinon, deux autres remarques :
        - la version avec polling a une race, si jamais le PID est recyclé entre deux polling. Une alternative serait par exemple d'ouvrir /proc//status, et de faire un read() régulièrement: si jamais le processus est mort entre temps, tu vas prendre ESRCH. Mais bon, ce n'est pas portable, et il y a déjà netlink pour Linux.
        - le socket netlink reçoit un message par fork()/exec()/exit(), pour tous les process sur la machine: s'il beaucoup de process sont créés, le socket buffer risque de se remplir.
        Démo:

        shell 1:

        $ while [ 1 ]; do /bin/false; done
        
        

        shell 2:

        # nice -n 19 strace ./wait4 1
        [...]
        recv(3, "L\0\0\0\3\0\0\0\261\252\t\0\0\0\0\0\1\0\0\0\1\0\0\0\261\252\t\0\0\0\0\0"..., 76, 0) = 76
        recv(3, "L\0\0\0\3\0\0\0\262\252\t\0\0\0\0\0\1\0\0\0\1\0\0\0\262\252\t\0\0\0\0\0"..., 76, 0) = 76
        recv(3, 0xbfa7a324, 76, 0)              = -1 ENOBUFS (No buffer space available)
        
        

        Tu devrais pouvoir mettre en place un filtre pour ne recevoir que les notifications d'intérêt, c'est pas trivial (BPF) mais ça peut être intéressant ;-)

        • [^] # Re: intéressant

          Posté par  (site web personnel) . Évalué à 2. Dernière modification le 21 mars 2013 à 18:00.

          la version avec polling a une race, si jamais le PID est recyclé entre deux polling.

          Oui, j’avais conscience de ça, mais je n’ai pas spécialement cherché à l’éviter pour l’instant.

          Je garde ton approche basée sur /proc/pid/status dans un coin, mais je dois admettre qu’étant sous GNU/Linux moi-même et disposant donc de Netlink, la méthode avec polling n’est vraiment qu’une solution de repli,¹ que je ne suis pas réellement motivé à améliorer…

          Pour les systèmes autres que Linux, je serais plutôt enclin à regarder du côté des mécanismes équivalents à Netlink, s’il en existe. Par exemple, je viens de voir que FreeBSD a un appel kevent qui devrait permettre de faire sensiblement la même chose, je vais regarder ça de plus près.

          le socket netlink reçoit un message par fork()/exec()/exit(), pour tous les process sur la machine: s'il beaucoup de process sont créés, le socket buffer risque de se remplir.
          […]
          Tu devrais pouvoir mettre en place un filtre pour ne recevoir que les notifications d'intérêt, c'est pas trivial (BPF) mais ça peut être intéressant ;-)

          Je vais regarder ça, merci pour l’info.


          ¹ En fait, la principale raison d’être de la méthode de repli en cas d’absence de Netlink est de m’éviter de passer pour un développeur linux-centrique (ce qui est perdu d’avance, puisque je suis linux-centrique — mais je me soigne).

          • [^] # Re: intéressant

            Posté par  (site web personnel) . Évalué à 1.

            Pour info, je viens de mettre à jour le dépôt.

            J’ai ajouté un filtre LSF (Linux Socket Filter, la version linuxienne de BPF) pour ne recevoir que les messages concernant les processus à attendre, et limiter ainsi le risque de ENOBUF.

            J’ai aussi ajouté le support de kevent(2) pour les systèmes BSD, du moins sur le papier parce que je ne peux pas tester si ça fonctionne correctement ni même si déjà ça compile (je n’ai pas de machine, même virtuelle, sous BSD et je n’ai pas la motivation d’en installer une juste pour ça)… Ça devrait marcher si j’ai bien compris le manuel, mais si un utilisateur de BSD est intéressé pour tester, je suis intéressé par les retours.

  • # Commentaire sur l'API

    Posté par  . Évalué à 2.

    Bonjour,
    Ce code m'intéresse énormément. C'est en effet assez compliqué de monitorer proprement le cycle de vie d'un process.
    Les solutions habituelles (signaux, erreurs sur file descriptors) posent plein de soucis…
    Et je suis très content de voir qu'il y a une solution propre de disponible par netlink.
    Je n'ai pas encore regardé de très près le code, mais une chose me pose un petit soucis, c'est l'API.
    En effet, le seul moyen de profiter de ce code, c'est de passer par une fonction bloquante, qui oblige à utiliser des threads si le programme qui l'utilise, doit pouvoir faire autre chose pendant ce temps.
    Il vaudrait mieux que l'API expose un file descriptor qui serait pollable (avec par exemple select/poll ou epoll) et propose, au choix, une fonction pour traiter les événements en attente sur le fd en notifiant le client de ta librairie, ou alors permette de lister les événements qui se sont produits pour que le client les traite.
    C'est ce qui est fait par exemple dans la libudev, ça marche très bien et c'est très pratique pour le développeur qui l'utilise ^
    En tout cas, merci beaucoup d'avoir partagé.

    • [^] # Re: Commentaire sur l'API

      Posté par  (site web personnel) . Évalué à 1.

      Je n'ai pas encore regardé de très près le code, mais une chose me pose un petit soucis, c'est l'API.
      En effet, le seul moyen de profiter de ce code, c'est de passer par une fonction bloquante, qui oblige à utiliser des threads si le programme qui l'utilise, doit pouvoir faire autre chose pendant ce temps.

      Je n’ai effectivement pas réellement écrit la fonction wait4all en ayant à l’esprit qu’elle devrait être ré-utilisable dans n’importe quel contexte. Elle est faite pour répondre aux besoins de ce programme, pas plus.

      En faire une sorte de bibliothèque générique de monitoring de processus n’était pas du tout mon objectif. Je n’ai rien contre l’idée (si le besoin existe, pourquoi pas ?), mais franchement je ne pense pas vouloir m’y atteler.

      Peut-être qu’améliorer la bibliothèque libkqueue (en y apportant le support de Netlink) serait une meilleure idée ?

      En tout cas, merci beaucoup d'avoir partagé.

      De rien, merci pour le retour.

  • # Merci

    Posté par  . Évalué à 1.

    Bonjour,
    J'ai eu un peu plus le temps de me pencher sur le code, et du coup, en lisant un ou deux tutos, j'ai compris comment ça marchait et l'API netlink process connector + bpf correspondent parfaitement au besoin que j'avais. Donc encore merci beaucoup, j'ai appris pas mal de trucs…

    Par contre, d'après la page de man netlink 7 :
    "If a process owns several netlink sockets, then nl_pid can only be equal to the process ID for at most one socket."
    du coup, le
    89 hdr->nlmsg_pid = getpid();
    est problématique pour être utilisé dans une lib. En effet, rien n'indique qu'une autre socket netlink n'est pas utilisée ailleurs dans le programme, ce qui devrait provoquer un EADDRINUSE au bind ou un truc du genre…
    De ce que j'ai vu, il semble ce genre de problème se soit posé pour les développeurs de la libnl.

    Du coup, personnellement j'utilise hdr->nlmsg_pid = 0, qui laisse au kernel le choix du nl_pid.
    D'ailleurs, je ne comprends pas pourquoi il est laissé au développeur, la possibilité de choisir ce nl_pid, puisque ça ne semble servir qu'à générer des bugs…

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.