Liste de  cours langage c

Programmation sous Linux en C ANSI documentation de cours


Télécharger



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 conten



u 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)

$

Les messages «Terminated» ou « Segmentation fault» sont affichés par le shell lorsqu'il se rend compte que son processus se termine anormalement.

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 sigsetjmp( ) est invoqué dans le programme, il mémorise dans le buffer transmis en premier argument le contexte d'exécution et renvoie 0. Si son second argument est non nul, il mémorise également le masque de blocage des signaux dans le premier argument.

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

Entrez le dividende p : (Contrôle-C)

$

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);

taille_lue = read (fd socket, buffer, taille_buffer);

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.

Cet exemple va servir à temporiser la saisie d'une valeur numérique depuis le clavier. Nous lirons une ligne complète, puis nous essayerons d'y trouver un nombre entier. En cas d'échec, nous recommencerons. Malgré tout, un délai maximal de 5 secondes est programmé, après lequel le programme abandonne.

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;

sigemptyset (& (action . sa_mask));

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 fonctionnalités temps-réel pour les systèmes Unix sont décrites par la norme Posix.1b, et leur support par Linux à partir du noyau 2.2 est une grosse évolution pour le champ des applications industrielles et scientifiques utilisables sur ce système d'exploitation.

Les signaux temps-réel présentent donc les caractéristiques suivantes par rapport aux signaux classiques :

  • nombre plus important de signaux utilisateur ;
  • empilement des occurrences des signaux bloqués ;
  • délivrance prioritaire des signaux ;
  • informations supplémentaires fournies au gestionnaire.

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)

On vérifiera également que le nombre de signaux temps-réel soit suffisant pour l'application. Toutefois, les valeurs SIGRTMIN et SIGRTMAX peuvent être implémentées sous forme de variables, et pas de constantes symboliques. Cette vérification doit donc avoir lieu durant l'exécution du programme, pas pendant sa compilation. On emploiera ainsi un code du genre :

#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.

Il existe bien entendu une limite au nombre de signaux pouvant être mémorisés simultanément. Cette limite n'est pas précisée par Posix.l b. Sous Linux 2.2, on peut empiler 1 024 signaux par processus, à moins que la mémoire disponible ne soit pas suffisante. L'appel-système sigqueue( ), que nous verrons plus bas et qui remplace kill( ) pour les signaux temps-réel, permet d'avoir la garantie que le signal est bien empilé.

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

Les signaux temps-réel sont capables de transmettre une — petite — quantité d'information au gestionnaire associé. Cette information est contenue dans une valeur de type union sigval . Cette union peut prendre deux formes :

  • un entier int, en employant son membre sigval_int ;
  • un pointeur void *, avec le membre sigvalptr.

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.

Il n'y a aucun moyen dans le gestionnaire de déterminer si l'argument de type union sigval a été rempli, lors de l'invocation de sigqueue( ) avec une valeur entière (champ sigval_int) ou un pointeur (champ sigval_ptr). Il est donc nécessaire que l'application reste cohérente entre l'envoi du signal et sa réception. Lorsque le signal est transmis entre deux processus distincts, on ne peut bien sûr passer de pointeurs que sur une zone de mémoire partagée.

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 :

  • Il est possible d'envoyer un signal temps-réel avec l'appel-système kill( ). Simplement, les informations supplémentaires ne seront pas disponibles. Leur valeur dans ce cas n'est pas précisée par Posix.1b, mais sous Linux, le champ de type sigval correspondant est mis à zéro. Il est donc possible d'employer les signaux temps-réel en remplacement pur et simple de SIGUSR1 et SIGUSR2 dans une application déjà existante, en profitant de l'empilement des signaux, mais en restant conscient du problème que nous avons évoqué, concernant la priorité de délivrance.
  • Il existe un certain nombre de sources de signaux temps-réel possibles, en supplément de la programmation manuelle avec sigqueue( ) ou kill( ). Plusieurs fonctionnalités introduites par la norme Posix.1b permettent en effet à l'application de programmer un travail et de recevoir un signal lorsqu'il est accompli. C'est le cas, par exemple, des files de messages utilisant les fonctions mq_open( ), mq_close( ), mq_notify( ), ou encore des temporisations programmées avec timer_create( ), timer_delete( ) et timer_settime( ).

Malheureusement, ces fonctionnalités temps-réel ne sont pas implémentées sous Linux et ne nous concernent donc pas pour le moment. Par contre, les entrées-sorties asynchrones permettent de programmer un message à recevoir quand l'opération désirée est terminée. Ces fonctions seront étudiées dans le chapitre 30.

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;

valeur_arrivee [nb_signaux] = info -> si_value . sival_int;

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.

133