• Aucun résultat trouvé

Attendre la fin d’un processus fils

Dans le document Programmation système en C sous Linux (Page 123-134)

fprintf(stdout, "Gestion Sortie appelée... code %d\n", code);

if (pointeur == NULL) {

fprintf(stdout, "Pas de fichier à fermer \n");

} else {

fprintf(stdout, "Fermeture d’un fichier \n");

fclose((FILE *) pointeur);

} }

L’exécution suivante nous montre que les fonctions sont appelées, comme pour atexit(), dans l’ordre inverse de leur programmation (le pointeur NULL programmé en dernier apparaît en premier). Nous voyons également que le code de retour de main(), 4, est bien transmis à la routine de terminaison.

$ ./exemple_on_exit

Allez... on quitte en revenant de main() Gestion Sortie appelée... code 4

Pas de fichier à fermer

Gestion Sortie appelée... code 4 Fermeture d’un fichier

Gestion Sortie appelée... code 4 Fermeture d’un fichier

$

Attendre la fin d’un processus fils

L’une des notions fondamentales dans la conception des systèmes Unix est la mise à disposi-tion de l’utilisateur d’un grand nombre de petits utilitaires très spécialisés et très configura-bles grâce à des options en ligne de commande. Ces petits utilitaires peuvent être associés, par des redirections d’entrée-sortie, en commandes plus complexes, et regroupés dans des fichiers scripts simples à écrire et à déboguer.

Il est primordial dans ces scripts de pouvoir déterminer si une commande a réussi à effectuer son travail correctement ou non. On imagine donc l’importance qui peut être portée à la lecture du code de retour d’un processus. Cette importance est telle qu’un processus qui se termine passe automatiquement par un état spécial, zombie1, en attendant que le processus père ait lu son code de retour. Si le processus père ne lit pas le code de retour de son fils, celui-ci peut rester indéfiniment à l’état zombie. Voicelui-ci un exemple, dans lequel le processus fils attend deux secondes avant de se terminer, tandis que le processus père affiche régulièrement l’état de son fils en invoquant la commande ps.

exemple_zombie_1.c : #include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

1. Les différents états d’un processus seront détaillés dans le chapitre 11 consacré aux mécanismes d’ordonnancement disponibles sous Linux.

int

Le "S" en deuxième colonne indique que le processus fils est endormi au début, puis il se termine et passe à l’état zombie "Z" :

$ ps 949

PID TTY STAT TIME COMMAND

$

Lorsque le processus père se finit, on invoque manuellement la commande ps, et on s’aperçoit que le fils zombie a disparu. Dans ce cas, le processus numéro 1, init, adopte le processus fils orphelin et lit son code de retour, ce qui provoque sa disparition. Dans ce second exemple, le processus père va se terminer au bout de 2 secondes, alors que le fils va continuer à afficher régulièrement le PID de son père.

exemple_zombie_2.c : #include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

int main (void) {

pid_t pid;

if ((pid = fork()) < 0) {

fprintf (stderr, "échec fork()\n");

exit(EXIT_FAILURE);

}

if (pid != 0) {

/* processus père */

fprintf(stdout, "Père : mon PID est %ld\n", (long)getpid());

sleep(2);

fprintf(stdout, "Père : je me termine \n");

exit(EXIT_SUCCESS);

} else {

/* processus fils */

fprintf(stdout, "Fils : mon père est %ld\n", (long)getppid ());

sleep(1);

fprintf(stdout, "Fils : mon père est %ld\n", (long)getppid ());

sleep(1);

fprintf(stdout, "Fils : mon père est %ld\n", (long)getppid ());

sleep(1);

fprintf(stdout, "Fils : mon père est %ld\n", (long)getppid ());

sleep(1);

fprintf(stdout, "Fils : mon père est %ld\n", (long)getppid ());

}

return EXIT_SUCCESS;

}

L’exécution suivante montre bien que le processus 1 adopte le processus fils dès que le père se termine. Au passage, on remarquera que, aussitôt le processus père terminé, le shell reprend la main et affiche immédiatement son symbole d’accueil ($) :

$ ./exemple_zombie_2 Père : mon PID est 1006 Fils : mon père est 1006 Fils : mon père est 1006 Père : je me termine

$ Fils : mon père est 1 Fils : mon père est 1 Fils : mon père est 1

Pour lire le code de retour d’un processus fils, il existe quatre fonctions : wait(), waipid(), wait3() et wait4(). Nous les étudierons dans cet ordre, qui est de complexité croissante. Les trois premières sont d’ailleurs des fonctions de bibliothèque implémentées en invoquant wait4() qui est le seul véritable appel-système.

La fonction wait() est déclarée dans <sys/wait.h>, ainsi : pid_t wait (int * status);

Lorsqu’on l’invoque, elle bloque le processus appelant jusqu’à ce qu’un de ses fils se termine.

Elle renvoie alors le PID du fils terminé. Si le pointeur status est non NULL, il est renseigné avec une valeur informant sur les circonstances de la mort du fils. Si un processus fils était déjà en attente à l’état zombie, wait() revient immédiatement. Si on n’est pas intéressé par les circonstances de la fin du processus fils, il est tout à fait possible de fournir un argument NULL. La manière dont sont organisées les informations au sein de l’entier status est opaque, et il faut utiliser les macros suivantes pour analyser les circonstances de la fin du fils :

WIFEXITED(status) est vraie si le processus s’est terminé de son propre chef en invoquant exit() ou en revenant de main(). On peut obtenir le code de retour du processus fils, c’est-à-dire la valeur transmise à exit(), en appelant WEXITSTATUS(status).

WIFSIGNALED(status) indique que le fils s’est terminé à cause d’un signal, y compris le signal SIGABRT, envoyé lorsqu’il appelle abort(). Le numéro du signal ayant tué le processus fils est disponible en utilisant la macro WTERMSIG(status). À ce moment, la macro WCORE-DUMP(status) signale si une image mémoire core a été créée.

WIFSTOPPED(status) indique que le fils est stoppé temporairement. Le numéro du signal ayant stoppé le processus fils est accessible en utilisant WSTOPSIG(status).

La fonction wait() peut échouer et renvoyer –1, en plaçant l’erreur ECHILD dans errno si le processus appelant n’a pas de fils. Dans notre premier exemple, le processus père va se dédoubler en une série de fils qui se termineront de manières variées. Le processus père restera en boucle sur wait() jusqu’à ce qu’il ne reste plus de fils.

Attention

Les macros WEXITSTATUS, WTERMSIG et WSTOPSIG n’ont de sens que si les macros WIFxxx correspon-dantes ont renvoyé une valeur vraie.

exemple_wait_1.c :

void affichage_type_de_terminaison (pid_t pid, int status);

int processus_fils (int numero_fils);

affichage_type_de_terminaison(pid, status);

return EXIT_SUCCESS;

} void

affichage_type_de_terminaison (pid_t pid, int status) {

fprintf(stdout, "Le processus %ld ", (long)pid);

if (WIFEXITED(status)) {

fprintf(stdout, "s’est arrêté à cause du signal %d (%s)\n", WSTOPSIG(status),

sys_siglist[WSTOPSIG(status)]);

} } int

processus_fils (int numero_fils) {

switch (numero_fils) { case 0 :

return 1;

case 1 : exit(2);

case 2 : abort();

case 3 :

raise(SIGUSR1);

}

return numero_fils;

}

L’exécution suivante montre bien les différents types de fin des processus fils :

$ ./exemple_wait_1 Fils 0 : PID = 1353 Fils 1 : PID = 1354 Fils 2 : PID = 1355

Le processus 1355 s’est terminé à cause du signal 6 (Aborted) Fichier image core créé

Le processus 1354 s’est terminé normalement avec le code 2 Le processus 1353 s’est terminé normalement avec le code 1 Fils 3 : PID = 1356

Le processus 1356 s’est terminé à cause du signal 10 (User defined 1)

$

Notons qu’il n’y a pas de différence entre un retour de la fonction main()(fils 0, PID 1353) et un appel exit()(fils 1, PID 1354). De même, on voit que l’appel abort() (fils 2, PID 1355) se traduit bien par un envoi du signal SIGABRT (6 sur notre machine), avec création d’un fichier core. Le signal SIGUSR1 termine le processus mais ne crée pas d’image core.

Il y a deux inconvénients avec la fonction wait(), qui ont conduit à développer la fonction waitpid() que nous allons voir ci-dessous. Le premier problème, c’est que l’appel reste bloquant tant qu’aucun fils ne s’est terminé. Il n’est donc pas possible d’appeler systémati-quement wait() dans une boucle principale du programme pour savoir où en est le fils. La solution est d’installer un gestionnaire pour le signal SIGCHLD qui est émis dès qu’un fils se termine ou est stoppé temporairement.

Le second problème vient du fait qu’il n’est pas possible d’attendre la fin d’un fils particulier.

Dans ce cas, il faut alimenter dans le gestionnaire du signal SIGCHLD une liste des fils terminés, qu’on consultera dans le programme principal en attente d’un processus donné. Il ne faut pas oublier de bloquer temporairement SIGCHLD lors de la consultation de la liste, pour éviter qu’elle ne soit modifiée pendant ce temps par l’arrivée d’un signal.

Pour pallier ces deux problèmes, un appel-système supplémentaire est disponible, waitpid(), dont le prototype est déclaré dans <sys/wait.h> ainsi :

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

Le premier argument, pid, permet de déterminer le processus fils dont on désire attendre la fin.

• Si pid est strictement positif, la fonction attend la fin du processus dont le PID correspond à cette valeur.

• Si pid vaut 0, on attend la fin de n’importe quel processus fils appartenant au même groupe que le processus appelant.

• Si pid vaut –1, on attend la fin de n’importe quel fils, comme avec la fonction wait().

• Si pid est strictement inférieur à –1, on attend la fin de n’importe quel processus fils appar-tenant au groupe de processus dont le numéro est -pid.

Le second argument, status, a exactement le même rôle que wait().

Le troisième argument permet de préciser le comportement de waitpid(), en associant par un éventuel OU binaire les constantes suivantes :

Comme on le devine, il est aisé d’implémenter wait() à partir de waitpid() : pid_t

mon_wait (int * status) {

return waitpid(-1, status, 0);

}

Nous allons utiliser un programme de démonstration dans lequel un processus fils, qui reste en boucle, sera surveillé par le processus père, alors qu’un second fils, qui se termine au bout de quelques secondes, n’est pas pris en considération. Nous agirons sur une seconde console pour examiner le status des fils avec la commande ps, et pour stopper et relancer le premier fils.

exemple_wait_2.c : #include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <signal.h>

#include <sys/wait.h>

int main (void) {

Nom Signification

WNOHANG Ne pas rester bloqué si aucun processus correspondant aux spécifications fournies par l’argument pid n’est terminé. Dans ce cas, waitpid() renverra 0.

WUNTRACED Accéder également aux informations concernant les processus fils temporairement stoppés.

C’est dans ce cas que les macros WIFSTOPPED(status) et WSTOPSIG(status) prennent leur signification.

pid_t pid;

L’exécution suivante montre en seconde colonne les actions depuis l’autre Xterm :

$ ./exemple_wait_2 Fils 1 : PID = 1525 Fils 2 : PID = 1526

$ ps 1525 1526

PID TTY STAT COMMAND

1525 pts/0 S ./exemple_wait_2

1526 pts/0 Z [exemple_wait_2 <defunct>]

$ kill -STOP 1525 1525 stoppé par signal 19

$ ps 1525 1526

PID TTY STAT COMMAND

1525 pts/0 T ./exemple_wait_2

1526 pts/0 Z [exemple_wait_2 <defunct>]

$ kill -CONT 1525 $ kill -TSTP 1525 1525 stoppé par signal 20

$ kill -CONT 1525 $ kill -TERM 1525 1525 terminé par signal 15

$

Nous voyons que le fils 2, de PID 1526, reste à l’état zombie dès qu’il se finit car le père ne demande pas son code de retour. Le fait d’avoir appelé l’option WUNTRACED nous permet d’être informé lorsque le processus fils 1 est temporairement stoppé par un signal.

Les fonctions wait() et waitpid() sont définies par SUSv3, contrairement aux deux autres fonctions que nous allons étudier, wait3() et wait4(), qui sont d’inspiration BSD. Elles permettent, par rapport à wait() ou waitpid(), d’obtenir des informations supplémentaires sur le processus qui s’est terminé. Ces renseignements sont transmis par l’intermédiaire d’une structure rusage, définie dans <sys/resource.h>.

Cette structure regroupe des statistiques sur l’utilisation par le processus des ressources système. Le type struct rusage contient les champs suivants :

Type Nom Signification

struct timeval ru_utime Temps passé par le processus en mode utilisateur.

struct timeval ru_stime Temps passé par le processus en mode noyau.

long ru_maxrss Taille maximale des données placées simultanément en mémoire (exprimée en Ko).

long ru_ixrss Taille de la mémoire partagée avec d’autres processus (exprimée en Ko).

long ru_idrss Taille des données non partagées (en Ko).

long ru_isrss Taille de la pile exprimée en Ko.

long ru_minflt Nombre de fautes de pages mineures (n’ayant pas nécessité de recharge-ment depuis le disque).

long ru_majflt Nombre de fautes de pages majeures (ayant nécessité un rechargement des données depuis le disque).

long ru_nswap Nombre de fois où le processus a été entièrement swappé.

long ru_inblock Nombre de fois où des données ont été lues.

long ru_oublock Nombre de fois où des données ont été écrites.

long ru_msgsnd Nombre de messages envoyés par le processus.

long ru_msgrcv Nombre de messages reçus par le processus.

La structure rusage est assez riche, malheureusement Linux ne remplit que très peu de champs.

Les seules informations renvoyées sont les suivantes :

• Les temps ru_utime et ru_stime.

• Les nombres de fautes de pages ru_minflt et ru_majflt.

• Le nombre de fois où le processus a été swappé ru_nswap.

Les données fournies par cette structure sont surtout utiles lors de la mise au point d’applica-tions assez critiques, si on désire suivre très précisément la gestion mémoire ou les entrées-sorties d’un processus.

La structure timeval qu’on rencontre dans les deux premiers champs se décompose elle-même ainsi (<sys/time.h>) :

Suivant les bibliothèques utilisées, les membres de la structure timeval ont parfois un autre type que long int, notamment time_t. Quoi qu’il en soit, ces types de données peuvent être affichées comme des entiers longs, avec le format « %ld » de fprintf().

Le prototype de la fonction wait3() est le suivant :

pid_t wait3 (int * status, int options, struct rusage * rusage);

Les options de wait3() sont les mêmes que celles de waitpid(), c’est-à-dire WNOHANG et WUNTRACED. Le status est également le même qu’avec wait() ou waitpid(), et on utilise les mêmes macros pour l’analyser.

Nous pourrions définir wait() en utilisant wait3(status, 0, NULL).

Dans notre exemple, nous allons afficher les valeurs de la structure rusage représentant le temps passé par le processus en mode utilisateur et le temps passé en mode noyau.

exemple_wait_3.c : #include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <signal.h>

#include <errno.h>

#include <sys/wait.h>

#include <sys/resource.h>

long ru_nsignals Nombre de signaux reçus par le processus

long ru_nvcsw Nombre de fois où le programme a volontairement cédé le processeur en attendant la disponibilité d’une ressource.

long ru_nivcsw Nombre de fois où le processus a été interrompu par l’ordonnanceur car il était arrivé à la fin de sa tranche de temps impartie, ou qu’un processus plus prioritaire était prêt.

Type Nom Signification

Type Nom Signification

long tv_sec Nombre de secondes long tv_usec Nombre de microsecondes

int

break;

} }

return EXIT_SUCCESS;

}

Le processus fils effectue une boucle, en mode utilisateur uniquement, avant de se stopper lui-même. Le processus père le relance alors. Le fils passe ensuite à une boucle au sein de laquelle il fait des appels-système, et fonctionne donc en mode noyau. Voici un exemple d’exécution :

$ ./exemple_wait_3 Fils : PID = 2017

2017 stoppé par signal 19 Je le relance

Temps utilisateur 0 s, 100000 µs Temps en mode noyau 0 s, 0 µs 2017 terminé par exit (0) Temps utilisateur 1 s, 610000 µs Temps en mode noyau 2 s, 290000 µs

$

La dernière fonction de cette famille est wait4(). Elle permet d’obtenir à la fois des statisti-ques, comme avec wait3(), et d’attendre un processus fils particulier, comme waitpid(). Le prototype de cette fonction est le suivant :

pid_t wait4 (pid_t pid, int * status,

int options, struct rusage * rusage);

La fonction wait4() est la seule qui soit un véritable appel-système sous Linux ; les autres fonctions de cette famille en découlent ainsi :

Depuis Linux 2.6, un nouvel appel-système a fait son apparition : waitid(). Il permet en outre d’attendre qu’un processus fils redémarre après un arrêt, et d’obtenir les informations de status dans un format différent (une structure siginfo_t que nous verrons dans le chapitre 8).

Les constantes symboliques nécessaires au fonctionnement de waitid n’étant pas disponibles dans la bibliothèque C au moment de la rédaction de ces lignes, nous ne le détaillerons pas plus.

Le lecteur intéressé pourra se reporter au standard SUSv3 qui décrit son fonctionnement.

Dans le document Programmation système en C sous Linux (Page 123-134)