• Aucun résultat trouvé

Principe de la fonction fork()

7.2.4 Les processus zombies (et comment les éviter)

Il existe une erreur classique dans le développement de programme Unix/Linux gérant plusieurs processus : un processus père qui crée des fils, et qui ne s'occupe pas ensuite d'acquérir leur code de fin (c’est à dire la valeur retournée par la fonction exit(), ou retournée par le return() de la fonction main()).

Ces processus fils restent dans un état « fin d’exécution du programme », jusqu’à ce que le père s’inquiète du code retour du processus fils. On parle alors de processus « zombie ».

A noter que les processus zombies ne peuvent pas être supprimés par les méthodes classiques (y compris pour les utilisateurs privilégiés). Les ressources (mémoire, fichiers, etc.) son libérées, mais le processus prend encore une place dans la table des processus.

Comme cette dernière n’est pas extensible, le système se retrouve alors encombré de processus inactifs (ils ont comme état la valeur « TASK_ZOMBIE »).

Normalement, les processus zombies sont « purgés » à la mort du processus père. Si cette purge se passe mal, seul un redémarrage système sera efficace.

Plusieurs méthodes permettent de les éviter (on parle de synchronisation père/fils) :

• Utilisation des fonctions wait() et waitpid() :

#include <sys/types.h>

#include <sys/wait.h>

pid_t wait(int *status);

pid_t waitpid(pid_t pid, int *status, int options);

extrait de « man wait » ou de « man waitpid » : <<

La fonction wait() suspend l'exécution du processus courant jusqu'à ce qu'un enfant se termine, ou jusqu'à ce qu'un signal à intercepter arrive. Si un processus fils s'est déjà terminé au moment de l'appel (il est devenu "zombie"), la fonction revient immédiatement.

Toutes les ressources utilisées par le fils sont libérées.

La fonction waitpid() suspend l'exécution du processus courant jusqu'à ce que le processus fils numéro pid se termine, ou jusqu'à ce qu'un signal à intercepter

arrive. Si le fils mentionné par pid s'est déjà terminé au moment de l'appel (il est devenu "zombie"), la

fonction revient immédiatement. Toutes les ressources utilisées par le fils sont libérées.

La valeur de pid peut également être l'une des suivantes :

< -1 : attendre la fin de n'importe quel processus fils appartenant à un groupe de processus d'ID pid.

-1 : attendre la fin de n'importe quel fils. C'est le même comportement que wait.

0 : attendre la fin de n'importe quel processus fils du même groupe que l'appelant.

> 0 : attendre la fin du processus numéro pid.

La valeur de l'argument option options est un OU binaire entre les constantes suivantes :

WNOHANG : ne pas bloquer si aucun fils ne s'est terminé.

WUNTRACED : recevoir l'information concernant

également les fils bloqués si on ne l'a pas encore reçue.

Si status est non NULL, wait() et waitpid() y stockent l'information sur la terminaison du fils.

Cette information peut être analysée avec les macros suivantes, qui réclament en argument le buffer status (un int, et non pas un pointeur sur ce buffer) : WIFEXITED(status) : non nul si le fils s'est terminé normalement

WEXITSTATUS(status) : donne le code de retour tel qu'il a été mentionné dans l'appel exit() ou dans le return de la routine main.

Cette macro ne peut être évaluée que si WIFEXITED est non nul.

WIFSIGNALED(status) : indique que le fils s'est terminé à cause d'un signal non intercepté.

WTERMSIG(status) : donne le nombre de signaux qui ont causé la fin du fils.

Cette macro ne peut être

évaluée que si WIFSIGNALED est non nul.

WIFSTOPPED(status) : indique que le fils est actuellement arrêté. Cette macro n'a de sens que si l'on a effectué l'appel avec

l'option WUNTRACED.

WSTOPSIG(status) : donne le nombre de signaux qui ont causé l'arrêt du fils.

Cette macro ne peut être évaluée que si WIFSTOPPED est non nul.

Valeur Renvoyée : en cas de réussite, le PID du fils qui s'est terminé est renvoyé, en cas d'echec -1 est renvoyé et errno contient le code d'erreur :

ECHILD : Le processus indiqué par pid n'existe pas, ou n'est pas un fils du processus

appelant (Ceci peut arriver pour son propre fils si l'action de SIGCHLD est placé sur SIG_IGN, voir également le passage de la section NOTES concernant les threads).

EINVAL : L'argument options est invalide.

ERESTARTSYS : WNOHANG n'est pas indiqué, et un signal à intercepter ou SIGCHLD a été reçu. Cette erreur est renvoyée par l'appel système. La routine de bibliothèque d'interface n'est pas autorisée à renvoyer ERESTARTSYS, mais renverra EINTR.

>>

• Le problème de la fonction wait(), et de la fonction waitpid() dans son fonctionnement par défaut est qu’elles sont bloquantes (suite à leur appel, le programme s’arrête jusqu’à ce qu’un fils se termine). Une première solution à utiliser waitpid() en mettant le paramètre « option » à « WNOHANG ». Mais comme nous ne savons pas à quel moment un fils se termine, il faut lancer régulièrement cette fonction waitpid(). On dit alors que nous faisons du

« polling ». Ce mode de fonctionnement, qui consomme du CPU et des appels systèmes n’est pas optimum. Une solution bien plus esthétique consiste à n’appeler waitpid()que lorsque nous sommes sûr qu’un processus fils est terminé. Nous y reviendrons très largement dans le chapitre prévu à cet effet, mais vous pouvez noter dès à présent que le père reçoit un « signal » du système d’exploitation lorsqu’un de ses fils se termine. Ce signal peut être « intercepté » à l’aide de la fonction signal(). Voici un extrait de la documentation de cette fonction :

#include <signal.h>

void (*signal(int signum, void (*handler)(int)))(int);

L'appel-système signal() installe un nouveau gestionnaire pour le signal numéro signum. Le

gestionnaire de signal est handler (NDLR : c’est à dire le pointeur vers une fonction) qui peut être soit une fonction spécifique de l'utilisateur, soit l'une des constantes SIG_IGN ou SIG_DFL.

Lors de l'arrivée d'un signal correspondant au numéro signum, les évènements suivants se produisent :

- si le gestionnaire correspondant est configuré avec SIG_IGN, le signal est ignoré.

- si le gestionnaire vaut SIG_DFL, l'action par défaut pour le signal est entreprise, comme décrit dans le manuel signal(7).

- finalement, si le gestionnaire est dirigé vers une fonction handler(), alors tout d'abord, le

gestionnaire est re-configuré à SIG_DFL, ou le signal est bloqué, puis handler() est appelé avec l'argument signum.

Utiliser une fonction comme gestionnaire de signal est appelé "intercepter - ou capturer - le signal". Les signaux SIGKILL et SIGSTOP ne peuvent ni être ignoré, ni être interceptés.

Valeur Renvoyée : signal() renvoie la valeur précédente du gestionnaire de signaux, ou SIG_ERR en cas d'erreur.

>>

En pratique, lorsqu’un fils se termine, le père reçoit un signal de type « SIGCHLD ».

L’idée consiste à intercepter ce signal « SIGCHLD » lorsque le fils se termine, et seulement à ce moment là, lancer une des fonctions bloquante waitpid() en mode non bloquant. Voici un squelette d’un tel programme :

#include <sys/types.h>

#include <sys/wait.h>

#include <signal.h>

[...]

void fin_d_un_enfant(int num_signal) {

/* On ne l’utilise pas, mais notez qu’à ce */

/* stade, le paramètre num_signal vaut */

/* obligatoirement la valeur SIGCHLD (vu que */

/* cette fonction ne sert qu’à intercepter */

/* le signal SIGCHLD). */

/* Nettoyage des processus fils */

/* Comme plusieurs fils peuvent se terminer au */

/* même instant, nous ne savons pas si la */

/* fonction est appelée suite à la fin d’un ou de */

/* plusieurs fils. Il faut lancer waitpid() */

/* plusieurs fois (à l’aide d’une boucle while), */

/* tant qu’il reste des zombies à faire */

/* disparaître : */

while(waitpid(-1, NULL, WNOHANG) > 0);

/* on repositionne la présente fonction */

/* comme vecteur d’interception (handler) */

/* pour le prochain fils qui viendra à se */

/* terminer */

signal(SIGCHLD, fin_d_un_enfant);

} [...]

int main(int argc, char *argv[]) {

...

signal(SIGCHLD, fin_d_un_enfant);

...

if (fork() == 0) {

/* Code du fils... */

/* Pour le fils, on n’a pas à intercepter */

/* le signe SIGCHLD => on positionne le */

/* handler par défaut. */

signal(SIGCHLD, SIG_DFL);

[...] /* suite du code du fils */

} else {

/* Le code du père... */

[...]

} }