Programmation C/Gestion des signaux

Un livre de Wikilivres.
Sauter à la navigation Sauter à la recherche
Ébauche

Cette page est considérée comme une ébauche à compléter. Si vous possédez quelques connaissances sur le sujet, vous pouvez les partager en éditant dès à présent cette page (en cliquant sur le lien « modifier »).

Ressources suggérées : Aucune (vous pouvez indiquer les ressources que vous suggérez qui pourraient aider d'autres personnes à compléter cette page dans le paramètre « ressources » du modèle? engendrant ce cadre)

Les signaux permettent une communication, assez sommaire, entre le système et un processus, ou entre différents processus. Cette communication est sommaire, car un signal ne porte qu'une seule information: son numéro, de type int. Un processus peut aussi s'envoyer un signal à lui-même.

Ces signaux sont envoyés de manière asynchrone: lorsqu'un processus reçoit un signal, son exécution est interrompue, et une fonction spécifique, dite gestionnaire de signal, est appelée, avec en paramètre le numéro du signal reçu, pour traiter l'événement. Lorsque cette fonction se termine, le processus reprend là où il s'était arrêté.

C'est à chaque processus de déterminer ce qu'il fait quand il reçoit un signal de numéro donné, en définissant un gestionnaire de signal pour tous les signaux qu'il le souhaite. Vous l'aurez remarqué, dans tous les exemples de ce livre rencontrés jusqu'à présent, nous ne nous sommes pas occupés de savoir quels signaux nous pourrions recevoir, et comment il fallait les traiter. En l'absence de précisions dans nos programmes, l'implémentation fournira en effet un gestionnaire de signal par défaut, qui le plus souvent se contentera d'arrêter le programme.

Les fonctions suivantes, fournies par l'en-tête <signal.h>, sont utilisées dans la gestion des signaux :

  • signal() pour définir un gestionnaire de signal;
  • et raise() pour envoyer un signal au processus courant.

Le C définit ces deux fonctions, et pas plus, alors que les signaux peuvent faire beaucoup plus... En particulier, aucune fonction pour envoyer un signal à un autre processus n'est définie. Cela est dû au fait que, sur certains systèmes, la notion de processus n'existe pas, et une seule tâche s'exécute. Dans de telles situations, il serait aberrant de demander à un compilateur C pour un tel système de fournir des moyens de communications inter-processus ! Par contre, les communications inter-processus étant chose courante dans de nombreux autres systèmes, des extensions fournissent des moyens de le faire (voir par exemple la fonction kill() définie dans POSIX, cf. le chapitre Gestion des signaux du livre Programmation POSIX).

Définir un gestionnaire de signal[modifier | modifier le wikicode]

La fonction

void (*signal(int sig, void (*func)(int)))(int);

permet de définir un gestionnaire de signal. La signature de cette fonction étant un peu complexe, on peut la simplifier en utilisant un typedef:

typedef void (*t_handler)(int);
 
t_handler signal(int sig, t_handler func);

Le comportement de signal() varie selon les versions d’Unix, et a aussi varié au cours du temps dans les différentes versions de Linux. Évitez de l’utiliser : utilisez plutôt sigaction(2).

Ainsi, on voit mieux comment fonctionne cette fonction (notez que le type t_handler n'est pas défini par la norme, et n'est ici que pour clarifier la syntaxe). Elle prend deux paramètres:

  • le numéro du type de signal à traiter;
  • et un pointeur vers la fonction de gestion du signal.

Le premier paramètre est le numéro du signal. La norme définit un faible nombre de signaux, par des macros dans l'en-tête <signal.h>, par exemple SIGSEGV qui indique un accès illégal à la mémoire (SEGmentation Violation), et laisse à l'implémentation la liberté de définir d'autres types de signaux.

Pour le deuxième paramètre, on peut donner trois valeurs possibles:

  • SIG_IGN (macro définie dans <signal.h>): tout signal ayant le numéro sig sera ignoré (i.e. rien ne se passera, le programme continue de s'exécuter);
  • SIG_DFL (id.): le gestionnaire de signal par défaut sera mis en place pour ce type de signal.
  • un pointeur vers une fonction de type t_handler: cette fonction sera appelée lorsqu'un signal de type sig sera reçu par le processus.

Cette fonction renvoie la valeur du dernier gestionnaire de signal pour le numéro donné (qui peut être SIG_IGN ou SIG_DFL).

En cas d'erreur, la fonction renvoie SIG_ERR et place la variable errno à une valeur significative.

Les gestionnaires de signaux[modifier | modifier le wikicode]

Les gestionnaires de signaux sont des fonctions au prototype simple :

void fonction(int);

Elles prennent un argument de type int, qui est le numéro du signal, et ne renvoient rien. En effet, ces fonctions étant appelées de manière asynchrone, leur éventuelle valeur de retour ne serait récupérée, et encore utilisée, par personne...

Comme on utilise signal pour associer à chaque numéro de signal une fonction qui va gérer les signaux de ce numéro, on pourrait penser qu'il n'est pas nécessaire de donner ce numéro en paramètre à la fonction. Cependant cela s'avère pratique, car on peut ainsi définir une seule fonction qui peut gérer plusieurs types de signaux, et dont le comportement variera en fonction du signal à traiter.

Voici un exemple simple de définition et de mise en place d'un gestionnaire de signaux, pour le type de signal SIG_FPE, qui concerne des exceptions lors des calculs en virgule flottante (Floating Point Exception):

Le comportement de signal() varie selon les versions d’Unix, et a aussi varié au cours du temps dans les différentes versions de Linux. Évitez de l’utiliser : utilisez plutôt sigaction(2).

#include <signal.h>
#include <stdio.h>

void sig_fpe(int sig)
{
    /* ... */
}

int main(void)
{
    if (signal(SIG_FPE, sig_fpe) == SIG_ERR)
    {
        puts("Le gestionnaire de signal pour SIG_FPE n'a pu etre defini.");
    }
    else
    {
        puts("Le gestionnaire de signal pour SIG_FPE a pu etre defini.");
    }
    /* ... */
    return 0;
}