©Najib TOUNSI
Novembre 2000
![]() |
Les processus qui lisent/écrivent dans un tube sont
en général des processus concurrents et doivent
être liés dans une même
généalogie (e.g. Père/Fils). En effet, un
tube, ou du moins ses descripteurs, fait partie des
caractéristiques hérités par un processsus
après un fork(). Pour que deux processus puissent
donc communiquer des données par un tube, il faut qu'ils
disposent tous les deux du tube, et pour cela descendre d'un
même père (ou ancêtre commun) qui crée le
tube. Ce dernier pouvant être lui-même l'un des
processus communiquants. (Une coopération semblable mais
entre processus quelconques est réalisable par un
mécanisme de tubes nommés, qui sont des
fichiers disques "réels". Voir plus bas [[section]]2.)
![]() |
On utilise un tube à l'aide de deux descripteurs entiers: un en écriture pour l'écrivain, et un en lecture pour le lecteur. La création de tels descripteurs (et donc du tube) se fait à l'aide de la primitive pipe(). La lecture et l'écriture se font avec les primitives read() et write().
int pipe (int p[2]);crée un tube et lui associe deux descripteurs rendus dans le tableau de deux entiers p. Par définition p[0] est le descripteur pour lire (sortie du tube) et p[1] celui pour écrire (entrée du tube). La valeur retour de pipe() est 0 en cas de succès, -1 sinon (trop de descripteurs actifs, ou de fichiers ouverts, etc...)
Exemple:
int p[2];Comme déjà dit, la création d'un tube doit se faire avant le lancement des processus qui vont l'utiliser. Ce sera le cas par exemple dans une coopération père/fils ou fils/fils..., quand le processus père crée le tube avant de faire fork().if ( pipe(p) == -1 )
fprintf(stderr, "Impossible ouvrir tube\n");
...
Remarque: Il n'y a pas la notion d'ouverture d'un tube (open()). Après sa création, un tube est directement utilisable. Par contre, la fermeture close() s'applique à un tube.
char buf[100];on a une demande de lecture de 20 caractères dans le tube p. Les caractères lus sont rendus disponibles dans la zone buf. Leur nombre est la valeur retour de read().
int p[2];
...
read(p[0], buf, 20);
Une précaution voudrait qu'un processus ferme systématiquement les descripteurs dont il n'a pas besoin: ici on a besoin de lire dans le tube p. On doit fermer le descripteur p[1].
close(p[1]);Cela permet d'éviter des erreurs aboutissant parfois à des situations d'interblocage (deadlock): des processus communiquent, mais chacun attend que l'autre commence.
read(p[0], buf, 20);
char buf[100];est une demande d'écriture de 20 caractères dans le tube de descripteur ouvert p[1]. La séquence à écrire est prise dans la zone buf. La valeur retour de write() est le nombre d'octets ainsi écrits. Là aussi, on a fermé le descripteur p[0] de lecture. L'écriture dans un tube est atomique (tout est écrit ou rien n'est écrit) et n'interfère pas avec d'autres écrivains éventuels. Les 20 caractères écrits ici seront consécutifs dans le tube. En d'autres termes, sur la figure VI-2 ci-dessus, chaque séquence "tic" ou "tac" est écrite en bloc. Il n'y aura pas d'enchevêtrement comme "ti" puis "tac" et ensuite "c".
int p[2];
...
close p[0];
buf = "texte a ecrire ";
write(p[1], buf, 20);
Remarque: La taille d'un tube est bien sûr limitée. Elle a pour valeur 4096 en général (constante PIPE_BUF de <limits.h>). S'il arrive que le nombre de caractères à écrire soit supérieur à cette limite, le message peut être écrit mais décomposé par le système en plusieurs lots. L'atomicité serait alors perdue.
read(p[0], buf, 20);on a:
1) Si le tube désigné contient 20 caractères ou plus, alors 20 caractères seront lus et mis dans la zone désignée par la variable buf. S'il contient moins de 20 caractères, ils seront lus de la même façon. En tout cas, la valeur retour de read() est le nombre de caractères effectivement lus; 20 ou moins.
2) Si le tube ne contient aucun caractères (tube vide), alors deux cas peuvent se produire:
Remarque: avec la primitive fcntl(), la lecture peut être rendue non-bloquante.
fcntl(p[0], F_SETFL, O_NONBLOCK);Dans ce cas read() dans un tube vide (avec ecrivains potentiels) ne bloque pas et renvoie -1. Ce n'est pas considéré comme fin de fichier, mais erreur dans ce cas.
Quand à la primitive
write(p[1], buf, 20);elle se comporte comme suit:
1) Dans le cas où il n'y a aucun lecteur sur le tube (tous les descripteurs en lecture sont fermés), c'est une erreur fatale: le processus écrivain se termine par le signal SIGPIPE. C'est normal car l'écriture est inutile s'il n'y a pas de lecteurs. C'est le cas par exemple du message "broken pipe" sous shell.
2) Dans le cas où il y a encore des lecteurs sur ce tube alors,
Mais, avec les tubes, l'erreur à ne pas commettre
est une situation d'interblocage illustrée, par exemple, par
deux processus qui communiquent dans les deux sens à travers
deux tubes p et q
PROCESSUS_1 read(p[0], ch, n); ... write(q[1], ch, n); |
PROCESSUS_2 read(q[0], ch, n); ... write(p[1], ch, n); |
Comme les tubes sont vides au départ, les deux lectures vont bloquer, car l'écriture ne vient qu'après. Cela peut être dû plus à une erreur de programmation qu'à une erreur d'analyse (il existe des méthodes formelles de description de processus coopérants pour synchroniser les échanges).
Exemple 1:
Le père envoi au fils un message dans un tube. Message envoyé en bloc et lu caractère par caractère puis imprimé.
Remarquer l'arrêt de read() sur valeur retour nulle (réalisé par close(p[1]); dans le processus père). Exécution#include <stdio.h> char message[25] = "Cela provient d'un tube"; main() { /* * communication PERE --> FILS par pipe */ int p[2]; int pipe(int[2]); if (pipe(p) == -1) { fprintf(stderr, "erreur ouverture pipe\n"); exit(1); } if (fork() == 0) { /* fils */ char c; close(p[1]); while (read(p[0], &c, 1) != 0) printf("%c", c); close(p[0]); exit(0); } else { /* suite pere */ close(p[0]); write(p[1], message, 24); close(p[1]); exit(0); } }
tounsi@shems 53>cc pipe1.cExercice: Faire un programme où ce sont deux fils qui communiquent, l'un écrivain et l'autre lecteur.
tounsi@shems 54>a.out
Cela provient d'un tube
Voici maintenant le cas où un lecteur tente de lire 30 caractères dans un tube qui n'en contient que 24. Dans le même exemple, on a remplacé
parwhile ( read(p[0], &c, 1) != 0) printf("%c", c);
nb_lu = read(p[0], buf, 30); /* 30 > 24 */pour imprimer le message lu dans le tube précédé de sa taille.
printf("%d %s\n", nb_lu, buf);
tounsi@emi 58>cc pipe11.cLe lecteur n'a lu que ce qui est disponible. 24 au lieu de 30 caractères.
tounsi@emi 59>a.out
24 Cela provient d'un tube
Exemple 2:
Considérons maintenant deux écrivains et un lecteur sur le même tube. Deux fils écrivent respectivement les séquences "ABC...Z" et "abc...z" par blocs de trois caractères, et un troisième fils lit dans le tube par blocs de 4 caractères.
Les appels sleep() entre chaque écriture des écrivains, sont là pour simuler d'autres traitements des processus.#include <stdio.h> /* Cummunication par pipe * FILS1 et FILS2 Ecrivains * FILS3 lecteur, meme pipe */ void f1(), f2(), f3(); /* les 3 fils */ void (*tab_fonct[3]) () = { f1, f2, f3 }; int p[2]; char seq1[27] = "ABCDEFGHIJKILMOPQRSTUVWXYZ"; /* ecrite par FILS1 */ char seq2[27] = "abcdefghijklmnopqrstuvwxyz"; /* ecrite par FILS2 */ main() { int i; int pipe(); if (pipe(p) == -1) { fprintf(stderr, "erreur ouverture pipe\n"); exit(1); } for (i = 0; i < 3; i++) switch (fork()) { case 0: (*tab_fonct[i]) (); exit(0); default:; } exit(0); } void f1() { int i; close(p[0]); for (i = 0; i < 26; i += 3) { write(p[1], &seq1[i], 3); sleep(3); } close(p[1]); } void f2() { int i; close(p[0]); for (i = 0; i < 26; i += 3) { write(p[1], &seq2[i], 3); sleep(4); } close(p[1]); } void f3() { char s[5]; int nb_lu; close(p[1]); while ((nb_lu = read(p[0], s, 4)) > 0) printf("%s", s); close(p[0]); printf("\n"); }
Exécution:
tounsi@shems 66>cc pipe3.cPoints à noter:
tounsi@shems 67>a.out
ABCabcDEFdefGHIghiJKILMOjklPQRmnoSTUpqrVWXYZstuvwxyz
Indication: On passera les numéros pid en paramètres pour réaliser les tests chez le lecteur. On utilisera une chaîne de longueur 10, comme "12345 DEF" contenant le numéro du pid sur 6 cases, un blanc est les 3 caractères du message à transmettre. Les processus lisent et écrivent donc par chaînes de 10 caractères. On codera la chaîne par sprintf() et on la décodera par sscanf().
Exemple 3:
Cette fois-ci, il y un fils écrivain et deux fils lecteurs. Ces derniers lisent selon leur rythme de progression et affichent ce qu'ils lisent. Soit :
char seq[27]="ABCDEFGHIJKLMNOPQRSTUVWXYZ";Le processus écrivain écrit la séquence d'un seul trait
write(p[1], seq, 26);et les processus lecteurs lisent l'un par 2 octets et l'autre par 3:
* PROCESSUS 1 */Pour distinguer entre les résultats des deux processus, nous les avons écrits sur la sortie standard pour l'un et la sortie erreur pour l'autre. Exécution:
while ((nb_lu = read(p[0], s, 2)) > 0) {
fprintf(stderr, "%s ", s);
sleep(1); /* autre traitement */
}/* PROCESSUS 2 */
while ((nb_lu = read(p[0], s, 3)) > 0) {
fprintf(stdout, "%s ", s);
sleep(1);
}
tounsi@shems 86>cc pipe4.coù on voit que les deux lecteurs passent dans un ordre quelconque, l'un lisant par 2 et l'autre par 3.
tounsi@shems 87>(a.out >res)>&err
tounsi@shems 88>cat res
EFG HIJ MNO RST YZT
tounsi@shems 89>cat err
AB CD KL PQ UV WX
AB CD KL PQ UV WX(le T de YZT est un reliquat du message RST précédent, la dernière lecture étant YZ)
EFG HIJ MNO RST YZT
On a deux processus, le père redirige sa sortie standard (stdout) sur le tube et fait execl ("ls -l" ...) et le fils redirige son entrée standard (stdin) vers le tube et fait execl("wc -l"...). Pour cela, on va utiliser la primitive
int dup(int desc);qui duplique son descripteur. Rappelons qu'un appel dup (d); rend un nouveau descripteur associé au même fichier que d (i.e. on accède au même fichier avec les deux descripteurs). Le nouveau descripteur rendu est la plus petite valeur de descripteur de fichier disponible chez le processus appelant. Il suffirait par exemple, avant un appel
dup(p[0]);de fermer stdin par
close(STDIN_FILENO);pour que STDIN_FILENO, qui est égale à 0, soit associé au même fichier que p[0] et ainsi toute lecture standard se fera dans le tube p.
Voici donc le programme complet pour réaliser "ls -l | wc -l":
Exécution:#include <unistd.h> #include <stdio.h> main() { /* ls -l | wc -l */ int p[2]; int pipe(int[2]); pipe(p); if (fork() == 0) { close(STDIN_FILENO); /* fermer stdin */ dup(p[0]); /* stdin devient entrée tube */ close(p[1]); close(p[0]); execl("/usr/ucb/wc", "wc", "-l", NULL); } else { close(STDOUT_FILENO); /* fermer stdout */ dup(p[1]); /* stdout devient sortie tube */ close(p[1]); close(p[0]); execl("/bin/ls", "ls", "-l", NULL); } }
tounsi@shems 97>cc pipeShell.cOn obtient le même résultat avec ce programme qu'avec la commande shell correspondante.
tounsi@shems 98>a.out
7
tounsi@shems 99>ls -l | wc -l
7
#include <stdio.h>qui convertit un descripteur entier en un descripteur flot, résultat de l'appel. type_ouv est le type d'ouverture, lecture ou écriture, désiré.
FILE* fdopen(int desc, char* type_ouv);
Dans l'exemple 1 ci-dessus le processus fils peut lire dans le tube avec le code suivant:
FILE *f;La lecture se fera dans le tube considéré comme un flot FILE* f. Ici, on a utilisé la fonction fgetc() pour lire caractère par caractère avec le test sur EOF. Les caractéristiques de fonctionnement du tube, en particulier la condition fin de fichier (plus d'écrivains) sont bien sûr les mêmes.
f = fdopen(p[0], "r");
while((c=fgetc(f)) != EOF)
putchar(c);
Il faut néanmoins faire attention à l'écriture dans un tube avec les fonctions de <stdio.h>. Ce qui y est écrit n'est pas rendu immédiatement disponible comme avec la primitive write(), mais transféré dans le tampon associé à f. Il faut que le tube soit plein pour que le système le transfert, sinon faire fflush(f).
Exercice: Dans le même exemple 1, faire en sorte que le processus écrivain utilse fprintf(f, "%s", message); pour envoyer son message dans le tube désigné cette fois-ci par le flot f.
processus | commande shellle processus envoie des donnée sur l'entrée standard d'une commande, ou bien par
commande shell | processusle processus lit des données provenant de la sortie standard de la commande. Pour se faire, le processus crée un tube tout en lancant la commande. La fonction pour cela est popen().
#include <stdio.h>qui crée un tube entre le processus appelant et la commande commande à exécuter. La valeur retourne est un descripteur (de type flot) vers ce tube, ouvert selon le type d'ouverture type_ouv. Un tube ouvert par popen() doit être fermée par pclose().
FILE* popen( char* commande, char* type_ouv);
L'exemple qui suit lance la commande hostname et récupère son résultat (affiché ensuite).
Exécution:/* Execution de la commande hostname et * recuperation du resultat */ main() { FILE *f; char s[20]; f = popen("hostname\0", "r"); /* Ouverture lecture */ fscanf(f, "%s", s); printf("Ce processus s'execute sur %s\n", s); pclose(f); }
tounsi@shems 97>cc popen.cDans l'exemple suivant, c'est le schéma inverse. Le processus envoie un texte à la commande wc, qui en compte les lignes, mots et caractères.
tounsi@shems 98>a.out
Ce processus s'execute sur shemstounsi@shems 99>rlogin yasmina
tounsi@yasmina 1>cc popen.c
tounsi@yasmina 2>a.out
Ce processus s'execute sur yasmina
Exécution:/* envoie de texte à la commande wc */ char s[30] = "5 mots et 23 caracteres"; main() { FILE *f; f = popen("wc \0", "w"); /* Ouverture ecriture */ fprintf(f, "%s", s); pclose(f); }
tounsi@shems 45>cc popen1.cNoter que la sortie standard de la commande wc lancée est bien l'ecran traditionnel.
tounsi@shems 46>a.out
0 5 23
prw-r--r-- 1 tounsi 0 Jun 26 17:11 canalUn tube nommé est donc un fichier spécial permettant à des processus quelconques d'échanger des données en mode fifo comme dans un tube normal. En particulier, il est
En particulier, il est ouvrable avec la primitive
open()
Par contre, il est créé avec la primitive
mkfifo()
prévue à cet effet. Les opérations de lecture/écriture sont read() et write() classiques.
#include <sys/types.h>Le paramètre nom indique le nom du tube à créer et le paramètre mode, ses droits d'accès, généralement 0644 (cf. mode dans primitive open() des fichiers).
int mkfifo(char *nom, mode_t mode);
mkfifo() rend 0 en cas de succès (tube créé) ou -1 en cas d'échec (fichier de même nom existe etc ...).
Exemple:
mkfifo("NotreTube", 0644);Remarque:
Un tube nommé peut être créé en interactif par la commande shell
mkfifo nomqui crée un tube nommé de nom donné.
Exemple:
tounsi@shems 49>mkfifo canalOn va maintenant ouvrir le tube et lire son contenu (par la commande cat).
tounsi@shems 50>ls -l canal
prw-r--r-- 1 tounsi 0 Jun 26 17:11 canal
tounsi@shems 51>cat canal &Comme le tube est encore vide, la commande - l'ouverture en fait - sera suspendue (d'où le & pour garder la main). Il faut qu'un écrivain ouvre le tube pour mettre quelque chose dedans. Ce sera le résultat d'un processus ls -l par exemple, qui aura pour effet immédiat de relancer le processus cat suspendue. D'où les lignes
[2] 501
tounsi@shems 52>ls -l >canalNoter que le canal est vidé (taille 0).
total 20
prw-r--r-- 1 tounsi 0 Jun 26 17:11 canal
-rw-r--r-- 1 tounsi 1972 Apr 15 7:13 client.c
-rw-r--r-- 1 tounsi 2340 Apr 15 19:23 serveur.c
Voici maintenant un exemple inverse: le processus écrivain doit attendre un éventuel lecteur.
tounsi@shems 63>ls -l >canal& ---> Ecriture dans canal
[1] 511tounsi@shems 64>ls -l canal
prw-r--r-- 1 tounsi 0 Jun 26 17:11 canal ---> canal toujours videtounsi@shems 65>cat canal ---> Voici un lecteur
total 20
prw-r--r-- 1 tounsi 0 Jun 26 17:11 canal
-rw-r--r-- 1 tounsi 1972 Apr 15 7:13 client.c
-rw-r--r-- 1 tounsi 2340 Apr 15 19:23 serveur.c
[1] + Done ls -l > canal
#include <sys/types.h>Le paramètre nom indique le nom du tube fifo à créer et le paramètre pmode ses droits d'accès (généralement 0644 cf. mode fichiers ordinaires). mkfifo() rend 0 en cas de succès (tube créé) ou -1 en cas d'échec (fichier de même nom existe ou autre anomalie etc.).
#include <sys/stat.h>int mkfifo(char *nom, mode_t pmode);
Exemple:
if (mkfifo("canal", 0644) < 0)Une fois créé, un fifo peut être ouvert. L'appel
fprintf(stderr,"erreur mkfifo\n");
#include <fcntl>ouvre le fifo canal en écriture pour retourner son descripteur. Cette ouverture va bloquer le processus jusqu'à ce qu'un autre processus ouvre le même fifo en lecture (si ce n'est déjà fait). Par rapport au tubes ordinaires donc, c'est l'ouverture qui bloque un processus, et non pas la tentative de lecture/écriture.
int fd;
...
fd = open("canal", O_WRONLY);
L'ouverture non bloquante d'un fifo est possible. Elle doit se faire par conjonction bit à bit avec l'indicateur O_NONBLOCK (ou O_NDELAY) . Par exemple,
if (fd == open("canal", O_RDONLY|O_NONBLOCK)) < 0)ouvre le fifo canal en lecture mais ne bloque pas le processus en attente d'un écrivain.
fprintf(stderr,"erreur open sur fifo\n");
En cas de succès, fd recoit le descripteur de fichier associé, sinon open() retourne -1. C'est le cas par exemple d'une ouverture en écriture non bloquante alors qu'il n'y a aucun processus detenant un descripteur en lecture (car sinon le prochain appel à write() est fatal). Par contre, l'ouverture en lecture non bloquante sur un fifo sans rédacteur, reussit et renvoie un descripteur. Un futur read() renverra fin de fichier.
La primitive open() appliquée à des fifos présente une caractéristique nouvelle propre aux fifos. Elle peut servir à synchroniser les processus communiquants. L'ouverture normale d'un fifo est bloquante pour un processus jusqu'à ce qu'un autre processus communiquant réalise à son tour une ouverture du même fifo. Ce mécanisme de rendez-vous, permet à des processus de se rejoindre à un même point de leur déroulement.
![]() |
Le programme client.c envoie une requête de type a + b, dans le tube cli2serv, et le programme serveur.c lui retourne la réponse dans le tube serv2cli. C'est le programme serveur.c qui crée les tubes (désignés par les variables QUESTION et REPONSE) et qui les supprime en fin d'exécution. Il doit donc être lancé avant le programme client qui suppose les tubes existants. Les deux programmes se synchronisents sur l'ouverture des tubes qui doit se faire dans le même ordre. L'arrêt de la communication se fait quand le programme client envoi le message bye; le serveur répond alors par ciao.
% cat serveur.c /* Serveur: retourne resultat (requete a+b) * Cree les fifos cli2serv et serv2cli * LANCER LE SERVEUR D'ABORD (car il ecrase fifos) */ #include <sys/types.h> #include <fcntl.h> #include <string.h> #include <stdio.h> #define QUESTION "cli2serv" #define REPONSE "serv2cli" void trait(); /* traitement du serveur */ main() { int fdq, fdr; unlink(QUESTION); unlink(REPONSE); /* Creation fifos */ if (mkfifo(QUESTION, 0644) == -1 || mkfifo(REPONSE, 0644) == -1) { perror("Impossible creer fifos"); exit(2); } /* Attente des ouvertures Clients */ fdq = open(QUESTION, O_RDONLY); fdr = open(REPONSE, O_WRONLY); trait(fdr, fdq); close(fdq); close(fdr); unlink(QUESTION); unlink(REPONSE); exit(0); } void trait(fdr, fdq) int fdr, fdq; /* fdr et fdq descripteurs reponse/question */ { int opd1, opd2, res; char opr; char quest[11]; char rep[11]; /* traitement serveur * envoi reponse a question * a + b venant de client. * arret question = "Ciao" */ while (1) { read(fdq, quest, 10); sscanf(quest, "%d%1s%d", &opd1, &opr, &opd2); if (strcmp(quest, "Ciao") == 0) { strcpy(rep, "Bye"); write(fdr, rep, 10); break; } res = opd1 + opd2; sprintf(rep, "%d", res); write(fdr, rep, 10); } }
% cat client.c /* Client: envoie expressions * Les fifos sont supposes crees * par le serveur et sont cli2serv * et serv2cli */ #include <sys/types.h> #include <fcntl.h> #include <string.h> #include <stdio.h> #define QUESTION "cli2serv" #define REPONSE "serv2cli" void trait(); /* traitement client */ main() { int fdq, fdr; fdq = open(QUESTION, O_WRONLY); if (fdq == -1) { fprintf(stderr, "Impossible ouvrire fifo %s\n", QUESTION); fprintf(stderr, "Lancer serveur d\'abord?\n"); exit(2); } fdr = open(REPONSE, O_RDONLY); if (fdr == -1) { fprintf(stderr, "Impossible ouvrire fifo %s\n", REPONSE); fprintf(stderr, "Lancer serveur d\'abord?\n"); exit(2); } trait(fdr, fdq); close(fdq); close(fdr); exit(0); } void trait(fdr, fdq) int fdr, fdq; /* fdr et fdq descripteurs reponse/question */ { char rep[11]; char quest[10]; /* traitement client * lecture expression a op b * dans stdin et ecriture reponse * dans stdout. Arret rep = "Bye" */ while (1) { if (gets(quest) == NULL) exit(2); write(fdq, quest, 10); printf("Client -> %s \n", quest); read(fdr, rep, 10); printf("Serveur -> %s \n", rep); if (strcmp(rep, "Bye") == 0) break; } }
% ls cli_serv serv_cli cli_serv not found serv_cli not found % client Impossible ouvrire fifo cli_serv Lancer serveur d'abord? % serveur & [1] 21653 % client 2 +4 Client -> 2 +4 Serveur -> 6 3 + 7 Client -> 3 + 7 Serveur -> 10 Ciao Client -> Ciao Serveur -> Bye [1] + Done serveur % logout Good bye Tounsi, come again _______________________ What sane person could live in this world and not be crazy? -- Ursula K. LeGuin _______________________