TP Fonctions du Noyau pour la Gestion des Processus (IFN)

Conseil Préliminaire Dans vos programmes, pour conserver un minimum de lisibilité tous les programmes utilisant l'appel système fork() devront être structurés comme suit, où fils() est une fonction qui se termine par exit():

int pid;
if ( (pid=fork()) == -1 ) {
    perror("fork");
    exit(1);
}
if ( 0 == pid )
    fils();
else 
    pere();

1 Création de processus, synchronisation et communication par tube

On met en oeuvre la création de processus fils, la communication par tube entre père et fils, et la synchronisation par attente de terminaison ou sur signal.

1.1 Un père, deux fils

Écrivez un programme en C qui crée deux fils. Le processus père et les processus fils doivent indiquer leur numéro de processus, le numéro de leur père, ainsi que la valeur renvoyée par fork() au moment de la création. Exécutez le programme plusieurs fois. Les valeurs affichées et l'ordre d'affichage sont-ils toujours les mêmes ? Que signifie le fait qu'un processus ait pour père le processus numéro 1 ?

1.2 Saisie de nombres

Le processus père doit maintenant permettre à l'utilisateur de rentrer des nombres. Il transmet, à l'aide d'un ou plusieurs tubes, les nombres impairs au premier fils et les nombres pairs au second fils (on transmet, au choix, soit la valeur binaire d'un int soit la suite des caractères constituant sont écriture décimale). Quand l'utilisateur rentre la valeur 0, le père indique à ses fils qu'ils doivent se terminer en leur transmettant la valeur 0, puis se termine lui-même. Chaque processus affiche un message d'adieu avant de se terminer.

1.3 Terminaison

Modifiez votre programme pour que chaque fils attende 5 secondes après avoir reçu la valeur 0, puis se termine.

  • Modifiez votre programme pour que le processus père, après avoir transmis la valeur 0 à ses fils, attende que ses deux fils soient terminés avant de terminer lui-même. Le processus père doit annoncer la fin de ses fils avant de se terminer lui-même.
  • Dans une nouvelle variante, lorsque l'utilisateur entre 0, le processus père envoie à chacun de ses fils un signal SIGUSR1 qui les tue. Quel est l'inconvénient de procéder ainsi ?
  • Dernière variante : lorsque l'utilisateur entre 0, le processus père ferme ses tubes et se termine. Lorsque les fils s'aperçoivent que leur tube est vide et fermé, alors ils doivent se terminer.

2 Communication multiple par tube

Exercice similaire à la question précédente mais on cherche à comprendre comment deux fils se partagent la lecture dans un tube commun établi avec un père.

2.1 Le père écrit, le fils lit

Ecrire un programme qui créé un tube, puis à l'aide d'un fork, créé un père et un fils. Le père écrit ensuite des caractères, qui sont lus et affichés par le fils.

2.2 Deux fils écrivent, le père lit

De manière similaire à la question précédente, écrire un programme qui à l'aide de fork() créé un processus père, et deux processus fils. Les deux processus fils vont produire des caractères, qui seront écrits dans leur entrée de tube. Ces deux entrées sont connectées à une sortie, lue par le père.

Chaque fils exécute une fonction de prototype void producteur( char id, int iter, int tube[])id est l'identité de fils ('A' ou 'B'), iter est le nombre d'octets à écrire dans tube. Le père joue le rôle de consommateur en lisant en permance l'entrée du tube et affiche les caractères qui en sortent. Cet exercice montre comment les fils accèdent de manière concurrente au tube.

3 Création de processus et table des fichiers ouverts

3.1 Partage de fichier

On cherche à montrer que l'environnement d'un processus relatif aux fichiers est partagé lors d'un fork().

  1. Écrivez un programme C qui commence par un appel à fork() pour dupliquer le processus initial (le père) de manière à obtenir un fils. Le père et le fils ont ensuite le même comportement : ils ouvrent le fichier ./test.data puis y lisent 10 caractères et les affichent sur la sortie standard après avoir affiché leur identité.
  2. Effectuez ensuite le fork() juste avant la lecture. Répétez l'exécution plusieurs fois et déterminez quels sont les caractères lus par le processus père et ceux lus par le processus fils.
  3. Dans le père, positionnez le pointeur de lecture sur le 30e caractère par un appel de lseek() avant l'appel à fork(). Quelle est alors la position des caractères lus par le fils ?
  4. Modifiez le programme pour que le fork() ait lieu avant l'ouverture du fichier. Comparez les résultat.

3.2 Partage de la table des fichiers ouverts et redirections

On cherche à montrer que la table des fichiers ouverts d'un processus créé par fork() est héritée de son père. Ainsi, en mettant en place des redirections avec l'appel système dup(), celles ci sont transmises aux fils, permettant la communication entre plusieurs processus à l'aide des entrées-sorties.

  1. Écrivez un programme C appelé pg1.c (utilisant l'appel système read() dans lequel une fonction lire() tente de lire en une seule fois 100 caractères sur l'entrée standard (c'est-à-dire le clavier). Elle affiche sur sa sortie erreur (l'écran) le nombre de caractères effectivement lus, puis les affiche sur la sortie standard (l'écran aussi).
  2. Écrivez un programme pg2.c en utilisant l'appel noyau execlp() pour lancer l'exécution de pg1 (sans modifier ce dernier) de telle façon que pg1 lise en fait le fichier ./test.data et écrive dans le fichier ./res.data plutôt que sur l'écran.
  3. Placez la fonction lire() dans un fichier lire.c puis compilez là à l'aide la commande gcc -c lire.c (produit lire.o). On peut utiliser la fonction lire() depuis un programme pg3.c relié à lire.o par la commande gcc pg3.c lire.o -o pg3. Il ne faut pas modifier le source de la fonction lire(). Le programme pg3 doit appeler la fonction lire() de telle sorte qu'elle lise en fait sur le fichier ./test.data et écrive dans le fichier ./res.data. On souhaite également pouvoir à nouveau utiliser l'entrée et la sortie standard dans le programme pg3 après l'exécution de lire(). Vous devez donc sauvegarder les valeurs par défaut de l'entrée standard, sortie standard et erreur (en utilisant dup()) avant de les fermer.

4 Émulation de l'interpréteur de commandes

De manière similaire à l'exercice précédent, on se sert des redirections héritées lors d'un fork() pour implémenter l'équivalent de commandes shell synchronisées par tube.

Écrivez un programme C qui réalise exactement la même chose que l'interpréteur de commandes lorsqu'on tape :

% ps aux | grep bash | sort

À la fin de l'exécution, votre programme doit indiquer si tout s'est bien passé (c'est-à-dire si les programmes ps, grep et sort ont renvoyé un code d'erreur égal à zéro).

Indication : adoptez la structure suivante

Date: 12 octobre 2015

Author: A. Ancel, S. Genaud et D. Grad

Created: 2015-10-12 Lun 10:43

Emacs 24.4.51.2 (Org mode 8.2.10)

Validate