Puisqu'il printf
n'est pas réentrant, il n'est pas censé être sûr de l'utiliser dans un gestionnaire de signaux. Mais j'ai vu beaucoup d'exemples de codes qui utilisent printf
cette façon.
Ma question est donc la suivante: quand devons-nous éviter d'utiliser printf
dans un gestionnaire de signaux, et y a-t-il un remplacement recommandé?
printf
appel dans ce signal hander? Supprime-le.Réponses:
Vous pouvez utiliser une variable d'indicateur, définir cet indicateur dans le gestionnaire de signal et en fonction de cette
printf()
fonction d' appel d'indicateur dans main () ou dans une autre partie du programme pendant le fonctionnement normal.Remarquez dans l'exemple ci-dessous, le gestionnaire de signaux ding () a mis un drapeau
alarm_fired
à 1 comme SIGALRM capturé et dans laalarm_fired
valeur de la fonction principale est examinée pour appeler correctement printf de manière conditionnelle.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, exactement votre code est expliqué (ce que vous voulez), Chapitre 11: Processus et signaux, page 484
De plus, vous devez faire particulièrement attention lors de l'écriture des fonctions de gestionnaire car elles peuvent être appelées de manière asynchrone. Autrement dit, 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 toute incertitude sur l'interruption de l'accès à une variable. (lire: Accès aux données atomiques et traitement du signal pour une expiation détaillée).Lisez Définition des gestionnaires de signaux : pour apprendre à écrire une fonction de gestionnaire de signaux pouvant être établie avec les fonctions
signal()
ousigaction()
.Liste des fonctions autorisées dans la page de manuel , l'appel de cette fonction à l'intérieur du gestionnaire de signaux est sûr.
la source
volatile sigatomic_t alarm_fired;
Le problème principal 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 du gestionnaire de signaux appelle une fonction qui appelle ensuitemalloc()
, 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 signaux:
POSIX est beaucoup plus généreux sur ce que vous pouvez faire dans un gestionnaire de signaux.
Signal Concepts dans l'édition POSIX 2008 dit:
Cependant, la
printf()
famille de fonctions est notablement absente de cette liste et peut ne pas être appelée en toute sécurité depuis un gestionnaire de signaux.La mise à jour POSIX 2016 étend la liste des fonctions sûres pour inclure, en particulier, un grand nombre de fonctions
<string.h>
, ce 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 parprintf()
et al, ou vous finissez par définir un indicateur que vous testez (périodiquement) aux endroits appropriés dans 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:
Pour la plupart des fonctions
<string.h>
, il est difficile de voir pourquoi ils ont pas été déclarés en sécurité signal async, et je suis d' accord l'strlen()
est un bon exemple, avecstrchr()
,strstr()
, etc. D'autre part, d' autres fonctions telles questrtok()
,strcoll()
etstrxfrm()
sont plutôt complexes et ne sont probablement pas sûrs pour les signaux asynchrones. Parce questrtok()
conserve l'état entre les appels, et le gestionnaire de signal ne pouvait pas facilement dire si une partie du code utiliséstrtok()
serait gâchée. Les fonctionsstrcoll()
etstrxfrm()
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 toutes sensibles aux paramètres régionaux et peuvent donc rencontrer les mêmes problèmes questrcoll()
etstrxfrm()
.J'ai du mal à comprendre pourquoi les fonctions mathématiques de
<math.h>
ne sont pas sûres pour les signaux asynchrones, à moins que ce ne soit parce qu'elles pourraient être affectées par un SIGFPE (exception en virgule flottante), bien que la seule fois où je vois l'une de ces dernières de nos jours est pour un entier division par zéro. Une incertitude similaire provient de<complex.h>
,<fenv.h>
et<tgmath.h>
.Certaines fonctions de
<stdlib.h>
peuvent être exemptées,abs()
par exemple. D'autres sont particulièrement problématiques:malloc()
et la famille en sont les premiers 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 tellement restrictif qu'il n'y a aucun intérêt à les analyser dans un environnement Standard C pur.) Ceux marqués comme `` dépendants de la locale '' 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>
- Peut - être sûr<ctype.h>
- Pas sécurisé<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ées)<iso646.h>
- Aucune fonction<limits.h>
- Aucune fonction<locale.h>
- Fonctions sensibles aux paramètres régionaux (non sécurisées)<math.h>
- Peut - être sûr<setjmp.h>
- Pas sécurisé<signal.h>
- Permis<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écurisé<stdlib.h>
- Pas tous sûrs (certains sont autorisés, d'autres non)<stdnoreturn.h>
- Aucune fonction<string.h>
- Pas tout en sécurité<tgmath.h>
- Peut - être sûr<threads.h>
- Probablement pas sûr<time.h>
- Dépend des paramètres régionaux (maistime()
est explicitement autorisé)<uchar.h>
- Dépend des paramètres régionaux<wchar.h>
- Dépend des paramètres régionaux<wctype.h>
- Dépend des paramètres régionauxAnalyser les en-têtes POSIX serait… plus difficile car 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ûres pour les signaux asynchrones (pas beaucoup d'entre elles). Notez qu'un en-tête comme
<pthread.h>
a trois fonctions sûres et de nombreuses fonctions non sûres.NB: La quasi-totalité de l'évaluation des fonctions C et des en-têtes dans un environnement POSIX est une approximation semi-instruite. Cela n'a aucun sens une déclaration définitive d'un organisme de normalisation.
la source
<string.h>
ou les fonctions de classe de caractères<ctype.h>
et de nombreuses autres fonctions de bibliothèque standard C ne figurent pas dans la liste ci-dessus? Une implémentation devrait être délibérément mauvaise pour rendrestrlen()
l'appel d'un gestionnaire de signaux dangereux.<ctype.h>
contenu, il est spécifique à la locale et pourrait causer des problèmes si le signal interrompt une fonction de réglage de la locale, mais une fois que la locale est chargée, leur utilisation devrait être sûre. Je suppose que, dans certaines situations complexes, le chargement des données locales pourrait être effectué de manière incrémentielle, rendant ainsi les fonctions<ctype.h>
dangereuses. La conclusion demeure: en cas de doute, abstenez-vous.Évitez toujours cela, dira: Ne l'utilisez pas
printf()
dans les gestionnaires de signaux.Au moins sur les systèmes conformes POSIX, vous pouvez utiliser à la
write(STDOUT_FILENO, ...)
place deprintf()
. Le formatage peut cependant ne pas être facile: Imprimez int à partir du gestionnaire de signaux à l'aide de fonctions d'écriture ou de sécurité asynchronela source
Always avoid it.
signifie? A éviterprintf()
?printf()
dans les gestionnaires de signaux.2
point, cochez OP demandant Comment éviter d'utiliserprintf()
dans les gestionnaires de signaux?À des fins de débogage, j'ai écrit un outil qui vérifie que vous n'appelez en fait que les fonctions de la
async-signal-safe
liste 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ésout pas le problème de vouloir appeler des fonctions non asynchrones à partir d'un contexte de signal, cela vous aide au moins à trouver des cas où vous l'avez fait accidentellement.Le code source est sur GitHub . Il fonctionne en surchargeant
signal/sigaction
, puis en détournant temporairement lesPLT
entrées des fonctions non sécurisées; cela provoque la redirection des appels à des fonctions non sécurisées vers un wrapper.la source
Implémentez votre propre sécurité du signal asynchrone
snprintf("%d
et utilisezwrite
Ce n'est pas aussi mauvais que je le pensais, comment convertir un int en chaîne en C?a plusieurs implémentations.
Puisqu'il n'y a que deux types de données intéressants auxquels les gestionnaires de signaux peuvent accéder:
sig_atomic_t
globauxint
argument de signalcela couvre essentiellement tous les cas d'utilisation intéressants.
Le fait que
strcpy
soit également un signal sûr rend les choses encore meilleures.Le programme POSIX ci-dessous imprime sur stdout le nombre de fois qu'il a reçu SIGINT jusqu'à présent, que vous pouvez déclencher avec
Ctrl + C
, et l'ID de signal et.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: * /programming/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: /programming/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; }
Compilez et exécutez:
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
où
2
est le numéro de signal pourSIGINT
.Testé sur Ubuntu 18.04. GitHub en amont .
la source
Une technique qui est particulièrement utile dans les programmes qui ont une boucle de sélection consiste à écrire un octet dans un tube à la réception d'un signal, puis à traiter le signal dans la boucle de sélection. Quelque chose du genre (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 de quel signal il s'agissait, l'octet le long du tube peut être le numéro du signal.
la source
Vous pouvez utiliser printf dans les gestionnaires de signaux si vous utilisez la bibliothèque pthread. unix / posix spécifie que printf est atomique pour les threads cf Dave Butenhof répondez ici: https://groups.google.com/forum/#!topic/comp.programming.threads/1-bU71nYgqw Notez que pour avoir une image plus claire de la sortie printf, vous devez exécuter votre application dans une console (sous Linux, utilisez ctl + alt + f1 pour démarrer la console 1), plutôt qu'un pseudo-tty créé par l'interface graphique.
la source