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[])
où 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().
- É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é. - 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. - 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 ? - 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.
- Écrivez un programme C appelé
pg1.c
(utilisant l'appel systèmeread()
dans lequel une fonctionlire()
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). - Écrivez un programme
pg2.c
en utilisant l'appel noyauexeclp()
pour lancer l'exécution depg1
(sans modifier ce dernier) de telle façon quepg1
lise en fait le fichier./test.data
et écrive dans le fichier./res.data
plutôt que sur l'écran. - Placez la fonction
lire()
dans un fichierlire.c
puis compilez là à l'aide la commandegcc -c lire.c
(produitlire.o
). On peut utiliser la fonctionlire()
depuis un programmepg3.c
relié àlire.o
par la commandegcc pg3.c lire.o -o pg3
. Il ne faut pas modifier le source de la fonctionlire()
. Le programmepg3
doit appeler la fonctionlire()
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 programmepg3
après l'exécution delire()
. Vous devez donc sauvegarder les valeurs par défaut de l'entrée standard, sortie standard et erreur (en utilisantdup()
) 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