• Aucun résultat trouvé

Exemples d'utilisation de sigaction( )

Dans le document Programmation système en C sous (Page 84-87)

Notre premier exemple consistera à installer deux gestionnaires :l'un pour SIGQUIT (que nous déclenchons au clavier avec Contrôle-AltGr-\), qui ne fera pas redémarrer les appels-système lents interrompus, le second, celui de SIGINT (Contrôle-C), aura pour particularité de ne pas se réinstaller automatiquement. La seconde occurrence de SIGINT déclenchera donc le comportement par défaut et arrêtera le processus.

exemple_sigaction_1.c #include <signal.h>

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <errno.h>

void

gestionnaire (int numero) {

switch (numero) { case SIGQUIT :

fprintf (stdout, "\n SIGQUIT reçu \n");

fflush (stdout);

break;

case SIGINT :

fprintf (stdout, "\n SIGINT reçu \n");

fflush (stdout);

break;

} } int main (void) {

struct sigaction action;

action . sa_handler = gestionnaire;

sigemptyset (& (action . sa_mask));

action . sa_flags = 0;

if (sigaction (SIGQUIT, & action, NULL) != 0) { fprintf (stderr, "Erreur %d \n", errno);

exit (1);

}

action . sa_handler = gestionnaire;

sigemptyset (& (action . sa_mask));

action . sa_flags = SA_RESTART | SA_RESETHAND;

if (sigaction (SIGINT, & action, NULL) != 0) { fprintf (stderr, "Erreur %d \n", errno);

exit (1);

}

/* Lecture continue, pour avoir un appel-système lent bloqué */

while (1) { int i;

fprintf (stdout, "appel read( )\n''):

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

fprintf (stdout, "EINTR \n");

}

return (0);

}

L'exécution de ce programme montre bien les différences de caractéristiques entre les signaux :

$ ./exemple_sigaction_1 appel read( )

Ctrl-AltGr-\

SIGQUIT reçu EINTR

appel read( )

Ctrl-c SIGINT reçu

Ctrl-AltGr-\

SIGQUIT reçu EINTR

appel read( )

Ctrl-c

$

SIGQUIT interrompt bien l'appel read( ), mais pas SIGINT. De même. le gestionnaire de SIGQUIT reste installé et peut être appelé une seconde fois, alors que SIGINT reprend son comportement par défaut et termine le processus la seconde fois.

Nous n'avons pas sauvegardé l'ancien gestionnaire de signaux lors de l'appel de sigaction( ) (troisième argument). Il est pourtant nécessaire de le faire si nous installons temporairement une gestion de signaux propre à une seule partie du programme. De même, lorsqu'un processus est lancé en arrière-plan par un shell ne gérant pas le contrôle des jobs, celui-ci force certains signaux à être ignorés. Les signaux concernés sont SIGINT et SIGQUIT. Dans le cas d'un shell sans contrôle de jobs, ces signaux seraient transmis autant au processus en arrière-plan qu'à celui en avant-plan.

Voici un exemple de programme permettant d'afficher les signaux dont le comportement n'est pas celui par défaut.

exemple_sigaction_2.c #include <stdio.h>

#include <stdlib.h>

#include <signal.h>

#include <errno.h>

int main (void)

{

int i;

struct sigaction action;

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

if (sigaction (i, NULL, & action) != 0)

fprintf (stderr, "Erreur sigaction %d \n", errno);

if (action . sa_handler != SIG_DFL) { fprintf (stdout, "%d (%s) comportement i, sys_siglist [i]);

if (action . sa_handler == SIG_IGN) fprintf (stdout, ": ignorer \n");

else

fprintf (stdout, "personnalisé \n");

} }

return (0);

}

Pour l'exécuter de manière probante, il faut arrêter le contrôle des jobs. Sous bash, cela s'effectue à l'aide de la commande « set +m ». Au début de notre exemple, bash a un contrôle des jobs activé.

$ ./exemple_sigaction_2

$ ./exemple_sigaction_2 &

[1] 983 [1]+ Done

$ set +m

$ ./exemple_sigaction_2

$ ./exemple_sigaction_2 &

[1] 985

2 (Interrupt) comportement : ignorer

$ 3 (Quit) comportement : ignorer

$

On voit qu'il n'y a effectivement de différence que pour les processus en arrière-plan si le contrôle des jobs est désactivé. Il vaut donc mieux vérifier, au moment de l'installation d'un gestionnaire pour ces signaux, si le shell ne les a pas volontairement ignorés. Dans ce cas, on les laisse inchangés :

struct sigaction action, ancienne;

action . sa_handler = gestionnaire;

/* ... initialisation de action ... *1

if (sigaction (SIGQUIT, & action, & ancienne) != 0) /* ... gestion d'erreur ... */

if (ancienne . sa_handler != SIG_DFL) {

/* réinstallation du comportement original */

sigaction (SIGQUIT, & ancienne, NULL);

}

Ceci n'est important que pour SIGQUIT et SIGINT.

Profitons de cet exemple pour préciser le comportement des signaux face aux appels-système fork( ) et exec( ) utilisés notamment par le shell. Lors d'un fork( ), le processus fils reçoit le même masque de blocage des signaux que son père. Les actions des signaux sont également

les mêmes, y compris les gestionnaires installés par le programme. Par contre, les signaux en attente n'appartiennent qu'au processus père.

Lors d'un exec( ), le masque des signaux bloqués est conservé. Les signaux ignorés le restent. C'est comme cela que le shell nous transmet le comportement décrit ci-dessus pour SIGINT et SIGQUIT. Mais les signaux qui étaient capturés par un gestionnaire reprennent leur comporte-ment par défaut. C'est logique car l'ancienne adresse du gestionnaire de signal n'a plus aucune signification dans le nouvel espace de code du programme exécuté.

On peut s'interroger sur la pertinence de mélanger dans un même programme les appels à signal( ) et à sigaction( ). Cela ne pose aucun problème majeur sous Linux avec GlibC. Le seul inconvénient vient du fait que signal( ) ne peut pas sauvegarder et rétablir ultérieurement autant d'informations sur le gestionnaire de signal que sigaction( ).

Ce dernier en effet peut sauver et réinstaller les attributs comme NOCLDSTOP ou NODEFER, au contraire de signal( ).

Lorsqu'il faut sauvegarder un comportement pour le restituer plus tard, il faut donc impérativement utiliser sigaction( ), sauf si tout le programme n'utilise que signal(

).

Lorsqu'on installe un gestionnaire avec signal( ) sous Linux et qu'on examine le comportement du signal avec sigaction( ), on retrouve dans le champ sa_handler la même adresse que celle de la routine installée avec signal( ). Ceci n'est toutefois pas du tout généralisable à d'autres systèmes, et il ne faut pas s'appuyer sur ce comportement.

Voici un exemple de test.

exemple_sigaction_3.c #include <signal.h>

#include <stdio.h>

#include <stdlib.h>

void

gestionnaire (int inutilise) {}

int main (void) {

struct sigaction action;

signal (SIGUSR1, gestionnaire);

sigaction (SIGUSR1, NULL, & action);

if (action . sa_handler = gestionnaire) fprintf (stdout, "Même adresse \n");

else

fprintf (stdout, "Adresse différente \n");

return (0);

}

Sous Linux, pas de surprise :

$ uname -sr Linux 2.0.31

$ ./exemple_sigaction_3 Même adresse

$

$ uname -sr Linux 2.2.12-20

$ ./exemple sigaction 3 Même adresse

$

Nous terminerons cette section avec un exemple d'installation d'une pile spécifique pour les gestionnaires de signaux. Rappelons que cette fonctionnalité n'est disponible que depuis Linux 2.2. Nous allons mettre en place un gestionnaire commun pour les signaux SIGQUIT et SIGTERM, qui vérifiera si la pile est en cours d'utilisation ou non en examinant le champ ss_flags. Nous n'installerons la pile spéciale que pour le signal SIGQUIT, ce qui nous permettra de vérifier la différence entre les deux signaux.

exemple_sigaltstack.c #include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <signal.h>

#include <errno.h>

void

gestionnaire (int numero_signal) {

stack_t pile;

fprintf (stdout, "\n Signal %d reçu \n", numero_signal);

if (sigaltstack (NULL, & pile) != 0) {

fprintf (stderr, "Erreur sigaltstack %d \n", errno);

return;

}

if (pile . ss_flags & SS_DISABLE)

fprintf (stdout, "La pile spéciale est inactive \n");

else

fprintf (stdout, "La pile spéciale est active \n");

if (pile . ss_flags & SS_ONSTACK)

fprintf (stdout, "Pile spéciale en cours d'utilisation \n");

else

fprintf (stdout, "Pile spéciale pas utilisée actuellement \n");

} int main (void) {

stack_t pile;

struct sigaction action;

if ((pile . ss_sp = malloc (SIGSTKSZ)) == NULL) { fprintf (stderr, "Pas assez de mémoire \n");

exit (1);

}

pile . ss_size = SIGSTKSZ;

pile . ss_flags = 0;

if (sigaltstack (& pile, NULL) != 0) {

fprintf (stderr, "Erreur sigaltstack( ) %d \n", errno);

exit (1);

}

action . sa_handler = gestionnaire;

sigemptyset (& (action . sa_mask));

action . sa_flags = SA_RESTART | SA_ONSTACK;

if (sigaction (SIGQUIT, & action, NULL) != 0) {

fprintf (stderr, "Erreur sigaction( ) %d \n", errno);

exit (1);

}

action . sa_handler = gestionnaire;

sigemptyset (& (action . sa_mask));

action . sa_flags = SA_RESTART;

if (sigaction (SIGTERM, & action, NULL) != 0) {

fprintf (stderr, "Erreur sigaction( ) %d \n", errno);

exit (1);

}

fprintf (stdout, "PID = %u \n", getpid( ));

fflush (stdout);

while (1) pause( );

return (0);

}

Voici un exemple d'utilisation :

$ ./exemple_sigaltstack PID = 815

$ kill -QUIT 815 Signal 3 reçu

La pile spéciale est active

Pile spéciale en cours d'utilisation $ kill -TERM 815

Signal 15 reçu

La pile spéciale est active

Pile spéciale pas utilisée actuellement $ kill -INT 815

$

Comme prévu, la pile spéciale des signaux reste active en permanence, mais elle n'est utilisée que lorsque le gestionnaire est invoqué par SIGQUIT.

Dans le document Programmation système en C sous (Page 84-87)