Page professionnelle – Thomas Robert

Maître de Conférence

TP processus lourd et léger et chargement

Durant ce TP, vous aller mettre en pratique votre compréhension des concepts suivants :

  • le rôle de la pile dans le contrôle de l’exécution d’un programme. mise en pratique d’une altération licite de la pile.
  • L’application directe du cours concernant fork, et exec, wait et exit dans les section II. (Requis pour note de 1)
  • Un usage avancé de fork exec wait pour mettre en œuvre diviser pour régner en section III  (participe à la note de 2).
  • La découverte de la fonction clone qui permet de créer soit un processus à part entière, soit un processus dit léger partageant tout sauf la pile d’exécution en section IV. (participe à 2)

I Observations et Réflexions autour de la pile d’exécution (pas inclus dans le rendu)

Nous allons utiliser ici Tutor C un outil qui permet de visualiser une version simplifiée du contenu de la pile d’exécution d’un programme.

Cela vous permet aussi de comprendre la notion de portée d’une variable locale au cas où ce n’est pas déjà le cas :

Copiez le code ci-dessous dans la zone de code la page suivante

#include <stdio.h>

int f (int a, int b){
int aux ;
aux=a+b; 
return aux ;
}

int main() {
int i,j ;
i=5;
j=12;
j=f(i,j);
printf("hello %d",j);
return 0;
}

  1. Utilisez le bouton « Visualize Execution » pour voir l’exécution pas à pas du code (bouton « Forward »).  Notez que cet outil offre une visualisation simplifiée et limitée du comportement des fonctions dont il connaît le code.
  2.  Que se passe-t-il lorsque vous cliquez sur Forward alors que le curseur est à sur l’instruction j= f(i,j);
    Notez que le but ici est que vous soyez capable d’expliquer l’affichage à un de vos collègues.
  3. Modifiez le code de sorte à renommer la variable aux en i dans la fonction f (pensez à changer tous les noms.) Visualisez l’exécution pour vous convaincre du principe de portée.
  4. Pourquoi à votre avis la pile d’exécution ne varie pas sur l’appel à printf () ?
  5. Utilisez le code de la fonction factorielle que vous avez développé en TP de C puis visualisez son exécution pas à pas pour la valeur de paramètre 4. (notez que vous pouvez utiliser ce code tout fait si vous ne retrouvez pas le votre ….)
  6. Modifiez le fonctionnement de f et le code de main de sorte que f ait la signature void f(int* a1, int*a2) et place à l’adresse a1 le résultat de la somme des deux entiers se trouvant avant l’appel à f aux adresses a1 et a2. utilisez cette fonction sur les variables local de main i et j. Visualisez l’exécution pas à pas.
  7. Utilisez malloc() pour instancier un tableau de 2 entiers et initialisez le tableau avec 12 et 3. Utilisez f sur respectivement le premier et dernier élément du tableau.

L’ensemble du contenu de la pile est stocké en mémoire dans la mémoire du processus qui exécute votre code séquentiel. Quand vous créez un nouveau processus il recopie juste l’ensemble de la mémoire du processus, contenu de la pile inclus au détail près que le processus créé à l’illusion d’exécuter l’instruction return pour fork() avec 0 pour valeur de retour.  Malheureusement Tutor C ne supporte pas le multi processing … il va falloir donc faire des observations un peu plus basiques.

 

II Usage de Fork et Exec premiers usages (code à rendre plus, fichier de sorties).

Modifier le programme suivant (appelez le fichier hellofork.c par exemple)  pour qu’à l’exécution la fonction printf(« Bonjour »); soit appelée dans deux processus différents.

#include <stdio.h>
int main(int argc, char*  argv[]){
printf("Bonjour!\n");
return 0;}

Fork ()

Notez que le manuel de la fonction fork() doit vous aider à savoir quel fichier de headers (fichier .h) vous devez inclure pour que la compilation se passe sans problème.

  1.  En supposant que vous avez compilé votre programme pour produire l’exécutable hellofork, exécutez ./hellofork puis strace -f ./hellofork. Vérifiez que le comportement vous semble cohérent.
    Notez que strace permet d’avoir un résumé en parallèle de l’exécution d’un programme de la séquence d’appel systèmes réalisés par le programme tracé (attention les appels de fonction définies dans le binaire ne sont pas tracées). L’option -f permet pour un programme multi contexte d’exécution de savoir quel contexte réalise l’appel.
  2. Améliorez la lisibilité de la sortie de cette commande en redirigeant la sortie standard vers le fichier progOut, et la sortie d’erreur vers traceOut (vous utiliserez les redirections 1> nom de fichier et 2> nom de fichier cf TP shell unix a priori ). Consultez le contenu des fichiers avec la commande more. Vous rendrez  ces deux fichiers de sortie et le code de hellofork().
  3. Exécutez le programme suivant et tentez d’attribuer chaque ligne à l’un des deux processus impliqués dans l’exécution de ce programme.

#include <unistd.h>
#include <stdio.h>

int main(int argc, char * argv[]){
int f=fork ();
printf(« Bonjour \n »);
if (f==0){ sleep(1);} else {sleep(2);}
printf(« Au revoir \n »);
return 0;
}

Un outil pratique : la commande pidof nom_de_fichier cette commande permet d’obtenir le process Identifier de tous les processus ayant chargé le fichier binaire dont vous avez passé le nom en paramètre (connu grâce à argv[0]). Attention il doivent encore être en cours d’exécution ….

Important : Note de vocabulaire, le processus créé est appelé fils, le processus appelant fork est appelé processus père.

execl() et execv()

La fonction execl() permet de charger un binaire dans un processus et d’en démarrer l’exécution à partir de son point d’entrée. L’usage classique de cette fonction est : execl(chemin_du_binaire, chaine0, chaine1, chaine2 …, chaineN, NULL);

Notez que chacun des paramètres soulignés est censé être une chaîne de caractères. les paramètres chaîne0…. chaineN sont utilisés pour initialiser les variables « argc » et « argv » du main du binaire chargé. (e.g. argc= N+1, et chaque chaîneI correspond à argv[I]).

  1. Écrivez un programme qui réalise uniquement un appel à execl() avec les paramètres permettant de charger le binaire correspondant à la commande ls de telle sorte qu’une fois chargée cette commande n’ait aucun paramètre. (rendez ce code dans execl1.c). On notera qu’un moyen d’obtenir le chemin d’une commande est d’exécuter la commande which ( e.g. which ls).
  2. En utilisant execv(), on souhaite exécuter cette fois ls mais avec la même liste de paramètres que celle utilisée pour lancer le programme exécutant execv(). (rendez ce code dans un fichier appelé execl2.c.)
  3. Exécutez ce programme en utilisant la ligne de commande suivante (qui fait l’hypothèse que vous avez compilé execl2.c pour produire execl2)
    ./execl2 -lpuis ./execl2 -l *.c
    (si le détail des fichiers ne change pas d’une commande à l’autre, c’est que votre code ne correspond pas à l’attendu).

Les fonctions exit() et wait()

Il est possible de synchroniser l’exécution du père et du fils via un modèle « join »: mettre un processus en attente de la fin de l’exécution de ses fils. Un usage de ce genre de pratique se justifie lorsque le processus père doit réaliser des actions qui n’ont de sens que si les processus fils sont terminés.

L’appel système wait(int *) permet de vérifier si un processus créé par le processus appelant a terminé son exécution (soit via l’exécution de return dans son main, soit via l’appel system exit()).

    • si c’est le cas wait() ne fait pas changer le processus appelant d’état d’ordonnancement et écrit dans l’entier dont l’adresse a été passée en paramètre différentes informations (cf plus bas pour leur interprétation).
    • si ce n’est pas le cas, soit il n’y a aucun processus fils pour le processus appelant et wait retourne -1, soit le processus passe dans l’état suspendu en attendant qu’un de ses fils ait terminé son exécution.

Le processus fils peut « communiquer » une information sur la manière dont il a terminé son exécution via le paramètre de l’appel à exit ou la valeur utilisée lors de l’exécution de return pour sa fonction main().  Dans les deux cas, ces valeurs doivent correspondre à un octet (-128 …127).

En supposant que l’entier dont vous avez passé l’adresse à wait s’appelle i. Vous ne pouvez pas lire directement la valeur de i. Nous vous conseillons d’utiliser des macros qui vous permettrons d’obtenir les valeurs souhaitées (voici leurs description en anglais):

  • WIFEXITED(wstatus)  returns true if the child terminated normally, that is, by calling exit(3) or _exit(2), or by returning from main().
  • WEXITSTATUS(wstatus) returns the exit status of the child. This consists of the least significant 8 bits of the status argument that the child specified in a call to exit(3) or _exit(2) or as the argumentfor a return statement in main(). This macro should be employed only if WIFEXITED returned true.
  1. En vous servant du code ci dessous, ajoutez les appels systèmes wait et exit nécessaire pour garantir que l’affichage du message « Tout est fini\n » ne puisse jamais avoir lieu avant l’affichage de « On m’a créé pour faire cet affichage\n » et ce quel que soient les valeurs des macro T1 et T2.  (Rendez ce code)

#include <unistd.h>
#include <stdio.h>
#define T1 10
#define T2 5

int main(int argc, char * argv[]){
int f=fork ();
if (f==0){ sleep(T1); printf(« On m’a cree pour faire cet affichage\n »);}
else{sleep(T2);printf(« Tout est fini \n »);}
return 0;
}

  1. Modifiez ce code de sorte à ce que l’exécution de votre programme entraîne la création d’un nombre de fils spécifié en paramètre (attention conversion numérique d’une chaîne de caractère nécessaire, utilisez atoi ()), et que chaque fils attende k * 1s  ou k est le nombre de processus fils créés +1.  Adaptez la synchronisation par wait de sorte à garantir qu’aucun processus fils ne fera son affichage après l’affichage de « Tout est fini\n » par le père. (Rendez ce code)

Rem : au cas où vous auriez une exécution « bloquée » vous pouvez la terminer de manière abrupte via Ctrl-C

III Usage « utile » de Fork (code à rendre)

Cet exercice est moins encadré que les précédents, nous vous demandons d’écrire un programme qui aura pour paramètre une chaine de caractère s suivie d’une liste de fichier non vide (tout ceci passé via la ligne de commande). L’exécution de ce programme consiste à exécuter le binaire de grep sur chaque fichier dont le nom est passé en paramètre pour y chercher la chaîne de caractère s dans un processus différent

Ainsi, si votre programme est compilé pour produire l’exécutable parallel_grep, alors l’exécution de la commande :

./parallel_grep « Lac » f1 f2 f3

Doit déclencher la création de 3 processus fils, chacun exécutant respectivement l’équivalent de

  • grep « Lac » f1
  • grep « Lac » f2
  • grep « Lac » f3

Le processus exécutant le main de votre programme doit lui afficher « Motif non trouvé\n » dans le cas où la recherche dans chaque fichier aurait échoué (indice cela revient à ce que chaque processus fils ait terminé son exécution dans un certains « status » cf page de man de grep section exit status).

Vous pouvez utiliser les fichiers placés dans le répertoire suivant /cal/homes/trobert/dataTP1 pour vos tests  (vous pouvez recopier les fichier en local dans /tmp par exemple). Notez que ces fichiers ne contiennent pas 23403.

En pratique il est possible de constater que pour un nombre de fichier important et des tailles de fichiers importantes, la parallélisation fait réellement gagner du temps. Cependant, c’est complexe à observer factuellement vous pouvez utiliser ltrace avec une option de time stamps pour faire cela.  (non attendu dans le rendu)

IV Usage de clone processus vs processus léger

La fonction clone () permet de choisir lors de la création de processus du degré d’indépendance entre les deux contextes d’exécution (l’ancien et le nouveau). En bref, vous pouvez exiger que les deux contextes partagent un plus ou moins grand nombre de choses (essentiellement en mémoire).

Voici un code qui permet de créer un processus ne partageant ni mémoire ni ressource (essentiellement similaire à fork) à un détail près… le nouveau processus démarre son exécution à partir d’une fonction dont l’adresse est passée en paramètre de clone. Cependant la ligne contenant l’appel effectif à clone a été oubliée …. pouvez vous la réécrire  (pour le paramètre flags de clone, utilisez la macro PROCESSFLAGS. Vous devriez comprendre en lisant le manuel pourquoi cette valeur permet de ne rien partager ….

#define _GNU_SOURCE
#include <sys/utsname.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define THREADFLAGS CLONE_THREAD|CLONE_VM|CLONE_SIGHAND
#define PROCESSFLAGS 0

char stack[64*1024];
int i=0;

int helloclone (void * texte ){
printf(« Hello from clone\n »);
i++;
printf(« From clone, i=%d\n », i);
printf(« Clone PID=%d\n », getpid());
return 0;
}

int main (int argc, char* argv[]){
// oups il manque une ligne ici …..
i–;
printf(« From first execution context, i=%d\n », i);
printf(« From first execution context, PID=%d\n », getpid());
printf(« bye !\n »);
return 0;
}

(rendez ce code sous le nom de fichier cloneprocess.c)

Modifiez ce code de sorte à ce que le nouveau contexte d’exécution partage sa mémoire, i.e. création d’un processus léger (un thread). Vous pouvez utiliser la macro THREADFLAGS pour vous faciliter la vie. (rendez ce code sous le nom de fichier clonethread.c)

Notez qu’il est possible que l’exécution de ce programme ait des problèmes d’intégrité, essayez de deviner pourquoi ? Vérifiez que le problème ne se pose plus en utilisant fprintf(STDOUT_FILENO , …) et fprintf(STDERR_FILENO , …..) dans respectivement le processus (lourd ou léger ) appelant clone, et dans celui résultant de l’exécution de clone.