Nous travaillons sur le programme
prog06_redir_exec.cpp.
Ce programme nous montrera que les tubes anonymes facilitent la réutilisation
de programmes déjà existants par redirection d'entrées/sorties et
recouvrement dans un processus enfant.
En l'état, le programme fourni est assez proche de ce qui a été réalisé
à l'étape précédente : une communication par tube est mise
en place entre un processus enfant et son processus parent.
Seulement, quand on exécute ce programme
$ ./prog06_redir_exec ↵
nous constatons que le processus parent ne détecte rien d'autre que
la fin de fichier en provenance du processus enfant.
En effet, ce dernier, lorsque aucun paramètre n'est transmis sur la
ligne de commande du programme, se contente d'afficher un message
sur sa sortie standard avec
crs::out() puis se termine avec
crs::exit().
Le tube n'est donc pas du tout utilisé par le processus enfant, ce qui
explique que le processus parent n'en obtient rien.
Intervenez alors au niveau du point
{1}
du programme fourni afin de
procéder à la
redirection de la sortie standard du processus enfant
vers le tube (selon les recommandations des commentaires).
L'appel système
dup2() sert à indiquer que la ressource désignée
par son premier descripteur de fichier en paramètre (le côté écriture
du tube ici) doit également être désignée par son second descripteur de
fichier en paramètre (la sortie standard ici).
Juste avant cet appel, le descripteur de fichier de la sortie standard
désignait le terminal ; juste après cet appel il désigne dorénavant
le côté écriture du tube (tout comme le premier paramètre)
De la même façon que plusieurs pointeurs peuvent désigner une même donnée
en mémoire, plusieurs descripteurs de fichier peuvent désigner la
même ressource de communication ; c'est le cas suite à l'appel
système dup2().
.
Désormais, écrire dans le descripteur de fichier de la sortie standard
provoquera le même effet qu'écrire dans le descripteur de fichier
qui désigne le côté écriture du tube.
Ce dernier descripteur de fichier devient donc inutile puisque la sortie
standard désigne dorénavant la ressource que nous avons l'intention
d'utiliser (le côté écriture du tube) ; il est alors recommandé de
fermer ce descripteur de fichier.
En relançant l'exécution de ce même programme, vous devriez constater
que le message produit par le processus enfant dans sa sortie standard
(
crs::out()) n'apparaît plus directement dans le terminal mais
est bien extrait depuis le tube par le processus parent qui peut
l'exploiter à sa convenance
Dans cet exemple simpliste le processus parent se contente d'afficher
le message extrait du tube (en provenance du processus enfant) mais
de manière plus générale la réception d'un tel message pourrait servir
à alimenter n'importe un traitement utile à l'application.
.
En l'état, nous pourrions objecter que la redirection de la sortie standard
vers le tube n'apporte pas grand chose par rapport à une écriture directe
dans le tube comme nous le faisions
à l'étape précédente.
En effet, en terme de rédaction de code source, il n'est pas plus compliqué
d'écrire dans un tube que dans la sortie standard.
Cependant, cette démarche prend tout son intérêt si après cette redirection
nous invoquons du code déjà existant qui écrit systématiquement dans
la sortie standard (parce qu'il ignore simplement l'existence de ce
tube).
Ce cas de figure est très fréquent lorsque nous effectuons un
recouvrement de processus.
Pour mettre en œuvre cette démarche de recouvrement, nous allons utiliser
comme prétexte la décompression d'un fichier au format
"gzip".
Tout d'abord, en vue de l'expérimentation, obtenons un simple fichier qui
contient du texte compressé grâce à la commande
shell suivante :
$ ls -l | gzip > output_file.gz ↵
(la liste détaillée du contenu du répertoire courant, affichée par la
commande
ls -l, est compressée par la commande
gzip
Sans argument, la command
gzip se contente de lire tout ce qui
arrive sur son entrée standard, le compresse, et inscrit le
contenu compressé sur sa sortie standard.
Ici le symbole
|
(
pipe) du
shell relie la sortie
standard de la commande
ls -l (des lignes de texte) à l'entrée
standard de la commande
gzip grâce à des redirections dans un
tube (exactement comme ce que nous sommes en train d'étudier !).
Le symbole
>
du
shell redirige la sortie standard de la
commande
gzip vers le fichier indiqué juste après.
et stockée
dans
output_file.gz).
Nous souhaitons donc que notre processus enfant consomme le contenu compressé
du fichier
output_file.gz, le décompresse et produise le contenu
décompressé dans le tube de communication afin que le processus parent
l'obtienne.
Seulement, pour réaliser cette opération nous allons nous appuyer
sur l'utilisation telle quelle de la commande
gunzip dans notre
processus enfant.
En effet, cette commande, lorsqu'elle est utilisé sans argument, se contente
de lire tout ce qui arrive sur son entrée standard, le décompresse, et
inscrit le contenu décompressé sur sa sortie standard.
Il s'agit alors dans un premier temps d'effectuer les redirections
d'entrée/sortie attendues.
Nous avions déjà redirigé la sortie standard au niveau du point
{1}
; il faut maintenant compléter le point
{2}
d'une
manière très similaire afin que l'entrée standard du processus enfant ne
corresponde plus à la saisie au clavier mais au fichier préalablement
ouvert en lecture.
Il reste enfin à faire en sorte que le processus enfant exécute le code
de la commande
gunzip.
C'est ici qu'intervient la notion de recouvrement : il s'agit de
complètement se débarrasser du code et des données d'un processus
pour les remplacer par un tout nouveau programme qui démarre son
exécution.
La fonction utilitaire
crs::exec() (qui repose en interne sur l'appel
système
execve()) attend comme paramètre un tableau de chaînes
qui sera interprété comme une ligne de commande : la première chaîne
est le nom du programme, la seconde son premier argument...
Complétez alors le point
{3}
pour recouvrir le processus enfant par
la simple commande
gunzip.
Lorsque cet appel réussi (le programme exécutable existe) nous n'en revenons
jamais !
En effet, tout le code qu'exécutait le processus enfant vient d'être remplacé
par celui du programme choisi ; toute instruction qui suivait l'appel
à
crs::exec() n'est désormais plus accessible à ce processus.
Toutefois, ce recouvrement ne concerne que le processus enfant ; le
processus parent n'a pas invoqué
crs::exec() et continue d'exécuter
son code pour exploiter ce qu'il extrait du tube de communication.
Il est important de noter que, bien que tout le code et toutes les données
d'un processus sont remplacés lors d'un recouvrement, ses moyens de
communication (tubes, fichiers...) restent ouverts et les redirections
restent effectives.
Ainsi, le processus enfant qui exécute désormais la commande
gunzip
a bien son entrée standard reliée au fichier choisi et sa sortie
standard au coté écriture du tube.
Dans ces conditions, vous pouvez tester votre programme en précisant
le nom du fichier compressé sur sa ligne de commande.
$ ./prog06_redir_exec output_file.gz ↵
Vous devriez constater que le processus parent obtient bien (et affiche) le
contenu décompressé du fichier en question
Si vous souhaitez visualiser le contenu décompressé de ce fichier
depuis le shell (sans votre programme) vous pouvez utiliser
la commande suivante :
$ gunzip < output_file.gz ↵
.
Grâce à la création d'un processus enfant, la redirection des ses
entrée/sortie standards et le recouvrement par un autre programme,
nous sommes parvenus à utiliser dans notre propre programme (le
processus parent) les services d'un programme existant (la commande
gunzip) en l'utilisant tel quel, sans avoir à le réécrire pour
l'adapter à notre besoin.
Le recouvrement que nous venons d'expérimenter ici est un élément central des
systèmes d'exploitation.
Il faut savoir que tous les programmes que nous lançons dans un système
d'exploitation (automatiquement au démarrage, par la saisie d'une commande,
en cliquant dans un outil...) reposent sur ce mécanisme.
Puisqu'il remplace le contenu d'un processus existant, il est très courant
de faire apparaître un processus enfant juste à cette occasion, ce qui
permet de conserver l'activité initiale du processus parent ; la
séquence d'appels système
fork()/
execve() est très représentative
du lancement d'un nouveau programme.
Les opérations de redirections d'entrées/sorties avec l'appel système
dup2() prennent tout leur intérêt dans ce contexte car cela permet de
relier entre eux et faire travailler de concert des programmes qui n'avaient
pas été explicitement conçus dans ce but.
C'est d'ailleurs un point marquant de la manière dont ont été conçus les
systèmes d'exploitation de la famille UNIX : proposer une multitude
de commandes élémentaires et des moyens d'interconnexion pour les faire
collaborer afin de réaliser des services de plus haut niveau
Les adeptes de la ligne de commande du
shell doivent reconnaître
ceci à travers l'usage des symboles
|
,
>
,
<
...
.
Documentation utile :
- man 1 gunzip
- crs::pipe() → man 2 pipe
- crs::fork() → man 2 fork
- crs::waitpid() → man 2 waitpid
- crs::close() → man 2 close
- crs::openR() → man 2 open
- crs::dup2() → man 2 dup2
- crs::exec() → man 2 execve
- crs::read() → man 2 read