15 Les tuyaux sous Unix Les tuyaux 1 permettent à un groupe de processus d’envo

15 Les tuyaux sous Unix Les tuyaux 1 permettent à un groupe de processus d’envoyer des données à un autre groupe de processus. Ces données sont envoyées directement en mémoire sans être stockées temporairement sur disque, ce qui est donc très rapide. Tout comme un tuyau de plomberie, un tuyau de données a deux côtés : un côté permettant d’écrire des données dedans et un côté permettant de les lire. Chaque côté du tuyau est un descripteur de fichier ouvert soit en lecture soit en écriture, ce qui permet de s’en servir très facilement, au moyen des fonctions d’entrée / sortie classiques. La lecture d’un tuyau est bloquante, c’est-à-dire que si aucune donnée n’est dis- ponible en lecture, le processus essayant de lire le tuyau sera suspendu (il ne sera pas pris en compte par l’ordonnanceur et n’occupera donc pas inutilement le processeur) jusqu’à ce que des données soient disponibles. L’utilisation de cette caractéristique comme effet de bord peut servir à synchroniser des processus entre eux (les processus lecteurs étant synchronisés sur les processus écrivains). La lecture d’un tuyau est destructrice, c’est-à-dire que si plusieurs processus lisent le même tuyau, toute donnée lue par l’un disparaît pour les autres. Par exemple, si un processus écrit les deux caractères ab dans un tuyau lu par les processus A et B et que A lit un caractère dans le tuyau, il lira le caractère a qui disparaîtra immédiatement du tuyau sans que B puisse le lire. Si B lit alors un caractère dans le tuyau, il lira donc le caractère b que A, à son tour, ne pourra plus y lire. Si l’on veut donc envoyer des informations identiques à plusieurs processus, il est nécessaire de créer un tuyau vers chacun d’eux. De même qu’un tuyau en cuivre a une longueur finie, un tuyau de données à une capacité finie. Un processus essayant d’écrire dans un tuyau plein se verra suspendu en attendant qu’un espace suffisant se libère. Vous avez sans doute déjà utilisé des tuyaux. Par exemple, lorsque vous tapez menthe22> ls | wc -l l’interprète de commandes relie la sortie standard de la commande ls à l’entrée standard de la commande wc au moyen d’un tuyau. 1. Le terme anglais est pipe, que l’on traduit généralement par tuyau ou tube. 303 Chapitre 15. Les tuyaux sous Unix Les tuyaux sont très utilisés sous UNIX pour faire communiquer des processus entre eux. Ils ont cependant deux contraintes : – les tuyaux ne permettent qu’une communication unidirectionnelle ; – les processus pouvant communiquer au moyen d’un tuyau doivent être issus d’un ancêtre commun qui devra avoir créé le tuyau. 15.1 Manipulation des tuyaux L’appel système pipe() Un tuyau se crée très simplement au moyen de l’appel système pipe() : #include <unistd.h> int tuyau[2], retour; retour = pipe(tuyau); if ( retour == -1 ) { /* erreur : le tuyau n’a pas pu etre cree */ } L’argument de pipe() est un tableau de deux descripteurs de fichier (un descripteur de fichier est du type int en C) similaires à ceux renvoyés par l’appel système open() et qui s’utilisent de la même manière. Lorsque le tuyau a été créé, le premier descripteur, tuyau[0], représente le côté lecture du tuyau et le second, tuyau[1], représente le côté écriture. Un moyen mnémotechnique pour se rappeler quelle valeur représente quel côté est de rapprocher ceci de l’entrée et de la sortie standard. L’entrée standard, dont le numéro du descripteur de fichier est toujours 0, est utilisée pour lire au clavier : 0 → lecture. La sortie standard, dont le numéro du descripteur de fichier est toujours 1, est utilisée pour écrire à l’écran : 1 →écriture. Néanmoins, pour faciliter la lecture des programmes et éviter des erreurs, il est préférable de définir deux constantes dans les programmes qui utilisent les tuyaux : #define LECTURE 0 #define ECRITURE 1 Mise en place d’un tuyau La mise en place d’un tuyau permettant à deux processus de communiquer est relativement simple. Prenons l’exemple d’un processus qui crée un fils auquel il va envoyer des données : 1. Le processus père crée le tuyau au moyen de pipe(). 2. Puis il crée un processus fils grâce à fork(). Les deux processus partagent donc le tuyau. 3. Puisque le père va écrire dans le tuyau, il n’a pas besoin du côté lecture, donc il le ferme. 304 15.1. Manipulation des tuyaux 4. De même, le fils ferme le côté écriture. 5. Le processus père peut dès lors envoyer des données au fils. Le tuyau doit être créé avant l’appel à la fonction fork() pour qu’il puisse être partagé entre le processus père et le processus fils (les descripteurs de fichiers ouverts dans le père sont hérités par le fils après l’appel à fork()). Comme indiqué dans l’introduction, un tuyau ayant plusieurs lecteurs peut poser des problèmes, c’est pourquoi le processus père doit fermer le côté lecture après l’appel à fork() (il n’en a de toute façon pas besoin). Il en va de même pour un tuyau ayant plusieurs écrivains donc le processus fils doit aussi fermer le côté écriture. Omettre de fermer le côté inutile peut entraîner l’attente infinie d’un des processus si l’autre se termine. Imaginons que le processus fils n’ait pas fermé le côté écriture du tuyau. Si le processus père se termine, le fils va rester bloqué en lecture du tuyau sans recevoir d’erreur puisque son descripteur en écriture est toujours valide. En revanche, s’il avait fermé le côté écriture, il aurait reçu un code d’erreur en essayant de lire le tuyau, ce qui l’aurait informé de la fin du processus père. Le programme suivant illustre cet exemple, en utilisant les appels système read() et write() pour la lecture et l’écriture dans le tuyau : Listing 15.1 – Utilisation des tuyaux #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <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 d’entrées / sorties standard avec les tuyaux Puisqu’un tuyau s’utilise comme un fichier, il serait agréable de pouvoir utiliser les fonctions d’entrées / sorties standard (fprintf(), fscanf()...) au lieu de read() et write(), qui sont beaucoup moins pratiques. Pour cela, il faut transformer les descrip- teurs de fichiers en pointeurs de type FILE *, comme ceux renvoyés par fopen(). La fonction fdopen() permet de le faire. Elle prend en argument le descripteur de fichier à transformer et le mode d’accès au fichier ("r" pour la lecture et "w" pour l’écriture) et renvoie le pointeur de type FILE * permettant d’utiliser les fonctions d’entrée/sortie standard. L’exemple suivant illustre l’utilisation de la fonction fdopen() : Listing 15.2 – Dialogue père / fils #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <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 à l’esprit que les fonctions d’entrées / sorties standard utilisent une zone de mémoire tampon lors de leurs opérations de lecture ou d’écriture. L’utilisation de cette zone tampon permet d’optimiser, en les regroupant, les accès au disque avec des fichiers classiques mais elle peut se révéler particulièrement gênante avec un tuyau. Dans ce cas, la fonction fflush() peut se révéler très utile puisqu’elle permet d’écrire la zone tampon dans le tuyau sans attendre qu’elle soit remplie. Il est à noter que l’appel à fflush() était inutile uploads/Litterature/ tp5-programmation-systeme-c.pdf

  • 16
  • 0
  • 0
Afficher les détails des licences
Licence et utilisation
Gratuit pour un usage personnel Attribution requise
Partager