Nous poursuivons l’exploration des fonctionnalités offertes par le noyau aux programmeurs C (appels système). Aujourd’hui nous allons voir ce qu’est un pipe, et comment l’utiliser pour simuler ce que fait l’interpréteur de commandes (shell) bash lorsqu’on y tape une commande mettant en oeuvre des pipes.
Qu’est ce qu’un pipe ?

Un pipe est un tuyau de communication uni-directionnel présent dans le noyau Linux en mémoire. Ce tuyau a donc une entrée et une sortie. Plusieurs processus peuvent se connecter à l’entrée et y écrire des données, tout comme plusieurs processus peuvent se connecter à la sortie et y lire les données.
Les données écrites à l’entrée ressortent (si lues) à la sortie, sans perte et dans le même ordre.
Le nombre de pipes présents dans le noyau est fixé avant la compilation du noyau.
A quoi sert un pipe ?
Nous avons vu dans les épisodes précédents que les processus Unix / Linux sont sur des pages mémoire virtuelles, ce qui garantit, entre autres, qu’ils sont étanches les uns par rapport aux autres.
Un processus A n’a aucun moyen en propre de communiquer des données à un processus B. Il peut le faire mais il doit passer par l’un des moyens de communication inter-processus offerts par le noyau, comme les pipes.
Comment utilise t’on les pipes en C ?
On utilise l’appel système pipe (voir man 2 pipe), en passant en paramètre une variable tube de type int *
A l’issue de cet appel système :
- tube[0] comporte le numéro du descripteur de fichier du processus qui pointe sur la sortie du pipe (côté lecture),
- tandis que tube[1] comporte le numéro du descripteur de fichier du processus qui pointe vers l’entrée du pipe (côté écriture). Le lecteur peu familier des descripteurs de fichiers se rapportera utilement à l’article qui en traitait.
Dis autrement, si le processus exécute write(tube[1],adresse, nombre_octets), il écrit dans le pipe, et s’il exécute read(tube[0],adresse,nombre_octets), il lit ce qu’il y a dans le pipe.
Dernier détail, comme le pipe est utilisé via la table des descripteurs de fichiers du processus, si celui-ci exécute close(tube[0]) il ferme son accès au pipe en lecture et s’il exécute close(tube[1]), il ferme son accès au pipe en écriture.
Cas d’usage
Le cas d’usage typique numéro 1 sera celui où deux processus (un père et un fils créé par fork() par exemple) s’envoient des données via le pipe en se prévenant en s’envoyant des signaux SIGUSR1 ou SIGUSR2 (voir l’article sur les signaux).
Le cas d’usage typique numéro 2 est quand on écrit un shell comme bash et qu’on doive gérer les pipes tapés en ligne de commande par les utilisateurs.
Travaux pratiques
C’est ce cas d’usage que nous allons traiter. L’exercice est simple : l’utilisateur tape dans le shell bash la commande :
ls / | wc -l
Tout le monde sait que cette commande donne le nombre d’entrées qu’il y à sous / dans le système de fichiers, puisque la première partie de la commande (ls /) liste ces entrées, et la deuxième partie de la commande (wc -l) compte le nombre de lignes. Entre ces deux parties, il y a un pipe, représenté par le symbole | dans la ligne de commande.
La question maintenant est la suivante : qu’a bien pu programmer le développeur du shell bash pour que le pipe se produise lorsqu’un utilisateur tape cette suite de commandes ?
Dans cet exercice, nous allons recréer le bout de code qui est dans bash et qui traite ce cas de figure.
Voici la séquence qui se joue :
- Tout d’abord bash va compter le nombre de pipes | qui sont écrits sur la ligne de commande. Ici ce sera 1.
- bash va alors exécuter l’appel système pipe pour réclamer 1 pipe au noyau
- bash va alors créer 2 processus fils (le nombre de caractères | +1). Le premier processus fera une commutation d’image vers /usr/bin/ls, tandis que le deuxième processus fera une commutation d’image vers /usr/bin/wc
Mais attention, tel quel cela ne marchera pas, car ls écrit ses données sur la sortie standard (de numéro 1 si vous avez suivi les épisodes précédents) et non sur tube[1], et par ailleurs wc lit ses données sur l’entrée standard (de numéro 0) et non sur tube[0].
Tel quel le chainage par le pipe ne peut pas se produire, car les branchements de tuyauterie ne sont pas correctement faits.
C’est là qu’un autre appel système indispensable apparaît : dup (voir man 2 dup)
dup va dupliquer le descripteur passé en paramètre dans le premier descripteur disponible.
Voyons donc concrètement comment cela est mis en oeuvre dans le code source commenté de notre exercice :
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
int tube[2]; // variable qu'on utilise pour se brancher à un pipe du noyau
int status;
pipe(tube); // à l'issue de cet appel système, tube[0] permet de lire dans le pipe, et tube[1] permet d'écrire dans le pipe
if(fork()==0)
{
// ici nous sommes dans le code du processus fils 1, qui fera bientôt une commutation d'image sur "ls /"
// Ce processus hérite de la table des descripteurs de fichiers de son père (tube[0] et tube[1] pointent donc vers le pipe)
close(tube[0]); // ce processus ne lira rien dans le pipe, cette entrée est donc inutile
close(1); // L'entrée 1 (sortie standard) est donc maintenant disponible
dup(tube[1]); // tube[1], qui point en écriture vers le pipe, est dupliqué dans la première entrée disponible, c'est à dire 1
// à partir de là, la sortie standard de ce processus pointe vers le pipe en écriture
close(tube[1]); // on peut fermer cette entrée, qui est devenue inutile car ls écrira sur la sortie standard
// à partir de là, tous les branchements de tuyauterie pour ls sont fait, il reste à commuter l'image
execl("/usr/bin/ls","ls","/",NULL);
}
if(fork()==0)
{
// ici nous sommes dans le code du processus fils 2, qui fera bientôt une commutation d'image sur "wc -l"
// Ce processus hérite de la table des descripteurs de fichiers de son père (tube[0] et tube[1] pointent donc vers le pipe)
close(tube[1]); // ce processus n'écrira rien dans le pipe, cette entrée est donc inutile
close(0); // L'entrée 0 (entrée standard) est donc maintenant disponible
dup(tube[0]); // tube[0], qui point en lecture vers le pipe, est dupliqué dans la première entrée disponible, c'est à dire 0
// à partir de là, la sortie standard de ce processus pointe vers le pipe en écriture
close(tube[0]); // on peut fermer cette entrée, qui est devenue inutile car wc lira sur l'entrée standard
// à partir de là, tous les branchements de tuyauterie pour wc sont fait, il reste à commuter l'image
execl("/usr/bin/wc","wc","-l",NULL);
}
// le processus père doit maintenant fermer tube[0] et tube[1] car sinon il y aurait toujours un processus pointant sur le pipe en écriture même après la mort de ls. ws resterait donc en attente permanente
close(tube[0]);
close(tube[1]);
// le processus père (bash) attend maintenant que les deux processus fils soient terminés pour rendre la main
wait(&status);
wait(&status);
}
Voici l’exécution :

ls / montre d’abord ce qu’il y a sous /
ls / | wc -l montre qu’il y a 23 entrées sous /. Ici c’est le shell bash qui a interprété la commande et à demandé un pipe au noyau
Puis on compile notre code source via gcc et on exécute le programme prog.
Et la magie opère : ce programme fait le même travail que le shell bash, en exécutant la même séquence que lui 😎
