Les pipes sont bien connus dans le monde d'Unix. En effet, ils permettent
de faire communiquer deux processus entre eux. Ils sont représentés par le
caractère "|". On les utilisent courrament dans les terminaux
pour rediriger la sortie d'une commande vers l'entré d'une autre commande,
par exemple : "ls | wc". Ce qui est moins courrant c'est de les
utiliser dans un programme en langage C. C'est cela que je vais expliquer
dans cet article.
Créer un pipe dans un processus unique n'a pas beaucoup d'interêt mais cela nous permet de comprendre ce qui caractérise un pipe :
Concrétement, pour créer un simple pipe en langage C, voila ce qu'il faut écrire :
#include <stdio.h>
#include <memory.h>
#include <unistd.h>
int main( int argc, char ** argv )
{
char buffer[BUFSIZ+1];
/* create the pipe */
int fd[2];
pipe(fd);
/* write into the pipe */
write(fd[1], "Hello World\n", strlen("Hello World\n"));
/* read the pipe and print the read value */
read(fd[0], buffer, BUFSIZ);
printf("%s", buffer);
}
Nous utilisons la fonction pipe(fd) qui va réserver deux
descripteurs de fichiers dans le tableau fd, fd[0] pour
l'extrémité à lire et fd[1] pour l'extrémité à écrire.
La différence avec l'exemple précedent est que, en plus de créer un pipe, notre processus cré un fils. Le pipe est alors automatiquement partagé entre le père et le fils. Si l'un écrit dans le pipe alors on ne sait pas lequel des deux va recevoir l'information. Ceci peut donner des résultats inattendus.
Pour être certain de qui va écrire et qui va lire dans le pipe, il faut que les processus ferment les extrémités qu'ils n'utilisent pas.
De cette façon le processus père peut être certain que s'il écrit dans le
pipe (fd[1]), le fils va reçevoir l'information en lecture
(fd[0]).
Concretement voici comment faire en C :
#include <stdio.h>
#include <memory.h>
#include <unistd.h>
int main( int argc, char ** argv )
{
/* create the pipe */
int pfd[2];
if (pipe(pfd) == -1)
{
printf("pipe failed\n");
return 1;
}
/* create the child */
int pid;
if ((pid = fork()) < 0)
{
printf("fork failed\n");
return 2;
}
if (pid == 0)
{
/* child */
char buffer[BUFSIZ];
close(pfd[1]); /* close write side */
/* read some data and print the result on screen */
while (read(pfd[0], buffer, BUFSIZ) != 0)
printf("child reads %s", buffer);
close(pfd[0]); /* close the pipe */
}
else
{
/* parent */
char buffer[BUFSIZ];
close(pfd[0]); /* close read side */
/* send some data into the pipe */
strcpy(buffer, "HelloWorld\n");
write(pfd[1], buffer, strlen(buffer)+1);
close(pfd[1]); /* close the pipe */
}
return 0;
}
On a vu dans cet exemple un échange d'informations entre le père et le fils. Si l'on souhaite échanger des informations dans l'autre sens il faut créer un deuxième pipe et l'initialiser dans l'autre sense.
Nous allons voir maintenant que chaque processus possède à sa création trois pipes nommés stdin, stdout, et stderr.
Ces trois pipes sont par défault créés dans chaques processus. Le premier, stdin, est branché par défaut sur l'entrée clavier tandis que stdout et stderr sont eux branchés sur la sortie écran. Des descripteurs de fichiers par défaut leur sont associés : 0 pour stdin, 1 pour stdout, et 3 pour stderr.
Maintenant que l'on connait le principe de communication par pipe, on est
tenté de connecter ces pipes entre eux. Par exemple essayons de connecter
stdout d'un premier processus avec le stdin d'un second. Cette opération est
effectuée par le shell à chaque fois que deux commandes séparées par un pipe
sont executées, par exemple pour relier la sortie stdout de la commande ls
avec l'entrée stdin de la commande wc, il faut taper ceci dans un terminal :
ls | wc.
Pour réaliser ceci dans un programme en langage C, il faut procéder en plusieurs étapes.
Il faut commencer par créer un pipe vide : fd=3 en écriture
et fd=4 en lecture. On utilise donc la fonction
pipe(fd) comme vue ci dessus.
Ensuite il faut que stdout du premier processus (fd1) soit connecté à
l'entrée de notre pipe (fd3) et que la sortie de notre pipe (fd4) soit
connecté à stdin de notre second processus (fd0). On appelera deux fois la
fonction dup2(param1, param2) pour connecter fd1 à fd3 et fd4 à
fd0. dup2 prend en argument deux paramètres, ce sont des
descripteurs de fichiers : param1 vaudra fd3 et param2 vaudra fd1 car on veut
que fd3 soit assimilé (ou connecté) à fd1. Pour le second processus, param1
vaudra fd4 et parm2 vaudra fd0.
Finalement il faut fermer les extrémités de notre pipe pour éviter les
comportements étranges, c'est un peu le même problème que dans la section
précedente. On utilisera la commande close(fd) pour fermer les
extremités de notre pipe.
Maintenant passons à la mise en oeuvre, nous allons simuler la commande
ls | wc en langage C :
#include <stdio.h>
#include <memory.h>
#include <unistd.h>
int main( int argc, char ** argv )
{
/* create the pipe */
int pfd[2];
if (pipe(pfd) == -1)
{
printf("pipe failed\n");
return 1;
}
/* create the child */
int pid;
if ((pid = fork()) < 0)
{
printf("fork failed\n");
return 2;
}
if (pid == 0)
{
/* child */
close(pfd[1]); /* close the unused write side */
dup2(pfd[0], 0); /* connect the read side with stdin */
close(pfd[0]); /* close the read side */
/* execute the process (wc command) */
execlp("wc", "wc", (char *) 0);
printf("wc failed"); /* if execlp returns, it's an error */
return 3;
}
else
{
/* parent */
close(pfd[0]); /* close the unused read side */
dup2(pfd[1], 1); /* connect the write side with stdout */
close(pfd[1]); /* close the write side */
/* execute the process (ls command) */
execlp("ls", "ls", (char *)0);
printf("ls failed"); /* if execlp returns, it's an error */
return 4;
}
return 0;
}
Ici on découvre l'utilisation d'une nouvelle fonction
execlp(...) qui permet de remplacer le processus en cours par
l'execution d'une commande : dans notre cas la commande wc est
appelée dans le processus fils et la commande ls est appelée
dans le processus père. Cette fonction ne retroune rien sauf si une erreure
se produit à l'execution de la commande.
On a vu dans cet article comment créer des pipes mais on peut se demander à quoi peuvent ils servire. Il y a certainement une infinité d'applications associées à ce mécanisme mais voici l'idée que j'avais derrière la tête et qui m'a poussé à étudier les pipes : créer une interface utilisateur (GUI) permettant de contrôler un programme en ligne de commande (qui utilise donc stdin, stdout et stderr pour communiquer avec l'exterieur). En effet, si on peut contrôler l'entré (stdin) et la sortie (stdout, stderr) d'un programme en l'encapsulant dans un autre programme père, on peut tout faire avec !
Dernière mise à jour le 2006-12-07