web-dev-qa-db-fra.com

Comment éviter d'utiliser printf dans un gestionnaire de signal?

Puisque printf n’est pas réentrant, il n’est pas supposé être sûr de l’utiliser dans un gestionnaire de signaux. Mais j'ai vu beaucoup d'exemples de codes qui utilisent printf de cette façon.

Ma question est donc la suivante: quand devons-nous éviter d'utiliser printf dans un gestionnaire de signal et existe-t-il un remplacement recommandé?

73
Yu Hao

Vous pouvez utiliser une variable d'indicateur, définir cet indicateur dans le gestionnaire de signaux et, en fonction de cet appel d'indicateur, utiliser la fonction printf() dans main () ou une autre partie du programme au cours d'un fonctionnement normal. 

Il n’est pas sûr d’appeler toutes les fonctions, telles que printf, à partir d’un gestionnaire de signaux. Une technique utile consiste à utiliser un gestionnaire de signal pour définir une flag, puis à vérifier que flag à partir du programme principal et imprimez un message si nécessaire. 

Remarquez dans l'exemple ci-dessous, le gestionnaire de signaux Ding () a défini un indicateur alarm_fired sur 1 lorsque SIGALRM est capturé et que, dans la fonction principale alarm_fired, la valeur est examinée pour appeler correctement conditionnel printf. 

static int alarm_fired = 0;
void Ding(int sig) // can be called asynchronously
{
  alarm_fired = 1; // set flag
}
int main()
{
    pid_t pid;
    printf("alarm application starting\n");
    pid = fork();
    switch(pid) {
        case -1:
            /* Failure */
            perror("fork failed");
            exit(1);
        case 0:
            /* child */
            sleep(5);
            kill(getppid(), SIGALRM);
            exit(0);
    }
    /* if we get here we are the parent process */
    printf("waiting for alarm to go off\n");
    (void) signal(SIGALRM, Ding);
    pause();
    if (alarm_fired)  // check flag to call printf
      printf("Ding!\n");
    printf("done\n");
    exit(0);
}

Référence: Début de la programmation Linux, 4e édition , Dans ce livre, votre code est explicité (ce que vous voulez), au chapitre 11: Processus et signaux, page 484 

De plus, vous devez prendre un soin particulier lors de l'écriture des fonctions du gestionnaire, car elles peuvent être appelées de manière asynchrone. Autrement dit, un gestionnaire peut être appelé à tout moment du programme, de manière imprévisible. Si deux signaux arrivent dans un très court intervalle, un gestionnaire peut s’exécuter dans un autre. Et il est considéré comme une meilleure pratique de déclarer volatile sigatomic_t, ce type est toujours utilisé de manière atomique, évitez les incertitudes quant à l’interruption de l’accès à une variable. (lire: Accès aux données atomiques et traitement du signal pour une expiation de détail). 

Lecture Définition des gestionnaires de signaux : pour apprendre à écrire une fonction de gestionnaire de signaux pouvant être établie avec les fonctions signal() ou sigaction().
Liste des fonctions autorisées dans page de manuel , l'appel de cette fonction à l'intérieur du gestionnaire de signaux est sécurisé.

52
Grijesh Chauhan

Le principal problème est que si le signal interrompt malloc() ou une fonction similaire, l'état interne peut être temporairement incohérent lorsqu'il déplace des blocs de mémoire entre la liste libre et utilisée ou d'autres opérations similaires. Si le code dans le gestionnaire de signaux appelle une fonction qui appelle ensuite malloc(), la gestion de la mémoire risque d'être complètement détruite.

La norme C adopte une vision très conservatrice de ce que vous pouvez faire avec un gestionnaire de signal:

ISO/IEC 9899: 2011 §7.14.1.1 La fonction signal

¶5 Si le signal survient autrement que suite à l'appel de la fonction abort ou raise, le comportement n'est pas défini si le gestionnaire de signaux fait référence à un objet dont la durée de stockage statique ou de threads n'est pas verrouillée. -objet atomique libre autrement qu'en attribuant une valeur à un objet déclaré comme volatile sig_atomic_t, ou que le gestionnaire de signaux appelle une fonction de la bibliothèque standard autre que la fonction abort, la fonction _Exit, la fonction quick_exit ou la fonction signal avec le premier argument égal au numéro de signal correspondant au signal qui a provoqué l'appel du gestionnaire. De plus, si un tel appel à la fonction signal aboutit à un retour SIG_ERR, la valeur de errno est indéterminée.252)

252) Si un signal est généré par un gestionnaire de signal asynchrone, le comportement n'est pas défini.

POSIX est beaucoup plus généreux sur ce que vous pouvez faire avec un gestionnaire de signal.

Signal Concepts dans l'édition POSIX 2008:

Si le processus est multi-threadé ou mono-thread et qu'un gestionnaire de signal est exécuté autrement que comme résultat de:

  • Le processus appelant abort(), raise(), kill(), pthread_kill() ou sigqueue() pour générer un signal non bloqué

  • Un signal en attente étant débloqué et remis avant l'appel qui l'a débloqué, il revient

le comportement n'est pas défini si le gestionnaire de signaux fait référence à un objet autre que errno avec une durée de stockage statique autre qu'en affectant une valeur à un objet déclaré comme volatile sig_atomic_t, ou si le gestionnaire de signaux appelle une fonction définie dans cette norme autre que l’une des fonctions énumérées dans le tableau suivant.

Le tableau suivant définit un ensemble de fonctions qui doivent être sécurisées de manière asynchrone. Par conséquent, les applications peuvent les invoquer, sans restriction, à partir de fonctions de capture de signal:

_Exit()             fexecve()           posix_trace_event() sigprocmask()
_exit()             fork()              pselect()           sigqueue()
…
fcntl()             pipe()              sigpause()          write()
fdatasync()         poll()              sigpending()

Toutes les fonctions qui ne figurent pas dans le tableau ci-dessus sont considérées comme dangereuses pour les signaux. En présence de signaux, toutes les fonctions définies par ce volume de POSIX.1-2008 doivent se comporter de la manière définie lorsqu'elles sont appelées depuis ou interrompues par une fonction de capture de signal, à une seule exception près: lorsqu'un signal interrompt une fonction non sécurisée et le signal la fonction interceptante appelle une fonction non sécurisée, le comportement est indéfini.

Les opérations qui obtiennent la valeur de errno et celles qui attribuent une valeur à errno doivent être protégées contre le signal asynchrone.

Lorsqu'un signal est délivré à une unité d'exécution, si l'action de ce signal spécifie la terminaison, l'arrêt ou la poursuite du processus, l'ensemble du processus doit être respectivement terminé, arrêté ou poursuivi.

Toutefois, la famille de fonctions printf() est notamment absente de cette liste et ne peut pas être appelée en toute sécurité par un gestionnaire de signaux.

La mise à jour POSIX 2016 étend la liste des fonctions sûres à notamment un grand nombre de fonctions de <string.h>, qui est un ajout particulièrement précieux (ou était un oubli particulièrement frustrant). La liste est maintenant:

_Exit()              getppid()            sendmsg()            tcgetpgrp()
_exit()              getsockname()        sendto()             tcsendbreak()
abort()              getsockopt()         setgid()             tcsetattr()
accept()             getuid()             setpgid()            tcsetpgrp()
access()             htonl()              setsid()             time()
aio_error()          htons()              setsockopt()         timer_getoverrun()
aio_return()         kill()               setuid()             timer_gettime()
aio_suspend()        link()               shutdown()           timer_settime()
alarm()              linkat()             sigaction()          times()
bind()               listen()             sigaddset()          umask()
cfgetispeed()        longjmp()            sigdelset()          uname()
cfgetospeed()        lseek()              sigemptyset()        unlink()
cfsetispeed()        lstat()              sigfillset()         unlinkat()
cfsetospeed()        memccpy()            sigismember()        utime()
chdir()              memchr()             siglongjmp()         utimensat()
chmod()              memcmp()             signal()             utimes()
chown()              memcpy()             sigpause()           wait()
clock_gettime()      memmove()            sigpending()         waitpid()
close()              memset()             sigprocmask()        wcpcpy()
connect()            mkdir()              sigqueue()           wcpncpy()
creat()              mkdirat()            sigset()             wcscat()
dup()                mkfifo()             sigsuspend()         wcschr()
dup2()               mkfifoat()           sleep()              wcscmp()
execl()              mknod()              sockatmark()         wcscpy()
execle()             mknodat()            socket()             wcscspn()
execv()              ntohl()              socketpair()         wcslen()
execve()             ntohs()              stat()               wcsncat()
faccessat()          open()               stpcpy()             wcsncmp()
fchdir()             openat()             stpncpy()            wcsncpy()
fchmod()             pause()              strcat()             wcsnlen()
fchmodat()           pipe()               strchr()             wcspbrk()
fchown()             poll()               strcmp()             wcsrchr()
fchownat()           posix_trace_event()  strcpy()             wcsspn()
fcntl()              pselect()            strcspn()            wcsstr()
fdatasync()          pthread_kill()       strlen()             wcstok()
fexecve()            pthread_self()       strncat()            wmemchr()
ffs()                pthread_sigmask()    strncmp()            wmemcmp()
fork()               raise()              strncpy()            wmemcpy()
fstat()              read()               strnlen()            wmemmove()
fstatat()            readlink()           strpbrk()            wmemset()
fsync()              readlinkat()         strrchr()            write()
ftruncate()          recv()               strspn()
futimens()           recvfrom()           strstr()
getegid()            recvmsg()            strtok_r()
geteuid()            rename()             symlink()
getgid()             renameat()           symlinkat()
getgroups()          rmdir()              tcdrain()
getpeername()        select()             tcflow()
getpgrp()            sem_post()           tcflush()
getpid()             send()               tcgetattr()

En conséquence, vous finissez par utiliser write() sans le support de formatage fourni par printf() et al, ou vous définissez un indicateur que vous testez (périodiquement) aux endroits appropriés de votre code. Cette technique est bien démontrée dans le réponse par Grijesh Chauhan .


Fonctions standard C et sécurité du signal

chqrliedemande une question intéressante à laquelle je n'ai qu'une réponse partielle:

Comment se fait-il que la plupart des fonctions de chaîne de <string.h> ou des fonctions de classe de caractère de <ctype.h> et de nombreuses autres fonctions de bibliothèque standard C ne figurent pas dans la liste ci-dessus? Une implémentation aurait besoin d'être délibérément mauvaise pour rendre strlen() imprudente l'appel depuis un gestionnaire de signaux.

Pour de nombreuses fonctions de <string.h>, il est difficile de comprendre pourquoi elles n'ont pas été déclarées comme étant à sécurité-signal asynchrone, et je conviens que la strlen() est un bon exemple, avec strchr() , strstr(), etc. D'autre part, d'autres fonctions telles que strtok(), strcoll() et strxfrm() sont plutôt complexes et ne risquent pas d'être protégées en tant que signaux asynchrones. Parce que strtok() conserve l'état entre les appels et que le gestionnaire de signaux ne peut pas facilement dire si une partie du code qui utilise strtok() serait fausse. Les fonctions strcoll() et strxfrm() fonctionnent avec des données sensibles aux paramètres régionaux. Le chargement des paramètres régionaux implique toutes sortes de réglages d'état.

Les fonctions (macros) de <ctype.h> sont toutes sensibles aux paramètres régionaux et peuvent donc rencontrer les mêmes problèmes que strcoll() et strxfrm().

J'ai du mal à comprendre pourquoi les fonctions mathématiques de <math.h> ne sont pas protégées contre les signaux asynchrones, à moins que cela ne soit dû au fait qu'elles pourraient être affectées par un SIGFPE (exception en virgule flottante), bien que ce soit à peu près la seule fois où j'en vois un. ces jours sont pour entier division par zéro. Une incertitude similaire provient de <complex.h>, <fenv.h> et <tgmath.h>.

Certaines fonctions de <stdlib.h> pourraient être exemptées - abs() par exemple. D'autres sont spécifiquement problématiques: malloc() et la famille en sont des exemples.

Une évaluation similaire pourrait être faite pour les autres en-têtes de la norme C (2011) utilisés dans un environnement POSIX. (Standard C est tellement restrictif que leur analyse dans un environnement purement standard C ne présente aucun intérêt.) Les marqués 'dépendant de la localisation' sont dangereux car la manipulation des locales peut nécessiter une allocation de mémoire, etc.

  • <assert.h> - Probablement pas sûr
  • <complex.h> - Peut-être sûr
  • <ctype.h> - Pas sûr
  • <errno.h> - Coffre-fort
  • <fenv.h> - Probablement pas sûr
  • <float.h> - Aucune fonction
  • <inttypes.h> - Fonctions sensibles aux paramètres régionaux (non sécurisée)
  • <iso646.h> - Aucune fonction
  • <limits.h> - Aucune fonction
  • <locale.h> - Fonctions sensibles aux paramètres régionaux (non sécurisée)
  • <math.h> - Peut-être sûr
  • <setjmp.h> - Pas sûr
  • <signal.h> - Autorisé
  • <stdalign.h> - Aucune fonction
  • <stdarg.h> - Aucune fonction
  • <stdatomic.h> - Peut-être sûr, probablement pas sûr
  • <stdbool.h> - Aucune fonction
  • <stddef.h> - Aucune fonction
  • <stdint.h> - Aucune fonction
  • <stdio.h> - Pas sûr
  • <stdlib.h> - Pas tous sûrs (certains sont autorisés, d'autres pas)
  • <stdnoreturn.h> - Aucune fonction
  • <string.h> - Pas tous en sécurité
  • <tgmath.h> - Peut-être sûr
  • <threads.h> - Probablement pas sûr
  • <time.h> - Dépend de l'environnement local (mais time() est explicitement autorisé)
  • <uchar.h> - Dépend de la localisation
  • <wchar.h> - Dépend de la localisation
  • <wctype.h> - Dépend de la localisation

Analyser les en-têtes POSIX serait… plus difficile dans la mesure où il y en a beaucoup, et certaines fonctions peuvent être sûres mais beaucoup ne le seront pas… mais aussi plus simple parce que POSIX dit quelles fonctions sont sécurisées (pas beaucoup). Notez qu'un en-tête comme <pthread.h> a trois fonctions sécurisées et de nombreuses fonctions non sécurisées.

NB: La quasi-totalité de l'évaluation des fonctions C et des en-têtes dans un environnement POSIX est une conjecture semi-instruite. Cela n’a aucun sens d’être une déclaration définitive d’un organisme de normalisation .

49
Jonathan Leffler

Comment éviter d'utiliser printf dans un gestionnaire de signal?

  1. Évitez-le toujours, vous direz: n'utilisez pas printf() dans les gestionnaires de signaux.

  2. Au moins sur les systèmes conformes à POSIX, vous pouvez utiliser write(STDOUT_FILENO, ...) au lieu de printf(). Le formatage peut ne pas être facile cependant: Print int à partir du gestionnaire de signaux à l'aide de fonctions d'écriture ou asynchrones

13
alk

À des fins de débogage, j'ai écrit un outil qui vérifie que vous appelez uniquement des fonctions de la liste async-signal-safe et affiche un message d'avertissement pour chaque fonction non sécurisée appelée dans un contexte de signal. Bien que cela ne résolve pas le problème d'appeler des fonctions non asynchronisées à partir d'un contexte de signal, cela vous aide au moins à détecter les cas où vous l'avez fait par accident.

Le code source est sur GitHub . Cela fonctionne en surchargeant signal/sigaction, puis en détournant temporairement les entrées PLT des fonctions dangereuses. cela provoque la redirection des appels vers des fonctions non sécurisées vers un wrapper.

6
dwks

Une technique particulièrement utile dans les programmes dotés d'une boucle de sélection consiste à écrire un seul octet dans un canal à la réception d'un signal, puis à gérer le signal dans la boucle de sélection. Quelque chose dans ce sens ((gestion des erreurs et autres détails omis par souci de concision)} _:

static int sigPipe[2];

static void gotSig ( int num ) { write(sigPipe[1], "!", 1); }

int main ( void ) {
    pipe(sigPipe);
    /* use sigaction to point signal(s) at gotSig() */

    FD_SET(sigPipe[0], &readFDs);

    for (;;) {
        n = select(nFDs, &readFDs, ...);
        if (FD_ISSET(sigPipe[0], &readFDs)) {
            read(sigPipe[0], ch, 1);
            /* do something about the signal here */
        }
        /* ... the rest of your select loop */
    }
}

Si vous vous souciez du signal lequel qu'il était, alors l'octet dans le tuyau peut être le numéro du signal.

0
John Hascall

Implémentez votre propre snprintf("%d async-signal-safe-safe et utilisez write

Ce n'est pas aussi mauvais que je pensais, Comment convertir un int en chaîne en C? a plusieurs implémentations.

Comme il n’ya que deux types de données intéressants auxquels les gestionnaires de signaux peuvent accéder:

  • sig_atomic_t globals
  • int argument de signal

cela couvre essentiellement tous les cas d'utilisation intéressants.

Le fait que strcpy soit également protégé du signal rend les choses encore meilleures.

Le programme POSIX ci-dessous imprime sur la sortie standard le nombre de fois où il a reçu SIGINT jusqu’à présent, que vous pouvez déclencher avec Ctrl + C, ainsi que l’ID de signal.

Vous pouvez quitter le programme avec Ctrl + \ (SIGQUIT).

principal c:

#define _XOPEN_SOURCE 700
#include <assert.h>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>

/* Calculate the minimal buffer size for a given type.
 *
 * Here we overestimate and reserve 8 chars per byte.
 *
 * With this size we could even print a binary string.
 *
 * - +1 for NULL terminator
 * - +1 for '-' sign
 *
 * A tight limit for base 10 can be found at:
 * https://stackoverflow.com/questions/8257714/how-to-convert-an-int-to-string-in-c/32871108#32871108
 *
 * TODO: get tight limits for all bases, possibly by looking into
 * glibc's atoi: https://stackoverflow.com/questions/190229/where-is-the-itoa-function-in-linux/52127877#52127877
 */
#define ITOA_SAFE_STRLEN(type) sizeof(type) * CHAR_BIT + 2

/* async-signal-safe implementation of integer to string conversion.
 *
 * Null terminates the output string.
 *
 * The input buffer size must be large enough to contain the output,
 * the caller must calculate it properly.
 *
 * @param[out] value  Input integer value to convert.
 * @param[out] result Buffer to output to.
 * @param[in]  base   Base to convert to.
 * @return     Pointer to the end of the written string.
 */
char *itoa_safe(intmax_t value, char *result, int base) {
    intmax_t tmp_value;
    char *ptr, *ptr2, tmp_char;
    if (base < 2 || base > 36) {
        return NULL;
    }

    ptr = result;
    do {
        tmp_value = value;
        value /= base;
        *ptr++ = "ZYXWVUTSRQPONMLKJIHGFEDCBA9876543210123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"[35 + (tmp_value - value * base)];
    } while (value);
    if (tmp_value < 0)
        *ptr++ = '-';
    ptr2 = result;
    result = ptr;
    *ptr-- = '\0';
    while (ptr2 < ptr) {
        tmp_char = *ptr;
        *ptr--= *ptr2;
        *ptr2++ = tmp_char;
    }
    return result;
}

volatile sig_atomic_t global = 0;

void signal_handler(int sig) {
    char key_str[] = "count, sigid: ";
    /* This is exact:
     * - the null after the first int will contain the space
     * - the null after the second int will contain the newline
     */
    char buf[2 * ITOA_SAFE_STRLEN(sig_atomic_t) + sizeof(key_str)];
    enum { base = 10 };
    char *end;
    end = buf;
    strcpy(end, key_str);
    end += sizeof(key_str);
    end = itoa_safe(global, end, base);
    *end++ = ' ';
    end = itoa_safe(sig, end, base);
    *end++ = '\n';
    write(STDOUT_FILENO, buf, end - buf);
    global += 1;
    signal(sig, signal_handler);
}

int main(int argc, char **argv) {
    /* Unit test itoa_safe. */
    {
        typedef struct {
            intmax_t n;
            int base;
            char out[1024];
        } InOut;
        char result[1024];
        size_t i;
        InOut io;
        InOut ios[] = {
            /* Base 10. */
            {0, 10, "0"},
            {1, 10, "1"},
            {9, 10, "9"},
            {10, 10, "10"},
            {100, 10, "100"},
            {-1, 10, "-1"},
            {-9, 10, "-9"},
            {-10, 10, "-10"},
            {-100, 10, "-100"},

            /* Base 2. */
            {0, 2, "0"},
            {1, 2, "1"},
            {10, 2, "1010"},
            {100, 2, "1100100"},
            {-1, 2, "-1"},
            {-100, 2, "-1100100"},

            /* Base 35. */
            {0, 35, "0"},
            {1, 35, "1"},
            {34, 35, "Y"},
            {35, 35, "10"},
            {100, 35, "2U"},
            {-1, 35, "-1"},
            {-34, 35, "-Y"},
            {-35, 35, "-10"},
            {-100, 35, "-2U"},
        };
        for (i = 0; i < sizeof(ios)/sizeof(ios[0]); ++i) {
            io = ios[i];
            itoa_safe(io.n, result, io.base);
            if (strcmp(result, io.out)) {
                printf("%ju %d %s\n", io.n, io.base, io.out);
                assert(0);
            }
        }
    }

    /* Handle the signals. */
    if (argc > 1 && !strcmp(argv[1], "1")) {
        signal(SIGINT, signal_handler);
        while(1);
    }

    return EXIT_SUCCESS;
}

Compiler et exécuter:

gcc -std=c99 -Wall -Wextra -o main main.c
./main 1

Après avoir appuyé quinze fois sur Ctrl + C, le terminal affiche:

^Ccount, sigid: 0 2
^Ccount, sigid: 1 2
^Ccount, sigid: 2 2
^Ccount, sigid: 3 2
^Ccount, sigid: 4 2
^Ccount, sigid: 5 2
^Ccount, sigid: 6 2
^Ccount, sigid: 7 2
^Ccount, sigid: 8 2
^Ccount, sigid: 9 2
^Ccount, sigid: 10 2
^Ccount, sigid: 11 2
^Ccount, sigid: 12 2
^Ccount, sigid: 13 2
^Ccount, sigid: 14 2

2 est le numéro du signal pour SIGINT.

Testé sur Ubuntu 18.04. GitHub en amont .