Blog ENI : Toute la veille numérique !
💥 Un livre PAPIER acheté
= La version EN LIGNE offerte pendant 1 an !
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici

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...