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
ouraise
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é commevolatile sig_atomic_t
, ou le gestionnaire de signal appelle n'importe quelle fonction de la bibliothèque standard autre queabort
fonction, le_Exit
fonction, laquick_exit
fonction, ou la fonctionsignal
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 ausignal
la fonction donne unSIG_ERR
renvoie, la valeur deerrno
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()
, ousigqueue()
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 fairestrlen()
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 (maistime()
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 ?
-
Toujours l'éviter, dira :n'utilisez simplement pas
printf()
dans les gestionnaires de signaux. -
Au moins sur les systèmes conformes POSIX, vous pouvez utiliser
write(STDOUT_FILENO, ...)
au lieu deprintf()
. 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 unflag
puis vérifiez queflag
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.