GNU/Linux >> Tutoriels Linux >  >> Linux

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

Le principal problème est que si le signal interrompt malloc() ou une fonction similaire, l'état interne peut être temporairement incohérent pendant qu'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 signal appelle une fonction qui appelle ensuite malloc() , cela peut complètement détruire la gestion de la mémoire.

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

ISO/CEI 9899:2011 §7.14.1.1 Le signal fonction

¶5 Si le signal se produit autrement qu'à la suite de l'appel du abort ou raise fonction, le comportement est indéfini si le gestionnaire de signal fait référence à un objet avec une durée de stockage statique ou de thread qui n'est pas un objet atomique sans verrou autrement qu'en attribuant une valeur à un objet déclaré comme volatile sig_atomic_t , ou le gestionnaire de signal appelle n'importe quelle fonction de la bibliothèque standard autre que abort fonction, le _Exit fonction, la quick_exit fonction, ou la fonction signal fonction avec le premier argument égal au numéro de signal correspondant au signal qui a provoqué l'invocation du gestionnaire. De plus, si un tel appel au signal la fonction donne un SIG_ERR renvoie, la valeur de errno est indéterminé.

Si un signal est généré par un gestionnaire de signal asynchrone, le comportement est indéfini.

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

Signal Concepts dans l'édition POSIX 2008 dit :

Si le processus est multithread, ou si le processus est monothread et qu'un gestionnaire de signal est exécuté autrement qu'à la suite de :

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

  • Un signal en attente étant débloqué et délivré avant le retour de l'appel qui l'a débloqué

le comportement est indéfini si le gestionnaire de signal fait référence à un objet autre que errno avec une durée de stockage statique autrement qu'en attribuant une valeur à un objet déclaré volatile sig_atomic_t , ou si le gestionnaire de signal appelle une fonction définie dans cette norme autre que l'une des fonctions répertoriées dans le tableau suivant.

Le tableau suivant définit un ensemble de fonctions qui doivent être sécurisées pour le signal 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 en ce qui concerne les signaux. En présence de signaux, toutes les fonctions définies par ce volume de POSIX.1-2008 doivent se comporter comme défini lorsqu'elles sont appelées ou interrompues par une fonction de capture de signal, à une seule exception :lorsqu'un signal interrompt une fonction non sécurisée et que le signal- la fonction de capture appelle une fonction non sécurisée, le comportement n'est pas défini.

Opérations qui obtiennent la valeur de errno et les opérations qui attribuent une valeur à errno doit être async-signal-safe.

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

Cependant, le printf() famille de fonctions est particulièrement absente de cette liste et peut ne pas être appelée en toute sécurité à partir d'un gestionnaire de signaux.

Le POSIX 2016 la mise à jour étend la liste des fonctions sûres pour inclure, en particulier, 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, soit vous finissez par utiliser write() sans le support de formatage fourni par printf() et al, ou vous finissez par définir un indicateur que vous testez (périodiquement) aux endroits appropriés de votre code. Cette technique est habilement démontrée dans la réponse de Grijesh Chauhan.

Fonctions standard C et sécurité des signaux

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

Comment se fait la plupart des fonctions de chaîne de <string.h> ou les fonctions de classe de caractères de <ctype.h> et bien d'autres fonctions de la bibliothèque standard C ne figurent pas dans la liste ci-dessus ? Une implémentation devrait être délibérément mauvaise pour faire strlen() dangereux d'appeler à partir d'un gestionnaire de signal.

Pour de nombreuses fonctions dans <string.h> , il est difficile de voir pourquoi ils n'ont pas été déclarés sûrs pour le signal asynchrone, et je suis d'accord avec le strlen() est un excellent exemple, avec strchr() , strstr() , etc. Par contre, d'autres fonctions comme strtok() , strcoll() et strxfrm() sont plutôt complexes et ne sont probablement pas sûrs pour le signal asynchrone. Parce que strtok() conserve l'état entre les appels, et le gestionnaire de signal ne peut pas facilement dire si une partie du code utilise strtok() serait foiré. Le strcoll() et strxfrm() les fonctions fonctionnent avec des données sensibles aux paramètres régionaux, et le chargement des paramètres régionaux implique toutes sortes de paramètres d'état.

Les fonctions (macros) de <ctype.h> sont tous 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 sûrs pour le signal asynchrone, à moins que ce ne soit parce qu'ils pourraient être affectés par un SIGFPE (exception à virgule flottante), bien que la seule fois où j'en vois un de ces jours soit pour entier division par zéro. Une incertitude similaire découle de <complex.h> , <fenv.h> et <tgmath.h> .

Certaines des fonctions de <stdlib.h> pourrait être exempté — abs() par exemple. D'autres sont particulièrement problématiques :malloc() et la famille en sont de parfaits exemples.

Une évaluation similaire pourrait être faite pour les autres en-têtes de la norme C (2011) utilisés dans un environnement POSIX. (Le standard C est si restrictif qu'il n'y a aucun intérêt à les analyser dans un environnement purement standard C.) Ceux marqués "dépendants des paramètres régionaux" ne sont pas sûrs car la manipulation des paramètres régionaux peut nécessiter une allocation de mémoire, etc.

  • <assert.h>Probablement pas sûr
  • <complex.h>Possiblement sûr
  • <ctype.h> — Pas sûr
  • <errno.h> — Sûr
  • <fenv.h>Probablement pas sûr
  • <float.h> — Aucune fonction
  • <inttypes.h> — Fonctions sensibles aux paramètres régionaux (non sécurisés)
  • <iso646.h> — Aucune fonction
  • <limits.h> — Aucune fonction
  • <locale.h> — Fonctions sensibles aux paramètres régionaux (non sécurisés)
  • <math.h>Possiblement 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 non)
  • <stdnoreturn.h> — Aucune fonction
  • <string.h> — Pas tous sûrs
  • <tgmath.h>Possiblement sûr
  • <threads.h>Probablement pas sûr
  • <time.h> — Dépendant des paramètres régionaux (mais time() est explicitement autorisé)
  • <uchar.h> — Selon les paramètres régionaux
  • <wchar.h> — Selon les paramètres régionaux
  • <wctype.h> — Selon les paramètres régionaux

L'analyse des en-têtes POSIX serait… plus difficile dans la mesure où il y en a beaucoup, et certaines fonctions pourraient être sûres mais beaucoup ne le seront pas… mais aussi plus simple parce que POSIX dit quelles fonctions sont sûres pour le signal asynchrone (pas beaucoup d'entre elles). Notez qu'un en-tête comme <pthread.h> a trois fonctions sûres et de nombreuses fonctions dangereuses.

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


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

  1. Toujours l'éviter, dira :n'utilisez simplement 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() . Cependant, le formatage peut ne pas être facile :imprimez int à partir du gestionnaire de signaux à l'aide des fonctions d'écriture ou de sécurité asynchrone


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

Il n'est pas sûr d'appeler toutes les fonctions, telles que printf , à partir d'un gestionnaire de signal. Une technique utile consiste à utiliser un gestionnaire de signal pour définir un flag puis vérifiez que flag depuis le programme principal et imprimer un message si nécessaire.

Remarquez dans l'exemple ci-dessous, le gestionnaire de signal ding() définit un indicateur alarm_fired à 1 car SIGALRM capturé et dans la fonction principale alarm_fired value est examinée pour appeler conditionnellement printf correctement.

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ébuter la programmation Linux, 4e édition, Dans ce livre, votre code est expliqué exactement (ce que vous voulez), Chapitre 11 : Processus et signaux, page 484

De plus, vous devez porter une attention particulière à l'écriture des fonctions de gestionnaire, car elles peuvent être appelées de manière asynchrone. C'est-à-dire qu'un gestionnaire peut être appelé à n'importe quel moment du programme, de manière imprévisible. Si deux signaux arrivent pendant un intervalle très court, un gestionnaire peut fonctionner dans un autre. Et il est considéré comme une meilleure pratique de déclarer volatile sigatomic_t , ce type est toujours accessible de manière atomique, évitez l'incertitude quant à l'interruption de l'accès à une variable. (lire:Accès aux données atomiques et traitement du signal pour l'expiation détaillée).

Lire Defining Signal Handlers :pour apprendre à écrire une fonction de gestionnaire de signal qui peut être établie avec le signal() ou sigaction() les fonctions.
Liste des fonctions autorisées dans la page de manuel, l'appel de cette fonction à l'intérieur du gestionnaire de signal est sûr.


Linux
  1. Comment écrire des caractères non ASCII en utilisant echo ?

  2. Utilisation des couleurs avec printf

  3. C++11 :Comment aliaser une fonction ?

  4. Comment écrire un entier dans un fichier binaire à l'aide de Bash ?

  5. IPC utilisant des signaux sur Linux

Comment créer un serveur CS:GO sur un VPS Linux

Comment basculer entre les TTY sans utiliser les touches de fonction sous Linux

Comment :une introduction à l'utilisation de Git

Comment se connecter à MySQL en utilisant PHP

Lors de l'utilisation de printf, comment échapper les caractères spéciaux dans le script shell?

Comment éviter les invites lors de l'utilisation d'azcopy sous Linux dans un script ?