Les signaux sous Linux sont un mécanisme de communication inter-processus utilisé par le noyau pour notifier un processus qu’un événement s’est produit. Nous avons déjà consacré une introduction aux signaux dans l’épisode 5.
Contrairement aux appels système classiques, ils sont asynchrones : un processus peut recevoir un signal à n’importe quel moment, ce qui interrompt temporairement son exécution normale.
Chaque signal possède un numéro et un nom symbolique défini dans le fichier d’en-tête signal.h. Les signaux standard vont de 1 à 31, tandis que les signaux temps réel, plus nombreux, occupent généralement les numéros 32 à 64.
Un signal peut être envoyé par le noyau lui-même, par un autre processus via l’appel système kill, ou par le processus lui-même avec raise.
#include <signal.h>
int kill(pid_t pid, int sig); // pid = numéro du processus à atteindre, sig numéro du signal à lui envoyer
int raise(int sig); // équivalent à kill(getpid(),sig);
L’utilitaire en ligne de commande kill permet également d’envoyer un signal à un processus identifié par son PID, et pkill ou killall le font par nom.

Chaque signal a une action par défaut. Pour la plupart, cette action est de terminer le processus. Certains signaux comme SIGSEGV ou SIGFPE indiquent une erreur matérielle ou logicielle grave.
D’autres signaux sont ignorés par défaut, comme SIGCHLD qui signale la fin d’un processus fils, ou SIGURG pour des données urgentes sur une socket.
Deux signaux ne peuvent pas être capturés ni ignorés : SIGKILL et SIGSTOP. Ces deux-là sont toujours traités directement par le noyau et servent de moyen ultime pour arrêter ou suspendre un processus récalcitrant. SIGKILL ne peut pas être bloqué, ignoré ou intercepté. SIGSTOP arrête le processus sans possibilité de refus.
Un processus peut choisir de modifier le comportement par défaut d’un signal de trois façons : le capturer en installant une fonction de traitement, l’ignorer avec SIG_IGN, ou rétablir le comportement par défaut avec SIG_DFL.
L’appel système classique pour installer un gestionnaire est signal, mais il est parait-il est considéré comme peu portable (n’importe quoi), avec un comportement qui varierait selon les implémentations.
La littérature conseille d’utiliser plutôt l’appel système moderne sigaction, mais mon esprit de contradiction et mon expérience me font conserver signal, qui non seulement fonctionne super bien sous Linux mais fonctionne également sous tous les Unices (pluriel d’Unix) sur lequel je l’ai utilisé (Solaris, Irix, etc.).
Lorsqu’un signal est capturé, le noyau interrompt le flux d’exécution normal du processus, exécute la fonction de traitement, puis reprend là où il s’était arrêté.
Le masque de signaux permet de bloquer temporairement la réception de certains signaux. Un signal bloqué n’est pas perdu : il reste en attente jusqu’à ce qu’il soit débloqué. Si plusieurs signaux identiques sont bloqués puis débloqués, un seul exemplaire est généralement délivré pour les signaux standard, mais les signaux temps réel peuvent être mis en file d’attente.
Les signaux standard ne transportent aucune information supplémentaire en dehors de leur numéro. Les signaux temps réel, introduits par POSIX, permettent d’envoyer une valeur entière ou un pointeur avec le signal.
Cas d’usage
Les signaux sont souvent utilisés pour des tâches simples : recharger un fichier de configuration sur réception du signal SIGHUP, arrêter proprement un démon sur réception de SIGTERM, demander un vidage de statistiques sur réception de SIGUSR1. Ils servent aussi à la synchronisation entre processus, par exemple pour notifier la fin d’un travail ou la disponibilité de données.
Cependant, ils ne remplacent pas des mécanismes plus riches comme les tubes, les sockets ou les files de messages, car ils ne transportent pas de données structurées.
Sous Linux, il existe aussi des signaux générés par le noyau pour signaler des événements matériels ou logiciels : SIGBUS pour une erreur d’accès mémoire, SIGILL pour une instruction illégale, SIGPIPE lorsqu’un processus écrit dans un tube dont l’autre extrémité est fermée, SIGALRM à l’expiration d’un timer créé avec l’appel système alarm ou setitimer.
SIGCHLD est envoyé au processus parent lorsqu’un fils se termine, ce qui permet de le récupérer avec wait sans rester bloqué. SIGWINCH est envoyé lorsqu’un terminal change de taille, ce qui permet aux applications plein écran de se redimensionner.
Travaux pratiques
Nous allons voir quelques mises en oeuvre concrètes en C.
prog1.c : Interception de SIGTERM
Le signal SIGINT est celui qui est envoyé au processus lorsqu’on tente de l’arrêter par Ctrl-C dans un shell bash. Notre programme compte jusqu’à 10 en 10 secondes. Voici son code source :
#include <stdio.h>
#include <unistd.h>
int main()
{
int i;
for(i=1;i<=10;i++)
{
printf("%d\n",i);
sleep(1);
}
}
Ce programme peut être arrêté facilement par Ctrl-C.

Modifions maintenant le code source du programme pour qu’il intercepte le signal SIGINT avec une fonction de traitement de ce signal qui affiche à l’écran que l’utilisation de Ctrl-C est interdite pour arrêter ce programme :
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void fonction_de_traitement(int sig)
{
printf(" Ctrl-C interdit\n");
signal(SIGINT,fonction_de_traitement); // on ré-associe le signal à la fonction de traitement
}
int main()
{
int i;
signal(SIGINT,fonction_de_traitement); // on associe la fonction à la réception de SIGINT
for(i=1;i<=10;i++)
{
printf("%d\n",i);
sleep(1);
}
}
Maintenant le programme ne peut plus être arrêté par Ctrl-C, le signal SIGINT étant intercepté par celui-ci.

Notez que la réception de SIGINT se fait principalement lorsque le programme exécute l’instruction sleep(1); ce qui a pour conséquence d’interrompre cette instruction sleep. Lorsqu’on fait des Ctrl-C interceptés, le programmes compte 10 en moins de 10 secondes puisqu’on interrompt des sleep.
prog2.c : s’envoyer un signal à soi-même avec SIGALRM
Dans ce deuxième exercice nous allons compter pareil mais jusqu’à 100, et au lieu d’intercepter SIGINT, nous allons intercepter le signal SIGALRM, qui est un signal que le processus peut s’envoyer à lui-même au bout d’un compte à rebours programmable grâce à l’appel système alarm.
Voici le code source de prog2.c :
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void fonction_de_traitement(int sig)
{
printf(" Alarme déclenchée !\n");
signal(SIGALRM,fonction_de_traitement); // on ré-associe le signal à la fonction de traitement
alarm(5); // prochaine alarme dans 5 secondes
}
int main()
{
int i;
signal(SIGALRM,fonction_de_traitement); // on associe la fonction à la réception de SIGINT
alarm(5); // première alarme dans 5 secondes
for(i=1;i<=100;i++)
{
printf("%d\n",i);
sleep(1);
}
}
Le programme compte jusqu’à 100, et toutes les 5 secondes déclenche la fonction de traitement.

prog3.c : SIGUSR1 et SIGUSR2 pour les besoins du programmeur
Les signaux SIGUSR1 et SIGUSR2 sont des signaux mis à disposition du programmeur pour l’aider à synchroniser ses processus entre eux.
Dans le programme prog3.c dont le code source figure ci-dessous, nous allons créer un processus fils qui va mettre en place une fonction de traitement pour le signal SIGUSR1, puis va dormir 100 secondes. Le processus père va lui mettre en place une fonction de traitement pour le signal SIGCHLD, puis dormir une seconde, puis envoyer le signal SIGUSR1 à son processus fils via l’appel système kill (qui porte bien mal son nom, on est d’accord). A la réception de SIGUSR1, le fils va afficher un message à l’écran puis revenir là où il était sauf que la réception du signal SIGUSR1 va casser le sleep qu’il était en train de faire. Le fils va donc mourir (instruction suivant sleep -> exit).
Pendant ce temps là, le père est entré dans une boucle infinie. A la mort du fils, le noyau Linux va lui envoyer le signal SIGCHLD, qui est normalement un signal ignoré par les processus. Sauf qu’ici ce signal a été associé à une fonction de traitement donc celle-ci sera exécutée : le père affichera un message et mourra.
Au final la durée de la séquence ne sera que d’une seconde, si vous avez bien compris la mécanique.
Voici le code source de prog3.c :
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void fonction_de_traitement1(int sig)
{
printf("Processus fils : bien reçu SIGUSR1\n");
}
void fonction_de_traitement2(int sig)
{
printf("Processus père : bien reçu SIGCHLD\n");
exit(0);
}
int main()
{
int pid_fils;
if((pid_fils=fork())==0)
{
// code du processus fils
signal(SIGUSR1,fonction_de_traitement1);
sleep(100);
exit(0);
}
// code du processus père
signal(SIGCHLD,fonction_de_traitement2);
sleep(1);
kill(pid_fils,SIGUSR1); // envoie le signal SIGUSR1 au processus fils au bout d'une seconde
while(1); // boucle infinie
}
Et voici son exécution :

Là on est sur un cas basique pour montrer la mécanique. Dans la vraie vie, le programmeur mettra par exemple en place un pipe (dont nous verrons la programmation dans un prochain épisode) entre son processus père et son processus fils, et utilisera le signal SIGUSR1 ou SIGUSR2 pour prévenir le fils que des données ont été mises dans le pipe et sont donc à lire par exemple.
Envoyer un signal à la main
Dans cet exercice nous allons lancer un programme prog4.c qui est une simple boucle infinie.
#include <stdio.h>
int main()
{
while(1); // boucle infinie
}
Nous allons lancer ce programme en tâche de fond, puis le stopper en lui envoyant à la main le signal SIGSTOP. Puis nous allons le réveiller en lui envoyant à la main le signal SIGCONT (pour « continuer »), puis nous allons le tuer en lui envoyant à la main le signal SIGKILL.
Pour SIGKILL, signal qui porte le numéro 9, on peut utiliser au choix (et ceci est vrai pour tous les signaux) :
kill -KILL pid
ou kill -9 pid (qu'on utilise le plus souvent)

