Aller au contenu principal

Signaux

Les signaux sont un mécanisme asynchrone de communication inter-processus. Intuitivement, ils sont comparables à des sonneries, les différentes sonneries indiquant des événements différents. Les signaux sont envoyés à un ou plusieurs processus. Un signal est en général associé à un événement. Peu portables entre BSD et AT&T (System V), ils deviennent plus commodes à utiliser et portables avec la norme POSIX, qui utilise la notion d'ensemble de signaux et qui fournit un mécanisme de masquage automatique pendant les procédures de traitement (comme BSD). Un signal est envoyé à un processus en utilisant l'appel système :

#include <signal.h>
int kill(pid_t pid, int sig);

sig est un numéro de signal compris entre 1 et NSIG - 1 (on utilise en pratique les constantes symboliques SIGINT, SIGTERM, etc. définies dans <signal.h>) et pid désigne le processus destinataire. Le processus visé reçoit le signal sous forme d'un drapeau positionné dans son bloc de contrôle. Le processus est interrompu et réalise éventuellement un traitement de ce signal. On peut considérer les signaux comme des interruptions logicielles : ils interrompent le flot normal d'un processus, mais ne sont pas pris en compte immédiatement comme les interruptions matérielles. Le noyau délivre un signal pendant au moment où le processus visé repasse en mode utilisateur (retour d'un appel système ou d'une interruption, ou élection par l'ordonnanceur).

Provenance des signaux​

Certains signaux peuvent être envoyés à partir d'un terminal grâce aux caractères spéciaux comme intr ou quit (souvent Ctrl-C et Ctrl-\), dont la frappe est transformée en l'envoi des signaux SIGINT et SIGQUIT au groupe de processus de premier plan. D'autres sont dus à des causes internes au processus, par exemple SIGSEGV, qui est envoyé en cas d'erreur d'adressage, ou SIGFPE (Floating Point Exception), envoyé en cas d'erreur arithmétique, y compris une division entière par zéro. Enfin, certains sont dus à des événements comme la déconnexion de la ligne (le terminal) utilisée : le noyau envoie alors SIGHUP au processus contrôlant le terminal (le leader de session), et la terminaison du leader de session provoque l'envoi de SIGHUP aux processus du groupe de premier plan.

Gestion interne des signaux​

C'est dans le bloc de contrôle (BCP) de chaque processus que l'on trouve la table de gestion des signaux. Cette table contient, pour chaque signal défini sur la machine, une structure de la forme suivante (pseudo-code) :

{
bit pendant;
void (*traitement)(int);
}

En BSD et POSIX, on a un champ supplémentaire : bit masque;. Le drapeau pendant indique que le processus a reçu un signal, mais n'a pas encore eu l'occasion de le prendre en compte. Un seul bit étant disponible par signal, les signaux classiques ne sont pas mémorisés en plusieurs exemplaires : plusieurs occurrences d'un même signal pendant peuvent n'être délivrées qu'une fois.

L'envoi de signaux : la primitive kill​

Il y a NSIG signaux sur une machine, déclarés dans le fichier /usr/include/signal.h. La valeur de pid désigne le ou les destinataires :

  • pid > 0 : le processus de PID pid ;
  • pid == 0 : tous les processus du groupe de l'appelant ;
  • pid == -1 : tous les processus auxquels l'appelant a le droit d'envoyer un signal (sauf init) ;
  • pid < -1 : tous les processus du groupe -pid.

Le paramètre sig est interprété comme un signal s'il est compris entre 1 et NSIG - 1, ou comme une demande d'information si sig == 0 : aucun signal n'est envoyé, mais les vérifications d'existence du processus et de droits sont effectuées. Toute autre valeur est un paramètre erroné (EINVAL). La fonction raise(int sig) est un raccourci pour kill(getpid(), sig) dans un programme monothread : le processus s'envoie à lui-même un signal.

La gestion simplifiée avec la fonction signal​

La fonction signal permet de spécifier le comportement du processus à la réception d'un signal donné et renvoie le comportement précédent. Il faut lui donner en paramètre le numéro du signal sig que l'on veut détourner et la fonction de traitement à exécuter à la réception du signal, ou l'une des valeurs SIG_DFL (comportement par défaut) et SIG_IGN (signal ignoré).

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int sig, sighandler_t handler);

Les signaux SIGKILL et SIGSTOP ne peuvent être ni capturés, ni ignorés, ni bloqués.

Problèmes de la gestion de signaux AT&T​

Les phénomènes suivants sont décrits comme des problèmes ; la norme POSIX permet d'en conserver certains, mais fournit aussi les moyens de les éviter.

  1. Le comportement associé à un signal est repositionné à sa valeur par défaut au début de son traitement : un second signal arrivant avant la réinstallation du gestionnaire provoque le comportement par défaut.
  2. Certains appels système peuvent être interrompus ; dans ce cas, l'appel retourne -1 avec errno = EINTR. Il faudrait, pour réaliser correctement le modèle d'une interruption logicielle, relancer l'appel système en fin de traitement du signal.
  3. Si un signal est ignoré par un processus endormi, celui-ci sera réveillé par le système uniquement pour apprendre qu'il ignore le signal et doit donc être endormi de nouveau.

La sémantique de signal varie selon les systèmes ; sigaction est l'interface portable à privilégier.

La norme POSIX​

Avec POSIX, le comportement des appels système interrompus se spécifie dans la structure de traitement du signal : avec le drapeau SA_RESTART, les appels interrompus sont automatiquement relancés ; sans lui, ils échouent avec EINTR.

Les ensembles de signaux. La norme POSIX introduit les ensembles de signaux : ces ensembles permettent de dépasser la contrainte classique qui veut que le nombre de signaux soit inférieur ou égal au nombre de bits des entiers de la machine. D'autre part, des fonctions de manipulation de ces ensembles sont fournies et permettent de définir simplement des masques. Ces ensembles de signaux sont du type sigset_t et sont manipulés grâce aux fonctions suivantes :

int sigemptyset(sigset_t *ens);          /* ens = {} */
int sigfillset(sigset_t *ens); /* ens = {1, 2, ..., NSIG - 1} */
int sigaddset(sigset_t *ens, int sig); /* ens = ens + {sig} */
int sigdelset(sigset_t *ens, int sig); /* ens = ens - {sig} */
int sigismember(const sigset_t *ens, int sig); /* 1 si sig est dans ens, 0 sinon */

Ces fonctions retournent -1 en cas d'échec et 0 (ou le résultat du test pour sigismember) sinon.

Le blocage des signaux​

La fonction suivante permet de manipuler le masque des signaux bloqués du processus :

#include <signal.h>
int sigprocmask(int op, const sigset_t *nouv, sigset_t *anc);

L'opération op :

  • SIG_SETMASK : le masque devient nouv ;
  • SIG_BLOCK : le masque devient l'union du masque courant et de nouv ;
  • SIG_UNBLOCK : les signaux de nouv sont retirés du masque courant.

Si anc n'est pas NULL, l'ancien masque y est stocké. Un signal bloqué qui arrive reste pendant jusqu'à son déblocage. On peut connaître l'ensemble des signaux pendants (reçus mais non encore délivrés, car bloqués) grâce à la fonction int sigpending(sigset_t *ens);, qui retourne -1 en cas d'échec et 0 sinon ; l'ensemble des signaux pendants est stocké à l'adresse ens.

sigaction​

La structure sigaction décrit le comportement utilisé pour le traitement d'un signal :

struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
};
  • sa_handler : fonction de traitement (ou SIG_DFL, SIG_IGN)
  • sa_sigaction : fonction de traitement alternative, recevant des informations sur le signal, utilisée si sa_flags contient SA_SIGINFO
  • sa_mask : ensemble de signaux supplémentaires à bloquer pendant le traitement (le signal traité est lui-même bloqué, sauf avec SA_NODEFER)
  • sa_flags : différentes options (SA_RESTART, SA_SIGINFO, SA_NOCLDWAIT…)

Le positionnement du comportement de réception d'un signal se fait par la primitive sigaction. Un gestionnaire positionné par sigaction reste en place jusqu'à ce qu'un autre gestionnaire soit positionné, à la différence des versions AT&T où le comportement par défaut est repositionné automatiquement au début du traitement du signal. Sous System V, l'installation d'une fonction de traitement du signal SIGCLD alors que le processus a des fils zombies provoque l'envoi immédiat de ce signal au processus : c'est toujours le problème lié à ce signal, qui n'a pas le même comportement que les autres signaux.

#include <signal.h>
int sigaction(int sig,
const struct sigaction *paction,
struct sigaction *paction_precedente);

Si paction est NULL, l'appel est une simple demande d'information : on obtient dans paction_precedente la structure sigaction courante. Sinon, c'est une demande de modification du comportement (l'ancien comportement étant stocké dans paction_precedente si ce pointeur n'est pas NULL). sigaction retourne 0 en cas de succès et -1 en cas d'échec.

Un gestionnaire ne doit appeler que des fonctions async-signal-safe (write, _exit, etc. ; voir signal-safety(7)), à l'exclusion de printf ou malloc. Le plus sûr est de se contenter de positionner une variable de type volatile sig_atomic_t :

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

static volatile sig_atomic_t recu = 0;

static void traitement(int sig)
{
(void)sig;
recu = 1;
}

int main(void)
{
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_handler = traitement;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
while (!recu)
pause();
printf("SIGINT reçu\n");
return 0;
}

Dans cet exemple, un signal arrivant entre le test de recu et l'appel à pause ne réveillerait pas le processus : c'est le problème que résout sigsuspend.

L'attente d'un signal​

En plus de l'appel pause, on trouve sous POSIX l'appel int sigsuspend(const sigset_t *ens);, qui permet de réaliser de façon atomique les actions suivantes :

  • l'installation du masque de blocage défini par ens ;
  • la mise en attente de la réception d'un signal non bloqué.

Au retour (après exécution du gestionnaire), l'ancien masque est restauré ; sigsuspend retourne toujours -1 avec errno = EINTR. Le schéma classique consiste à bloquer le signal avec sigprocmask, tester la condition, puis appeler sigsuspend avec un masque où le signal est débloqué.