Les signaux
Les principes
Les signaux sont un mécanisme géré par le noyau, ayant pour but d’informer un processus qu’un événement s’est produit. Cette notion est très comparable au principe des interruptions matérielles, on l’appelle parfois une interruption logicielle.
Linux a repris les signaux traditionnels issus d’Unix, mais en respectant la normalisation POSIX, pour remédier à des problèmes liés à leur conception originelle. Il implémente en réalité une forme de signaux plus évolués, dits signaux temps réel, dont le détail ne sera pas abordé dans cet ouvrage.
1. Qu’est-ce qu’un signal ?
Un signal est un indicateur, identifié par un numéro unique, géré par le noyau et destiné à un processus. Suivant les cas, ce dernier peut recevoir ou non cette information (un signal peut être ignoré par un processus, dans ce cas il ne lui est pas transmis). Seul un processus en état d’exécution peut effectivement recevoir un signal. Entre le moment où le signal est émis par le noyau et celui où il est reçu par le processus, le signal est en attente de réception, pendant (pending).
On peut représenter l’ensemble des signaux pendants à destination d’un processus, sous forme d’un ensemble ordonné de bits. Chaque bit correspond à un numéro de signal. Chaque processus a une table de signaux pendants. Quand le noyau émet un signal vers un processus, il positionne à 1 le bit correspondant dans la table de signaux pendants de ce processus, s’il n’est pas déjà positionné à 1. Une fois le signal traité par le processus destinataire, le bit correspondant est remis à zéro.
Cette représentation, bien que schématique, permet d’illustrer quelques caractéristiques des signaux :
-
Il n’y a pas de file d’attente de signaux....
Les types de signaux
Les signaux sont toujours gérés par le noyau, mais ils peuvent être de différentes origines.
1. Signaux d’origine utilisateur
Un utilisateur peut envoyer un signal à un ou plusieurs processus, explicitement ou implicitement.
a. Signaux liés au clavier
Il peut générer un signal par certaines combinaisons de touches clavier, variables suivant la configuration du terminal (réel, virtuel ou fenêtre de terminal).
Par exemple CTRL/C au clavier provoque généralement l’envoi du signal INT vers tous les processus d’avant-plan associés au terminal.
Quand un utilisateur se déconnecte du terminal, un signal HUP est envoyé à tous les processus de la session associée à son terminal.
b. La commande kill
Un utilisateur peut également utiliser la commande interne du shell kill. Cette commande envoie le signal spécifié (TERM par défaut) au(x) processus spécifié(s).
Syntaxe
kill [options] PID [...]
Options
-num|nomSignal |
Numéro ou nom symbolique du signal à envoyer. |
-l ou -L |
Affiche la liste des signaux gérés par le système. |
Arguments
Une liste d’identifiants de processus. Si un identifiant est négatif et différent de -1, le signal est envoyé à tous les processus dont c’est l’identifiant de groupe de processus. Si l’identifiant vaut -1, le signal est envoyé à tous les processus (sauf au processus de PID 1).
Description
Par défaut, la commande envoie le signal de terminaison TERM au(x) processus spécifié(s) en argument.
Un utilisateur non privilégié ne peut envoyer de signaux qu’à un processus ayant son compte utilisateur comme identifiant utilisateur effectif ou réel. Un utilisateur privilégié peut envoyer un signal à tous les processus, avec des restrictions concernant le processus de PID 1 (init ou systemd).
Le nom de la commande est trompeur. Elle n’a pas pour but de "tuer" les processus destinataires, mais plutôt de leur signifier un événement. Le nom vient du fait que le traitement par défaut associé à la plupart des signaux provoque la terminaison du processus destinataire.
2. Signaux émis à l’initiative du noyau...
Envoi d’un signal
Nous avons vu que les signaux peuvent être émis spontanément par le noyau, en réaction à certains événements (instruction illégale, demande d’accès à une adresse mémoire invalide ou interdite, terminaison de processus leaders, etc.), ou à la suite d’une action utilisateur (séquences de touches clavier particulières, déconnexion, commande shell kill).
Un processus peut également demander l’envoi d’un signal vers un ou plusieurs destinataires, y compris lui-même. Pour cela, il peut utiliser différents appels système, le principal étant l’appel kill().
1. Appel système kill()
Syntaxe
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
Arguments
pid |
Identifiant du ou des destinataires |
sig |
Numéro du signal à émettre |
Valeur retournée
-1 |
Erreur, code erreur positionné dans la variable errno |
0 |
Succès |
Description
Un processus privilégié peut envoyer un signal à n’importe quel processus, avec des restrictions concernant le processus de PID 1 (voir plus loin).
Un processus non privilégié peut envoyer un signal à tout processus ayant le même identifiant utilisateur effectif ou réel que lui. Il peut également envoyer le signal SIGCONT (sortie de suspension) aux processus membres de sa session.
Un processus peut s’envoyer un signal à lui-même.
Cet appel système permet d’envoyer n’importe quel numéro de signal.
Il faut être très prudent avec le signal SIGKILL, numéro 9, qui ne peut être ni ignoré, ni bloqué, ni traité et qui entraîne la terminaison du processus destinataire (à l’exception du processus de PID 1).
L’argument pid détermine le ou les destinataires du signal :
> 0 |
Processus ayant cet identifiant de processus |
<-1 |
Membres du groupe de processus ayant comme identifiant -pid |
0 |
Membres du groupe du processus émetteur |
-1 |
Tous les processus accessibles par le processus émetteur, sauf le processus de PID 1 |
L’argument sig contient le numéro du signal à émettre. S’il vaut zéro, le noyau n’émet aucun signal...
Traitement des signaux
Différents appels système permettent à un programme de modifier la gestion des signaux reçus.
L’appel système signal(), issu du système Unix d’origine, présente des limites et des différences d’implémentation entre systèmes de type Unix. Bien qu’existant dans Linux, cet appel système est en voie d’obsolescence. Il est donc conseillé d’utiliser plutôt son successeur, sigaction(), plus fiable et normalisé POSIX.
Nous présenterons les deux appels système, en commençant par signal(), que l’on peut rencontrer dans des programmes existants, et dont l’interface plus simple peut faciliter les premières expérimentations de traitement des signaux.
1. Appel système signal()
Syntaxe
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
Arguments
signum |
Numéro du signal à traiter |
handler |
Traitement du signal : SIG_IGN, SIG_DFL, adresse d’une fonction |
Valeur retournée
SIG_ERR |
Erreur, errno est positionnée |
!= SIG_ERR |
Traitement précédent du signal |
Description
L’appel système permet de définir quelle action associer à un signal reçu. Il permet d’ignorer le signal (SIG_IGN), de le réassocier à son action par défaut (SIG_DFL), ou de l’associer à une fonction gestionnaire de signal, dont on passe l’adresse.
Certains signaux ne peuvent pas être traités ou ignorés (le signal SIGKILL, numéro 9, en particulier).
La fonction gestionnaire de signal est décrite par le type sighandler_t : elle attend un entier comme paramètre (le numéro du signal) et elle ne retourne rien.
Une fois cet appel système exécuté, l’action associée est mise en œuvre chaque fois que le processus courant est destinataire d’un signal du numéro spécifié.
Cet appel système a été amélioré dans les versions récentes de Linux, pour contourner certaines de ses limitations historiques, mais son emploi reste déconseillé.
a. Description d’un signal : psignal()
Syntaxe
void psignal(int sig, const char *mess);
Description
Cette...
Signaux et démons
Un daemon (souvent traduit par démon) est un programme s’exécutant en tâche de fond, indépendamment de tout terminal, avec généralement une durée de vie importante. Ces programmes sont souvent lancés automatiquement, pendant la phase de démarrage du système.
Le terme DAEMON, démon en anglais, a été "justifié" a posteriori par l’invention d’un rétroacronyme : Disk And Execution MONitor (moniteur de disque et d’exécution).
Ces caractéristiques imposent au programmeur de prendre des précautions par rapport aux signaux susceptibles de terminer prématurément un daemon.
Comme un daemon s’exécute le plus souvent du démarrage jusqu’à l’arrêt du système, il doit être résistant aux changements pouvant survenir dans son environnement. Il doit être aussi susceptible de modifier sa configuration sans nécessiter d’être arrêté et redémarré. De plus, il doit prendre garde à gérer correctement les ressources qu’il alloue pour éviter des boucles de consommation, conduisant à des dépassements de limites (fuites mémoire, nombre de fichiers ouverts excessif, processus enfants zombies, etc.).
Concernant plus particulièrement la gestion des signaux, un daemon doit être implémenté de façon à ne pas être soumis aux signaux liés à la gestion des sessions et des groupes de processus. Il doit également mettre en place des gestionnaires...