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 PIDpid;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 (saufinit) ;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.
- 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.
- 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. - 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 devientnouv;SIG_BLOCK: le masque devient l'union du masque courant et denouv;SIG_UNBLOCK: les signaux denouvsont 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 (ouSIG_DFL,SIG_IGN)sa_sigaction: fonction de traitement alternative, recevant des informations sur le signal, utilisée sisa_flagscontientSA_SIGINFOsa_mask: ensemble de signaux supplémentaires à bloquer pendant le traitement (le signal traité est lui-même bloqué, sauf avecSA_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é.