• Aucun résultat trouvé

Gestion des threads au niveau noyau

9 Les signaux

9.2 Les signaux classiques

#define SIGXFSZ 25 /* File size limit exceeded (4.2 BSD). */

#define SIGVTALRM 26 /* Virtual alarm clock (4.2 BSD). */

#define SIGPROF 27 /* Profiling alarm clock (4.2 BSD). */

#define SIGWINCH 28 /* Window size change (4.3 BSD, Sun). */

#define SIGPOLL SIGIO /* Pollable event occurred (System V). */

#define SIGIO 29 /* I/O now possible (4.2 BSD). */

#define SIGPWR 30 /* Power failure restart (System V). */

#define SIGSYS 31 /* Bad system call. */

A noter que toutes les valeurs des signaux ne sont pas nécessairement les mêmes suivant les différents Unix, voire entre deux versions majeures de noyaux Linux. Aussi, il conviendra de toujours utiliser dans vos programmes le nom du signal (ex : SIGURG ou SIGKILL) plutôt que leur valeur numérique (23 ou 9).

Si la plupart des signaux on une signification imposée par le système, il existe deux signaux (SIGUSR1 et SIGUSR2) qui peuvent être utilisés par le programmeur à sa guise (il peut leurs assigner par convention la sémantique qu’il désire).

Pour traiter les signaux, le descripteur de processus (PCB) contient les champs suivants :

• le champ signal est de type sigset_t qui stocke les signaux envoyés au processus. Cette structure est constituée de deux entiers non signés de 32 bits chacun (soit 64 bits au total), chaque bit représentant un signal. Une valeur à 0 indique que le signal correspondant n’a pas été reçu tandis qu’une valeur à 1 indique que le signal a été reçu ;

• le champ blocked est de type sigset_t (64 bits) et stocke les signaux bloqués (c’est-à-dire les signaux dont Ia prise en compte est retardée) ;

• le champ sigpending est un drapeau (flag) indiquant s’il existe au moins un signal non bloqué en attente ;

• le champ gsig est un pointeur vers une structure de type signal_struct, qui contient notamment, pour chaque signal, la définition de l’action qui lui est associée.

En pratique, la gestion des signaux se retrouve à plusieurs niveaux dans le système d’exploitation :

• envoi d’un signal par un processus (par le noyau ou par le processus d’un utilisateur, ou suite à une exception),

• prise en compte du signal et exécution du code qui lui est associé ;

• enfin, il existe une interaction entre certains appels systèmes et les signaux.

Nous allons détailler chacun de ces mécanismes.

9.2.1 L’envoi d’un signal

L’envoi d’un signal peut être effectué :

• par un processus à destination d’un autre processus, à l’aide de la fonction système

« kill() » (Cf. prochains chapitres). Il existe une commande « kill » qui peut être lancée depuis un shell, et qui appelle la fonction système « kill() ». Exemple d’utilisation :

# kill -SIGTERM num_de_pid

• ou alors, l’exécution du processus a levé une trappe, et le gestionnaire d’exception associé positionne un signal pour signaler I’erreur détectée. Par exemple, suite à une division par zéro, le gestionnaire d’exception divide_error() positionne le signal SIGFPE.

Lors de l’envoi d’un signal, le noyau exécute la routine du noyau seng_sig_info() qui positionne tout simplement à 1 le bit correspondant au signal reçu dans le champ signal

du PCB du processus destinataire. La fonction se termine immédiatement dans les deux cas suivant :

• le numéro du signal émis est 0. Ce numéro n’étant pas un numéro de signal valable, le noyau retourne immédiatement une valeur d’erreur ;

ou lorsque le processus destinataire est dans l’état « zombie ».

Le signal ainsi délivré (mise à 1 du bit dans le champ signal) mais pas encore pris en compte par le processus destinataire (le handler de signal n’a pas encore été lancé) est qualifié de signal pendant.

A noter que ce mécanisme de mémorisation de la réception du signal permet effectivement de mémoriser qu’un signal a été reçu, mais ne permet pas de mémoriser combien de signaux d’un même type ont été reçus. Par exemple, si deux processus fils se terminent de façon rapprochée dans le temps, le premier enverra un signal SIGCHLD au père. Il est tout à fait possible que la mort du second fils engendre le même signal SIGCHLD au père, alors que le premier signal SIGCHLD était encore pendant (i.e. non pris en compte par le père).

Ainsi, la réception d’un signal SIGCHLD par le père lui indique « qu’au moins » un fils s’est terminé, sans que le système ne puisse lui indiquer combien.

9.2.2 La prise en compte d’un signal

La prise en compte d’un signal par un processus s’effectue lorsque celui-ci s’apprête à quitter le mode noyau pour repasser en mode utilisateur (c’est à dire lors du retour d’un appel à une fonction système, ou lorsque le scheduler élit ce processus et souhaite le faire démarrer). On dit alors que le signal (qui était pendant) est délivré au processus.

Cette prise en compte est réalisée par la routine du noyau do_signal() qui traite chacun des signaux pendants du processus. Trois types d’actions peuvent être réalisés:

• ignorer le signal ;

• exécuter l’action par défaut ;

• exécuter une fonction spécifique installée consciemment par le programmeur.

Ces actions sont stockées pour chaque signal dans le champ gsig.sa_handler du bloc de contrôle du processus (PCB). Ce champ prend la valeur SIG_IGN dans le cas où le signal doit être ignoré, la valeur SIG_DFL s’il faut exécuter le handler de signal par défaut, et enfin contient l’adresse de la fonction spécifique à exécuter dans le troisième cas.

Lorsque le signal est ignoré, aucune action n’est exécutée. Il existe une exception à cette règle : le signal SIGCHLD. En effet, si le processus a programmé le signal SIGCHLD afin que ce dernier soit ignoré, le noyau force le processus à lire le code retour des zombies, afin que ces derniers libèrent leurs ressources (principalement des entrées dans les tables des PCB) et soient effacés définitivement.

Les actions par défaut qui peuvent être exécutées suites à un envoi de signal sont au nombre de cinq :

• fin du processus,

• fin du processus et création (si l’utilisateur l’a décidé) d’un fichier core dans le répertoire courant, qui est un fichier contenant l’image mémoire du processus, et qui peut être analysé par un débogueur,

• le signal est ignoré,

• le processus est stoppé (il passe dans l’état TASK_STOPPED),

• le processus reprend son exécution (l’état est positionné à TASK_RUNNING).

Voici un tableau qui indique les actions par défaut liées aux différents signaux :

Actions par défaut

Nom du signal

Fin du process SIGHUP, SIGINT, SIGBUS, SIGKILL, SIGUSR1, SIGUSR2, SIGPIPE, SIGALRM, SIGTERM, SIGSTKFLT, SIGXCOU,

SIGXFSZ, SIGVTALRM, SIGPROF, SIGIO, SIGPOLL, SIGPWR, SIGUNUSED

Fin du process et création core

SIGQUIT, SIGILL, SIGTRAP, SIGABRT, SIGIOT, SIGFPE, SIGSEGV

Signal ignoré SIGCHLD, SIGURG, SIGWINCH Processus

stoppé

SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU

Processus redémarré

SIGCONT

Tout processus peut installer un traitement spécifique pour chacun des signaux, hormis pour le signal SIGKILL. Ce traitement spécifique remplace alors le traitement par défaut défini par le noyau. Ce traitement spécifique peut être de deux natures différentes :

• demande à ignorer Ie signal (prend alors la valeur SIG_IGN),

• définit une action particulière programmée dans une fonction C attachée au code de l’utilisateur (prend alors la valeur du handler ou gestionnaire du signal).

A noter que lorsqu’un signal pendant est délivré au processus, si ce signal est détourné par une fonction programmée par l’utilisateur, le noyau :

revient en mode « utilisateur »,

• lance l’exécution du handler,

• et à la fin de l’exécution de ce handler, le noyau s’arrange pour que le processus reprenne son exécution normale (c’est la fonction restore_sigcontext() qui effectue ce travail).

Voici en pratique ce qui se passe. Lorsque le noyau est prêt pour redonner la main à un processus en mode utilisateur (soit après un appel système, soit après que le scheduler ait élu ce processus), le noyau appelle tout d’abord la fonction do_signal(). Cette fonction vérifie qu’il n’existe pas de signal pendant. Si c’est le cas, le noyau appel le handler correspondant (avec le privilège utilisateur). Lorsque cette fonction se termine, si aucun autre signal n’est pendant, le noyau retourne à l’exécution du programme à l’aide de la fonction restore_sigcontext().

9.2.3 Signaux et appels système

Lorsqu’un processus exécute un appel système qui se révèle bloquant (exemple : appel de la fonction open() pour ouvrir un fichier qui n’est pas encore ouvert par un autre processus ; il va falloir aller lire et interpréter des blocs sur le disque dur ; pendant ce temps, le processus est « bloqué »), le processus est placé par le noyau dans l’état TASK_INTERRUPTIBLE ou dans l’état TASK_UNINTERRUPTIBLE :

• s’il est dans l’état TASK_INTERRUPTIBLE, le processus peut être réveillé par le système lorsqu’il reçoit un signal ;

• inversement, le système place le processus dans l’état TASK_UNINTERRUPTIBLE si le processus ne doit pas être réveillé par un signal.

Lorsqu’un processus placé dans l’état « TASK_INTERRUPTIBLE » suite à un appel système blocant est réveillé par le noyau (lorsqu’il reçoit un signal), une fois le handler correspondant terminé, le système positionne ce processus dans l’état

« TASK_RUNNING », et il positionne la variable errno à EINTR. Nous verrons dans les prochains chapitres comment faire pour que le processus ne reprenne pas son exécution normale, et attende bien la fin de son appel système.

Remarque importante : un processus fils n’hérite pas des signaux pendants de son père.

De plus, en cas de recouvrement du code hérité du père, les gestionnaires par défaut sont réinstallés pour tous les signaux du fils.