Le carnet On est
Cours 3 · encre bordeaux

Les tubes (pipes)

Communication entre processus sur une même machine : tubes anonymes (pipe + fork), tubes nommés (mkfifo), synchronisation FIFO, redirection avec dup2, gestion SIGPIPE.

~ 35 min de lecture TP — pipes anonymes/nommés 14 flashcards 8 quiz

📄 Ressources du cours

📥 Télécharger les slides  ·  🔗 Ouvrir dans un nouvel onglet

🎥 Vidéos recommandées

1. Pourquoi les tubes ?

Plusieurs processus tournent simultanément sur une même machine et veulent échanger de l'information. Les questions classiques : quels moyens de communication ? comment choisir ? comment être sûr que le bon processus reçoit la donnée ? comment hiérarchiser l'exécution ?

Le tube — définition Un tube (ou pipe) est un moyen de communication entre deux processus s'exécutant sur la même machine. C'est un fichier particulier géré par le noyau, qui se comporte comme une file FIFO en mémoire. Deux variantes :
  • Tube anonyme — pas de nom, créé par pipe(), nécessite un lien de parenté.
  • Tube nommé — visible dans le système de fichiers, créé par mkfifo(), accessible à tout processus connaissant son nom.

Un air de déjà vu — le pipe shell

La commande :

$ ps -a | wc -l

déclenche deux processus concurrents et un tube :

  • Un tube est créé entre ps -a et wc -l.
  • Les résultats de ps -a sont écrits dans ce tube.
  • wc -l lit dans le tube et compte les lignes.
  • Quand ps -a termine et que wc -l a fini de lire (tube vide, plus d'écrivain), wc -l détecte la fin de fichier sur son entrée standard et se termine à son tour.
📌 FIFO + destructive
Le tube se comporte comme une file FIFO (premier entré, premier sorti) unidirectionnelle. Les lectures sont destructrices : une donnée lue par un processus disparaît du tube — un autre processus ne pourra plus la lire.

2. Caractéristiques générales d'un tube

Un tube est presque identique à un fichier ordinaire :

  • Taille limitée — définie par la constante PIPE_BUF dans <limits.h>.
  • Deux extrémités : une pour lire, une pour écrire.
  • Au plus deux entrées dans la table des fichiers ouverts : une pour la lecture, une pour l'écriture.
  • L'opération de lecture est destructive : une donnée ne peut être lue qu'une seule fois.

Synchronisation gérée par le système

Le système assure automatiquement la synchronisation entre lecteurs et écrivains :

Tube vide → lecture bloquante Le processus lecteur est mis en attente tant que le tube ne contient aucun caractère, en espérant qu'il se remplisse (à condition qu'il reste des écrivains).
Tube plein → écriture bloquante Si le lecteur est plus lent que l'écrivain et que le volume des données dépasse la capacité, l'écrivain est bloqué jusqu'à ce que le lecteur consomme.
Cas pathologiques sans lecteur/écrivain
  • Écriture sans lecteur (nombre de descripteurs en lecture = 0) : l'écrivain reçoit le signal SIGPIPE, comportement par défaut = fin du processus. write() renvoie -1 avec errno = EPIPE.
  • Lecture sans écrivain (nombre de descripteurs en écriture = 0) : le lecteur détecte une fin de fichier ; read() renvoie 0.

3. Tubes anonymes

Caractéristiques
  • Structure sans nom — aucune référence dans le système de fichiers.
  • Communication entre deux processus, sur deux descripteurs (lecture et écriture).
  • Pointeurs automatiques : lecture sur le 1er caractère non lu, écriture sur le 1er emplacement vide.
  • Lien de parenté obligatoire : processus de même filiation ou ancêtre commun ayant créé le tube.
  • Canal half-duplex : communication dans les deux sens possible, mais une seule direction à la fois (PA peut parler à PB et inversement, mais pas simultanément).
  • Destruction automatique à la fin de l'utilisation (tous les descripteurs fermés).

Primitive pipe()

#include <unistd.h>
int pipe(int p[2]);

Crée un tube anonyme et renseigne le tableau p avec deux descripteurs :

DescripteurRôle
p[0]descripteur en lecture
p[1]descripteur en écriture

Retourne 0 si succès, -1 sinon. Les deux descripteurs sont alloués dans la table des fichiers ouverts du processus appelant.

Le rituel : pipe()fork()

Pour qu'un tube anonyme serve à la communication entre deux processus, il faut :

  1. Le processus père crée le tube avec pipe().
  2. Le père fork : le fils hérite automatiquement des descripteurs du tube.
  3. Chaque processus ferme le descripteur qu'il n'utilise pas (sinon EOF jamais détecté).
  4. Communication par read() et write().
  5. Fermeture finale avec close().
Processus A (père) int fds[2]; pipe(fds); fork(); fds[0] fds[1] Processus B (fils) hérité du père : fds[0], fds[1] Noyau TUBE FIFO Le fils hérite des deux descripteurs : chacun ferme celui qu'il n'utilise pas.

Schéma de communication unidirectionnelle (père → fils)

Supposons que le père écrive et que le fils lise. Les cinq étapes :

  1. Le processus père crée un tube.
  2. Il fait fork() pour créer un fils.
  3. Les deux processus possèdent chacun un descripteur en lecture et en écriture sur le tube.
  4. Le père ferme son descripteur en lecture (close(fds[0])). Le fils ferme son descripteur en écriture (close(fds[1])).
  5. Le père peut écrire dans le tube ; les données pourront être lues par le fils.
⚠ Pourquoi fermer les descripteurs inutiles ? Si le fils garde fds[1] ouvert alors qu'il ne s'en sert pas, il y aura toujours un écrivain potentiel actif : le fils ne détectera jamais la fin de fichier quand le père termine. Le programme se bloque. Fermer ce qu'on n'utilise pas est indispensable.

Lire et écrire — read() et write()

// Lecture dans le tube
int read(int desc, char* buf, int nb);

Lit nb octets depuis le descripteur desc, les place dans buf, retourne le nombre réellement lu. Sémantique :

  • Tube non vide de taille t : extrait min(t, nb) caractères.
  • Tube vide avec écrivains actifs → lecture bloquante.
  • Tube vide sans écrivain → fin de fichier, renvoie 0.
// Écriture dans le tube
int write(int desc, char* buf, int nb);
  • Nombre de lecteurs = 0 → SIGPIPE envoyé à l'écrivain, fin du processus par défaut, message shell Broken pipe.
  • Lecteurs présents → écriture bloquante jusqu'à ce que les nb octets soient effectivement écrits.

Exemple complet — père écrit « Salut » au fils

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

int main() {
   const int Nbuff = 1000;
   char buff[Nbuff];
   int fds[2], pid, n, status;

   pipe(fds);                       // 1. création du tube

   if ((pid = fork()) > 0) {        // PÈRE
      close(fds[0]);                  // 2. père ferme la lecture
      write(fds[1], "Salut", 5);     // 3. écrit dans le tube
      wait(&status);                // 4. attend le fils
   } else {                          // FILS
      close(fds[1]);                  // 5. fils ferme l'écriture
      n = read(fds[0], buff, Nbuff - 1);
      buff[n] = '\0';
      printf("%s\n", buff);          // affiche "Salut"
      exit(0);
   }
   return 0;
}

Communication bidirectionnelle — modèle question/réponse

Pour faire dialoguer A et B dans les deux sens, on utilise deux tubes : un par direction. Chaque processus ferme les descripteurs inutiles pour éviter blocage et lectures parasites.

🔑 Une seule paire de tubes ?
Tenter d'utiliser un seul tube dans les deux sens pose des problèmes : si PA écrit puis lit, il risque de relire sa propre donnée avant que PB ne la prenne. Toujours deux tubes pour le bidirectionnel.

4. Redirection avec dup / dup2

On veut souvent connecter la sortie d'un tube à l'entrée standard d'un processus, ou son entrée à la sortie standard. Ainsi un programme qui lit habituellement stdin (comme wc -l) lit en réalité notre tube, sans modification.

Les deux primitives
  • int dup(int desc); — duplique desc sur le plus petit descripteur libre.
  • int dup2(int oldfd, int newfd); — fait pointer newfd sur le même objet que oldfd. Si newfd était ouvert, il est d'abord fermé. C'est la version utile pour rediriger.
Rappel : 0 = stdin, 1 = stdout, 2 = stderr.

Reproduire ls | wc -l en C

#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>

int main() {
   int p[2];
   pipe(p);

   if (fork() == 0) {              // fils — exécutera wc -l
      close(p[1]);                // pas d'écriture pour le fils
      dup2(p[0], 0);              // stdin ← extrémité lecture du tube
      close(p[0]);                // on n'a plus besoin du desc original
      execlp("wc", "wc", "-l", NULL);
   } else {                       // père — exécutera ls
      close(p[0]);                // pas de lecture pour le père
      dup2(p[1], 1);              // stdout → extrémité écriture du tube
      close(p[1]);
      execlp("ls", "ls", NULL);
   }
   return 0;
}

Après dup2(p[1], 1), tout printf ou write(1, ...) du père écrit en réalité dans le tube. Idem pour wc qui lit son stdin — donc le tube.

5. Tubes nommés (FIFO)

Différences avec les tubes anonymes Un tube nommé est un fichier avec un nom dans le système de fichiers (visible avec ls -l, type p dans la colonne mode). Il est caractérisé par :
  • Un i-node existant sur disque (mais pas de blocs de données — uniquement un buffer noyau).
  • Pas besoin de lien de parenté — tout processus connaissant le nom et ayant les droits peut l'utiliser.
  • Persiste sur disque jusqu'à un unlink explicite.

Création — mkfifo

En shell :

$ mkfifo /tmp/mon_tube
$ ls -l /tmp/mon_tube
prw-r--r-- 1 user user 0 mai 17 22:30 /tmp/mon_tube
    ↑ le type 'p' indique un tube nommé

En C :

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char* nomfichier, mode_t mode);
  • nomfichier — chemin du tube à créer.
  • mode — droits d'accès, comme chmod (S_IRUSR | S_IWUSR | S_IXUSR… ou en octal 0644).
  • Renvoie 0 si succès, -1 si échec (errno détaille).

Ouverture — open()

int desc = open(nom_du_tube, mode);

Le mode est typiquement O_RDONLY ou O_WRONLY. Le processus doit avoir les droits correspondants.

⚠ Ouverture bloquante par défaut
  • Une open(O_RDONLY) bloque tant qu'il n'y a pas d'écrivain.
  • Une open(O_WRONLY) bloque tant qu'il n'y a pas de lecteur.
C'est ce mécanisme qui permet d'établir un rendez-vous entre deux processus indépendants. Pour un comportement non-bloquant : O_NONBLOCK.

Lecture, écriture, fermeture

Identiques aux tubes anonymes : read, write, close. Suppression du fichier avec unlink ou en shell rm.

Exemple — écrivain et lecteur indépendants

Deux programmes séparés, lancés depuis deux terminaux différents.

ecrivain.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main() {
   mode_t mode = 0644;
   int tub;

   mkfifo("fictub", mode);
   tub = open("fictub", O_WRONLY);
   write(tub, "0123456789", 10);
   close(tub);
   exit(0);
}
lecteur.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main() {
   char buf[11];
   int tub;

   tub = open("fictub", O_RDONLY);
   read(tub, buf, 10);
   buf[10] = '\0';
   printf("J'ai lu %s\n", buf);
   unlink("fictub");
   exit(0);
}
# Terminal 1 :
$ ./ecrivain     # bloque sur open(O_WRONLY)

# Terminal 2 :
$ ls -l fictub   # vérifie la création du tube nommé
$ ./lecteur      # déclenche le rendez-vous → les deux progressent
J'ai lu 0123456789
⚠ Risque d'interblocage Pour un dialogue à deux sens avec un seul tube nommé, l'ordre d'ouverture par les deux processus doit être cohérent. Si A ouvre en lecture et B ouvre en lecture aussi simultanément, les deux bloquent indéfiniment : interblocage.

6. Tube anonyme vs tube nommé

CritèreTube anonyme (pipe)Tube nommé (mkfifo)
Nom dans le FSAucunOui, fichier de type p
Lien de parenté requisOui (créé puis hérité par fork)Non — tout processus connaissant le nom
VisibilitéPrivée à la lignée de processusGlobale (selon droits POSIX)
PersistanceDétruit à la fermeture du dernier descripteurPersiste tant que pas unlink
Créationpipe(int[2])mkfifo(nom, mode) + open()
Usage typiquePipeline shell, communication parent-enfantDémons, services FIFO sur disque

Réviser le chapitre

❓ Q/R éclair

Pourquoi un tube est-il qualifié de « FIFO unidirectionnel » ?

FIFO car les données sont lues dans l'ordre où elles ont été écrites (première écrite, première lue). Unidirectionnel car le canal a une seule direction à la fois (half-duplex) : il faut deux tubes pour un dialogue dans les deux sens.

Que se passe-t-il si on écrit dans un tube sans lecteur ?

Le système envoie le signal SIGPIPE à l'écrivain. Le comportement par défaut est la fin du processus. write() renvoie -1 avec errno = EPIPE. Le shell affiche « Broken pipe ».

Que renvoie read si le tube est vide et qu'il n'y a plus d'écrivain ?

Le lecteur détecte une fin de fichier : read renvoie 0. C'est ainsi qu'un programme comme wc -l sait quand s'arrêter.

Pourquoi faut-il fermer les descripteurs inutiles après fork ?

Si le fils garde ouvert le descripteur en écriture alors qu'il ne s'en sert pas, le système considère qu'il existe toujours un écrivain potentiel : le tube ne renverra jamais EOF, même quand le père termine. Le programme se bloque indéfiniment.

Quelle est la séquence pipe → fork → close → IO ?
  1. Père : pipe(fds)
  2. Père : fork() (le fils hérite des deux descripteurs)
  3. Chaque processus : close() du descripteur qu'il n'utilise pas
  4. Communication : read() / write()
  5. Fermeture finale : close()
À quoi sert dup2(p[0], 0) ?

À rediriger le stdin (descripteur 0) vers l'extrémité de lecture du tube. Après cet appel, toute lecture depuis stdin (par exemple scanf ou read(0, ...)) lit en réalité dans le tube. Indispensable pour reproduire un pipeline shell en C.

Pourquoi un tube nommé n'a-t-il pas besoin de lien de parenté ?

Parce qu'il est référencé par un nom dans le système de fichiers. Tout processus connaissant ce nom (et ayant les bons droits) peut l'ouvrir avec open() — pas besoin d'avoir été créé par un ancêtre commun.

Pourquoi open() sur un tube nommé est-il bloquant ?

Pour permettre un rendez-vous. Une open(O_RDONLY) attend qu'un autre processus fasse open(O_WRONLY) (et vice-versa). Quand les deux se rencontrent, ils débloquent simultanément et peuvent communiquer. Pour éviter ce blocage : O_NONBLOCK.

Que retourne pipe() ? Comment sont organisés les descripteurs ?

Renvoie 0 en cas de succès, -1 sinon. Renseigne le tableau p avec deux descripteurs : p[0] en lecture, p[1] en écriture. Mnémotechnique : 0 comme stdin (lecture), 1 comme stdout (écriture).

Quel est le rôle de PIPE_BUF ?

Constante définie dans <limits.h> qui donne la taille du buffer du tube. Sur Linux typique : 4096 octets. Une écriture de taille inférieure ou égale à PIPE_BUF est garantie atomique.

🃏 Flashcards

Signature de pipe ?
int pipe(int p[2]); · p[0] = lecture, p[1] = écriture. Retourne 0 si OK, -1 sinon. Inclure <unistd.h>.
Signature de mkfifo ?
int mkfifo(const char* nom, mode_t mode);. Crée un tube nommé. Inclure <sys/types.h> + <sys/stat.h>.
Différence anonyme vs nommé ?
Anonyme : pas de nom, lien de parenté requis, créé par pipe(). Nommé : nom dans le FS (type p), pas de parenté requise, créé par mkfifo().
Comportement de read sur tube vide ?
Si écrivains présents : bloquant. Si aucun écrivain : renvoie 0 (EOF).
Comportement de write sans lecteur ?
Signal SIGPIPE envoyé au processus écrivain → fin du processus par défaut. write renvoie -1 avec errno = EPIPE.
À quoi sert dup2(fd, 0) ?
À rediriger stdin sur le descripteur fd. Tout read(0, ...) ou scanf lit alors depuis fd. Si fd est p[0], on lit dans le tube.
Pourquoi half-duplex ?
Un tube anonyme permet la communication dans les deux sens, mais une seule direction à la fois. Pour un vrai bidirectionnel simultané : deux tubes.
Trois caractéristiques d'un tube ?
FIFO (premier entré, premier sorti) · ② Lectures destructrices (une donnée lue disparaît) · ③ Taille limitée (PIPE_BUF dans <limits.h>).
Pourquoi open() bloque sur un tube nommé ?
Pour permettre un rendez-vous. open(O_RDONLY) attend un écrivain ; open(O_WRONLY) attend un lecteur. Pour le contourner : O_NONBLOCK.
Séquence pour reproduire ls | wc -l en C ?
pipe(p)fork(). Père : close(p[0]), dup2(p[1], 1), execlp("ls", ...). Fils : close(p[1]), dup2(p[0], 0), execlp("wc", "-l", ...).
Comment savoir si un fichier est un tube nommé ?
Avec ls -l, le 1er caractère du mode est p (au lieu de - pour fichier régulier, d pour dossier).
Comment supprimer un tube nommé ?
En C : unlink(nom). En shell : rm nom. Le fichier disparaît même si des processus ont encore le tube ouvert (ils peuvent continuer à l'utiliser jusqu'à la fermeture).
Que partage le fils après fork() sur un tube créé par le père ?
Les deux descripteurs fds[0] et fds[1]. Chaque processus a une copie indépendante dans sa table de descripteurs, mais pointant vers le même tube dans la table des fichiers ouverts du noyau.
Différence dup vs dup2 ?
dup(fd) alloue le plus petit descripteur libre. dup2(old, new) force le numéro exact new (ferme d'abord new s'il était ouvert). Utiliser dup2 pour les redirections de stdin/stdout/stderr.

✎ Quiz éclair

1.Que renvoie pipe(fds) en cas de succès ?
  • Le descripteur en écriture
  • 0, et remplit fds[0] et fds[1]
  • 1 si OK, 0 sinon
  • Le PID du processus créé
pipe renvoie 0 si succès. Le tableau fds est rempli : fds[0] en lecture, fds[1] en écriture.
2.Dans un tube anonyme entre père et fils, pourquoi le fils doit-il fermer fds[1] s'il ne fait que lire ?
  • Pour économiser de la mémoire
  • Pour respecter la norme POSIX
  • Sinon EOF ne sera jamais détecté quand le père termine
  • Pour éviter une fuite de descripteur
Tant qu'il existe au moins un descripteur en écriture ouvert (même inutilisé), read sur tube vide reste bloquant. Fermer fds[1] dans le fils est indispensable.
3.Que fait dup2(p[1], 1) ?
  • Crée un nouveau pipe
  • Ferme le descripteur 1
  • Lit du tube vers stdout
  • Redirige stdout (1) vers l'extrémité écriture du tube
Après cet appel, tout printf ou write(1, ...) écrit dans le tube. C'est ainsi qu'on connecte la sortie d'un programme à un pipe.
4.Un tube anonyme peut être utilisé entre deux processus qui :
  • Ont un lien de parenté (un ancêtre commun a créé le tube)
  • Tournent sur deux machines différentes
  • Connaissent un nom commun de fichier
  • Partagent le même utilisateur Unix
Sans lien de parenté, pas d'héritage du descripteur via fork, donc impossible d'accéder au tube anonyme. Pour processus indépendants : tube nommé.
5.Que se passe-t-il si on écrit dans un tube alors que tous les lecteurs sont fermés ?
  • L'écriture est mise en cache pour plus tard
  • Aucune erreur, les données sont perdues silencieusement
  • Signal SIGPIPE envoyé, fin du processus par défaut
  • Le programme attend qu'un lecteur ouvre le tube
SIGPIPE est envoyé à l'écrivain ; le comportement par défaut est de terminer le processus. On peut intercepter le signal et write renvoie alors -1 avec errno = EPIPE.
6.Comment ouvrir un tube nommé pour lecture non bloquante ?
  • open(nom, O_RDONLY) — c'est non-bloquant par défaut
  • open(nom, O_RDONLY | O_NONBLOCK)
  • open(nom, O_RDONLY | O_NOWAIT)
  • fopen(nom, "r")
Le flag O_NONBLOCK rend l'ouverture non-bloquante. Sans lui, open(O_RDONLY) attend un écrivain pour établir le rendez-vous.
7.Avec quel caractère ls -l signale-t-il un tube nommé ?
  • t
  • f
  • l (comme link)
  • p (comme pipe)
Le premier caractère du mode est p pour les pipes nommés. - = fichier régulier, d = directory, l = lien symbolique, c = char device, b = block device.
8.Pour communiquer dans les deux sens entre un père et un fils, on utilise :
  • Un seul tube — il est full-duplex
  • Un seul tube en alternant les read et write
  • Deux tubes, un par sens
  • Un tube et un signal
Le tube anonyme est half-duplex. Un seul tube pour les deux sens risque qu'un processus relise sa propre donnée. Deux tubes garantissent un comportement déterministe.

Score : 0 / 8 ·

📌 À retenir

  • pipe(fds)fds[0] lecture, fds[1] écriture
  • mkfifo(nom, mode) → tube nommé visible dans le FS, type p
  • Ordre canonique : pipe → fork → close inutiles → read/write → close
  • Fermer ce qu'on n'utilise pas — sinon EOF jamais détecté
  • read sur tube vide sans écrivain → renvoie 0
  • write sans lecteur → SIGPIPE, errno = EPIPE
  • dup2(fd, 0) redirige stdin, dup2(fd, 1) redirige stdout
  • Half-duplex — un seul sens à la fois, deux tubes pour bidirectionnel
  • Lien de parenté requis pour tubes anonymes (héritage via fork)
  • open() bloquant par défaut sur tube nommé = mécanisme de rendez-vous
  • unlink(nom) ou rm pour supprimer un tube nommé
  • PIPE_BUF dans <limits.h> = taille du buffer (atomicité garantie en dessous)