Programmation sous Linux en C ANSI documentation de cours
On remarquera que nous prenons soin de restituer l'ancien masque de blocage des signaux en sortie de routine, et qu'en transmettant cet ancien masque à sigsuspend( ) , nous permettons l'arrivée d'autres signaux que SIGUSR1.
Signalons qu'il existe un appel-système sigpause( ) obsolète, qui fonctionnait approximativement comme sigsuspend( ), mais en utilisant un masque de signaux contenu obligatoirement dans un entier de type int.
Écriture correcte d'un gestionnaire de signaux
En théorie, suivant le C Ansi, la seule chose qu'on puisse faire dans un gestionnaire de signaux est de modifier une ou plusieurs variables globales de type sig_atomic_t (défini dans ). Il s'agit d'un type entier – souvent un int d'ailleurs – que le processeur peut traiter de manière atomique, c'est-à-dire sans risque d'être interrompu par un signal. IL faut déclarer la variable globale avec l'indicateur « volatile» pour signaler au compilateur qu'elle peut être modifiée à tout moment, et pour qu'il ne se livre pas à des optimisations (par exemple en gardant la valeur dans un registre du processeur). Dans ce cas extrême, le gestionnaire ne fait que positionner l'état d'une variable globale, qui est ensuite consultée dans le corps du programme.
Nous avons vu qu'avec une gestion correcte des blocages des signaux, il est en fait possible d'accéder à n'importe quel type de données globales. Le même problème peut toutefois se présenter si un signal non bloqué arrive alors qu'on est déjà dans l'exécution du gestionnaire d'un autre signal. C'est à ce moment que le champ sa_mask de la structure sigaction prend tout son sens.
Une autre difficulté est de savoir si on peut invoquer, depuis un gestionnaire de signal, un appel-système ou une fonction de bibliothèque. Une grande partie des fonctions de bibliothèque ne sont pas réentrantes. Cela signifie qu'elles utilisent en interne des variables statiques ou des structures de données complexes, comme malloc( ), et qu'une fonction inter-rompue en cours de travail dans le corps principal du programme ne doit pas être rappelée depuis un gestionnaire de signal. Prenons l'exemple de la fonction ctime( ). Celle-ci prend en argument un pointeur sur une date du type time_t , et renvoie un pointeur sur une chaîne de caractères décrivant la date et l'heure. Cette chaîne est allouée de manière statique et est écrasée à chaque appel. Si elle est invoquée dans le corps du programme, interrompue et rappelée dans le gestionnaire de signal, au retour de ce dernier, la valeur renvoyée dans le corps du programme principal ne sera pas celle qui est attendue. Les fonctions de bibliothèque qui utilisent des variables statiques le mentionnent dans leurs pages de manuel. Il est donc nécessaire de les consulter avant d'introduire la fonction dans un gestionnaire.
Il est important également d'éviter résolument les fonctions qui font appel indirectement à malloc( ) ou à free( ), comme tempnam( ).
IL existe une liste minimale, définie par Posix.1, des appels-système réentrants qui pourront donc être invoqués depuis un gestionnaire. On notera que le fait d'être réentrante permet à une fonction d'être utilisable sans danger dans un programme multithread, mais que la réciproque n'est pas toujours vraie, comme on le voit avec malloc( ) qui est correct pour les programmes multithreads mais ne doit pas être invoqué dans un gestionnaire de signaux.
_exit
access, alarm
cfgetispeed, cfgetospeed, cfsetispeed, cfsetospeed, chdir, chmod, chown, close, creat dup, dup2
execle, execve
fcntl, fork, fstat
getegid, geteuid, getgroups, getpgrp, getpid, getppid,
kill
link, lseek
mkdir, mkfifo,
open
pathconf, pause, pipe
read, rename, rmdir
setgid, setpgid, set sid, setuid „ sigaction, sigaddset, sigemptyset,
sigdelset,
sigemptyset, sigfillset, sigismember, sigpending, sigsuspend, sleep stat,
sysconf tcdrain, tcflow, tcflush, tcgetattr, tcgetpgrp, tcsendbreak,
tcsetattr, tcsetpgrp,
time, times
umask, uname, unlink, utime
wait, waitpid, write
Les fonctions d'entrée-sortie sur des flux, fprintf( ) par exemple, ne doivent pas être utilisées sur le même flux entre le programme principal et un gestionnaire, à cause du risque important de mélange anarchique des données. Par contre, il est tout à fait possible de réserver un flux de données pour le gestionnaire (stderr par exemple), ou de l'employer si on est sûr que le programme principal ne l'utilise pas au même moment.
Il est très important qu'un gestionnaire de signal employant le moindre appel-système sauve-garde le contenu de la variable globale errno en entrée du gestionnaire et qu'il la restitue en sortie. Cette variable est en effet modifiée par la plupart des fonctions système, et le signal peut très bien s'être déclenché au moment où le programme principal terminait un appel-système et se préparait à consulter errno.
Notons, pour terminer, que dans les programmes s'appuyant sur l'environnement graphique X11, il ne faut en aucun cas utiliser les routines graphiques (Xlib, Xt, Motif...), qui ne sont pas réentrantes. Il faut alors utiliser des variables globales comme indicateurs des actions à exécuter dans le corps même du programme.
Il peut arriver que le travail du gestionnaire soit d'effectuer simplement un peu de nettoyage avant de terminer le processus. L'arrêt peut se faire avec l'appel-système _exit( ) ou exit( ). Néanmoins, il est souvent préférable que le processus père sache que son fils a été tué par un signal et qu'il ne s'est pas terminé normalement. Pour cela, il faut reprogrammer le comportement original du signal et se l'envoyer à nouveau. Bien sûr, cela ne fonctionne qu'avec des signaux qui terminent par défaut le processus (comme SIGTERM). De plus, dans certains cas (comme SIGSEGV), un fichier d'image mémoire core sera créé.
exemple_fatal.c
#include #include #include #include
void
gestionnaire_signal_fatal (int numero)
{
/* Effectuer le nettoyage : */
/* Couper proprement les connexions réseau */
/* Supprimer les fichiers de verrouillage */
/* Tuer éventuellement les processus fils */ fprintf (stdout, "\n 7e fais le ménage !\n"); fflush (stdout);
signal (numero, SIG_DFL);
raise (numero);
}
int
main( )
{
fprintf (stdout, "mon pid est %u\n", getpid( ));
signal (SIGTERM, gestionnaire_signal_fatal);
signal (SIGSEGV, gestionnaire_signal_fatal);
while (1)
pause ( ) ;
return (0);
}
Voici un exemple d'exécution. On envoie les signaux depuis une autre console. Le shell (bash en l'occurrence) nous indique que les processus ont été tués par des signaux.
$ ./exemple_fatal
mon pid est 6032
$ kill -TERM 6032
7e fais le ménage !
Terminated
$ ./exemple fatal
mon pid est 6033
$ kill -SEGV 6033
7e fais le ménage !,
Segmentation fault (core dumped)
$
Il y a ici un point de sécurité à noter : certains programmes, souvent Set-UID root, disposent temporairement en mémoire de données que même l'utilisateur qui les a lancés ne doit pas connaître. Cela peut concerner par exemple le fichier «shadow» des mots de passe ou les informations d'authentification servant à établir la liaison avec un fournisseur d'accès Internet. Dans ce genre d'application, il est important que le programme écrase ces données sensibles avant de laisser le gestionnaire par défaut créer une éventuelle image mémoire core qu'on pourrait examiner par la suite.
Utilisation d'un saut non local
Une troisième manière de terminer un gestionnaire de signaux est d'utiliser un saut non local siglongjmp( ). Dans ce cas, l'exécution reprend dans un contexte différent, qui a été sauve-gardé auparavant. On évite ainsi certains risques de bogues dus à l'arrivée intempestive de signaux, tels que nous en utiliserons pour SIGALRM à la fin de ce chapitre. De même, cette méthode permet de reprendre le contrôle d'un programme qui a, par exemple, reçu un signal indiquant une instruction illégale. Posix précise que le comportement d'un programme qui
ignore les signaux d'erreur du type SIGFPE, SIGILL, SIGSEGV est indéfini. Nous avons vu que certaines de ces erreurs peuvent se produire à la suite de débordements de pile ou de mauvaises saisies de l'utilisateur dans le cas des routines mathématiques. Certaines applications désirent rester insensibles à ces erreurs et reprendre leur exécution comme si de rien n'était. C'est possible grâce à l'emploi de sigsetjmp( ) et siglongjmp( ). Ces deux appels-système sont des extensions des anciens setjmp( ) et longjmp( ), qui posaient des problèmes avec les gestionnaires de signaux.
L'appel-système sigsetjmp( ) ale prototype suivant, déclaré dans : int sigsetjmp (sigjmp_buf contexte, int sauver_signaux);
Lorsque le programme rencontre l'appel-système siglongjmp( ), dont le prototype est : void siglongjmp (sigjmp_buf contexte, int valeur);
l'exécution reprend exactement à l'emplacement du sigsetjmp( ) correspondant au même buffer, et celui-ci renvoie alors la valeur indiquée en second argument de siglongjmp( ). Cette valeur permet de différencier la provenance du saut, par exemple depuis plusieurs gestionnaires de signaux d'erreur.
L'inconvénient des sauts non locaux est qu'un usage trop fréquent diminue sensiblement la lisibilité des programmes. Il est conseillé de les réserver toujours au même type de circonstances dans une application donnée, pour gérer par exemple des temporisations, comme nous le verrons ultérieurement avec le signal SIGALRM.
Nous allons pour l'instant créer un programme qui permette à l'utilisateur de saisir deux valeurs numériques entières, et qui les divise l'une par l'autre. Si un signal SIGFPE se produit (on a demandé une division par zéro), l'exécution reprendra quand même dans un contexte propre.
exemple_siglongjmp.c :
#include #include #include #include
sigjmp_buf contexte;
void
gestionnaire_sigfpe (int numero)
{
siglongjmp (contexte, 1);
/* Si on est ici le saut a raté, il faut quitter */
signal (numero, SIG_DFL);
raise (numero);
}
int
main (void) {
int p, q, r;
signal (SIGFPE, gestionnaire_sigfpe);
while (1) {
if (sigsetjmp (contexte, 1) != 0) {
/* On est arrivé ici par siglongjmp( ) */
fprintf (stdout, "Aie ! erreur mathématique ! \n");
fflush (stdout);
}
r = p / q ;
fprintf (stdout, "rapport p / q = %d\n", r);
}
return (0);
}
Un petit exemple d'exécution :
$ ./exemple_siglongjmp
Entrez le dividende p : 8
Entrez le diviseur q : 2
rapport p / q = 4
Entrez le dividende p : 6
Entrez le diviseur q : 0
Aie 1 erreur mathématique !
Entrez le dividende p : 6
Entrez le diviseur q : 3
rapport p / q = 2
$
Ce genre de technique est surtout utilisée dans les interpréteurs de langages comme Lisp pour permettre de revenir à une boucle principale en cas d'erreur.
Les anciens appels-système setjmp( ) et longjmp( ) fonctionnaient de la même manière, mais ne sauvegardaient pas le masque des signaux bloqués (comme si le second argument de siglongjmp( ) valait 0). Le masque retrouvé dans le corps du programme n'est donc pas nécessairement celui qui est attendu ; en effet, au sein d'un gestionnaire, le noyau bloque le signal concerné, ce qui n'est sûrement pas ce qu'on désire dans la boucle principale du programme.
Un signal particulier : l'alarme
Le signal SIGALRM est souvent utilisé comme temporisation pour indiquer un délai maximal d'attente pour des appels-système susceptibles de bloquer. On utilise l'appel-système alarm( ) pour programmer une temporisation avant la routine concernée, et SIGALRM sera déclenché lorsque le délai sera écoulé, faisant échouer l'appel bloquant avec le code EINTR dans errno. Si la routine se termine normalement avant le délai maximal, on annule la temporisation avec alaram(0).
Il y a de nombreuses manières de programmer des temporisations, mais peu sont tout à fait fiables. On considérera que l'appel-système à surveiller est une lecture depuis une socket réseau.
Il est évident que SIGALRM doit être intercepté par un gestionnaire installé avec sigaction( ) sans l'option RESTART dans sa_flags (sinon l'appel bloqué redémarrerait automatiquement). Ce gestionnaire peut être vide, son seul rôle est d'interrompre l'appel-système lent.
void
gestionnaire_sigalrm (int inutilise) { /* ne fait rien */
}
L'installation en est faite ainsi : struct sigaction action;
sigemptyset (& (action . sa_mask)); action . sa_flags = 0;
action . sa_handler = gestionnaire_sigalrm;
sigaction (SIGALRM, action, NULL);
Nous allons commencer par cet exemple naïf :
alarm (delai_maximal);
alarm (0);
if ((taille lue != taille_buffer) && (errno == EINTR))
fprintf (stderr, "délai maximal écoulé \n");
return (-1);
}
/* ... suite ... */
Posix autorisant l'appel-système read( ) à renvoyer soit -1, soit le nombre d'octets lus lors d'une interruption par un signal, nous comparerons sa valeur de retour avec la taille attendue et non avec -1. Cela améliore la portabilité de notre programme.
Le premier problème qui se pose est qu'un signal autre que l'alarme peut avoir interrompu l'appel-système read( ). Cela peut se résoudre en imposant que tous les autres signaux gérés par le programme aient l'attribut SA_RESTART validé pour faire redémarrer l'appel bloquant. Toutefois, un problème subsiste, car le redémarrage n'a généralement lieu que si read( ) n'a pu lire aucun octet avant l'arrivée du signal. Sinon, l'appel se termine quand même en renvoyant le nombre d'octets lus.
Le second problème est que, sur un système très chargé, le délai peut s'écouler entièrement entre la programmation de la temporisation et l'appel-système lui-même. Il pourrait alors rester bloqué indéfiniment.
Ce qu'on aimerait, c'est disposer d'un équivalent à sigsuspend( ), qui permette d'effectuer atomiquement le déblocage d'un signal et d'un appel-système. Malheureusement, cela n'existe pas.
Nous allons donc utiliser une autre méthode, plus complexe. utilisant les sauts non locaux depuis le gestionnaire. Quel que soit le moment où le signal se déclenche, nous reviendrons au même emplacement du programme et nous annulerons alors la lecture. Bien entendu, le gestionnaire de signal doit être modifié. Il n'a plus à être installé sans l'option SA_RESTART puisqu'il ne se terminera pas normalement.
exemple_alarm.c
#include #include #include #include #include
sigjmp_buf contexte_sigalrm;
void
gestionnaire_sigalrm (int inutilise) {
} siglongjmp (contexte_sigalrm, 1);
int
main (void)
{
char ligne [80];
int i;
fprintf (stdout, "Entrez un nombre entier avant 5 secondes : "); signal (SIGALRM, gestionnaire_sigalrm);
if (sigsetjmp (contexte_sigalrm, 1) == 0) { /* premier passage, installation */ alarm (5);
/* Lecture et analyse de la ligne saisie */
while (1) {
if (fgets (ligne, 79, stdin) != NULL)
if (sscanf (ligne, "%d", & i) = 1)
break;
fprintf (stdout, "Un entier svp : ");
}
/* Ok - La ligne est bonne */
alarm (0);
fprintf (stdout, "0k !\n");
}else {
/* On est arrivé par SIGALRM */
fprintf (stdout, "\n Trop tard !\n");
exit (1);
}
return (0);
}
Voici quelques exemples d'exécution : $ ./exemple_alarm
Entrez un nombre entier avant 5 secondes : 6
Ok !
$ ./exemple_alarm
Entrez un nombre entier avant 5 secondes : a
Un entier svp : z
Un entier svp : e
Un entier svp : 8
Ok !
$ ./exemple_alarm
Entrez un nombre entier avant 5 secondes :
Trop tard ! $
Nous avons ici un exemple de gestion de délai fiable, fonctionnant avec n'importe quelle fonction de bibliothèque ou d'appel-système risquant de rester bloqué indéfiniment. Le seul inconvénient de ce programme est le risque que le signal SIGALRM se déclenche alors que le processus est en train d'exécuter le gestionnaire d'un autre signal (par exemple SIGUSR1). Dans ce cas, on ne revient pas au gestionnaire interrompu et ce signal est perdu.
La seule possibilité pour l'éviter est d'ajouter systématigpement SIGALRM dans l'ensemble des signaux bloqués lors de l'exécution des autres gestionnaires, c'est-à-dire en l'insérant dans chaque champ sa_mask des signaux interceptés :
struct sigaction action;
action . sahandler = gestionnaire_sigusrl; action . sa_flags = SA_RESTART;
sigaddset (& (action . sa_mask), SIGALARM); sigaction (SIGUSR1, & action, NULL);
Le signal SIGALRM n'interrompra alors jamais l'exécution complète du gestionnaire SIGUSR1.
Conclusion
Nous avons étudié dans les deux derniers chapitres l'essentiel de la programmation habituelle concernant les signaux. Certaines confusions interviennent parfois à cause d'appels-système obsolètes, qu'on risque néanmoins de rencontrer encore dans certaines applications.
Des précisions concernant le comportement des signaux sur d'autres systèmes sont disponibles dans [STEVENS 1993] Advanced Progrannning in the Unix Envirornnent. Le comportement des signaux Posix est décrit également en détail dans [LEWINE 1994] Posix Programmer 's Guide.
Le prochain chapitre sera consacré à un aspect plus moderne des signaux, qui n'a été introduit que récemment dans le noyau Linux : les signaux temps-réel Posix. lb.
Signaux temps-réel
Posix. lb
Avec Linux 2.2 est apparue la gestion des signaux temps-réel. Ceux-ci constituent une extension des signaux SIGUSR1 et SIGUSR2, qui présentaient trop de limitations pour des applications temps-réel. Il faut entendre, par le terme temps-réel, une classe de programmes pour lesquels le temps mis pour effectuer une tâche constitue un facteur important du résultat. Une application temps-réel n'a pas forcément besoin d'être très rapide ni de répondre dans des délais très brefs, mais simplement de respecter des limites temporelles connues.
Ceci est bien entendu contraire à tout fonctionnement multitâche préemptif, puisque aucune garantie de temps de réponse n'est fournie par le noyau. Nous verrons alors qu'il est possible de commuter l'ordonnancement des processus pour obtenir un séquencement beaucoup plus proche d'un véritable support temps-réel. Nous reviendrons sur ces notions dans le chapitre 11.
Les signaux temps-réel présentent donc les caractéristiques suivantes par rapport aux signaux classiques :
Caractéristiques des signaux temps-réel Nombre de signaux temps-réel
Nous avions déjà remarqué que le fait de ne disposer que de deux signaux réservés au programmeur était une contrainte importante pour le développement d'applications utilisant beaucoup cette méthode de communication.
La norme Posix.1b réclame la présence d'au moins huit signaux temps-réel. Linux en propose trente-deux, ce qui est largement suffisant pour la plupart des situations.
Les signaux temps-réel n'ont pas de noms spécifiques, contrairement aux signaux classiques. On peut employer directement leurs numéros, qui s'étendent de SIGRTMIN à SIGRTMAX compris. Bien entendu, on utilisera des positions relatives dans cet intervalle, par exemple
(SIGRTMIN + 5) ou (SIGRTMAX - 2), sans jamais préjuger de la valeur effective de ces constantes.
Il est de surcroît conseillé, pour améliorer la qualité du code source, de définir des constantes symboliques pour nommer les signaux utilisés dans le code. Par exemple. on définira dans un fichier d'en-tête de l'application des constantes :
#define SIGRT0 (SIGRTMIN)
#define SIGRT1 (SIGRTMIN + 1)
#define SIGRT2 (SIGRTMIN + 2)
ou, encore mieux, des constantes dont les noms soient parlants :
#define SIG_AUTOMATE_PRET (SIGRTMIN + 2) #define SIG_ANTENNE_AU_NORD (SIGRTMIN + 4) #define SIG_LIAISON_ETABLIE (SIGRTMIN + 1)
#include #include
#ifndef _POSIX_REALTIME_SIGNALS
#error "Pas de signaux temps-réel disponibles" #endif
#define SIGRTO (SIGRTMIN) [...]
#define SIGRT10 (SIGRTMIN + 10)
#define NB_SIGRT_UTILES 11
int
main (int argc, char ** argv []) {
if ((SIGRTMAX - SIGRTMIN + 1)< NBSIGRT_UTILES) {
fprintf (stderr, "Pas assez de signaux temps-réel \n"); exit (1);
}
[...]
}
Empilement des signaux bloqués
Nous avons vu que les signaux classiques ne sont pas empilés. Cela signifie que si deux occurrences d'un même signal arrivent alors que celui-ci est temporairement bloqué, une seule d'entre elles sera finalement délivrée au processus lors du déblocage. Rappelons que le blocage n'intervient pas nécessairement de manière explicite, mais peut aussi se produire simplement durant l'exécution du gestionnaire d'un autre signal.
Lorsqu'on veut s'assurer qu'un signal arrivera effectivement à un processus, il faut mettre au point un système d'acquittement, compliquant sérieusement le code.
Comme un signal est automatiquement bloqué durant l'exécution de son propre gestionnaire, une succession à court intervalle de trois occurrences consécutives du même signal risque de faire disparaître la troisième impulsion. Ce comportement n'est pas acceptable dès qu'un processus doit assurer des comptages ou des commutations d'état.
Pour pallier ce problème, la norme Posix.lb a introduit la notion d'empilement des signaux bloqués. Si un signal bloqué est reçu quatre fois au niveau d'un processus, nous sommes sûr qu'il sera délivré quatre fois lors de son déblocage.
Délivrance prioritaire des signaux
Lorsque le noyau a le choix entre plusieurs signaux temps-réel à transmettre au processus (par exemple lors d'un déblocage d'un ensemble complet), il délivre toujours les signaux de plus faible numéro en premier.
Les occurrences de SIGRTMIN seront donc toujours transmises en premier au processus, et celles de SIGRTMAX en dernier. Cela permet de gérer des priorités entre les événements représentés par les signaux. Par contre, Posix.1b ne donne aucune indication sur les priorités des signaux classiques. En général. ils sont délivrés avant les signaux temps-réel car ils indiquent pour le plupart des dysfonctionnements à traiter en urgence (SIGSEGV, SIGILL, SIGHUP...), mais nous n'avons aucune garantie concernant ce comportement.
La notion de priorité entre signaux peut néanmoins présenter un inconvénient si on n'y prend pas garde. Le revers de la médaille, c'est que les signaux ne sont plus indépendants, comme l'étaient SIGUSR1 et SIGUSR2, par exemple. On pourrait vouloir utiliser deux signaux temps-réel pour implémenter un mécanisme de bascule, un signal (disons SIGRTMIN+1) demandant le passage à l'état 1, et l'autre (SIGRTMIN+2) la descente au niveau 0. On aurait alors une séquence représentée sur la figure suivante :
Malheureusement, si les signaux sont bloqués pendant un moment, ils ne seront pas délivrés dans l'ordre d'arrivée, mais en fonction de leur priorité. Toutes les impulsions SIGRTMIN+1 sont délivrées d'abord, puis toutes les impulsions SIGRTMIN+2.
Figure 8.2 Séquence obtenue valeur
SIGRTMIN+1 SIGRTMIN+2 SIGRTMIN+1 SIGRTMIN+2 SIGRTMIN+1
Si des événements liés doivent être transmis à l'aide des signaux temps-réel, il faut se tourner vers une autre méthode, en utilisant un seul signal, mais en transmettant une information avec le signal lui-même.
Informations supplémentaires fournies au gestionnaire
Nous avons déjà évoqué la forme du gestionnaire de signal temps-réel dans le chapitre précédent, dans le paragraphe traitant de l'attribut SA_SIGINFO dans le champ sa_flags de sigaction.
void gestionnaire_signal (int numero,
struct siginfo * info, void * inutile);
...
Le troisième argument de cette routine n'est pas défini de manière portable. Certains systèmes Unix l'utilisent, mais apparemment le noyau Linux n'en fait pas usage. Toutes les informations supplémentaires se trouvent dans la structure siginfo sur laquelle un pointeur est transmis en deuxième argument.
Pour que ce gestionnaire soit installé, il faut le placer dans le membre sa_sigaction de la structure sigaction, et non plus dans le membre sa_handler. De même, le champ sa_fiags doit contenir l'attribut SA_SIGINFO.
L'initialisation se fait donc ainsi : struct sigaction action;
action . sa_sigaction = gestionnaire_signal_temps_reel;
sigemptyset (& action . sa_mask);
action . sa_flags = SA_SIGINFO;
if (sigaction (SIGRTMIN + 1, & action, NULL) < 0) {
perror ("sigaction");
exit (1);
}
Émission d'un signal temps-réel
Bien sûr, si on désire transmettre des données supplémentaires au gestionnaire de signal, il ne suffit plus d'employer la fonction kill( ) habituelle. Il existe un nouvel appel-système, nommé sigqueue( ), défini par Posix.1b :
int sigqueue (pid_t pid, int numero, const union sigval valeur)
Les deux premiers arguments sont équivalents à ceux de kill( ), mais le troisième correspond au membre si_sigval de la structure siginfo transmise au gestionnaire de signal.
Récapitulons les principaux champs de la structure siginfo reçue par le gestionnaire de signal :
Nom membre Type Posix.1b Signification
si_signo int • Numéro du signal, redondant avec le premier argument de l'appel du gestionnaire.
si_code int • Voir ci-dessous.
si_value.sigval_int int • Entier de l'union passée en dernier argument de sigqueue( ) .
si_value.sigval_ptr void * • Pointeur de l'union passée en dernier argument de
sigqueue( ). Ne doit pas être employé simultanément avec le membre précédent.
si_errno int Valeur de la variable globale errno lors du déclenchement du gestionnaire. Permet de rétablir cette valeur en sortie.
...
remarquons plusieurs choses :
Nous allons commencer par créer un programme servant de frontal à sigqueue( ), comme l'utilitaire système /bin/kill pouvait nous servir à invoquer l'appel-système kill( ) depuis la ligne de commande.
exemple_sigqueue.c :
#include #include #include #include
void
syntaxe (const char * nom)
{
fprintf (stderr, "syntaxe %s signal pid...\n", nom);
exit (1);
}
int
main (int argc, char * argv [])
{
int i;
int numero;
pid_t pid;
union sigval valeur;
if (argc == 1)
syntaxe(argv [0]; i = 1;
if (argc == 2) {
numero = SIGTERM; } else {
Finalement, nous lançons le programme exemple_siginfo, puis nous lui envoyons des signaux depuis une autre console (représentée en seconde colonne), en utilisant tantôt /bin/kill , tantôt exemple_sigqueue.
$ ./exemple_siginfo
PID=1069
9 non intercepté
19 non intercepté
$ kill -33 1069
Reçu 33
si_code = 0
$ ./exemple_sigqueue 33 1069
Reçu 33
si_code = -1
$ kill -TERM 1069
Reçu 33
si_code = 0
$ kill -KILL 1069
killed
$
Le champ si_code correspond bien à 0 (valeur de SI_USER) ou à -1 (valeur de SI_QUEUE) suivant le cas.
ATTENTION Si on utilise l'appel-système alarm( )pour déclencher SIGALRM, le champ si_code est rempli avec la valeur SI_USER et pas avec SI_TIMER, qui est réservée aux temporisations temps-réel.
Notre second exemple va mettre en évidence à la fois l'empilement des signaux temps-réel et leur respect d'une priorité. Notre programme va en effet bloquer tous les signaux, s'en envoyer une certaine quantité, et voir dans quel ordre ils arrivent. La valeur sigval associée aux signaux permettra de les reconnaître.
exemple_sigqueue_1.c
#include #include #include
int signaux_arrives [10]; int valeur_arrivee [10]; int nb_signaux = 0;
void
gestionnaire_signal_temps_reel (int numero,
siginfo_t * info, void * inutile)
{
signaux_arrives [nb_signaux] = numero - SIGRTMIN;
nb_signaux ++;
}
...
Nous remarquons bien que les signaux sont délivrés suivant leur priorité : tous les SIRTMIN+1 en premier, suivis des SIGRTMIN+2, puis des SIGRTMIN+3. De même, au sein de chaque classe, les occurrences des signaux sont bien empilées et délivrées dans l'ordre chronologique d'émission.
Traitement rapide des signaux temps-réel
La norme Posix.1b donne accès à des possibilités de traitement rapide des signaux. Ceci ne concerne que les applications qui attendent passivement l'arrivée d'un signal pour agir. Cette situation est assez courante lorsqu'on utilise les signaux comme une méthode pour implémenter un comportement multitâche au niveau applicatif.