C pipe
Introduction
Un tube est un canal par lequel les informations circulent de manière uni-directionnelle. Un processus écrit dans l'entrée du tube et un autre processus lit les informations en sortie.
Manipulation des tubes
Création
La première étape est la création d'un tube:
#include <unistd.h>
int tube[2];
int pipe(int tube[2]);
- tube[0] → contiendra le fichier descripteur de l'extrémité de lecture
- tube[1] → contiendra le fichier descripteur de l'extrémité d'écriture
- Le retour sera :
- '0' si tout s'est bien passé
- '1' si une erreur survient et errno est positionné
Écriture
Pour écrire dans un tube :
#include <unistd.h>
ssize_t write(int tube[1], const void *message, size_t longueur);
- tube[1] → le fichier descripteur de l'extrémité d'écriture
- message → le message à écrire
- longueur → la longueur du message
- ssize_t → le nombre d'octets écrits
Lecture
Pour lire dans un tube :
#include <unistd.h>
ssize_t read(int tube[0], void *message, size_t longueur);
- tube[0] → le fichier descripteur de l'extrémité de lecture
- message → un tableau de caractères qui contiendra le message à lire
- longueur → la longueur du message à lire
- ssize_t → le nombre d'octets lus
fermeture
Un fois la lecture terminée on ferme le tube:
int close(int descripteur);
- descripteur : une extrémité du tube
Utilisation dans un contexte d'exécution parallélisé
Utilisation uni-directionnelle
Ci-dessous un exemple qui permet au père de communiquer avec ses fils:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
// Nombre total de thread
#define NB_FORK 2
// Taille du message
#define LENGTH_MSG 30
// Tableau contenant le message
char message[LENGTH_MSG] = "";
// Fonction exécutée dans le fork
void job(int * tube) {
int tid = getpid();
// timer pour attendre maximum 5 secondes
int i = 5;
while (i > 0) {
// lecture dans le tube
if (read(*tube, message, LENGTH_MSG) > 0) {
printf("Message du processus [%i] : %s", tid, message);
break;
}
sleep(1);
}
}
// Fonction qui attend chacun des processus fils
void waitForAll() {
int status;
pid_t pid;
int n = 0;
while (n < NB_FORK) {
pid = wait(&status);
n++;
}
}
int main() {
for (int i = 0; i < NB_FORK; i++) {
int tube[2];
pipe(tube);
pid_t pid = fork();
if (pid == -1) {
// Il y a une erreur
perror("fork");
return EXIT_FAILURE;
} else if (pid == 0) {
// On est dans le fils
close(tube[1]);
job(&tube[0]);
close(tube[0]);
exit(EXIT_SUCCESS);
} else {
// On est dans le père
// Ecriture du message dans le tableau
sprintf(message, "Fork [%i], je suis ton père !\n", pid);
// Ecriture du message dans le tube
write(tube[1], message, LENGTH_MSG);
close(tube[0]);
close(tube[1]);
}
}
waitForAll();
return EXIT_SUCCESS;
}
Utilisation bidirectionnelle
Dans cet exemple, on crée une structure qui va contenir les deux tubes pour plus de facilité:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
// Structure avec des tubes full-duplex
typedef struct fdpipe {
int father[2];
int son[2];
} fdpipe;
// Nombre total de thread
#define NB_FORK 10
// Taille du message
#define LENGTH_MSG 50
// Tableau contenant le message
char message[LENGTH_MSG] = "";
// Fonction exécutée dans le fork
void job(fdpipe * pipes) {
int tid = getpid();
// timer pour attendre maximum 5 secondes
int i = 5;
while (i > 0) {
// lecture dans le tube
if (read((*pipes).father[0], message, LENGTH_MSG) > 0) {
printf("Message du processus [%i] : %s\n", tid, message);
// Ecriture du message dans le tableau
sprintf(message, "je suis [%i] et j'ai bien reçu ton message !\n", tid);
write((*pipes).son[1], message, LENGTH_MSG);
break;
}
sleep(1);
}
}
// Fonction qui attend chacun des processus fils
void waitForAll() {
int status;
pid_t pid;
int n = 0;
while (n < NB_FORK) {
pid = wait(&status);
n++;
}
}
int main() {
for (int i = 0; i < NB_FORK; i++) {
fdpipe pipes;
pipe(pipes.father);
pipe(pipes.son);
pid_t pid = fork();
if (pid == -1) {
// Il y a une erreur
perror("fork");
return EXIT_FAILURE;
} else if (pid == 0) {
// On est dans le fils
close(pipes.father[1]);
job(&pipes);
close(pipes.father[0]);
close(pipes.son[0]);
close(pipes.son[1]);
exit(EXIT_SUCCESS);
} else {
// On est dans le père
// Ecriture du message dans le tableau
sprintf(message, "Fork [%i], je suis ton père !\n", pid);
// Ecriture du message dans le tube
write(pipes.father[1], message, LENGTH_MSG);
while (i > 0) {
// lecture dans le tube
if (read(pipes.son[0], message, LENGTH_MSG) > 0) {
printf("Réponse du fils : %s\n", message);
break;
}
sleep(1);
}
close(pipes.father[1]);
close(pipes.father[0]);
close(pipes.son[0]);
close(pipes.son[1]);
}
}
waitForAll();
return EXIT_SUCCESS;
}
Ce qui nous donne le résultat suivant :
Message du processus [18523] : Fork [18523], je suis ton père ! Réponse du fils : je suis [18523] et j'ai bien reçu ton message ! Message du processus [18522] : Fork [18522], je suis ton père ! Message du processus [18524] : Fork [18524], je suis ton père ! Réponse du fils : je suis [18524] et j'ai bien reçu ton message ! Message du processus [18525] : Fork [18525], je suis ton père ! Réponse du fils : je suis [18525] et j'ai bien reçu ton message !
Tube nommé
Fonctions utilisées
Il est possible de nommer un tube pour pouvoir y accéder depuis un autre processus. Pour ce faire nous allons utiliser les fonctions suivantes :
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo (const char* name, mode_t mode);
- name est le nom du tube ;
- mode correspond aux permissions du tube (idem chmod) ;
- le code retour de la fonction varie entre :
- EACCES : droits insuffisants pour créer le tube ;
- EEXIST : un tube de même nom existe déjà ;
- ENAMETOOLONG : nom trop long ;
- ENOENT : le chemin n'existe pas ;
- ENOSPC : espace insuffisant sur le système de fichiers.
#include <fcntl.h>
int open (const char* name, int options);
- name est le nom du tube ;
- options permet de désigner l'extrémité du tube:
- O_WRONLY: pour l'entrée ;
- O_RDONLY: pour la sortie.
- le retour est le descripteur pour la lecture ou l'écriture ou -1 en cas d'échec.
Exemple d'utilisation
Ci-dessous un exemple d'utilisation avec un thread qui lit dans un tube nommé:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
// Nom du tube
#define FIFO_NAME "/tmp/test.fifo"
// Longeur du buffer de lecture
#define BUFFER_LENGTH 30;
// Mode de lecture du tube
const mode_t FIFO_MODE = 0760;
// Fonction exécutée par le thread
void * job(void * args) {
// Récupération du descripteur en lecture
int fdread;
if ((fdread = open(FIFO_NAME, O_RDONLY)) == -1) {
fprintf(stderr, "Impossible d'ouvrir le tube en lecture: %s\n",
strerror(errno));
exit(EXIT_FAILURE);
}
// Création d'un tampon pour stocker le contenu du tube
char buffer[30];
// Lecture du contenu du tube
read(fdread, buffer, 30);
// Affichage du contenu
printf("Le tube contient : %s", buffer);
// Fin de l'exécution du thread
pthread_exit(EXIT_SUCCESS);
}
int main() {
// On supprime le tube s'il existe déjà
if (mkfifo(FIFO_NAME, FIFO_MODE) == -1) {
printf("Suppression du tube existant: %s\n", FIFO_NAME);
unlink(FIFO_NAME);
}
// Création de la variable qui va contenir le thread
pthread_t thread;
// Création du tube FIFO
if (mkfifo(FIFO_NAME, FIFO_MODE) == -1) {
printf("Erreur de création du tube: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}else{
printf("Création du tube: %s\n", FIFO_NAME);
}
// Création du thread
pthread_create(&thread, NULL, job, NULL);
// Récupération du descripteur en écriture
int fdwrite;
if ((fdwrite = open(FIFO_NAME, O_WRONLY)) == -1) {
printf("Impossible d'ouvrir le tube en écriture: %s\n",
strerror(errno));
exit(EXIT_FAILURE);
}
// Ecriture dans le tube
char message[] = "Bonjour thread";
write(fdwrite, message, sizeof(message));
// En attente de l'éxécution du thread
pthread_join(thread, NULL);
// Fin de l'exécution
return EXIT_SUCCESS;
}
Dans l'exemple précédent, vous remarquerez que le thread est créé avant l'ouverture du tube. C'est absolument important de démarrer le thread avant, car la fonction open va bloquer le fil d'exécution principal tant que le thread n'ouvre pas le tube en lecture ! Pour eviter cela il faut utiliser l'option O_NONBLOCK |