C pipe

De The Linux Craftsman
Aller à la navigation Aller à la recherche

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;
}
Warning manual.jpg

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