• Aucun résultat trouvé

Réception des signaux avec l'appel-système signal( )

Dans le document Programmation système en C sous (Page 76-82)

Un processus peut demander au noyau d'installer un gestionnaire pour un signal particulier, c'est-à-dire une routine spécifique qui sera invoquée lors de l'arrivée de ce signal. Le processus peut aussi vouloir que le signal soit ignoré lorsqu'il arrive, ou laisser le noyau appliquer le comportement par défaut (souvent une terminaison du programme).

Pour indiquer son choix au noyau, il y a deux possibilités :

L'appel-système signal( ), défini par Ansi C et Posix.1, présente l'avantage d'être très simple (on installe un gestionnaire en une seule ligne de code), mais il peut parfois poser des problèmes de fiabilité de délivrance des signaux et de compatibilité entre les divers systèmes Unix.

L'appel-système sigaction( ) est légèrement plus complexe puisqu'il implique le remplissage d'une structure, mais il permet de définir précisément le comportement désiré pour le gestionnaire, sans ambiguïté suivant les systèmes puisqu'il est complètement défini par Posix.1.

Nous allons tout d'abord voir la syntaxe et l'utilisation de signal( ) , car il est souvent employé, puis nous étudierons dans le prochain chapitre sigagacion( ), qui est généralement plus adéquat pour contrôler finement le comportement d'un programme.

Notons au passage l'existence d'une ancienne fonction. sigvec( ), obsolète de nos jours et approximativement équivalente à sigaction( ).

L'appel-système signal( ) présente un prototype qui surprend toujours au premier coup d'oeil, alors qu'il est extrêmement simple en réalité :

void (*signal (int numero_signal, void (*gestionnaire) (int))) (int);

Il suffit en fait de le décomposer, en créant un type intermédiaire correspondant à un pointeur sur une routine de gestion de signaux :

typedef void (*gestion_t)(int);

et le prototype de signal( ) devient :

gestion_t signal (int numero_signal, gestion_t gestionnaire);

En d'autres termes, signal( ) prend en premier argument un numéro de signa]. Bien entendu, il faut utiliser la constante symbolique correspondant au nom du signal, jamais la valeur numérique directe. Le second argument est un pointeur sur la routine qu'on désire installer comme gestionnaire de signal. L'appel-système nous renvoie un pointeur sur l'ancien gestionnaire, ce qui permet de le sauvegarder pour éventuellement le réinstaller plus tard.

Il existe deux constantes symboliques qui peuvent remplacer le pointeur sur un gestionnaire, SIG_IGN et SIG_DFL, qui sont définies dans <signal.h>.

La constante SIGIGN demande au noyau d'ignorer le signal indiqué. Par exemple l'appel-système signal(SIGCHLD, SIG_IGN) — déconseillé par Posix — a ainsi pour effet sous Linux d'éliminer directement les processus fils qui se terminent, sans les laisser à l'état zombie.

Avec la constante SIG_DFL, on demande au noyau de réinstaller le comportement par défaut pour le signal considéré. Nous avons vu l'essentiel des actions par défaut. Elles sont également documentées dans la page de manuel signal (7).

Si l'appel-système signal( ) échoue, il renvoie une valeur particulière, elle aussi définie dans <signal.h> : SIG_ERR.

L'erreur positionnée dans errno est alors généralement EINVAL, qui indique un numéro de signal inexistant. Si on essaie d'ignorer ou d'installer un gestionnaire pour les signaux SIGKILL ou SIGSTOP, l'opération n'a pas lieu. La documentation de la fonction signal( ) de GlibC indique que la modification est silencieusement ignorée, niais en réalité l'appel-système sigagacion( ) - interne au noyau —. sur lequel cette fonction est bâtie, renvoie EINVAL dans errno dans ce cas.

L'erreur EFAULT peut aussi être renvoyée dans errno si le pointeur de gestionnaire de signal n'est pas valide.

Un gestionnaire de signal est une routine comme les autres, qui prend un argument de type entier et qui ne renvoie rien. L'argument transmis correspond au numéro du signal ayant déclenché le gestionnaire. Il est donc possible d'écrire un unique gestionnaire pour plusieurs signaux, en répartissant les actions à l'aide d'une construction switch-case.

Il arrive que le gestionnaire de signal puisse recevoir d'autres informations dans une structure transmise en argument supplémentaire (comme le PID du processus ayant envoyé le signal). Ce n'était pas le cas sous Linux 2.0. Par contre, cette fonctionnalité est disponible depuis Linux 2.2. Pour cela, il faut installer nécessairement le gestionnaire avec l'appel-système sigaction( ) que nous verrons plus bas.

Le gestionnaire de signal étant une routine sans spécificité, il est possible de l'invoquer directement dans le corps du programme si le besoin s'en fait sentir.

Nous allons pouvoir installer notre premier gestionnaire de signal. Nous allons tenter de capturer tous les signaux. Bien entendu, signal( ) échouera pour SIGKILL et SIGSTOP.

Pour tous les autres signaux, notre programme affichera le PID du processus en cours, suivi du numéro de signal et de son nom. Il faudra disposer d'une seconde console (ou d'un autre Xterm) pour pouvoir tuer le processus à la fin.

exemple_signal.c

#include <sgacdio.h>

#include <stdlib.h>

#include <unistd.h>

void

gesgationnaire (int numero_signal) {

fprintf (stdout, "\n %u a reçu le signal %d (%s)\n",

getpid( ), numero_signal, sys_siglisgac[numero_signal]);

} int main (void) {

for (i = 1; i < NSIG; i++)

if (signal (i, gesgationnaire) == SIG_ERR)

fprintf (stderr, "Signal %d non capturé \n", i);

while (1) { pause( );

}

return (0);

}

Voici un exemple d'exécution avec, en seconde colonne, l'action effectuée sur un autre terminal :

$ ./exemple_signal Signal 9 non capturé Signal 19 non capturé (Contrôle-C)

6240 a reçu le signal 2 (Interrupt) (Contrôle-Z)

6240 a reçu le signal 20 (Stopped) (Contrôle-\)

6240 a reçu le signal 3 (Quit)

$ kill -TERM 6240 6240 a reçu le signal 15 (Terminated) $ kill -KILL 6240 Killed

$

On appuie sur les touches de contrôle sur la console du processus exemple_signal, alors que les ordres Kill sont envoyés depuis une autre console. Le signal 9 non capturé correspond à SIGKILL, et le 19 à SIGSTOP.

Ce programme a également un comportement intéressant vis-à-vis du signal SIGSTOP, qui le stoppe temporairement. Le shell reprend alors la main. Nous pouvons toutefois ramener le processus en avant-plan, ce qui lui transmet le signal SIGCONT :

$ ./exemple_signal Signal 9 non capturé Signal 19 non capturé (Contrôle-0)

6241 a reçu le signal 2 (Interrupt) $ kill -STOP 6241

[1]+ Stopped (signal) ./exemple_signal

$ ps 6241

PID TTY STAT TIME COMMAND

6241 p5 T 0:00 ./exemple_signal

$ fg

./exemple_signal

6241 a reçu le signal 18 (Continued) (Contrôle-\)

6241 a reçu le signal 3 (Quit) $ kill -KILL 6241 Killed

$

Le champ STAT de la commande ps contient T, ce qui correspond à un processus stoppé ou suivi (traced).

Il faut savoir que sous Linux, la constante symbolique SIG_DFL est définie comme valant 0 (c'est souvent le cas, même sur d'autres systèmes Unix). Lors de la première installation d'un gestionnaire, l'appel-système signal( ) renvoie donc, la plupart du temps, cette valeur (à moins que le shell n'ait modifié le comportement des signaux auparavant). Il y a là un risque d'erreur pour le programmeur distrait qui peut écrire machinalement : if (signal (...) != 0)

/* erreur */

comme on a l'habitude de le faire pour d'autres appels-système. Ce code fonctionnera à la première invocation, mais échouera par la suite puisque signal( ) renvoie l'adresse de l'ancien gestionnaire. Ne pas oublier, donc, de détecter les erreurs ainsi :

if (signal (...) == SIG_ERR) /* erreur */

Nous avons pris soin dans l'exécution de l'exemple précédent de ne pas invoquer deux fois de suite le même signal. Pourtant, cela n'aurait pas posé de problème avec Linux et la GlibC, comme en témoigne l'essai suivant :

$ ./exemple signal Signal 9 non capturé Signal 19 non capturé (Contrôle-C)

6743 a reçu le signal 2 (Interrupt) (Contrôle-C)

6743 a reçu le signal 2 (Interrupt) (Contrôle-C)

6743 a reçu le signal 2 (Interrupt) (Contrôle-Z)

6743 a reçu le signal 20 (Stopped) (Contrôle-Z)

6743 a reçu le signal 20 (Stopped)

$ kill -KILL 6743 Killed

$

Il existe toutefois de nombreux systèmes Unix (de la famille Système V) sur lesquels un gestionnaire de signal ne reste pas en place après avoir été invoqué. Une fois que le signal est

arrivé, le noyau repositionne le comportement par défaut. Ce dernier peut être observé sous Linux avec GlibC en définissant la constante symbolique _XOPEN_SOURCE avant d'inclure <signal.h>. En voici un exemple :

exemple_signal_2.c

#define _XOPEN_SOURCE #include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <signal.h>

void

gestionnaire (int numero_signal) {

fprintf (stdout, "\n %u a reçu le signal %d\n", getpid( ).

numero_signal);

} int main (void) { int i;

for (i = 1; i < _NSIG; i++)

if (signal (i, gestionnaire) = SIG_ERR)

fprintf (stderr, "%u ne peut capturer le signal %d\n", getpid( ), i);

while (1) { pause( );

} }

Voici un exemple d'exécution dans lequel on remarque que la première frappe de Contrôle-Z est interceptée, mais pas la seconde, qui stoppe le processus et redonne la main au shell. On redémarre alors le programme avec la commande fg, et on invoque Contrôle-C.

Sa première occurrence sera bien interceptée, mais pas la seconde.

$ ./exemple_signal_2

6745 ne peut capturer le signal 9 6745 ne peut capturer le signal 19 (Contrôle-Z)

6745 a reçu le signal 20 (Contrôle-Z)

[1]+ Stopped ./exemple_signal_2

$ ps 6745

PID TTY STAT TIME COMMAND

6745 p5 T 0:00 /exemple_signal_2

$ fg

./exemple signal 2 6745 a reçu le signal 18 (Contrôle-C)

6745 a reçu le signal 2 (Contrôle-C)

$

Le signal 18 correspond à SIGCONT, que le shell a envoyé en replaçant le processus en avant-plan. Sur ce type de système, il est nécessaire que le gestionnaire de signaux s'installe à nouveau à chaque interception d'un signal. On doit donc utiliser un code du type :

int

gestionnaire (int numero_signal) {

signal (numero_signal, gestionnaire);

/* Traitement effectif du signal reçu */

$

Il est toutefois possible que le signal arrive de nouveau avant que le gestionnaire ne soit réinstallé. Ce type de comportement à risque conduit à avoir des signaux non fiables.

Un deuxième problème se pose avec ces anciennes versions de signal( ) pour ce qui concerne le blocage des signaux. Lorsqu'un signal est capturé et que le processus exécute le gestionnaire installé, le noyau ne bloque pas une éventuelle occurrence du même signal.

Le gestionnaire peut alors se trouver rappelé au cours de sa propre exécution. Nous allons le démontrer avec ce petit exemple, dans lequel un processus fils envoie deux signaux à court intervalle à son père, lequel utilise un gestionnaire lent, qui compte jusqu'à 3.

exemple_signal_3.c

#define _XOPEN_SOURCE #include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <signal.h>

void

gestionnaire (int numero_signal) {

int i;

signal (numero_signal, gestionnaire);

fprintf (stdout, "début du gestionnaire de signal %d \n", numero_signal);

for (i = 1; i < 4; i++) { fprintf (stdout, "%d\n", i);

sleep (1);

}

fprintf (stdout, "fin du gestionnaire de signal %d\n", numero_signal);

} int main (void) {

signal (SIGUSR1, gestionnaire);

if (fork( ) = 0) {

kill (getppid( ) , SIGUSR1);

sleep (1);

kill (getppid( ), SIGUSR1);

} else { while (1) { pause( );

} }

return (0);

}

Voici ce que donne l'exécution de ce programme :

$ ./exemple_signal_3

début du gestionnaire de signal 10 1

début du gestionnaire de signal 10 1

2 3

fin du gestionnaire de signal 10 2

3

fin du gestionnaire de signal 10 (Contrôle-C)

$

Les deux comptages sont enchevêtrés, ce qui n'est pas grave car la variable i est allouée de manière automatique dans la pile, et il y a donc deux compteurs différents pour les deux invocations du gestionnaire. Mais cela pourrait se passer autrement si la variable de comptage était statique ou globale. Il suffit de déplacer le « int i » pour le placer en variable globale avant le gestionnaire, et on obtient l'exécution suivante :

$ ./exemple_signal_3

début du gestionnaire de signal 10 1

début du gestionnaire de signal 10 1

2 3

fin du gestionnaire de signal 10 fin du gestionnaire de signal 10 (Contrôle-C)

$

Cette fois-ci, le compteur global était déjà arrivé à 4 lorsqu'on est revenu dans le premier gestionnaire, celui qui avait lui-même été interrompu par le signal. Pour éviter ce genre de désagrément, la version moderne de signal( ), disponible sous Linux, bloque automatique-ment un signal lorsqu'on exécute son gestionnaire, puis le débloque au retour. On peut le vérifier en supprimant la ligne #define _XOPEN_SOURCE et on obtient (même en laissant le compteur en variable globale) :

$ ./exemple_signal_3

début du gestionnaire de signal 10 1

2

3

fin du gestionnaire de signal 10 début du gestionnaire de signal 10 1

2 3

fin du gestionnaire de signal 10 (Contrôle-C)

$

Comme on pouvait s'y attendre, les deux exécutions du gestionnaire de signal sont séparées. On peut noter au passage que si on rajoute un troisième

sleep (1);

kill (getppid( ), SIGUSR1);

dans le processus fils, il n'y a pas de différence d'exécution. Seules deux exécutions du gestionnaire ont lieu. C'est dû au fait que, sous Linux, les signaux classiques ne sont pas empilés, et l'arrivée du troisième SIGUSR1 se fait alors que le premier gestionnaire n'est pas terminé. Aussi, un seul signal est mis en attente. Remarquons également que lorsqu'on élimine la définition _XOPEN_SOURCE, on peut supprimer l'appel signal( ) à l'intérieur du gestionnaire : celui-ci est automatiquement réinstallé, comme on l'a déjà indiqué.

Bien sûr, toutes ces expérimentations tablent sur le fait que l'exécution des processus se fait de manière homogène. sur un système peu chargé. Si tel n'est pas le cas, les temps de commutation entre les processus père et fils, ainsi que les délais de délivrance des signaux, peuvent modifier les comportements de ces exemples.

Nous voyons que la version de signal( ) disponible sous Linux, héritée de celle de BSD, est assez performante et fiable puisqu'elle permet, d'une part, une réinstallation automatique du gestionnaire lorsqu'il est invoqué et. d'autre part, un blocage du signal concerné au sein de son propre gestionnaire. Une dernière question se pose, qui concerne le redémarrage automatique des appels-système lents interrompus.

Pour cela, la bibliothèque GlibC nous offre une fonction de contrôle nommée siginterrupt( ).

int siginterrupt (int numero, int interrompre);

Elle prend en argument un numéro de signal, suivi d'un indicateur booléen. Elle doit être appelée après l'installation du gestionnaire et, si l'indicateur est nul, les appels-système lents seront relancés automatiquement. Si l'indicateur est non nul, les appels-système échouent, avec une erreur EINTR dans errno.

Voici un petit programme qui prend une valeur numérique en argument et la transmet à siginterrupt( ) après avoir installé un gestionnaire pour le signal TSTP (touche Contrôle-Z). Il exécute ensuite une lecture bloquante depuis le descripteur de fichier 0 (entrée standard). Le programme nous indique à chaque frappe sur Contrôle-Z si la lecture est interrompue ou non. On peut terminer le processus avec Contrôle-C.

exemple_siginterrupt.c #include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <errno.h>

#include <signal.h>

void

gestionnaire (int numero_signal) {

fprintf (stdout, "\n gestionnaire de signal %d\n", numero_signal);

} int

main (int argc, char * argv H) {

int i;

if ((argc != 2) || (sscanf (argv [1], "%d", & i) != 1)) { fprintf (stderr, "Syntaxe : %s {0|1}\n", argv [0]);

exit (1);

}

signal (SIGTSTP, gestionnaire);

siginterrupt (SIGTSTP, i);

while (1) {

fprintf (stdout, "appel read( )\n");

if (read (0, &i, sizeof (int)) < 0) if (errno = EINTR)

fprintf (stdout, "EINTR \n");

}

return (0);

}

Voici un exemple d'exécution :

$ ./exemple_siginterrupt 0 appel read( )

(Contrôle-Z)

gestionnaire de signal 20 (Contrôle-Z)

gestionnaire de signal 20 (Contrôle-C)

$ ./exemple_siginterrupt 1 appel read( )

(Contrôle-Z)

gestionnaire de signal 20 EINTR

appel read( ) (Contrôle-Z)

gestionnaire de signal 20 EINTR

appel read( ) (Contrôle-C)

$

En supprimant la ligne siginterrupt( ), on peut s'apercevoir que le comportement est identique à «exemple_siginterrupt 0 ». Les appels-système lents sont donc relancés automatiquement sous Linux par défaut. Si, par contre, nous définissons la constante _XOPEN_SOURCE comme nous l'avons fait précédemment, en supprimant la ligne siginterrupt( ), on observe que les appels lents ne sont plus relancés.

Conclusions

Nous voyons donc que l'appel-système signal( ) donne accès, sous Linux, avec la bibliothèque GlibC, à des signaux fiables. Nous pouvons aussi compiler des sources datant d'anciens systèmes Unix et se fondant sur un comportement moins fiable de signal( ) , simplement en définissant des constantes symboliques à la compilation (consulter à ce sujet le fichier /usr/include/features.h).

Malheureusement, ce n'est pas le cas sur tous les systèmes, aussi est-il préférable d'employer la fonction sigagaction( ), que nous allons étudier dans le prochain chapitre et qui permet un paramétrage plus souple du comportement du programme.

L'appel signal( ) doit surtout être réservé aux programmes qui doivent être portés sur des systèmes non Posix. Dans tous les autres cas, on préférera sigaction( ).

7

Gestion des signaux

Dans le document Programmation système en C sous (Page 76-82)