3. Les pipes et les descripteurs de fichiers standards

Avant de se lancer dans l'écriture d'un chargeur et d'un greffon, quelques notions nous seront utiles.

3.1. Les descripteurs de fichiers standards

Lorsque vous lancez votre programme, trois descripteurs de fichiers sont ouverts par défaut. Ce sont l'entrée standard, appelée habituellement STDIN, la sortie standard STDOUT et la sortie d'erreur STDERR. Faites un fprintf(stdout,"bonjour, vous!\n"); et vous devriez voir apparaître le message sur la console, comme si vous aviez fait un printf("bonjour, vous!\n");. Vous pouvez aussi lire ce que l'utilisateur tape au clavier en lisant dans stdin avec fread() de la même manière que si vous aviez lu dans un fichier.

3.2. Les pipes

Un tuyau, en anglais pipe n'est rien d'autre qu'un tuyau dont chaque ouverture est identifiée par un descripteur de fichier. L'instruction pipe() prend un argument qui est un tableau de deux entiers. Elle met dans le premier entier la valeur du descripteur de fichiers pour la lecture, et dans le second la valeur du descripteur de fichiers pour l'écriture. Ainsi, tout ce qui est écrit via le premier descripteur de fichiers est disponible en lecture via le second descripteur de fichiers.

Voici un petit exemple qui illustre l'utilisation des tubes, avec en prime un fork() et les instructions getpid() et getppid().

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <unistd.h>
 4 
 5 int
 6 main (int argc, char *argv[])
 7 {
 8   int p[2];
 9   int pid;
10   int val;
11   int i;
12 
13   if (pipe (p))
14     {
15       printf ("Erreur de creation du pipe\n");
16       exit (EXIT_FAILURE);
17     }
18 
19   switch (pid = fork ())
20     {
21     case -1:
22       printf ("Erreur de fork\n");
23       exit (EXIT_FAILURE);
24     case 0:
25       for (i = 0; i < 10; i++)
26 	{
27 	  read (p[0], &val, sizeof (int));
28 	  printf ("%d %d\n", i, val);
29 	}
30       break;
31     default:
32       for (i = 0; i < 10; i++)
33 	{
34 	  val = random () / 1000;
35 	  write (p[1], &val, sizeof (int));
36 	}
37     }
38   printf ("Fini (pid=%d; pere=%d)\n", getpid (), getppid ());
39   exit (EXIT_SUCCESS);
40 }

Quelques explications rapides. Ligne 13, nous avons l'appel à la fonction pipe() qui crée le tube. pipe() renvoie 0 si tout s'est bien passé. Puis ligne 19, nous avons le fork() sur lequel je me suis déjà étendu dans un article précédent, avec le cas ligne 21 où le fork() a échoué, le cas ligne 24 où nous exécutons le fils, et le cas ligne 31 où nous exécutons le père. Dans cet exemple, le père remplit le tube avec dix entiers aléatoires. Et le fils les lit via le tube et les affiche. La ligne 38 nous affiche le numéro de PID du processus courant (getpid()) et celui de son père (getppid()), à la fois pour notre processus père et notre processus fils. Peut-être remarquerez-vous une bizarrerie, à savoir que le père du processus fils porte le numéro 1? Dans ce cas, cela signifie que le père a fini son travail avant, qui a quitté, et le système a rattaché le processus fils, devenu orphelin, au processus 1. Dernière remarque: pour être propre, cet exemple devrait faire un appel à wait3(), wait4() ou waitpid() pour que le processus fils ne devienne pas un processus zombie. Je ne l'ai pas fait pour ne pas alourdir l'exemple.

3.3. Manipulation des descripteurs de fichiers

Les descripteurs de fichiers sont contenus dans une table. Pour le système, tout est contenu dans des tables, et les descripteurs de fichiers n'échappent pas à la règle. Les descripteurs de fichiers STDIN, STDOUT et STDERR portent les numéros définis dans unistd.h:

  • STDIN_FILENO

  • STDOUT_FILENO

  • STDERR_FILENO

On vous dira que ces constantes sont partout fixées à 0, 1 et 2 mais cela a peu d'importance: utilisez les constantes.

Dupliquons un de ces descripteurs de fichiers. dup() est faite pour cela. Elle prend un descripteur de fichier en argument et renvoie un autre descripteur de fichiers, correspondant au même fichier que le descripteur utilisé en argument. Cela est intéressant par exemple pour sauvegarder un descripteur de fichier.

Si je ferme un de ces fichiers, par exemple close(STDOUT_FILENO), il me sera impossible d'afficher quoi que ce soit à l'écran par la suite, via la sortie standard. Cette opération est donc à déconseiller sauf dans quelques cas particuliers. De plus, si j'ouvre un fichier ensuite, son descripteur de fichier sera celui de la sortie standard puisque l'entrée dans la table des descripteurs est devenue disponible! Attention aux dysfonctionnements!

Un cas particulier, justement, nous intéresse: Que se passe-t-il si je crée un tube (descipteurs de fichiers p[0] et p[1] après l'appel à pipe(p)), que je ferme l'entrée standard, et que je duplique le descripteur de fichier p[0]?

Explication.

  • Je crée le tube. J'obtiens donc deux descripteurs de fichiers p[0] en lecture (comme STDIN_FILENO, qui est 0) et p[1] en écriture. Mettons que p[0] soit égal à 3 et p[1] à 4

  • Je ferme l'entrée standard. L'entrée numéro zéro est donc disponible.

  • Je duplique p[0]. Le résultat, appelons my_stdin, va donc correspondre à l'entrée (lecture) du tube et aura le numéro 0, comme anciennement l'entrée standard.

  • Je peux optionnellement fermer p[0], ce qui ne fermerait pas le tube, mais fait que son entrée aurait pour descripteur de fichiers uniquement my_stdin (et plus p[0]), et que sa sortie reste toujours p[1].

Je ne pourrai plus lire sur la véritable entrée standard, mais cette manipulation me permet, si vous m'avez suivi, de simuler l'entrée standard. Ainsi, toutes les instructions telles que getchar() ou scanf() utilisant l'entrée stanrdard, ces instructions liront tout ce que VOUS écrirez via le descripteur de fichier p[1].

création est mise à disposition sous un contrat Creative Commons