Envoyer et intercepter un signal en C
- Mia Combeau
- C
- 11 novembre 2022
Table des matières
À force d’être confrontés à des segfaults ou a des erreurs de bus, on se sera déjà familiarisé avec l’idée d’un signal informatique. On reçoit un SIGSEGV
ou un SIGBUS
qui met fin à notre programme sans préavis, sans explications et sans autre recours. Mais qu’est-ce qu’un signal véritablement ? Est-ce juste le bâton de police du système d’exploitation ? Et comment peut-on envoyer, bloquer ou même intercepter un signal depuis notre propre programme ? C’est ce que nous allons explorer dans cet article.
Qu’est-ce qu’un signal ?
Un signal est un message de notification standardisé utilisé dans les systèmes d’exploitation compatibles POSIX ou de type Unix. On l’envoie à un programme en cours d’exécution de façon asynchrone, pour le signaler de l’apparition d’un événement. Le système interrompt alors l’exécution normale du processus en question pour déclencher une réaction spécifique comme, entres autres, la terminaison du processus. On peut donc dire qu’un signal est une forme limitée de communication inter-processus.
Étudions donc comment se passe le transfert d’un signal vers un processus destinataire.
L’envoi d’un signal
Le noyau du système d’exploitation peut envoyer un signal pour l’une des deux raisons suivantes :
- Il a détecté un événement système comme une erreur de division par zéro ou la terminaison d’un processus fils,
- Un processus lui en a fait la demande avec l’appel système
kill
, sachant qu’un processus peut s’envoyer lui-même un signal.
“L’envoi” d’un signal est en réalité plutôt une livraison : le système met à jour le contexte du processus destinataire du signal. En effet, pour chaque processus, le système maintient deux vecteurs de bits : pending
pour surveiller les signaux en attente, et blocked
pour suivre les signaux bloqués. Lorsqu’il envoie un signal, le système ne fait que mettre le bit associé au signal à 1 dans le vecteur pending
du processus destinataire.
Il est important de noter qu’il ne peut pas y avoir plusieurs signaux du même type en attente. Dans l’image ci-dessus, le processus a déjà le signal 17, SIGCHLD
, en attente. Le système ne peut donc pas lui envoyer d’autres signaux SIGCHLD
jusqu’à ce que ce signal soit réceptionné. Il n’y a pas non plus de file d’attente pour les signaux en attente : tant que ce signal n’est pas réceptionné, tous les signaux du même type qui suivent sont perdus.
La réception d’un signal
Le système d’exploitation donne l’impression de pouvoir exécuter une multitude de programmes à la fois, mais ce n’est qu’une illusion. En réalité, il passe constamment d’un processus à l’autre à une vitesse fulgurante. C’est ce qu’on appelle une commutation de contexte (“context switch” en anglais).
Lorsque le noyau du système reprend le fil d’exécution du processus par exemple après une commutation de contexte ou un appel système, il vérifie l’ensemble de signaux non-bloqués en attente pour ce processus. C’est à dire qu’il fait l’ opération bitwise pending & ~blocked
. Si cet ensemble est vide, comme c’est généralement le cas, le noyau passe à la prochaine instruction du programme. Par contre si l’ensemble n’est pas vide, le noyau choisit un signal (généralement le plus petit) et force le processus à y réagir avec une action. C’est ce moment-là qu’on appelle la réception du signal. Selon le signal, le processus pourra soit :
- ignorer le signal,
- mettre fin à son exécution,
- intercepter le signal en exécutant sa propre routine de gestion pour le signal reçu.
Une fois le signal reçu et une de ces actions réalisées, le noyau remet le bit qui lui correspond à 0 dans le vecteur pending
et passe à la prochaine instruction du programme s’il n’a pas terminé.
Les signaux POSIX et leurs actions par défaut
Alors quels sont tous ces signaux que le système peut envoyer aux processus ? La liste ci-dessous montre les signaux sous Linux. Les autres systèmes compatibles avec POSIX auront aussi les signaux suivants, mais ceux-ci pourront correspondre à d’autres valeurs numériques. Il est possible de consulter la liste des signaux disponibles et leur numéros correspondants avec la commande :
kill -l
Par défaut, lorsqu’un processus reçoit un signal, il effectuera l’une de ces quatre actions :
- Terminer : le processus se termine immédiatement,
- Core : le processus se termine immédiatement et fait un core dump (un fichier contenant une copie de sa mémoire vive et de ses registres qui peut être analysée par la suite),
- Ignorer : le signal est simplement ignoré et le programme poursuit son exécution normale,
- Stop : l’exécution du processus est suspendu jusqu’à recevoir le signal
SIGCONT
.
On verra plus loin comment changer l’action par défaut associée à un signal. Toutefois, il est impossible d’intercepter, d’ignorer, de bloquer ou de changer l’action des signaux SIGKILL
et SIGSTOP
.
Liste des signaux Linux
Numéro | Nom | Action par défaut | Description | ||||
---|---|---|---|---|---|---|---|
1 | SIGHUP | Terminer | Rupture détectée sur le terminal contrôleur ou mort du processus parent | ||||
2 | SIGINT | Terminer | Interruption du clavier ( ctrl-c ) |
||||
3 | SIGQUIT | Terminer | Fin du processus, parfois du clavier ( ctrl-\ ) |
||||
4 | SIGILL | Terminer | Instruction illégale | ||||
5 | SIGTRAP | Core | Point d’arrêt rencontré | ||||
6 | SIGABRT | Core | Arrêt anormal du processus (fonction abort ) |
||||
7 | SIGBUS | Terminer | Erreur de bus | ||||
8 | SIGFPE | Core | Erreur mathématique virgule flottante | ||||
9 | SIGKILL | Terminer | Fin immédiate du processus | ||||
10 | SIGUSR1 | Terminer | Signal utilisateur 1 | ||||
11 | SIGSEGV | Core | Référence mémoire non valide (segfault) | ||||
12 | SIGUSR2 | Terminer | Signal utilisateur 2 | ||||
13 | SIGPIPE | Terminer | Écriture dans un tube (pipe) sans lecteur | ||||
14 | SIGALRM | Terminer | Signal du temporisateur définit par alarm |
||||
15 | SIGTERM | Terminer | Signal de fin | ||||
16 | SIGSTKFLT | Terminer | Erreur de pile sur coprocesseur | ||||
17 | SIGCHLD | Ignorer | Processus fils arrêté ou terminé | ||||
18 | SIGCONT | Ignorer | Continuer processus si arrêté | ||||
19 | SIGSTOP | Stop | Suspend le processus | ||||
20 | SIGTSTP | Stop | Suspend le processus depuis le terminal ( ctrl-z ) |
||||
21 | SIGTTIN | Stop | Lecture sur terminal en arrière plan | ||||
22 | SIGTTOU | Stop | Écriture sur terminal en arrière plan | ||||
23 | SIGURG | Ignorer | Condition urgente sur socket | ||||
24 | SIGXCPU | Terminer | Limite de temps CPU dépassée | ||||
25 | SIGXFSZ | Terminer | Limite de taille de fichier dépassée | ||||
26 | SIGVTALRM | Terminer | Temporisateur virtuel expiré | ||||
27 | SIGPROF | Terminer | Temporisateur de profilage expiré | 28 | SIGWINCH | Ignorer | Fenêtre redimensionnée |
29 | SIGIO | Terminer | I/O à nouveau possible | ||||
30 | SIGPWR | Terminer | Chute d’alimentation | ||||
31 | SIGSYS | Terminer | Mauvais appel système |
Envoyer des signaux
Dans les systèmes de type Unix, il y a plusieurs mécanismes pour envoyer des signaux aux processus. Tous ces mécanismes font appel à la notion de groupes de processus.
Chaque processus dans le système appartient à un groupe identifié par un entier positif, le PGID
( p rocess g roup id entifier). C’est un moyen facile d’identifier les processus apparentés. Par défaut, les processus fils appartiennent tous au groupe de leur père. Cela permet au système d’envoyer un signal à tous les processus dans un groupe à la fois.
Toutefois, un processus peut changer son propre groupe ou celui d’un autre processus. C’est ce que fait notre shell lorsqu’il crée ses processus fils lors de l’exécution de la saisie de l’utilisateur. Affichons l’identifiant du processus ( PID
), l’identifiant du processus père ( PPID
) et l’identifiant du groupe de processus ( PGID
) de tous les processus associés à notre shell à l’aide de la commande ps
:
ps -eo "%c: [PID = %p] [PPID = %P] [PGID = %r]" | grep $$
On peut voir ici que le shell, ici Bash, utilise son propre identifiant comme identifiant de groupe, et non celui de son processus père. De plus, il crée un nouvel identifiant de groupe (ici, 5213) pour ses processus fils, ps
et grep
(respectivement 5213 et 5214). Cela lui permet d’envoyer un seul signal à tous ses fils à la fois, si besoin est. Évidemment dans cet exemple, l’existence des fils est très éphémère. Mais si elle ne l’était pas, comment pourrait-on leur envoyer un signal ?
Envoyer un signal depuis le clavier
Depuis le shell dans notre terminal, il y a trois raccourcis de clavier qui nous permettent d’interrompre tous les processus en avant-plan en cours d’exécution :
ctrl-c
: envoieSIGINT
pour les interrompre,ctrl-\
: envoieSIGQUIT
pour les tuer,ctrl-z
: envoieSIGTSTP
pour les suspendre.
Bien sûr, ces raccourcis n’affectent pas les tâches de fond, c’est à dire les processus en cours d’exécution en arrière-plan. Mais comment faire pour envoyer l’un des 28 autres signaux ?
Envoyer des signaux avec la commande kill
Pour envoyer un autre type de signal depuis notre terminal, il faudra utiliser la commande kill
. Et ce, même si le signal en question n’a rien à voir avec la terminaison du processus ! Certains shells possèdent leur propre commande kill interne. Ici, nous parlerons uniquement du programme qui se situe dans /bin/kill
.
Tout d’abord, il faut trouver le PID
ou le PGID
du ou des processus auxquels on souhaite l’envoyer un signal. Pour afficher le PID
d’un processus, on peut utiliser les commandes pidof
, pgrep
, top
ou encore, comme on l’a vu plus haut, ps
.
Disons, par exemple, que l’on souhaite envoyer le signal de fin SIGKILL
(numéro 9) au processus avec le PID
4242. On peut faire cela de trois manières avec la commande kill
:
/bin/kill -9 4242
/bin/kill -KILL 4242
/bin/kill -SIGKILL 4242
Maintenant, mettons que ce processus 4242 a plusieurs fils qui sont par conséquent tous dans le groupe 4242. Comment peut-on faire la distinction entre le processus 4242 et le groupe de processus 4242 ? Pour la commande kill
, un nombre négatif lui indique que c’est un PGID
et non un PID
. Donc pour tuer tous les processus dans le groupe 4242, on peut faire :
/bin/kill -9 -4242
/bin/kill -KILL -4242
/bin/kill -SIGKILL -4242
Pour signaler tous les processus dans le groupe actuel, on peut mettre 0 à la place du PID
. Pour envoyer le signal à tous les processus dans le système sauf kill
lui-même et init
(dont le PID
est toujours 1), on peut indiquer -1 à la place du PID
.
De plus, la commande /bin/kill
nous permet d’afficher une liste de tous les signaux avec son option -L
et, avec l’option -l
, de rechercher un signal par numéro ( /bin/kill -l 11
) ou par nom ( /bin/kill -l SIGSEGV
ou /bin/kill -l SEGV
).
Envoyer un signal avec l’appel système kill en C
Dans un précédent article sur la création et la terminaison de processus fils, on a rapidement vu l’appel système kill
de la bibliothèque <signal.h>
. Il existe plusieurs autres appels systèmes pour demander au système d’envoyer un signal depuis notre programme en C, mais celui-ci est le plus communément utilisé. Rappelons son prototype :
int kill(pid_t pid, int sig);
Cet appel système fonctionne de la même manière que la commande /bin/kill
décrite ci-dessus. Ses paramètres sont :
- pid : l’identifiant du processus ou du groupe de processus auquel envoyer le signal. On peut ici spécifier :
- un entier positif : le
PID
d’un processus, - un entier négatif : le
PGID
d’un groupe de processus, - 0 : tous les processus dans le groupe du processus appelant,
- -1 : tous les processus dans le système pour lequel le processus appelant a la permission d’envoyer un signal (sauf le processus 1, init). Voir la page de manuel kill (2) pour la question des permissions.
- un entier positif : le
- sig : le signal à envoyer au processus.
La fonction kill
renvoie 0 en cas de succès et en cas d’erreur, -1, avec errno mis à jour pour indiquer les détails de l’erreur.
Intercepter et changer l’action d’un signal en C
On a vu dans la liste des signaux ci-dessus les actions par défaut associées à chacun d’entre eux. Par exemple, l’action par défaut déclenchée par SIGKILL
est de mettre fin au processus, tandis que l’action pas défaut à la reception de SIGCHLD
est de l’ignorer complètement. Cependant, nous ne sommes pas restreints à ces actions par défaut : un processus peut changer l’action à effectuer en réaction à un signal grâce à un ou plusieurs routines de gestion de signaux. Les seules exceptions sont SIGKILL
et SIGSTOP
, qui ne peuvent pas être interceptés, ou modifiés.
Intercepter un signal avec sigaction
La bibliothèque <signal.h>
propose deux fonctions pour intercepter un signal : signal
et sigaction
. La première n’est pas recommandée par souci de portabilité ; nous étudierons donc ici sigaction
dont voici le prototype :
int sigaction(int signum, const struct sigaction *restrict act,
struct sigaction *restrict oldact);
Ce prototype quelque peu intimidant, alors expliquons ses paramètres assez obscurs :
- signum : le signal pour lequel on souhaite changer l’action,
- act : un pointeur vers une structure de type
sigaction
qui va permettre entres autres d’indiquer une routine de gestion de signaux. On va examiner ceci de plus près dans un instant, - oldact : un pointeur vers une autre structure de type
sigaction
dans lequel on souhaiterait sauvegarder l’ancien comportement en réaction au signal. Si l’on a pas particulièrement besoin de sauvegarder l’ancienne réaction, on peut simplement mettreNULL
ici.
En cas de succès, sigaction
renvoie 0. En cas d’erreur, elle renvoie -1 et renseigne errno.
Indiquer une routine de gestion de signaux dans la structure sigaction
On l’aura compris, “sigaction” c’est le nom de la fonction mais aussi celui du type de structure dont la fonction a besoin pour effectuer sa tâche. Jetons donc un œil à cette structure dans la page manuel de sigaction. La variable qui va surtout nous intéresser, c’est sa_handler
. C’est elle qui spécifie l’action qui doit être associée au signal. On peut lui indiquer une de trois choses :
SIG_DFL
pour l’action par défaut,SIG_IGN
pour ignorer le signal,- un pointeur vers une routine de gestion de signal, c’est à dire une fonction qui se déclenchera en réponse à ce signal, qui doit avoir pour prototype
void nom_de_fonction(int signal);
. On remarquera que cette fonction prend en paramètres le signal, ce qui veut dire qu’on peut utiliser cette même routine pour gérer plusieurs signaux différents !
La structure sigaction
comporte une autre variable, sa_flags
, qui propose diverses options pour modifier l’action du signal, mais nous ne nous attarderons pas dessus dans cet article. La variable sa_mask
permet de bloquer les signaux qui y sont spécifiés le temps de l’exécution de la routine de gestion. Nous verrons ce point dans la section sur le blockage de signaux.
Il faut aussi garder à l’esprit que l’une des autres variables de la structure ( sa_sigaction
) est incompatible avec sa_handler
: il ne faut pas les renseigner toutes deux. Il est donc prudent de s’assurer que tous les bits de la structure sont mis à 0 avec une fonction comme bzero
ou memset
avant de la remplir.
Sigaction en action
Testons ceci avec un programme tout simple qui va simplement imprimer un message à chaque fois qu’on fait ctrl-c
, c’est à dire à chaque fois qu’on lui envoie SIGINT
:
#include <signal.h>
#include <stdio.h>
#include <strings.h>
// Routine de gestion de SIGINT
void sigint_handler(int signal)
{
if (signal == SIGINT)
printf("\nIntercepted SIGINT!\n");
}
void set_signal_action(void)
{
// Déclaration de la structure sigaction
struct sigaction act;
// Met à 0 tous les bits dans la structure,
// sinon on aura de mauvaises surprises de valeurs
// non-initialisées...
bzero(&act, sizeof(act));
// On voudrait invoquer la routine sigint_handler
// quand on reçoit le signal :
act.sa_handler = &sigint_handler;
// Applique cette structure avec la fonction à invoquer
// au signal SIGINT (ctrl-c)
sigaction(SIGINT, &act, NULL);
}
int main(void)
{
// Change l'action associée à SIGINT
set_signal_action();
// Boucle infinie pour avoir le temps de faire ctrl-c autant
// de fois que ça nous chante
while(1)
continue ;
return (0);
}
Résultat :
Quand on fait ctrl-c
, le message est imprimé ce qui veut dire que le signal a bien été intercepté et qu’il déclenche bien notre routine de gestion. On peut alors mettre fin au processus avec ctrl-\
( SIGQUIT
).
Sécuriser une routine de gestion de signaux
Les signaux sont asynchrones, c’est à dire qu’ils peuvent intervenir à n’importe quel moment dans l’exécution de notre programme. Lorsqu’on les intercepte avec une routine de gestion, on ne sait pas où le programme en est dans sont exécution. Si la routine accède à une variable que le programme est en train d’utiliser lors de son interruption, les résultats pourraient être désastreux. De plus, il ne faut pas oublier qu’une routine de gestion de signal peut elle-même être interrompue par une autre routine si le processus reçoit un autre signal entre temps !
Les routines de gestion de signaux sont une forme de programmation concurrente. Or, comme nous l’avons vu dans un article précédent sur les threads et les mutex, la programmation concurrente peut entraîner d’imprévisibles erreurs qui sont extrêmement difficiles à déboguer. Pour éviter ce genre d’erreur, nous devons prendre beaucoup de précautions lors de l’élaboration de nos routines de gestion de signaux pour qu’ils soient aussi sûrs que possible. Dans cette optique, voici quelques recommendations à garder à l’esprit.
1. Garder les routines de gestion aussi simples et courtes que possible
Parfois, il suffit de mettre à jour un drapeau global et laisser le programme principal s’occuper du traitement du signal. Le programme n’aura qu’à vérifier ce drapeau pour savoir s’il faut traiter un signal ou non.
2. Uniquement utiliser des fonctions sûres pour signaux asynchrones dans les routines
La page manuel de signal (7) maintient une liste des fonctions sûres qui peuvent être utilisées dans une routine de gestion de signal. Remarquons que la plupart des fonctions populaires comme printf
et même exit
ne figurent pas sur cette liste ! Cela veut aussi dire que notre exemple dans la section précédente devrait sans doute être reconsidérée…
3. Sauvegarder errno et la restaurer
La plupart des fonctions sûres dont nous venons de parler mettent errno à jour si elles échouent. La routine de gestion de signaux risque d’interférer avec d’autres parties du programme dépendent d’errno. Pour éviter ceci, on peut faire une sauvegarde d’errno au début de la routine et la restaurer à la fin.
4. Bloquer temporairement tous les signaux lors d’un accès à une donnée partagée entre le programme principal et la routine de gestion
Lire depuis et écrire dans une variable ou une structure de données nécessite une séquence d’instructions. Si la routine de gestion interrompt le programme principal et accède en même temps à la même variable, il risque de trouver les données qui y sont stockés dans un état incohérent. Bloquer les signaux le temps d’accéder aux données garantit que la routine de gestion des signaux n’interrompe pas un accès antérieur. Nous verrons comment bloquer des signaux dans la prochaine section.
5. Déclarer une variable globale partagée avec volatile
Si la routine de gestion des signaux met à jour une variable globale et que le programme principal la lit de temps à autre, il est possible qu’un compilateur considère que le programme principal ne modifie jamais la variable. Dans un souci d’optimisation, le compilateur pourrait alors décider de mettre la variable en mémoire cache et que, par conséquent, le programme principal ne voie jamais la valeur mise à jour par la routine de gestion. Le mot-clef volatile
indique au compilateur de ne pas mettre la variable en mémoire cache.
6. Déclarer un drapeau de type sig_atomic_t
Si la routine de gestion de signaux met à jour un drapeau que le programme principal lit périodiquement pour répondre au signal puis annuler le drapeau, on peut utiliser le type sig_atomic_t
. Pour ce type d’entier, les lectures et écritures sont atomiques, c’est à dire qu’elles ne peuvent pas être interrompues. Comme cela, on n’a pas besoin de bloquer temporairement les signaux le temps d’accéder au drapeau.
Bloquer un signal en C
Pour bloquer un signal, on doit tout d’abord l’ajouter à un ensemble de signaux à bloquer. Puis il faudra spécifier cet ensemble soit dans la variable sa_mask
de la structure sigaction
qu’on fournira à la fonction du même nom, soit dans la fonction dédiée, sigprocmask
. C’est toujours la bibliothèque <signal.h>
qui nous fournit les types de variables et les fonctions qu’il nous faut.
Rapellons ici que rien ne sert de tenter de bloquer les signaux SIGKILL
et SIGSTOP
!
Créer un ensemble de signaux à bloquer
Avant tout, nous aurons besoin d’une variable de type sigset_t
pour stocker l’ensemble des signaux que l’on souhaite bloquer.
sigset_t signal_set;
Puis, il nous faut l’initialiser avec l’une des fonctions suivantes :
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
Bien sûr, sigemptyset
met tous les bits de l’ensemble qu’on lui fournit à 0, ce qui indique qu’aucun signal n’est stocké dans l’ensemble. La fonction sigfillset
fait le contraire en mettant tous les bits à 1, ce qui veut dire qu’elle ajoute tous les signaux à l’ensemble. Ces deux fonctions renvoient 0 en cas de succès ou -1 en cas d’erreur.
Ensuite, on peut ajouter ou retirer un signal de l’ensemble avec les deux fonctions suivantes :
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
La fonction sigaddset
ajoute le signal signum à l’ensemble set tandis que sigdelset
retire le signal signum de l’ensemble set. Ces deux fonctions renvoient aussi 0 en cas de succès ou -1 en cas d’erreur.
Pour déterminer si un signal fait partie de l’ensemble ou non, on peut utiliser la fonction suivante :
int sigismember(const sigset_t *set, int signum);
Si le signal signum qu’on lui fournit fait partie de l’ensemble set, la fonction sigismember
renvoie 1, sinon elle renvoie 0. En cas d’erreur, elle renvoie -1.
Bloquer les signaux de l’ensemble
Une fois qu’on a notre ensemble de signaux à bloquer, on peut utiliser soit sigaction
soit sigprocmask
pour mettre en place le blocage.
Notons que sigaction
nous permet de bloquer un signal uniquement pendant l’exécution de la routine de gestion de signal. Pour cela il faut lui renseigner à la fois l’ensemble à bloquer dans la variable sa_mask
de sa structure, et une routine de gestion dans sa variable sa_handler
.
La fonction sigprocmask
, elle, nous permet de bloquer et de débloquer un signal à tout moment. Son prototype est :
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
Ses paramètres sont :
- how : un entier qui représente l’opération à effectuer avec l’ensemble fourni plus loin. On peut ici spécifier :
SIG_BLOCK
pour ajouter les signaux dans l’ensemble au vecteurblocked
du processus et donc les bloquer. Représente l’opération bitwiseblocked = blocked | set
.SIG_UNBLOCK
pour retirer les signaux dans l’ensemble du vecteurblocked
, et donc les débloquer. Représente l’opération bitwiseblocked = blocked & ~set
.SIG_SETMASK
pour remplacer le vecteur blocked avec l’ensemble fourni. Représente simplementblocked = set
.
- set : un pointeur vers l’ensemble des signaux qui doivent être ajoutés/retirés ou qui doivent remplacer le vecteur
blocked
signaux bloqués. - oldset : un pointeur vers une zone mémoire où placer la valeur du vecteur
blocked
qui sera remplacée. Si l’on ne souhaite pas restaurer le vecteurblocked
plus tard, on pourra renseignerNULL
ici.
Exemple de blockage de signaux
Modifions notre programme de test de tout à l’heure. On interceptera toujours SIGINT
( ctrl-c
au clavier), mais cette fois de façon sécurisée. Maintenant, elle ne fera que modifier une variable globale que le programme principal ira vérifier régulièrement. Pour protéger cette variable globale, on bloquera le signal SIGINT
lorsqu’on la lit ou lorsqu’on y écrit. Au début du programme, on bloquera d’office le signal SIGQUIT
( ctrl-\
). Celui-ci ne sera débloqué que suite au signal SIGINT
.
#include <signal.h>
#include <strings.h>
#include <stdio.h>
#include <unistd.h>
// Variable globale partagée entre le programme
// principal et la routine SIGINT. La routine mettra
// cette variable à 1 lorsqu'on fait ctrl-c.
// Déclarée volatile pour éviter quelle se retrouve
// dans la mémoire cache à cause du compilateur
volatile int g_unblock_sigquit = 0;
// Fonction pour bloquer le signal spécifié
void block_signal(int signal)
{
// L'ensemble des signaux à bloquer
sigset_t sigset;
// Initialise l'ensemble à 0
sigemptyset(&sigset);
// Ajoute le signal à l'ensemble
sigaddset(&sigset, signal);
// Ajoute les signaux de l'ensemble aux
// signaux bloqués pour ce processus
sigprocmask(SIG_BLOCK, &sigset, NULL);
if (signal == SIGQUIT)
printf("\e[36mSIGQUIT (ctrl-\\) blocked.\e[0m\n");
}
// Fonction pour débloquer le signal spécifié
void unblock_signal(int signal)
{
// L'ensemble des signaus à débloquer
sigset_t sigset;
// Initialise l'ensemble à 0
sigemptyset(&sigset);
// Ajoute le signal à l'ensemble
sigaddset(&sigset, signal);
// Retire les signaux de l'ensemble des
// signaux bloqués pour ce processus
sigprocmask(SIG_UNBLOCK, &sigset, NULL);
if (signal == SIGQUIT)
printf("\e[36mSIGQUIT (ctrl-\\) unblocked.\e[0m\n");
}
// Routine de gestion du signal SIGINT
void sigint_handler(int signal)
{
if (signal != SIGINT)
return ;
// Bloque les autres signaux SIGINT pour
// protéger la variable globale le temps
// de la modifier
block_signal(SIGINT);
g_unblock_sigquit = 1;
unblock_signal(SIGINT);
}
void set_signal_action(void)
{
// Déclaration de la structure sigaction
struct sigaction act;
// Initialiser la structure à 0.
bzero(&act, sizeof(act));
// Nouvelle routine de gestion de signal
act.sa_handler = &sigint_handler;
// Applique la nouvelle routine à SIGINT (ctrl-c)
sigaction(SIGINT, &act, NULL);
}
int main(void)
{
// Change l'action par défaut de SIGINT (ctrl-c)
set_signal_action();
// Bloque le signal SIGQUIT (ctrl-\)
block_signal(SIGQUIT);
// Boucle infinie pour avoir le temps de faire ctrl-\ et
// ctrl-c autant de fois que ça nous chante.
while(1)
{
// Bloque le signal SIGINT le temps de lire la variable
// globale.
block_signal(SIGINT);
// Si la routine de gestion de SIGINT a indiqué qu'elle a
// été invoquée dans la variable globale
if (g_unblock_sigquit == 1)
{
// SIGINT (ctrl-c) a été reçu.
printf("\n\e[36mSIGINT detected. Unblocking SIGQUIT\e[0m\n");
// Débloque SIGINT et SIGQUIT
unblock_signal(SIGINT);
unblock_signal(SIGQUIT);
}
// Sinon, on débloque SIGINT et on continue la boucle
else
unblock_signal(SIGINT);
sleep(1);
}
return (0);
}
Dans ce résultat, on peut voir que quand on fait ctrl-\
, c’est à dire quand on envoie le signal SIGQUIT
, rien ne se passe (à part le fait que le terminal affiche ^\
). Ce signal est correctement bloqué dans notre processus. Dès qu’on fait ctrl-c
pour envoyer le signal SIGINT
, le signal SIGQUIT
se voit débloqué.
Mais comme on peut le voir ici, on n’a même pas le temps de refaire un ctrl-\
pour l’envoyer de nouveau. On ne voit même pas notre printf
de la ligne 44 dans notre code, qui confirme le déblocage de SIGQUIT
! C’est parce que le signal SIGQUIT
était en attente depuis notre premier ctrl-\
. Dès qu’il a été débloqué, l’action qui lui est associée par défaut (“Quit (code dumped)”, c’est à dire terminer sans générer de fichier core) s’est déclenchée.
Une autre astuce à partager, une petite question à poser, ou une découverte intéressante à propos des signaux et de leur gestion ? Je serai ravie de lire et de répondre à tout ça dans les commentaires. Bon code !
Sources et lectures supplémentaires
-
Bryant, R. E., O’Hallaron, D. R., 2016, Computer Systems: A Programmer’s Perspective, Third Edition, Chapter 8 : Exceptional Control Flow, pp. 792-817
-
Manuel du programmeur Linux :
-
Stack Exchange, Linux Process Group Example [stackexchange.com]