Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

TP5 - Programmation Système C

Télécharger au format pdf ou txt
Télécharger au format pdf ou txt
Vous êtes sur la page 1sur 38

15

Les tuyaux sous Unix


Les tuyaux 1 permettent un groupe de processus denvoyer des donnes un autre
groupe de processus. Ces donnes sont envoyes directement en mmoire sans tre
stockes temporairement sur disque, ce qui est donc trs rapide.
Tout comme un tuyau de plomberie, un tuyau de donnes a deux cts : un ct
permettant dcrire des donnes dedans et un ct permettant de les lire. Chaque ct du
tuyau est un descripteur de fichier ouvert soit en lecture soit en criture, ce qui permet
de sen servir trs facilement, au moyen des fonctions dentre / sortie classiques.
La lecture dun tuyau est bloquante, cest--dire que si aucune donne nest disponible en lecture, le processus essayant de lire le tuyau sera suspendu (il ne sera pas
pris en compte par lordonnanceur et noccupera donc pas inutilement le processeur)
jusqu ce que des donnes soient disponibles. Lutilisation de cette caractristique
comme effet de bord peut servir synchroniser des processus entre eux (les processus
lecteurs tant synchroniss sur les processus crivains).
La lecture dun tuyau est destructrice, cest--dire que si plusieurs processus lisent
le mme tuyau, toute donne lue par lun disparat pour les autres. Par exemple, si un
processus crit les deux caractres ab dans un tuyau lu par les processus A et B et que
A lit un caractre dans le tuyau, il lira le caractre a qui disparatra immdiatement du
tuyau sans que B puisse le lire. Si B lit alors un caractre dans le tuyau, il lira donc
le caractre b que A, son tour, ne pourra plus y lire. Si lon veut donc envoyer des
informations identiques plusieurs processus, il est ncessaire de crer un tuyau vers
chacun deux.
De mme quun tuyau en cuivre a une longueur finie, un tuyau de donnes une
capacit finie. Un processus essayant dcrire dans un tuyau plein se verra suspendu en
attendant quun espace suffisant se libre.
Vous avez sans doute dj utilis des tuyaux. Par exemple, lorsque vous tapez
menthe22> ls | wc -l

linterprte de commandes relie la sortie standard de la commande ls lentre


standard de la commande wc au moyen dun tuyau.
1. Le terme anglais est pipe, que lon traduit gnralement par tuyau ou tube.

303

Chapitre 15. Les tuyaux sous Unix


Les tuyaux sont trs utiliss sous UNIX pour faire communiquer des processus
entre eux. Ils ont cependant deux contraintes :
les tuyaux ne permettent quune communication unidirectionnelle ;
les processus pouvant communiquer au moyen dun tuyau doivent tre issus dun
anctre commun qui devra avoir cr le tuyau.

15.1

Manipulation des tuyaux

Lappel systme pipe()

Un tuyau se cre trs simplement au moyen de lappel systme pipe() :


#include <unistd.h>
int tuyau[2], retour;
retour = pipe(tuyau);
if ( retour == -1 ) {
/* erreur : le tuyau na pas pu etre cree */
}

Largument de pipe() est un tableau de deux descripteurs de fichier (un descripteur


de fichier est du type int en C) similaires ceux renvoys par lappel systme open()
et qui sutilisent de la mme manire. Lorsque le tuyau a t cr, le premier descripteur,
tuyau[0], reprsente le ct lecture du tuyau et le second, tuyau[1], reprsente le
ct criture.
Un moyen mnmotechnique pour se rappeler quelle valeur reprsente quel ct
est de rapprocher ceci de lentre et de la sortie standard. Lentre standard, dont le
numro du descripteur de fichier est toujours 0, est utilise pour lire au clavier : 0
lecture. La sortie standard, dont le numro du descripteur de fichier est toujours 1, est
utilise pour crire lcran : 1 criture.
Nanmoins, pour faciliter la lecture des programmes et viter des erreurs, il est
prfrable de dfinir deux constantes dans les programmes qui utilisent les tuyaux :
#define LECTURE 0
#define ECRITURE 1

Mise en place dun tuyau

La mise en place dun tuyau permettant deux processus de communiquer est


relativement simple. Prenons lexemple dun processus qui cre un fils auquel il va
envoyer des donnes :
1. Le processus pre cre le tuyau au moyen de pipe().
2. Puis il cre un processus fils grce fork(). Les deux processus partagent donc
le tuyau.
3. Puisque le pre va crire dans le tuyau, il na pas besoin du ct lecture, donc il
le ferme.
304

15.1. Manipulation des tuyaux


4. De mme, le fils ferme le ct criture.
5. Le processus pre peut ds lors envoyer des donnes au fils.
Le tuyau doit tre cr avant lappel la fonction fork() pour quil puisse tre
partag entre le processus pre et le processus fils (les descripteurs de fichiers ouverts
dans le pre sont hrits par le fils aprs lappel fork()).
Comme indiqu dans lintroduction, un tuyau ayant plusieurs lecteurs peut poser
des problmes, cest pourquoi le processus pre doit fermer le ct lecture aprs lappel
fork() (il nen a de toute faon pas besoin). Il en va de mme pour un tuyau ayant
plusieurs crivains donc le processus fils doit aussi fermer le ct criture. Omettre de
fermer le ct inutile peut entraner lattente infinie dun des processus si lautre se
termine. Imaginons que le processus fils nait pas ferm le ct criture du tuyau. Si
le processus pre se termine, le fils va rester bloqu en lecture du tuyau sans recevoir
derreur puisque son descripteur en criture est toujours valide. En revanche, sil avait
ferm le ct criture, il aurait reu un code derreur en essayant de lire le tuyau, ce
qui laurait inform de la fin du processus pre.
Le programme suivant illustre cet exemple, en utilisant les appels systme read()
et write() pour la lecture et lcriture dans le tuyau :
Listing 15.1 Utilisation des tuyaux
#include
#include
#include
#include
#include

<sys/types.h>
<stdio.h>
<stdlib.h>
<string.h>
<unistd.h>

#define LECTURE 0
#define ECRITURE 1
int main(int argc, char *argv[]) {
int tuyau[2], nb, i;
char donnees[10];
if (pipe(tuyau) == -1) { /* creation du pipe */
perror("Erreur dans pipe()");
exit(EXIT_FAILURE);
}
switch (fork()) { /* les deux processus partagent le pipe */
case -1 :
/* erreur */
perror("Erreur dans fork()");
exit(EXIT_FAILURE);
case 0 :
/* processus fils, lecteur */
close(tuyau[ECRITURE]); /* on ferme le cote ecriture */
/* on peut alors lire dans le pipe */
nb = read(tuyau[LECTURE], donnees, sizeof(donnees));
for (i = 0; i < nb; i++) {
putchar(donnees[i]);
}
putchar(\n);
close(tuyau[LECTURE]);
exit(EXIT_SUCCESS);
default :
/* processus pere, ecrivain */
close(tuyau[LECTURE]); /* on ferme le cote lecture */
strncpy(donnees, "bonjour", sizeof(donnees));

305

Chapitre 15. Les tuyaux sous Unix


/* on peut ecrire dans le pipe */
write(tuyau[ECRITURE], donnees, strlen(donnees));
close(tuyau[ECRITURE]);
exit(EXIT_SUCCESS);
}
}

Fonctions dentres / sorties standard avec les tuyaux

Puisquun tuyau sutilise comme un fichier, il serait agrable de pouvoir utiliser les
fonctions dentres / sorties standard (fprintf(), fscanf()...) au lieu de read() et
write(), qui sont beaucoup moins pratiques. Pour cela, il faut transformer les descripteurs de fichiers en pointeurs de type FILE *, comme ceux renvoys par fopen().
La fonction fdopen() permet de le faire. Elle prend en argument le descripteur
de fichier transformer et le mode daccs au fichier ("r" pour la lecture et "w" pour
lcriture) et renvoie le pointeur de type FILE * permettant dutiliser les fonctions
dentre/sortie standard.
Lexemple suivant illustre lutilisation de la fonction fdopen() :
Listing 15.2 Dialogue pre / fils
#include
#include
#include
#include

<sys/types.h>
<stdio.h>
<stdlib.h>
<unistd.h>

#define LECTURE 0
#define ECRITURE 1
int main (int argc, char *argv[]) {
int tuyau[2];
char str[100];
FILE *mon_tuyau ;
if ( pipe(tuyau) == -1 ) {
perror("Erreur dans pipe()");
exit(EXIT_FAILURE);
}
switch (fork()) {
case -1 :
/* erreur */
perror("Erreur dans fork()");
exit(EXIT_FAILURE);
case 0 :
/* processus fils, lecteur */
close(tuyau[ECRITURE]);
/* ouvre un descripteur de flot FILE * a partir */
/* du descripteur de fichier UNIX */
mon_tuyau = fdopen(tuyau[LECTURE], "r");
if (mon_tuyau == NULL) {
perror("Erreur dans fdopen()");
exit(EXIT_FAILURE);
}
/* mon_tuyau est un FILE * accessible en lecture */
fgets(str, sizeof(str), mon_tuyau);
printf("Mon pere a ecrit : %s\n", str);
/* il faut faire fclose(mon_tuyau) ou a la rigueur */
/* close(tuyau[LECTURE]) mais surtout pas les deux */

306

15.1. Manipulation des tuyaux


fclose(mon_tuyau);
exit(EXIT_SUCCESS);
default :
/* processus pere, ecrivain */
close(tuyau[LECTURE]);
mon_tuyau = fdopen(tuyau[ECRITURE], "w");
if (mon_tuyau == NULL) {
perror("Erreur dans fdopen()");
exit(EXIT_FAILURE);
}
/* mon_tuyau est un FILE * accessible en ecriture */
fprintf(mon_tuyau, "petit message\n");
fclose(mon_tuyau);
exit(EXIT_SUCCESS);
}
}

Il faut cependant garder lesprit que les fonctions dentres / sorties standard
utilisent une zone de mmoire tampon lors de leurs oprations de lecture ou dcriture.
Lutilisation de cette zone tampon permet doptimiser, en les regroupant, les accs au
disque avec des fichiers classiques mais elle peut se rvler particulirement gnante
avec un tuyau. Dans ce cas, la fonction fflush() peut se rvler trs utile puisquelle
permet dcrire la zone tampon dans le tuyau sans attendre quelle soit remplie.
Il est noter que lappel fflush() tait inutile dans lexemple prcdent en
raison de son appel implicite lors de la fermeture du tuyau par fclose().
Lexemple suivant montre lutilisation de la fonction fflush() :
Listing 15.3 Forcer lcriture dans le tuyau
#include
#include
#include
#include

<sys/types.h>
<stdio.h>
<stdlib.h>
<unistd.h>

#define LECTURE 0
#define ECRITURE 1
int main(int argc, char *argv[]) {
int tuyau[2];
char str[100];
FILE *mon_tuyau;
if ( pipe(tuyau) == -1 ) {
perror("Erreur dans pipe()");
exit(EXIT_FAILURE);
}
switch (fork()) {
case -1 :
/* erreur */
perror("Erreur dans fork()");
exit(EXIT_FAILURE);
case 0 :
/* processus fils, lecteur */
close(tuyau[ECRITURE]);
mon_tuyau = fdopen(tuyau[LECTURE], "r");
if (mon_tuyau == NULL) {
perror("Erreur dans fdopen()");
exit(EXIT_FAILURE);
}
fgets(str, sizeof(str), mon_tuyau);
printf("[fils] Mon pere a ecrit : %s\n", str);

307

Chapitre 15. Les tuyaux sous Unix


fclose(mon_tuyau);
exit(EXIT_SUCCESS);
default :
/* processus pere, ecrivain */
close(tuyau[LECTURE]);
mon_tuyau = fdopen(tuyau[ECRITURE], "w");
if (mon_tuyau == NULL) {
perror("Erreur dans fdopen()");
exit(EXIT_FAILURE);
}
fprintf(mon_tuyau, "un message de test\n");
printf("[pere] Je viens decrire dans le tuyau,\n");
printf("
mais les donnees sont encore\n");
printf("
dans la zone de memoire tampon.\n");
sleep(5);
fflush(mon_tuyau);
printf("[pere] Je viens de forcer lecriture\n");
printf("
des donnees de la memoire tampon\n");
printf("
vers le tuyau.\n");
printf("
Jattends 5 secondes avant de fermer\n");
printf("
le tuyau et de me terminer.\n");
sleep(5);
fclose(mon_tuyau);
exit(EXIT_SUCCESS);
}
}

Redirection des entre et sorties standard

Une technique couramment employe avec les tuyaux est de relier la sortie standard
dune commande lentre standard dune autre, comme quand on tape
ls | wc -l

Le problme, cest que la commande ls est conue pour afficher lcran et pas
dans un tuyau. De mme pour wc qui est conue pour lire au clavier.
Pour rsoudre ce problme, Unixfournit une mthode lgante. Les fonctions dup()
et dup2() permettent de dupliquer le descripteur de fichier pass en argument :
#include <unistd.h>
int dup(int descripteur);
int dup2(int descripteur, int copie);

Dans le cas de dup2(), on passe en argument le descripteur de fichier dupliquer


ainsi que le numro du descripteur souhait pour la copie. Le descripteur copie est
ventuellement ferm avant dtre rallou.
Pour dup() 2 , le plus petit descripteur de fichier non encore utilis permet alors
daccder au mme fichier que descripteur.
Mais comment dterminer le numro de ce plus petit descripteur ? Sachant que
lentre standard a toujours 0 comme numro de descripteur et que la sortie standard a
toujours 1 comme numro de descripteur, cest trs simple lorsquon veut rediriger lun
de ces descripteurs (ce qui est quasiment toujours le cas). Prenons comme exemple la
redirection de lentre standard vers le ct lecture du tuyau :
2. Il est recommand dutiliser plutt dup2() que dup() pour des raisons de simplicit.

308

15.1. Manipulation des tuyaux


1. On ferme lentre standard (descripteur de fichier numro 0 ou, de manire
plus lisible, STDIN_FILENO, dfini dans <unistd.h> ). Le plus petit numro de
descripteur non utilis est alors 0.
2. On appelle dup() avec le numro de descripteur du ct lecture du tuyau comme
argument. Lentre standard est alors connecte au ct lecture du tuyau.
3. On peut alors fermer le descripteur du ct lecture du tuyau qui est maintenant
inutile puisquon peut y accder par lentre standard.
La faon de faire pour connecter la sortie standard avec le ct criture du tuyau est
exactement la mme.
Le programme suivant montre comment lancer ls | wc -l en utilisant la fonction
dup2() :
Listing 15.4 Duplication des descripteurs
#include
#include
#include
#include
#include

<sys/types.h>
<stdio.h>
<stdlib.h>
<string.h>
<unistd.h>

#define LECTURE 0
#define ECRITURE 1
int main(int argc, char *argv[]) {
int tuyau[2];
if (pipe(tuyau) == -1) {
perror("Erreur dans pipe()");
exit(EXIT_FAILURE);
}
switch (fork()) {
case -1 :
/* erreur */
perror("Erreur dans fork()");
exit(EXIT_FAILURE);
case 0 :
/* processus fils, ls , ecrivain */
close(tuyau[LECTURE]);
/* dup2 va brancher le cote ecriture du tuyau */
/* comme sortie standard du processus courant */
if (dup2(tuyau[ECRITURE], STDOUT_FILENO) == -1) {
perror("Erreur dans dup2()");
}
/* on ferme le descripteur qui reste pour */
/* << eviter les fuites >> ! */
close(tuyau[ECRITURE]);
/* ls en ecrivant sur stdout envoie en fait dans le */
/* tuyau sans le savoir */
if (execlp("ls", "ls", NULL) == -1) {
perror("Erreur dans execlp()");
exit(EXIT_FAILURE);
}
default :
/* processus pere, wc , lecteur */
close(tuyau[ECRITURE]);
/* dup2 va brancher le cote lecture du tuyau */
/* comme entree standard du processus courant */
if (dup2(tuyau[LECTURE], STDIN_FILENO) == -1) {
perror("Erreur dans dup2()");

309

Chapitre 15. Les tuyaux sous Unix


}
/* on ferme le descripteur qui reste */
close(tuyau[LECTURE]);
/* wc lit lentree standard, et les donnees */
/* quil recoit proviennent du tuyau */
if (execlp("wc", "wc", "-l", NULL) == -1) {
perror("Erreur dans execlp()");
exit(EXIT_FAILURE);
}
}
exit(EXIT_SUCCESS);
}

La squence utilisant dup2() :


if ( dup2(tuyau[ECRITURE], STDOUT_FILENO) == -1 ) {
perror("Erreur dans dup2()");
}
close(tuyau[ECRITURE]);

est quivalente celle-ci en utilisant dup() :


close(STDOUT_FILENO);
if ( dup(tuyau[ECRITURE]) == -1 ) {
perror("Erreur dans dup()");
}
close(tuyau[ECRITURE]);

Synchronisation de deux processus au moyen dun tuyau

Un effet de bord intressant des tuyaux est la possibilit de synchroniser deux


processus. En effet, un processus tentant de lire un tuyau dans lequel il ny a rien est
suspendu jusqu ce que des donnes soient disponibles 3 . Donc, si le processus qui
crit dans le tuyau ne le fait pas trs rapidement que celui qui lit, il est possible de
synchroniser le processus lecteur sur le processus crivain.
Lexemple suivant met en uvre une utilisation possible de cette synchronisation :
Listing 15.5 Synchronisation des lectures / critures
#include
#include
#include
#include

<sys/types.h>
<stdio.h>
<stdlib.h>
<unistd.h>

#define LECTURE 0
#define ECRITURE 1
int main(int argc, char *argv[]) {
int tuyau[2], i;
char car;
if (pipe(tuyau) == -1) {
perror("Erreur dans pipe()");

3. moins quil nait indiqu au systme, grce loption O_NONBLOCK de la fonction fcntl(), de lui
renvoyer une erreur au lieu de le suspendre.

310

15.1. Manipulation des tuyaux


exit(EXIT_FAILURE);
}
switch (fork()) {
case -1 :
/* erreur */
perror("Erreur dans fork()");
exit(EXIT_FAILURE);
case 0 :
/* processus fils, lecteur */
close(tuyau[ECRITURE]);
/* on lit les caracteres un a un */
while (read(tuyau[LECTURE], &car, 1) != 0 ) {
putchar(car);
/* affichage immediat du caracter lu */
fflush(stdout);
}
close(tuyau[LECTURE]);
putchar(\n);
exit(EXIT_SUCCESS);
default :
/* processus pere, ecrivain */
close(tuyau[LECTURE]);
for (i = 0; i < 10; i++) {
/* on obtient le caractere qui represente le chiffre i */
/* en prenant le i-eme caractere a partir de 0 */
car = 0+i;
/* on ecrit ce seul caractere */
write(tuyau[ECRITURE], &car, 1);
sleep(1); /* et on attend 1 sec */
}
close(tuyau[ECRITURE]);
exit(EXIT_SUCCESS);
}
}

Le signal SIGPIPE

Lorsquun processus crit dans un tuyau qui na plus de lecteurs (parce que les
processus qui lisaient le tuyau sont termins ou ont ferm le ct lecture du tuyau), ce
processus reoit le signal SIGPIPE. Comme le comportement par dfaut de ce signal est
de terminer le processus, il peut tre intressant de le grer afin, par exemple, davertir
lutilisateur puis de quitter proprement le programme. Les lecteurs curieux pourront
approfondir lutilisation des signaux en consultant les pages de manuel des fonctions
suivantes :
sigaction() ;
sigemptyset() ;
sigsuspend() ;
...
Lquipe enseignante se tient leur disposition pour des complments dinformation.
Autres moyens de communication entre processus

Les tuyaux souffrent de deux limitations :


ils ne permettent quune communication unidirectionnelle ;
311

Chapitre 15. Les tuyaux sous Unix


les processus pouvant communiquer au moyen dun tuyau doivent tre issus dun
anctre commun.
Ainsi, dautres moyens de communication entre processus ont t dvelopps pour
pallier ces inconvnients :
Les tuyaux nomms (FIFOs) sont des fichiers spciaux qui, une fois ouverts, se
comportent comme des tuyaux (les donnes sont envoyes directement sans
tre stockes sur disque). Comme ce sont des fichiers, ils peuvent permettre
des processus quelconques (pas ncessairement issus dun mme anctre) de
communiquer.
Les sockets permettent une communication bidirectionnelle entre divers processus,
fonctionnant sur la mme machine ou sur des machines relies par un rseau.
Les tuyaux nomms sutilisent facilement et sont abords dans le paragraphe
suivant.
Les tuyaux nomms

Comme on vient de le voir, linconvnient principal des tuyaux est de ne fonctionner quavec des processus issus dun anctre commun. Pourtant, les mcanismes de
communication mis en jeu dans le noyau sont gnraux et, en fait, seul lhritage des
descripteurs de fichiers aprs un fork() impose cette restriction.
Pour sen affranchir, il faut que les processus dsirant communiquer puissent
dsigner le tuyau quils souhaitent utiliser. Ceci se fait grce au systme de fichiers.
Un tuyau nomm est donc un fichier :
menthe22> mkfifo fifo
menthe22> ls -l fifo
prw-r--r-- 1 in201

in201

0 Jan 10 17:22 fifo

Il sagit cependant dun fichier dun type particulier, comme le montre le p dans
laffichage de ls.
Une fois cr, un tuyau nomm sutilise trs facilement :
menthe22> echo coucou > fifo &
[1] 25312
menthe22> cat fifo
[1] + done
echo coucou > fifo
coucou

Si lon fait abstraction des affichages parasites du shell, le tuyau nomm se comporte
tout fait comme on sy attend. Bien que la faon de lutiliser puisse se rvler
trompeuse, un tuyau nomm transfre bien ses donnes dun processus lautre en
mmoire, sans les stocker sur disque. La seule intervention du systme de fichiers
consiste permettre laccs au tuyau par lintermdiaire de son nom.
Il faut noter que :
Un processus tentant dcrire dans un tuyau nomm ne possdant pas de lecteurs
sera suspendu jusqu ce quun processus ouvre le tuyau nomm en lecture. Cest
312

15.2. Exercices
pourquoi, dans lexemple, echo a t lanc en tche de fond, afin de pouvoir
rcuprer la main dans le shell et dutiliser cat. On peut tudier ce comportement
en travaillant dans deux fentres diffrentes.
De mme, un processus tentant de lire dans un tuyau nomm ne possdant pas
dcrivains se verra suspendu jusqu ce quun processus ouvre le tuyau nomm
en criture. On peut tudier ce comportement en reprenant lexemple mais en
lanant dabord cat puis echo.
En particulier, ceci signifie quun processus ne peut pas utiliser un tuyau nomm
pour stocker des donnes afin de les mettre la disposition dun autre processus une
fois le premier processus termin. On retrouve le mme phnomne de synchronisation
quavec les tuyaux classiques.
En C, un tuyau nomm se cre au moyen de la fonction mkfifo() :
#include <sys/stat.h>
int retour;
retour = mkfifo("fifo", 0644);
if ( retour == -1 ) {
/* erreur : le tuyau nomme na pas pu etre cree */
}

Il doit ensuite tre ouvert (grce open() ou fopen()) et sutilise au moyen des
fonctions dentres / sorties classiques.

15.2

Exercices

Question 1
crire un programme tuyau1 qui met en place un tuyau et cre un processus fils.
Le processus fils lira des lignes de caractres du clavier, les enverra au processus pre
par lintermdiaire du tuyau et ce dernier les affichera lcran.
Le processus fils se terminera lorsquon tapera Control-D (qui fera renvoyer NULL
fgets()), aprs avoir ferm le tuyau. Le processus fils se terminera de la mme
faon.
Cet exercice permet de mettre en vidence la synchronisation de deux processus au
moyen dun tuyau.
Fonctions utiliser :
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
int pipe(int tuyau[2])
pid_t fork()
FILE *fdopen(int fichier, char *mode)
char *fgets(char *chaine, size_t taille, FILE *fichier)

Question 2
On se propose de raliser un programme mettant en communication deux commandes par lintermdiaire dun tuyau, comme si lon tapait dans un shell
313

Chapitre 15. Les tuyaux sous Unix

ls | wc -l

La sortie standard de la premire commande est relie lentre standard de la seconde.


crire un programme tuyau2 qui met en place un tuyau et cre un processus fils.
Le processus pre excutera la commande avec option sort +4 -n grce la fonction
execlp() et le processus fils excutera la commande avec option ls -l. Pour cela,
il faut auparavant rediriger lentre standard du processus pre vers le ct lecture du
tuyau et la sortie standard du processus fils vers le ct criture du tuyau au moyen de
la fonction dup2().
Fonctions utiliser :
#include <sys/types.h>
#include <unistd.h>
int pipe(int tuyau[2])
pid_t fork()
int dup2(int descripteur, int copie)
int execlp(const char *fichier, const char *arg, ...)

Question 3
Reprenez le programme myshell du TP prcdent et modifiez-le afin quil reconnaisse aussi les commandes de la forme
ls -l | sort +4 -n

Fonctions utiliser :
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
char *strtok(char *chaine, char *separateurs)
int pipe(int tuyau[2])
pid_t fork()
int dup2(int descripteur, int copie)
int execlp(const char *fichier, const char *arg, ...)

La fonction strtok() vous sera utile pour analyser la ligne tape au clavier.
Si vous avez le temps, essayez de modifier le programme pour quil reconnaisse un
enchanement quelconque de commandes :
cat toto | grep tata | wc -l

Dans quel ordre vaut-il mieux lancer les processus fils ?

314

15.3. Corrigs

15.3

Corrigs

#include
#include
#include
#include

<sys/types.h>
<stdio.h>
<stdlib.h>
<unistd.h>

Listing 15.6 Corrig du premier exercice

#define LECTURE 0
#define ECRITURE 1
int main(int argc, char *argv[]) {
int tuyau[2];
FILE *mon_tuyau;
char ligne[80];
if (pipe(tuyau) == -1) {
perror("Erreur dans pipe()");
exit(EXIT_FAILURE);
}
switch (fork()) {
case -1 :
/* erreur */
perror("Erreur dans fork()");
_
exit(EXIT FAILURE);
case 0 :
/* processus fils, ecrivain */
close(tuyau[LECTURE]); /* on ferme le cote lecture */
/* ouvre un descripteur de flot FILE * a partir */
/* du descripteur de fichier UNIX */
mon_tuyau = fdopen(tuyau[ECRITURE], "w");
if (mon_tuyau == NULL) {
perror("Erreur dans fdopen()");
exit(EXIT_FAILURE);
}
printf("Je suis le fils, tapez des phrases svp\n");
/* on lit chaque ligne au clavier */
/* mon_tuyau est un FILE * accessible en ecriture */
while (fgets(ligne, sizeof(ligne), stdin) != NULL) {
/* on ecrit la ligne dans le tuyau */
fprintf(mon_tuyau, "%s", ligne);
fflush(mon_tuyau);
}
/* il faut faire fclose(mon_tuyau) ou a la rigueur */
/* close(tuyau[LECTURE]) mais surtout pas les deux */
fclose(mon_tuyau);
exit(EXIT_SUCCESS);
default :
/* processus pere, lecteur */
close(tuyau[ECRITURE]); /* on ferme le cote ecriture */
_
mon tuyau = fdopen(tuyau[LECTURE], "r");
if (mon_tuyau == NULL) {
perror("Erreur dans fdopen()");
exit(EXIT_FAILURE);
}
/* on lit chaque ligne depuis le tuyau */
/* mon_tuyau est un FILE * accessible en lecture */
while (fgets(ligne, sizeof(ligne), mon_tuyau)) {
/* et on affiche la ligne a lecran */
/* la ligne contient deja un \n a la fin */
printf(">>> %s", ligne);
}
fclose(mon_tuyau);

315

Chapitre 15. Les tuyaux sous Unix


exit(EXIT_SUCCESS);
}
}

Listing 15.7 Corrig du deuxime exercice


#include
#include
#include
#include

<sys/types.h>
<stdio.h>
<stdlib.h>
<unistd.h>

#define LECTURE 0
#define ECRITURE 1
int main(int argc, char *argv[]) {
int tuyau[2];
if (pipe(tuyau) == -1) {
perror("Erreur dans pipe().");
exit(EXIT_FAILURE);
}
switch (fork()) {
case -1 :
/* erreur */
perror("Erreur dans fork().");
_
exit(EXIT FAILURE);
case 0 :
/* processus fils */
close(tuyau[LECTURE]);
/* dup2 va brancher le cote ecriture du tuyau */
/* comme sortie standard du processus courant */
dup2(tuyau[ECRITURE],STDOUT_FILENO);
/* on ferme le descripteur qui reste */
close(tuyau[ECRITURE]);
if (execlp("ls", "ls", "-l", NULL) == -1) {
perror("Erreur dans execlp()");
exit(EXIT_FAILURE);
}
default :
/* processus pere */
close(tuyau[ECRITURE]);
/* dup2 va brancher le cote lecture du tuyau */
/* comme entree standard du processus courant */
dup2(tuyau[LECTURE], STDIN_FILENO);
/* on ferme le descripteur qui reste */
close(tuyau[LECTURE]);
if (execlp("sort", "sort", "+4", "-n", NULL) == -1) {
perror("Erreur dans execlp()");
exit(EXIT_FAILURE);
}
}
exit(EXIT_SUCCESS);
}

Listing 15.8 Corrig du troisime exercice


#include
#include
#include
#include
#include
#include

<sys/types.h>
<stdio.h>
<stdlib.h>
<string.h>
<unistd.h>
<sys/wait.h>

316

15.3. Corrigs
#define LECTURE 0
#define ECRITURE 1
int main(int argc, char *argv[]) {
char ligne[80], *arg1[10], *arg2[10], *tmp;
int i, tuyau[2];
printf("myshell> ");
/* lecture de lentree standard ligne par ligne */
while (fgets(ligne, sizeof(ligne), stdin) != NULL) {
/* fgets lit egalement le caractere de fin de ligne */
if (strcmp(ligne, "exit\n") == 0) {
exit(EXIT_SUCCESS);
}
/* on decoupe la ligne en sarretant des que */
/* lon trouve un | : cela donne la premiere commande */
for (tmp = strtok(ligne, " \t\n"), i = 0;
tmp != NULL && strcmp(tmp , "|") != 0;
tmp = strtok(NULL, " \t\n"), i++) {
arg1[i] = tmp;
}
arg1[i] = NULL;
/* on decoupe la suite si necessaire pour obtenir la */
/* deuxieme commande */
arg2[0] = NULL;
if (tmp != NULL && strcmp(tmp, "|") == 0) {
for (tmp = strtok(NULL, " \t\n"), i = 0;
tmp != NULL;
tmp = strtok(NULL, " \t\n"), i++) {
arg2[i] = tmp;
}
arg2[i] = NULL;
}
if (arg2[0] != NULL) { /* il y a une deuxieme commande */
pipe(tuyau);
}
switch (fork()) {
case -1 :
/* erreur */
perror("Erreur dans fork()");
_
exit(EXIT FAILURE);
case 0 :
/* processus fils */
if (arg2[0] != NULL) { /* tuyau */
close(tuyau[LECTURE]);
dup2(tuyau[ECRITURE], STDOUT_FILENO);
close(tuyau[ECRITURE]);
}
execvp(arg1[0], arg1);
/* on narrivera jamais ici, sauf en cas derreur */
perror("Erreur dans execvp()");
exit(EXIT_FAILURE);
default :
/* processus pere */
if (arg2[0] != NULL) {
/* on recree un autre fils pour la deuxieme commande */
switch (fork()) {
case -1 :
/* erreur */
perror("Erreur dans fork()");
_
exit(EXIT FAILURE);
case 0 :
/* processus fils */
close(tuyau[ECRITURE]);
dup2(tuyau[LECTURE], STDIN_FILENO);

317

Chapitre 15. Les tuyaux sous Unix


close(tuyau[LECTURE]);
execvp(arg2[0], arg2);
perror("Erreur dans execvp()");
exit(EXIT_FAILURE);
default :
/* processus pere */
/* on ferme les deux cote du tuyau */
close(tuyau[LECTURE]);
close(tuyau[ECRITURE]);
/* et on attend la fin dun fils */
wait(NULL);
}
}
/* on attend la fin du fils */
wait(NULL);
}
printf("myshell> ");
}
exit(EXIT_SUCCESS);
}

Listing 15.9 Corrig du quatrime exercice


#include
#include
#include
#include
#include
#include
#include

<sys/types.h>
<signal.h>
<stdio.h>
<stdlib.h>
<string.h>
<unistd.h>
<sys/wait.h>

#define LECTURE 0
#define ECRITURE 1
/* le type liste chainee darguments */
typedef struct argument {
char *arg;
struct argument *suivant;
} *t_argument;
/* le type commande, liste doublement chainee */
typedef struct commande {
int nombre;
t_argument argument;
struct commande *suivant;
struct commande *precedent;
} *t_commande;
int main(int argc ,char *argv[]) {
char ligne[80];
t_commande commande;
commande = (t_commande)malloc(sizeof(struct commande));
printf("myshell> ");
while (fgets(ligne, sizeof(ligne), stdin) != NULL) {
char *tmp;
t_commande com;
t_argument arg, precedent;
int tuyau[2], ecriture;
pid_t pid, dernier;
if (strcmp(ligne ,"exit\n") == 0) {
exit(EXIT_SUCCESS);
}

318

15.3. Corrigs

/* premier maillon */
com = commande;
com->argument = NULL ;
com->nombre
= 0 ;
com->suivant
= NULL ;
com->precedent = NULL ;
precedent = NULL ;
do {
/* remplissage des arguments dune commande */
/* avec allocation des maillons de la liste darguments */
for (tmp = strtok((commande->argument == NULL)?ligne:NULL,
" \t\n");
tmp != NULL && strcmp(tmp, "|") != 0;
tmp = strtok(NULL, " \t\n")) {
arg = (t_argument)malloc(sizeof(struct argument));
if (arg == NULL) {
perror("Erreur dans malloc()");
exit(EXIT_FAILURE);
}
/* chainage de la liste darguments */
if (precedent != NULL) {
precedent->suivant = arg;
precedent = arg;
} else {
com->argument = arg;
precedent = arg;
}
arg->arg = tmp;
arg->suivant = NULL;
com->nombre++;
}
if (tmp != NULL) { /* vrai uniquement si on a un | */
/* allocation dune nouvelle commande */
com->suivant = (t_commande)malloc(sizeof(struct commande));
if (com->suivant == NULL) {
perror("Erreur dans malloc()");
exit(EXIT_FAILURE);
}
/* chainage double du nouveau maillon */
com->suivant->precedent = com;
com = com->suivant;
com->argument = NULL;
com->nombre = 0;
com->suivant = NULL;
precedent = NULL;
}
} while (tmp != NULL); /* do */
/* on cree les processus en remontant la ligne de commande */
for (; com != NULL; com = com->precedent) {
int i;
char **args;
/* creation du tuyau */
if (com->precedent != NULL) {
if (pipe(tuyau) == -1) {
perror("Erreur dans pipe()");
exit(EXIT_FAILURE);
}
}

319

Chapitre 15. Les tuyaux sous Unix


/* creation du processus devant executer la commande */
switch (pid = fork()) {
case -1 :
/* erreur */
perror("Erreur dans fork()");
exit(EXIT_FAILURE);
case 0 :
/* processus fils : commande */
if (com->precedent != NULL) { /* redirection de stdin */
close(tuyau[ECRITURE]);
dup2(tuyau[LECTURE], STDIN_FILENO);
close(tuyau[LECTURE]);
}
if (com->suivant != NULL) { /* redirection de stdout */
/* ecriture est le cote ecriture du tuyau */
/* allant vers la commande plus a droite sur la ligne */
/* qui a ete lancee au tour de boucle precedent */
dup2(ecriture, STDOUT_FILENO);
close(ecriture);
}
/* allocation du tableau de pointeurs darguments */
args = (char **)malloc((com->nombre+1)*sizeof(char *));
for (i = 0, arg = com->argument;
arg != NULL;
i++, arg = arg->suivant) {
args[i] = arg->arg;
}
args[i] = NULL;
/* recouvrement par la commande */
execvp(args[0], args);
perror("erreur execvp");
exit(EXIT_FAILURE);
default :
/* processus pere : shell */
if (com->suivant == NULL) {
dernier = pid;
}
/* on ferme les extremites inutiles des tuyaux */
if (com->suivant != NULL) {
close(ecriture);
}
if (com->precedent != NULL) {
close(tuyau[LECTURE]);
/* attention, cest lentree du bloc de droite */
/* il va servir lors du tour de boucle suivant */
ecriture = tuyau[ECRITURE];
} else {
/* on attend la fin du dernier processus directement */
waitpid(dernier, NULL, 0);
}
}
}
/* liberation de la memoire allouee */
/* on reavance jusqua la derniere commande */
for (com = commande; com->suivant != NULL; com = com->suivant) ;
while (com != NULL) { /* pour toutes les commandes */
arg = com->argument;
while (arg != NULL) { /* on libere les arguments */
t_argument suivant;
suivant = arg->suivant;
free(arg);
arg = suivant;
}
com = com->precedent;

320

15.3. Corrigs
if (com != NULL) {
free(com->suivant); /* on libere la commande */
}
}
printf("myshell> ");
}
free(commande);
exit(EXIT_SUCCESS);
}

Enfin voici une possibilit de code nutilisant ni strtok() ni allocation dynamique.


La gestion complte de lentre standard pour le premier processus de la ligne est
laisse aux soins du lecteur.
Listing 15.10 Une deuxime version du corrig du quatrime exercice
#include
#include
#include
#include
#include
#include
#include
#include

<sys/types.h>
<signal.h>
<stdio.h>
<stdlib.h>
<string.h>
<unistd.h>
<ctype.h>
<sys/wait.h>

#define LECTURE 0
#define ECRITURE 1
/* nombre maximal darguments dune commande */
#define MAXARG 10
/* compte le nombre darguments de la commande */
/* la plus a droite de la ligne, sarrete au | */
char *recherche_pipe(char *str, int *narg) {
int l,i;
int n = 1;
l = strlen(str);
for (i = l-1; str[i] != | && i > 0; i--) {
if (isspace(str[i-1]) && !isspace(str[i])) n++;
}
*narg = n;
if (str[i] == |) {
str[i] = \0;
return(str+i+1);
}
return(str);
}
/* Decoupage de la ligne en place (sans recopie) */
void decoupe_commande(char *ligne, char *arg[]) {
int i=0;
/* on avance jusquau premier argument */
while (isspace(*ligne)) ligne++;
/* on traite les arguments tant que ce nest */
/* pas la fin de la ligne */
while (*ligne != \0 && *ligne != \n) {
arg[i++]=ligne;
/* on avance jusquau prochain espace */
while (!isspace(*ligne) && *ligne!=\0)
ligne++;
/* on remplace les espaces par \0 ce qui marque */
/* la fin du parametre precedent */

321

Chapitre 15. Les tuyaux sous Unix


while (isspace(*ligne) && *ligne!=\0)
*ligne++=\0;

}
arg[i]=NULL;
}

/* execute un processus prenant ecriture en entree */


int execute_commande(char *arg[], int *ecriture) {
int tuyau[2];
int pid;
pipe(tuyau);
switch (pid = fork()) {
case -1 :
/* erreur */
perror("Erreur dans fork()");
exit(EXIT_FAILURE);
case 0 :
/* processus fils */
close(tuyau[ECRITURE]);
dup2(tuyau[LECTURE], STDIN_FILENO);
close(tuyau[LECTURE]);
if (*ecriture >= 0) {
dup2(*ecriture, STDOUT_FILENO);
close(*ecriture);
}
execvp(arg[0], arg);
/* on narrivera jamais ici, sauf en cas derreur */
perror("Erreur dans execvp()");
exit(EXIT_FAILURE);
default :
/* processus pere */
close(tuyau[LECTURE]);
close(*ecriture);
*ecriture = tuyau[ECRITURE];
return pid;
}
}

int main(int argc ,char *argv[]) {


char ligne[80];
char *arg[MAXARG];
char *basecommande;
int narg, letuyau;
int i, pid;
printf("myshell> ");
while (fgets(ligne, sizeof(ligne), stdin) != NULL) {
if (strcmp(ligne ,"exit\n") == 0) {
exit(EXIT_SUCCESS);
}
/* on supprime le caractere de fin de ligne */
/* sil existe (fgets le lit) */
i=strlen(ligne)-1;
if (ligne[i] == \n) ligne[i]=\0;
basecommande = NULL;
letuyau = -1;
while (basecommande != ligne) {
/* recherche la base de la commande la plus a droite */
basecommande = recherche_pipe(ligne, &narg);
if (narg > MAXARG) {
fprintf(stderr, "Trop de parametres\n");
exit(EXIT_FAILURE);
}

322

15.3. Corrigs
/* decoupe cette commande en tableau arg[] */
decoupe_commande(basecommande, arg);
/* lance la commande, en notant le pid de la derniere */
if (letuyau == -1) {
pid = execute_commande(arg, &letuyau);
} else {
execute_commande(arg, &letuyau);
}
}
/* le premier processus de la ligne na pas de stdin */
close(letuyau);
waitpid(pid, NULL, 0);
printf("myshell> ");
}
exit(EXIT_SUCCESS);
}

323

Chapitre 15. Les tuyaux sous Unix

F IGURE 15.1 Le descripteur de fichier entier ne permet quune manipulation octet


par octet. Le descripteur de fichier de type FILE permet quant lui une manipulation
plus structure.

15.4

Corrections dtailles

Nous allons dans un premier temps revenir trs rapidement sur les fichiers afin que
les diffrentes fonctions que nous utiliserons pour manipuler les tuyaux soient bien
prsentes dans les esprits des lecteurs.
Les fichiers sous Unix

Les fichiers reprsentent en fait des ensembles de donnes manipules par lutilisateur. Un fichier nest donc rien dautre quun rceptacle donnes. Le systme nous
fournit diffrents moyens de manipuler ces rceptacles, car avant de pouvoir obtenir les
donnes il faut avant tout pouvoir se saisir du bon rceptacle. Entre lutilisateur et le
fichier, linterface la plus simple est le descripteur de fichier, cest--dire un entier : il
sagit du numro de la ligne de la table des fichiers ouverts faisant rfrence au fichier
souhait. Ce descripteur est obtenu trs simplement par lappel systme open().
Un descripteur de fichier (point dentre dans la table des fichiers ouverts) ne permet
que des manipulations simples sur les fichiers : la lecture et lcriture se feront par
entit atomique, donc des octets, via les deux fonctions systme read() et write().
Afin de pouvoir manipuler des entits plus structures (des entiers, des mots, des
double, etc.) il nous faut utiliser une autre interface que le descripteur entier comme
le montre la figure 15.1.
Linterface mise en place par le descripteur complexe FILE permet, outre une
manipulation plus simple des entits lire ou crire, une temporisation des accs au
fichier. Ainsi au lieu dcrire octet par octet dans le fichier, le systme placera dans un
premier temps les diffrentes donnes dans une zone tampon. Quand la ncessit sen
fera sentir, la zone tampon sera vide. Ceci vous explique pourquoi certaines critures
ne se font jamais lorsquun programme commet une erreur et sarrte, mme si lerreur
est intervenu aprs lappel fprintf().
Nous pouvons aisment passer du descripteur entier au descripteur complexe en
324

15.4. Corrections dtailles


utilisant la fonction fdopen().
int descrip_trivial;
FILE *descript_complexe;
descript_trivial = open("Mon_beau_sapin.tex",O_RDONLY,NULL);
descript_complexe = fdopen(descript_trivial,"r");
fscanf(descript_complexe,"%d %f %d",
&entier,&reel,&autre_entier);

Lorsque nous utilisons la fonction fdopen(), nous nouvrons pas le fichier une
seconde fois, nous nous contentons simplement dobtenir une nouvelle faon de manipuler les donnes qui y sont stockes. Donc, lorsque nous fermons le fichier, nous
devons utiliser soit la fonction systme close(), soit la fonction de la bibliothque
standard fclose(), mais certainement pas les deux !
Quel lien avec les tuyaux ? Et bien les tuyaux sont des fichiers un peu particuliers
donc mieux vaut savoir comment manipuler les fichiers ! Il est important de garder
lesprit quun descripteur de fichier est obtenu en demandant une ouverture de fichier
avec un mode : lecture ou criture, il en sera de mme avec les tuyaux.
Les tuyaux

Un tuyau est une zone dchange de donnes entre deux ou plusieurs processus.
Cette zone est cependant assez restrictive quant aux changes autoriss, ceux-ci sont
unidirectionnels et destructifs :
la communication ne peut se fait que dun groupe de processus (les crivains)
vers dautres (les lecteurs). Une fois quun processus a choisit sa nature (crivain
ou lecteur), impossible de revenir en arrire.
toute donne lue par un lecteur nest plus disponible pour les autres lecteurs.
Comme le nom le suggre assez bien, un tuyau doit possder deux descripteurs de
fichier, lun permettant dcrire, et lautre permettant de lire. La figure 15.2 dcrit de
manire schmatique un tuyau et les deux descripteurs attachs.
On subodore donc que la fonction permettant de crer un tel objet doit soit renvoyer
un tableau de deux entiers, soit prendre en paramtre un tableau de deux entiers. Mais
comme nous savons que les fonctions systmes retournent en gnral un code derreur,
la bonne solution doit tre la seconde :
int pipe(int filedes[2]);

Il existe un moyen simple pour se souvenir quid des deux descripteurs est celui utilis
en lecture (du coup lautre sera utilis en criture). Le priphrique servant lire nest
rien dautre que le clavier, soit de manire un peu plus gnrale stdin, dont le numro
est 0. Le descripteur utilis en lecture est donc fildes[0].
Nous allons commencer par utiliser un tuyau au sein dun seul processus, ce qui ne
servira pas grand chose sinon fixer les esprits sur les diffrents moyens mis notre
disposition pour lire et crire dans un tuyau.
325

Chapitre 15. Les tuyaux sous Unix

F IGURE 15.2 Un tuyau permet de ranger (dans lordre darrive selon les critures)
des donnes en mmoire pour les rcuprer. Il est donc fourni avec deux descripteurs
de fichier, lun servant crire et lautre lire.

Listing 15.11 Utilisation basique


#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char **argv)
{
FILE *fpin,*fpout;
int tuyau[2];
int retour,k;
char phrase_in[256];
char phrase_out[256];
double pi;
retour = pipe(tuyau);
if (retour == -1) {
perror("Impossible de creer le tuyau");
exit(EXIT_FAILURE);
}
for(k=0;k<10;k++) phrase_in[k] = a+k;
write(tuyau[1],phrase_in,10);
printf("Jecris dans mon tuyau\n");
sleep(1);
read(tuyau[0],phrase_out,10);
printf("Jai lu dans mon tuyau\n");
for(k=0;k<10;k++) printf("%c ",phrase_out[k]);
printf("\n");
fpout = fdopen(tuyau[0],"r");
fpin = fdopen(tuyau[1],"w");

326

15.4. Corrections dtailles


fprintf(fpin,"Bonjour voici une phrase\n");fflush(fpin);
sleep(1);
fgets(phrase_out,255,fpout);
printf("Jai lu: \"%s\" dans le tuyau\n",phrase_out);
fprintf(fpin,"3.141592\n");fflush(fpin);
sleep(1);
fscanf(fpout,"%lf",&pi);
printf("Jai lu pi dans le tuyau: %f\n",pi);
close(tuyau[0]);
close(tuyau[1]);
exit(EXIT_SUCCESS);
}

La cration du tuyau est trs simple, lappel systme pipe() nous renvoie un code
derreur si le tuyau na pas pu tre cr. Si tout se passe bien nous obtenons nos deux
descripteurs entiers dans le tableau tuyau[] pass en paramtre.
Dans un premier temps nous allons simplement crire 10 octets dans le tuyau
par lintermdiaire du descripteur dcriture. Nous utilisons la fonction write(),
intermdiaire dcriture incontournable pour les descripteurs de fichier entiers. Puis
nous lisons, mais sur le ct lecture du tuyau, laide de la fonction read(). On lit
exactement le bon nombre de caractres (ou moins la rigueur, mais surtout pas plus).
Afin de faire appel des fonctions manipulant des donnes un peu plus volues
que les octets, nous demandons une reprsentation plus complexe des deux descripteurs,
et pour ce faire nous utilisons la fonction fdopen() qui nous renvoie un descripteur
complexe (FILE *) partir dun descripteur entier.
Nous pouvons alors crire et lire des donnes structures, comme le montre la
suite du programme, avec toutefois un bmol important, lutilisation de la fonction
fflush(), car nous le savons les descripteurs complexes utilisent des tampons et ne
placent les donnes dans leur fichier destination immdiatement.
Les tuyaux nomms

Lutilisation des tuyaux doit permettre diffrents processus de communiquer.


Avant dutiliser les fonctions vues dans le prcdent chapitre (duplication et recouvrement), nous allons nous intresser une forme particulire de tuyaux, les tuyaux
nomms. Puisque nous avons bien compris que les tuyaux ntaient ni plus ni moins
que des sortes de fichiers, il doit tre possible de crer vritablement des fichiers qui se
comportent comme des tuyaux. Pour cela, nous avons notre disposition la fonction
mkfifo(). Elle permet de crer un fichier qui se comporte comme un tuyau, et puisque
ce fichier possde un nom, nous pourrons crire et lire dedans en louvrant au moyen de
son nom ! Les trois petits programmes qui suivent vous permettent de voir lutilisation
et surtout le squencement des oprations dcriture et de lecture.
Tout dabord le programme de cration, dcriture et puis de destruction du tuyau
nomm :
Listing 15.12 Cration simple, criture et fermeture
327

Chapitre 15. Les tuyaux sous Unix


#include
#include
#include
#include
#include

<unistd.h>
<stdlib.h>
<stdio.h>
<sys/types.h>
<sys/stat.h>

int main(int argc, char **argv)


{
int retour;
FILE *fp;
int k;
retour = mkfifo("ma_fifo",0644);
if (retour == -1) {
perror("Impossible de creer le tuyaux nomme");
exit(EXIT_FAILURE);
}
fp = fopen("ma_fifo","w");
for(k=0;k<6;k++) {
fprintf(fp,"Ecriture numero %d dans la fifo\n",k);
fflush(fp);
sleep(2);
}
fclose(fp);
unlink("ma_fifo");
exit(EXIT_SUCCESS);
}

Ensuite le programme de lecture qui prend un argument afin de pouvoir tre identifi
car nous allons le lancer dans 2 fentres diffrentes :
Listing 15.13 Le programme de lecture complmentaire au prcdent
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char **argv)
{
FILE *fp;
char phrase[256];
if (argc < 2) {
fprintf(stderr,"usage: %s <un nom>\n",argv[0]);
exit(EXIT_FAILURE);
}
if ((fp = fopen("ma_fifo","r")) == NULL) {
perror("P2: Impossible douvrir ma_fifo");
exit(EXIT_FAILURE);
}
while(fgets(phrase,255,fp)!=NULL) {
printf("%s: \"%s\"\n",argv[1],phrase);
}
fclose(fp);
exit(EXIT_SUCCESS);
}

Nous commenons par lancer le programme de cration / criture. Comme lcriture


dans un tuyau sans lecteur est bloquante, cela nous laisse le temps de lancer les deux
328

15.4. Corrections dtailles


programmes lecteurs. Laffichage donne ceci (chaque fentre est identifie par un
numro diffrent dans linvite de linterprte de commandes) :
menthe31>p1
menthe22>p2 premier
premier: "Ecriture numero 0 dans la fifo"
premier: "Ecriture numero 2 dans la fifo"
premier: "Ecriture numero 4 dans la fifo"
menthe22>
menthe12>p2 deuxieme
deuxieme: "Ecriture numero 1 dans la fifo"
deuxieme: "Ecriture numero 3 dans la fifo"
deuxieme: "Ecriture numero 5 dans la fifo"
menthe22>

Nous voyons tout dabord que le programme p1 reste bloqu tant quil reste des
lecteurs. Ensuite nous remarquons que le programme p2 lanc dans deux fentres
diffrentes, lit alternativement les diffrentes critures ce qui reflte bien la notion de
lecture destructive.
Il est important de garder lesprit que les donnes changes ne sont jamais crites
sur le disque. Le fichier ma_fifo ne sert qu donner un nom, mais tout les changes
restent confins la mmoire.
Nous allons maintenant passer aux tuyaux permettant diffrents processus issus
dun mme pre de partager des donnes.
Tuyaux et processus

Nous savons que lappel fork() permet de conserver les descripteurs de fichier
ouverts lors de la duplication. Nous allons tirer partie de cela pour crer un processus
fils qui va pouvoir dialoguer avec son pre.
Il est fondamental de bien comprendre que lappel fork() vient dupliquer les
diffrents descripteurs et quune fois la duplication ralise, le tuyau possde deux
points dentre et deux points de sortie comme le montre la figure 15.3.
Nous allons mettre en uvre cela en rpondant la question 15.2. Notre programme
va sarticuler autour dune fonction qui lira des caractres sur le clavier (lentre
standard), une fonction qui les crira dans le tuyau et une fonction qui lira dans le
tuyau.
Dans un premier temps nous crons le tuyau, car il est impratif quil existe
avant la duplication pour tre partag. la suite de la duplication, nous scinderons le
programme en deux, la partie fils ralisera lenvoi des donnes dans le tuyau, la
partie pre les lira et les affichera lcran.
Listing 15.14 Utilisation de la duplication
#include
#include
#include
#include

<unistd.h>
<stdlib.h>
<stdio.h>
<string.h>

#define LECTURE 0

329

Chapitre 15. Les tuyaux sous Unix

F IGURE 15.3 Un fois le tuyau cr, la duplication va offrir deux points dentre
et deux points de sortie sur le tuyau puisque les descripteurs sont partags entre le
processus pre et le processus fils. Nous avons donc un tuyau possdant deux lecteurs
et deux crivains, il faut imprativement faire quelque chose !

#define ECRITURE 1

int lire_clavier(char *ligne, int length)


{
int ret;
ret = (int)fgets(ligne,length,stdin);
return ret;
}
void proc_fils(int tuyau[])
{
char ligne[256];
FILE *fp;
/* Fermeture du tuyau en lecture */
close(tuyau[LECTURE]);
if ((fp = fdopen(tuyau[ECRITURE],"w"))==NULL) {
perror("Impossible dobtenir un descripteur decent");
close(tuyau[ECRITURE]);
exit(EXIT_FAILURE);
}
while(lire_clavier(ligne,256)) {
fprintf(fp,"%s",ligne);fflush(fp);
}
close(tuyau[ECRITURE]);
fprintf(stdout,"Fin decriture tuyau\n");
exit(EXIT_SUCCESS);
}
void proc_pere(int tuyau[])
{
char ligne[256];
FILE *fp;

330

15.4. Corrections dtailles

/* Fermeture du tuyau en ecriture */


close(tuyau[ECRITURE]);
if ((fp = fdopen(tuyau[LECTURE],"r"))==NULL) {
perror("Impossible dobtenir un descripteur decent");
close(tuyau[LECTURE]);
exit(EXIT_FAILURE);
}
while(fgets(ligne,256,fp)!=NULL) {
ligne[strlen(ligne)-1] = \0;
fprintf(stdout,"Pere:%s\n",ligne);
}
close(tuyau[LECTURE]);
fprintf(stdout,"Fin de lecture tuyau\n");
exit(EXIT_SUCCESS);
}
int main(int argc, char **argv)
{
int tuyau[2];
if (pipe(tuyau) == -1) {
perror("Impossible de tuyauter");
exit(EXIT_FAILURE);
}
switch(fork()) {
case -1:
perror("Impossible de forker");
exit(EXIT_FAILURE);
case 0: /* processus fils */
proc_fils(tuyau);
default: /* processus pere */
proc_pere(tuyau);
}
exit(EXIT_FAILURE);
}

Il est essentiel de bien comprendre le fonctionnement dcrit par la figure 15.3.


lissue de la commande fork(), il existe un descripteur en criture pour le fils, un
autre pour le pre, ainsi quun descripteur en lecture pour le fils et un autre pour le pre.
Notre tuyau possde donc deux points dentre et deux points de sortie. Il est impratif
de mettre fin cette situation un peu dangereuse. En effet un tuyau ouvert en criture
(avec un ou plusieurs points dentre) ne peut pas signifier son (ou ses lecteurs) quil
nest ncessaire dattendre des donnes.
Le processus fils, qui est lcrivain dans le tuyau, va tout dabord fermer son ct
lecteur. De la mme faon, le processus pre, qui est le lecteur du tuyau, va fermer son
ct crivain. Ainsi nous obtenons, comme le montre le diagramme chronologique de
la figure 15.4, le tuyau ne possde plus un instant donn quun crivain, le processus
fils, et un lecteur, le processus pre.
331

Chapitre 15. Les tuyaux sous Unix

F IGURE 15.4 Une fois le tuyau cr et la duplication effective, on ferme lun des
lecteurs et lun des crivains. Le tuyau ne possde plus quune entre (le fils) et une
sortie (le pre).

Tuyaux et recouvrement

Une des utilisations possibles est de pouvoir faire communiquer des programmes
entre eux, nous lavons vu avec les tuyaux nomms. Peut-on aboutir la mme chose
sans ces tuyaux nomms ? Avec des programmes que nous concevons nous-mmes et
que nous pouvons intgrer dans un dveloppement, la chose est strictement identique
ce que nous venons de faire dans le programme prcdent. Par contre, comment
procder lexcution de cette commande assez simple : ls -l | wc -l ? Il faut
brancher la sortie standard de la commande ls sur lentre standard de la commande
wc. Pour ce faire nous allons utiliser une nouvelle commande : dup2().
Cette commande cre une copie dun descripteur de fichier, et en paraphrasant le
manuel, aprs un appel russi cette commande, le descripteur et sa copie peuvent tre
utiliss de manire interchangeable. La solution est donc l : on cre un tuyau et on
duplique son entre (le ct crivain) sur la sortie standard de la premire commande
(ls -l). Puis on duplique sa sortie (le ct lecteur) sur lentre standard de la deuxime
commande (wc -l). Ainsi, la premire commande crit dans le tuyau et la deuxime
commande lit dans le tuyau. La figure 15.5 dcrit ce cheminement.
Nous allons donc pouvoir programmer laide de fork() et dup2() lexercice
de recouvrement. Nous avons deux commandes et nous allons utiliser fork() pour
obtenir deux processus. Lequel du pre ou du fils doit se charger de la premire
commande ? Il faudrait que le pre se termine aprs le fils, donc le pre doit attendre
des informations du fils, ce qui implique ncessairement que le pre soit en charge de
la dernire commande et le fils de la premire. La figure 15.6 dcrit la chronologie des
vnements.

332

15.4. Corrections dtailles

F IGURE 15.5 La duplication de chaque descripteur de fichier permet de faire aboutir


un descripteur (ne ou nl) sur le tuyau. Si lon choisit par exemple de dupliquer stdin
sur tuyau[0] alors un processus qui partage le tuyau et qui lit dans stdin lira en fait
dans le tuyau.

F IGURE 15.6 Aprs la duplication de processus, nous avons quatre descripteurs


disponibles sur le tuyau, deux pour le pre et deux pour le fils. Dans le processus pre,
lutilisation de la duplication de descripteur et deux fermetures adquates permettent
de couper lentre en lecture du pre et de brancher lentre standard sur le tuyau
en lecture. Le pre ferme aussi son entre en criture. Du ct du fils, la duplication
de descripteur permet de brancher la sortie standard sur le ct criture du tuyau
et de fermer lentre criture du fils. Le fils ntant pas un lecteur, il ferme lentre
lecture du tuyau. On obtient donc bien un tuyau dans lequel il ny a quune entre en
lecture (lentre standard qui servira au pre lors de son recouvrement) et une entre
en criture (la sortie standard qui servira au fils lors de son recouvrement).

333

Chapitre 15. Les tuyaux sous Unix


Listing 15.15 Tuyau et recouvrement
#include
#include
#include
#include
#include

<unistd.h>
<stdlib.h>
<stdio.h>
<sys/types.h>
<sys/wait.h>

#define LECTURE 0
#define ECRITURE 1

int main(int argc, char **argv)


{
int tuyau[2];
if (pipe(tuyau) == -1) {
perror("Impossible de tuyauter");
exit(EXIT_FAILURE);
}
switch(fork()) {
case -1:
perror("Impossible de forker");
exit(EXIT_FAILURE);
case 0: /* processus fils */
/* Il est juste ecrivain, donc fermeture du cote */
/* lecteur.*/
close(tuyau[LECTURE]);
/* La premiere commande ls -l ecrit ses resultats */
/* dans le tuyau et non sur la sortie standard */
/* donc on duplique */
if (dup2(tuyau[ECRITURE],STDOUT_FILENO)==-1) {
perror("Impossible du dupliquer un descripteur");
close(tuyau[ECRITURE]);
exit(EXIT_FAILURE);
}
/* Comme le processus sera recouvert, le */
/* descripteur tuyau[ECRITURE] est inutile */
/* donc dangereux, on le ferme!*/
close(tuyau[ECRITURE]);
/* On recouvre le processus avec "ls -l" */
execlp("ls","ls","-l",NULL);
/* si on arrive ici cest que execlp va mal!*/
exit(EXIT_FAILURE);
default: /* processus pere */
/* Il est juste lecteur, donc fermeture du cote */
/* ecrivain.*/
close(tuyau[ECRITURE]);
/* la derniere commande wc -l doit lire ses */
/* arguments dans le tuyau et non */
/* sur lentree standard donc on duplique */
if (dup2(tuyau[LECTURE],STDIN_FILENO)==-1) {
perror("Impossible du dupliquer un descripteur");
close(tuyau[LECTURE]);
exit(EXIT_FAILURE);
}
/* Comme le processus sera recouvert, le */
/* descripteur tuyau[LECTURE] est inutile */
/* donc dangereux, on le ferme!*/
close(tuyau[LECTURE]);
/* On recouvre le processus avec "wc -l" */
execlp("wc","wc","-l",NULL);
/* si on arrive ici cest que execlp va mal!*/

334

15.4. Corrections dtailles


exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}

Linterprte de commandes

Le programme qui suit est relativement comment. Il est surtout important de


comprendre que dans ce programme il y a plusieurs tuyaux et que la sortie (ct
lecture) de lun est branch sur le ct criture dun autre afin dlaborer la suite des
commandes.
Listing 15.16 Ecriture dun shell
#include
#include
#include
#include
#include
#include
#include
#include

<sys/types.h>
<signal.h>
<stdio.h>
<stdlib.h>
<string.h>
<unistd.h>
<sys/wait.h>
<string.h>

#define LECTURE 0
#define ECRITURE 1
#define MAXCHAR_LIGNE 80
#define MAXARG 10
/* Structure de liste chainee pour les arguments */
/* dune commande */
typedef struct argument {
char *arg;
/* largument (non recopie)
*/
struct argument *suivant; /* ladresse du maillon suivant */
} Argument;
/* Structure de liste chainee pour les commandes */
typedef struct commande {
int numero;
/* le numero de la commande (4fun)
*/
int argc;
/* le nombre darguments de la commande*/
Argument *argv;
/* la liste chainee des arguments
*/
struct commande *suivante; /* ladresse du maillon suivant
*/
} Commande;
/* Cette fonction ajoute un argument (char *) a la liste
* chainee "l" des arguments.
* Si la liste est vide (l==NULL) la fonction retourne
* ladresse du nouveau maillon, sinon la fonction rajoute
* largument a la fin de la liste en se rappelant elle meme
* La liste construite a partir de :
* l=NULL;
* l = add_argument(l,arg1);
* l = add_argument(l,arg2);
* l = add_argument(l,arg3);
*
* sera donc : l(arg1) --> l(arg2) --> l(arg3) --> NULL
*/
Argument *add_argument(Argument *l, char *argvalue)
{

335

Chapitre 15. Les tuyaux sous Unix


Argument *newarg;
if (l==NULL) {
if ((newarg = malloc(sizeof(Argument))) == NULL) {
perror("Erreur dallocation dargument.");
exit(EXIT_FAILURE);
}
newarg->arg = argvalue;
newarg->suivant = NULL;
return newarg;
}
if (l->suivant == NULL) {
if ((newarg = malloc(sizeof(Argument))) == NULL) {
perror("Erreur dallocation dargument.");
exit(EXIT_FAILURE);
}
newarg->arg = argvalue;
newarg->suivant = NULL;
l->suivant = newarg;
return l;
}
add_argument(l->suivant,argvalue);
return l;
}
/* Cette fonction ajoute une commande darguments "arg"
* a la liste chainee de commandes "l".
* La nouvelle commande est rajoutee au debut de
* la liste (a linverse de la commande add_argument)
*/
Commande *add_commande(Commande *l, Argument *arg, int argc)
{
Commande *newcmd;
if (argc <= 0) return l;
if ((newcmd = malloc(sizeof(Commande))) == NULL) {
perror("Erreur dallocation de commande.");
exit(EXIT_FAILURE);
}
newcmd->argv = arg;
newcmd->argc = argc;
newcmd->suivante = l;
newcmd->numero = (l!=NULL?l->numero+1:0);
return newcmd;
}
/* Liberation des maillons (pas du champ (char *)arg
* car ce dernier nest pas recopie) de la liste
* darguments "l"
*/
Argument *free_argliste(Argument *l)
{
Argument *toBfree;
while(l != NULL) {
toBfree = l;
l=l->suivant;
free(toBfree);
}
return NULL;
}
/* Liberation des maillons (champ Argument *argv compris)
* de la liste des commandes "l"
*/

336

15.4. Corrections dtailles


Commande *free_cmdliste(Commande *l)
{
Commande *toBfree;
while(l != NULL) {
toBfree = l;
l=l->suivante;
free_argliste(toBfree->argv);
free(toBfree);
}
return NULL;
}
void show_argliste(Argument *l)
{
Argument *cur;
int i=0;
cur = l;
while(cur != NULL) {
fprintf(stdout,"\tArgument %3d: \"%s\" (%10p)\n",
i++,cur->arg,cur->arg);
cur = cur->suivant;
}
}
void show_commande(Commande *cur)
{
fprintf(stdout,"Commande %3d avec %2d argument(s): \n",
cur->numero,cur->argc);
show_argliste(cur->argv);
}
void show_allcommande(Commande *c)
{
Commande *cur;
cur = c;
while(cur != NULL) {
show_commande(cur);
cur = cur->suivante;
}
}
/* Fonction permettant de construire la liste chainee
* darguments a partir dune chaine de caracteres.
* Chaque argument est separe du suivant par " " ou par
* un caractere de tabulation ou par un retour chariot (ce
* qui ne devrait pas arriver
*/
Argument *parse_argument(char *ligne, int *argc)
{
char *curarg,*ptrtmp;
Argument *listearg=NULL;
*argc=0;
while((curarg =
strtok_r(listearg==NULL?ligne:NULL," \t\n",&ptrtmp)) != NULL) {
listearg = add_argument(listearg,curarg);
*argc += 1;
}
return listearg;
}
/* Fonction danalyse dune ligne de commandes. Chaque commande
* est separee de la suivante par le caractere | (tuyau). Une

337

Chapitre 15. Les tuyaux sous Unix


* fois reperee la fin de la commande, les caracteres compris entre
* le debut (start) et la fin (on remplace | par \0) sont
* envoyes a parse_argument puis la nouvelle commande est placee
* dans la liste chainee des commandes.
*/
Commande *parse_ligne(char *ligne)
{
Commande *com=NULL;
Argument *args=NULL;
int argc;
char *start;
int i,len;
/* on supprime le dernier caractere: \n */
ligne[strlen(ligne)-1] = \0;
len = strlen(ligne);
for (i = 0,start=ligne;i<len;i++) {
if (ligne[i] == |) {
ligne[i] = \0;
args = parse_argument(start,&argc);
com = add_commande(com,args,argc);
start = ligne+i+1;;
}
}
args = parse_argument(start,&argc);
com = add_commande(com,args,argc);
return com;
}
/* Cette fonction prend une liste chainee darguments et construit
* un tableau dadresses telles que chacune pointe sur un argument
* de la liste chainee. Ceci permet dutiliser execvp() pour recouvrir
* le processusen cours par la commande souhaitee avec ses arguments.
*/
char **construire_argv(Commande *curcom)
{
Argument *curarg;
char **argv;
int i;
argv = malloc((curcom->argc+1)*sizeof(char *));
for(i=0,curarg=curcom->argv;i<curcom->argc;
i++,curarg=curarg->suivant) {
argv[i] = curarg->arg;
}
argv[i] = NULL;
return argv;
}
/*
*
*
*
*
*
*
*
*
*
*
*
*
*
*

Cette fonction met en place lexecution dune commande. Elle


commence par creer un tuyau puis procede a un appel a fork().
Le processus fils ferme le cote ecriture, branche son entree
standard sur le cote lecture puis ferme le cote lecture.
Si le fils ne correspond pas au premier appel (donc a la
derniere commande de la ligne analysee), il branche sa sortie
standard sur le cote ecriture du tuyau precedent puis il ferme
le cote ecriture du tuyau precedent. Il se recouvre ensuite
avec la commande desiree.
Le pere ferme le cote lecture du tuyau en cours et si il ne
sagit pas du premier appel (donc de la derniere commande de
la ligne analysee) il ferme le cote ecriture du tuyau precedent.

338

15.4. Corrections dtailles


* Il met a jour la variable prec_ecriture en disant que pour
* les appels a venir, le tuyau precedent en ecriture est le tuyau
* courant en ecriture. Il retourne enfin le pid de son fils.
*/
int execute_commande(char **argv, int *prec_ecriture)
{
int tuyau[2];
int pid;
if (pipe(tuyau) == -1) {
perror("Impossible de creer le tuyau");
exit(EXIT_FAILURE);
}
pid = fork();
switch (pid) {
case -1:
perror("Impossible de creer un processus");
exit(EXIT_FAILURE);
case 0:
/* pas besoin detre ecrivain dans ce tuyau */
close(tuyau[ECRITURE]);
/* branchement de stdin sur la lecture du tuyau */
dup2(tuyau[LECTURE],STDIN_FILENO);
close(tuyau[LECTURE]);
/* branchement de stdout sur lecriture du tuyau precedent */
/* sil y a un tuyau precedent */
if (*prec_ecriture >= 0) {
dup2(*prec_ecriture,STDOUT_FILENO);
close(*prec_ecriture);
}
execvp(argv[0],argv);
perror("Impossible de proceder au recouvrement");
fprintf(stderr,"commande: \"%s\"\n",argv[0]);
exit(EXIT_FAILURE);
default :
/* pas besoin de lire sur le tuyau car on est le pere,
* et on se contente de regarder passer les anges
*/
close(tuyau[LECTURE]);
if (*prec_ecriture >= 0) {
if(close(*prec_ecriture)) {
fprintf(stderr,"Pere:Impossible de fermer");
fprintf(stderr,"le tuyau precedent en ecriture\n");
}
}
*prec_ecriture = tuyau[ECRITURE];
return pid;
}
exit(EXIT_FAILURE);
}
int main(int margc, char **margv)
{
char ligne[MAXCHAR_LIGNE];
Commande *com=NULL;
Commande *curcom;
char **argv;
int prec_ecriture;
int pid=-1;
/* On affiche une invitation! */
printf("myshell>");

339

Chapitre 15. Les tuyaux sous Unix


/* Tant que lon peut lire des trucs, on lit */
while (fgets(ligne, sizeof(ligne),stdin) != NULL) {
/* Si lutilisateur veut sortir de ce magnifique interprete
* on sort en tuant le processus en court
*/
if (!strncmp(ligne,"exit",4)) exit(EXIT_SUCCESS);
/* On analyse la ligne de commandes afin de construire
* les differentes listes chainees.
*/
com = parse_ligne(ligne);
/*
/*
/*
/*

Les commandes sont maintenant rangees dans une liste


*/
la derniere commande de la ligne est en tete de liste */
puis vient la suivante, etc. jusqua la premiere de la */
ligne qui se trouve en fin de liste */

/* Le tuyau precedent nexiste pas encore, donc -1 */


prec_ecriture=-1;
curcom = com;
/* On analyse chaque commande de la ligne en commencant par
* celle qui est la plus a droite
*/
while (curcom != NULL) {
/* on construit le fameux tableau char *argv[] */
argv = construire_argv(curcom);
/* Sil sagit de la premiere commande (celle de droite!)
* on recupere son pid() histoire de pouvoir attendre quelle
* soit finie avant de rendre la main a lutilisateur
*/
if (prec_ecriture < 0)
pid = execute_commande(argv,&prec_ecriture);
else
execute_commande(argv,&prec_ecriture);
free(argv);
argv=NULL;
curcom = curcom->suivante;
}
/* Fin danalyse de la ligne */
if (prec_ecriture >= 0) {
if (close(prec_ecriture)) {
fprintf(stderr,"Pere:Impossible de fermer ");
fprintf(stderr,"le tuyau precedent en ecriture ");
fprintf(stderr,"en fin de boucle\n");
}
}
if (pid >=0) {
waitpid(pid,NULL,0);
}
com=free_cmdliste(com);
printf("myshell>");
}
exit(EXIT_SUCCESS);
}

340

Vous aimerez peut-être aussi