Signaux
Introduction
Les signaux sont liés à l’interruption du déroulement d’un programme. Une interruption consiste en l’arrêt du flot d’exécution afin de lancer le code approprié. Les signaux sont à l’origine de ces interruptions qui provoquent en général l’arrêt du programme, et souvent après l’écriture d’un fichier core sur le disque.
Un signal peut être généré de plusieurs manières. Sur les systèmes POSIX, les plus connus sont probablement SIGTERM et SIGKILL qui sont lancés afin de demander au programme de s’arrêter pour le premier, et afin de l’arrêter de force pour le second. Un autre que nous connaissons par ses effets est celui issu d’une faute de segmentation. Les signaux POSIX sont les suivants :
Tableau des signaux POSIX
Nom |
Numéro |
Action |
Commentaires |
SIGHUP |
1 |
fin |
Raccrocher (hangup). Souvent utilisé pour indiquer que l’application peut redémarrer. |
SIGINT |
2 |
fin |
Vous venez de faire [Ctrl] C. |
SIGQUIT |
3 |
fin |
Abandonne le programme. |
SIGILL |
4 |
fin |
Instruction illégale. |
SIGABRT |
6 |
core+fin |
Signal d’arrêt envoyé par abort(). |
SIGFPE |
8 |
core+fin |
Erreur mathématique d’opération impossible sur des nombres à virgule flottante (division par zéro, racine carrée d’un nombre négatif...). |
SIGKILL |
9 |
fin |
Demande au système d’exploitation d’arrêter le processus. Ce signal ne peut pas être intercepté. |
SIGUSR1 |
10,16,30 |
fin |
Signal utilisateur... |
Mettre en place un gestionnaire de signaux
Problème
Vous voulez gérer vous-même les signaux.
Solution
Mettez en place un gestionnaire de signaux avec sigaction().
Discussion
Un gestionnaire de signaux consiste en une structure, de type struct sigaction, et en une fonction à appeler en cas d’interruption. La structure contient un pointeur sur cette fonction, ainsi que des options et un masque de signaux empêchant l’apparition d’autres signaux pendant la gestion de l’interruption.
La mise en place du gestionnaire de signaux passe donc par l’écriture d’une fonction comme celle-ci :
void
signal_handler (int signal_id)
{
/* Votre code ici */
}
Puis nous créons la structure qui décrit le gestionnaire de signaux et nous appelons sigaction() avec cette structure pour gérer un signal, ici SIGINT :
struct sigaction sa;
memset (&sa, 0, sizeof *sa);
sa.sa_handler = signal_handler;
sa.sa_flags = 0;
sigemptyset (&(sa.sa_mask));
if (sigaction (SIGINT, &sa, NULL) != 0)
{
fprintf (stderr, "Problème avec sigaction()\n");
exit (EXIT_FAILURE);
}
Les arguments de sigaction() sont donc l’identifiant du signal, suivi d’un pointeur sur une struct sigaction qui décrit le gestionnaire de signaux. Le dernier argument est un pointeur vers une autre struct sigaction, mais vide cette fois, qui contiendra après l’appel à sigaction() la description du précédent gestionnaire de signaux. NULL signifie que nous ne souhaitons pas connaître la valeur du précédent gestionnaire de signaux. C’est cet argument que nous exploitons dans la recette "Sauvegarder un gestionnaire de signaux et le restaurer par la suite".
La structure sigaction n’est pas la même sur tous les systèmes, mais vous pouvez la trouver définie ainsi, tant que tous les champs ci-dessous sont disponibles :
struct sigaction
{
void (*sa_handler) (int);
void (*sa_sigaction) (int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer) (void);
}
C’est donc dans le champ sa_handler que nous précisons la fonction qui sera appelée lors de l’interruption. Vous pouvez également...
Bloquer des signaux
Problème
Vous souhaitez bloquer des signaux pour les traiter plus tard.
Solution
Créez un masque de signaux avec sigemptyset(), sigfillset(), sigaddset() et sigdelset(), puis appelez sigprocmask().
Discussion
La création d’un masque de signaux commence par l’initialisation de ce masque avec sigemptyset() ou sigfillset() suivant que nous voulons partir d’un jeu de signaux vide ou plein. Puis pour chaque signal que nous voulons respectivement ajouter ou supprimer du masque, nous appelons sigaddset() ou sigdelset(). Voici quelques exemples :
sigset_t masque;
/* Création d'un masque ne contenant que les signaux INT et TERM */
sigemptyset (&masque);
setaddset (&masque, SIGINT);
setaddset (&masque, SIGTERM);
/* Création d'un masque contenant tous les signaux sauf FPE et BUS */
sigfillset (&masque);
setdelset (&masque, SIGFPE);
setdelset (&masque, SIGBUS);
Lorsque le masque de signaux est créé, nous appelons sigprocmask() avec en premier argument une des constantes suivantes : SIG_SETMASK pour remplacer l’ancien masque ; SIG_BLOCK pour ajouter les signaux du masque à ceux déjà bloqués (le résultat est l’union du masque en place et de celui que nous venons de créer) ; SIG_UNBLOCK pour supprimer les signaux du masque de ceux déjà...
Connaître le masque de blocage des signaux
Problème
Vous voulez connaître les signaux filtrés par le masque de blocage des signaux.
Solution
Utilisez sigprocmask() avec un masque vide :
sigset_t vide, masque_actuel;
sigemptyset (&vide);
sigprocmask (SIG_BLOCK, &vide, &masque_actuel);
Discussion
En ajoutant un masque de signaux vide, vous ne changez pas le masque en place. Par conséquent, ici, sigprocmask() retourne le masque actuel via le pointeur fourni en troisième argument.
Pour tester si un signal se trouve dans le masque, vous pouvez utiliser sigismember(). Par exemple, pour tester si le masque contient le signal TERM, exécutez sigismember(&masque, SIGTERM) qui renvoie 1 si oui, sinon 0.
Prototypes
#include <signal.h>
int sigprocmask (int how, const sigset_t *set, sigset_t *oset);
int sigismember (const sigset_t *set, int signo);
Voir aussi les pages de manuel des fonctions sigprocmask() et sigemptyset().
Savoir si un signal a été bloqué
Problème
Vous voulez savoir si un signal est bloqué et en attente d’être traité.
Solution
Utilisez sigpending() qui remplit un masque de signaux, puis testez avec sigismember() si le masque contient le signal en question.
Discussion
Pour tester si le signal TERM est bloqué, exécutez les lignes suivantes :
sigset_t masque_test;
sigpending (&masque_test);
if (1 == sigismember (&masque_test, SIGTERM))
printf ("Le signal TERM est bloqué\n");
Prototypes
#include <signal.h>
int sigpending (sigset_t *set);
int sigismember (const sigset_t *set, int signo);
Voir aussi les pages de manuel des fonctions sigpending() et sigismember().
Intercepter [Ctrl] C
Problème
Vous ne voulez pas que la séquence [Ctrl] C interrompe votre programme.
Solution
Mettez en place un gestionnaire de signaux comme ci-dessous :
struct sigaction sa;
memset (&sa, 0, sizeof *sa);
sa.sa_sigaction = SIG_IGN;
sa.sa_flags = 0;
sigemptyset (&(sa.sa_mask));
if (sigaction (SIGINT, &sa, NULL) != 0)
{
fprintf (stderr, "Erreur sur sigaction");
}
Discussion
Cette méthode consiste à purement et simplement ignorer le signal INT que [Ctrl] C envoie au processus. Il est aussi possible de mettre en place un gestionnaire de signaux avec une fonction de traitement du signal comme vu dans la première recette de ce chapitre.
Envoyer un signal
Problème
Vous voulez envoyer un signal à un processus.
Solution
Utilisez kill().
Il existe aussi une commande kill et une page de manuel associée. Pour obtenir celle de la fonction, exécutez man 2 kill ou man -s 2 kill qui devraient fonctionner.
Discussion
L’utilisation de kill() est simple : le premier argument est l’identifiant du processus auquel envoyer le signal et le second argument est le signal. Cependant, cela ne fonctionnera que pour un processus appartenant à l’utilisateur qui exécute kill(), à moins d’avoir les privilèges administrateur.
Il est intéressant de savoir que killpg() permet d’envoyer un signal (second argument) à tous les processus du même groupe que celui spécifié en premier argument. Notons que kill() et killpg() sont équivalents si leur premier argument est zéro : le signal est envoyé à tous les processus du même groupe que celui qui envoie le signal. De plus, kill(-1, signal) envoie le signal à tous les processus sauf quelques processus vitaux, à condition qu’il ait les permissions appropriées.
La fonction kill() a par ailleurs la capacité de tester qu’un processus est présent en fonction de son identifiant. Pour cela, donnez la valeur zéro en guise de signal. Aucun signal n’est alors envoyé, mais en cas d’absence...
Sauvegarder un gestionnaire de signaux et le restaurer par la suite
Problème
Vous souhaitez sauvegarder un gestionnaire de signaux pour en installer un nouveau temporairement, et ensuite, restaurer l’ancien.
Solution
Utilisez le troisième argument de sigaction().
Discussion
Le troisième argument de sigaction() permet de connaître le précédent masque de signaux. C’est ainsi que vous le sauvegardez, tout en installant un nouveau masque. Pour le restaurer, il suffira de rappeler sigaction() en donnant l’ancien masque en second argument.
struct sigaction ancien, nouveau;
/* initialisation du nouveau masque */
sigaction (signal, &nouveau, &ancien);
/* instructions */
sigaction (signal, &ancien, NULL);
Voir aussi la page de manuel de la fonction sigaction().
Limiter le temps d’exécution d’une partie d’un programme
Problème
Vous voulez limiter le temps d’exécution d’une partie de programme pour reprendre la main si le processus est trop long.
Solution
Mettez en place un gestionnaire de signaux pour le signal ALRM, puis faites appel à alarm().
Discussion
La mise en place du gestionnaire de signaux s’effectue comme expliqué dans la première recette.
struct sigaction sa;
memset (&sa, 0, sizeof *sa);
sa.sa_sigaction = alarm_handler;
sa.sa_flags = 0;
sigemptyset (&(sa.sa_mask));
if (sigaction (SIGALRM, &sa, NULL) != 0)
{
fprintf (stderr, "Erreur sur sigaction");
}
Lorsque vous souhaitez lancer une opération que vous voulez limiter dans le temps, appelez alarm() avant, pour indiquer le temps maximum autorisé en secondes, et alarm() avec un argument nul pour désactiver l’envoi du signal. Voici un exemple qui limite à 2 minutes le temps d’exécution d’une opération :
alarm (120);
operation ();
alarm (0);
Le plus délicat dans la limitation du temps d’exécution d’une partie d’un programme est lorsque l’interruption survient. En effet, en fonction de la nature de l’opération, la main ne sera pas rendue au programme de la même manière. Lorsqu’il...