• Aucun résultat trouvé

Un signal particulier : l’alarme

Dans le document Programmation système en C sous Linux (Page 192-197)

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 alarm(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 ... */

SUSv3 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 de manière atomique 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 signaux 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 <stdio.h>

#include <stdlib.h>

#include <signal.h>

#include <setjmp.h>

#include <unistd.h>

sigjmp_buf contexte_sigalrm;

void

gestionnaire_sigalrm (int inutilise) {

siglongjmp(contexte_sigalrm, 1);

} int main (void) {

char ligne[80];

int i;

struct sigaction action;

action.sa_handler = gestionnaire_sigalrm;

action.sa_flags = 0;

sigfillset(& action.sa_mask);

sigaction(SIGALRM, & action, NULL);

fprintf(stdout, "Entrez un nombre entier avant 5 secondes : ");

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, "Ok !\n");

} else {

/* On est arrivé par SIGALRM */

fprintf(stdout, "\n Trop tard !\n");

exit(EXIT_FAILURE);

}

return EXIT_SUCCESS;

}

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ématiquement 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.sa_handler = gestionnaire_sigusr1;

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 Programming in the Unix Environment. La programmation portable en utilisant les fonctions Posix (SUSv3 à présent) est décrite dans [LEWINE 1994]

Posix Programmer’s Guide.

Le prochain chapitre sera consacré à un aspect plus moderne des signaux, introduits dans le noyau Linux depuis sa version 2.2 : les signaux temps-réel.

8

Dans le document Programmation système en C sous Linux (Page 192-197)