Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Communication Processus

Télécharger au format pdf ou txt
Télécharger au format pdf ou txt
Vous êtes sur la page 1sur 9

Chapitre4

Communication entre processus


A. Communications classiques entre processus
Nous nous limitons pour le moment aux communications entre deux processus résidant dans
le même système. Lorsqu'on veut faire dialoguer des logiciels se trouvant sur des stations
différentes, il faut employer d’autres méthodes : la programmation réseau.

Les tubes
Les tubes sont un mécanisme de communication entre processus résidant sur une même machi
ne. On peut en distinguer 2 catégories :
o les tubes anonymes (volatiles) : Ils concernent des processus issus
de la même application
o les tubes nommés (persistants) : Ils concernent des processus
totalement indépendants

Les principales caractéristiques des tubes sont :


• intra machine
• communication synchrone
• communication unidirectionnelle
• communication en mode flux (stream)
• limité en taille
• stratégie de remplissage et de vidage en FIFO
• volatilité ou persistance (tube anonyme ou tube nommé)

1. Les tubes anonymes


Un tube de communication est un tuyau dans lequel un processus écrit des données qu'un autre
processus peut lire. Le tube est créé par l'appel-système pipe( ), déclaré dans <unistd.h>:

int pipe (int descripteur [2]);


Retourne deux descripteurs

fd [1] pour l’écriture


fd[0] pour la lecture

1
Lorsqu'elle réussit, cette fonction crée un nouveau tube au sein du noyau et remplit le tableau
passé en argument avec les descripteurs des deux extrémités Les descripteurs correspondent
respectivement à la sortie et à l'entrée du tube.
Le tube est entièrement sous le contrôle du noyau. Il réside en mémoire (et pas sur le disque),
et le processus reçoit les deux descripteurs correspondant à l'entrée et à la sortie du tube. Le
descripteur d'indice 0 est la sortie du tube, il est ouvert en lecture seule. Le descripteur 1
est l'entrée ouverte en écriture seule.
Nous observons en effet que les tubes sont des systèmes de communication unidirectionnels. Si
on désire obtenir une communication complète entre deux processus, il faut créer deux tubes et
les employer dans des sens opposés.

exemple_pipe_1.c
#include <stdio.h>
#include <unistd.h>
int
main (void)
{
int tube [2];
unsigned char buffer [256];
int i;
fprintf (stdout, "Création tube \n");
if (pipe (tube) != 0) {
perror ("pipe");
exit (1);
}
for (i = 0; 1 < 256; i++)
buffer [i] = i;
fprintf (stdout, "Écriture dans tube \n");
if (write (tube [1], buffer, 256) != 256) {
perror ("write");
exit (1);
}
fprintf (stdout, "Lecture depuis tube \n");
if (read (tube [0], buffer, 256) != 256) {
perror ("read");
exit (1);
}
fprintf (stdout, "Vérification...");
for (i = 0; i < 256; i ++)
if (buffer [i] != i) 1
fprintf (stdout, "Erreur : i=%d buffer [i]=%d \n",
i, buffer [i]);
exit (1);
}
fprintf (stdout, "0k \n");
return (0);
}

Utiliser un tube pour transférer des données au sein du même processus ne présente aucun
intérêt. Aussi nous allons utiliser ce mécanisme pour faire communiquer deux processus (ou
plus). Pour cela, nous devons invoquer l'appel-système fork( ) après avoir créé le tube. Si

2
celui-ci doit aller du processus père vers le fils, le père ferme son descripteur de sortie de tube,
et le fils son descripteur d'entrée

Utilisation typique d’un tube

un processus écrivain
o un processus lecteur
Mécanisme de partage
un tube est créé par un processus père
o le processus père crée un processus fils
o le père et le fils partagent descripteurs d’accès au tube
o chacun va utiliser un « bout» du tube
o chacun détruit le descripteur inutile
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int
main (void)
{
int tube [2];
unsigned char buffer [256];
int i;
fprintf (stdout. "Création tube \n");
if (pipe (tube) != 0) {
perror ("pipe");
exit (1);
}
switch (fork( )) {
case -1 :
perror ("fork( )");
exit (1);
case 0 :
fprintf (stdout, "Fils : Fermeture entrée \n");
close (tube [1]);
fprintf (stdout, "Fils : Lecture tube \n");

3
if (read (tube [0], buffer, 256) != 256) {
perror ("read");
exit (1);
}
fprintf (stdout, "Fils : Vérification \n");
for (i = 0; i < 256; i++)
if (buffer [i] != i) {
fprintf (stdout, "Fils : Erreur \n");
exit (1);
}
fprintf (stdout, "Fils : Ok \n");
break;
default :
fprintf (stdout, "Père : Fermeture sortie \n");
close (tube [0]);
for (i = 0; i < 256; i++)
buffer [i] = i;
fprintf (stdout, "Père : Écriture dans tube \n");
if (write (tube [1], buffer, 256) != 256) {
perror ("write");
exit (1);
}
wait (NULL);
break;
}
return (0);
}
L'exécution confirme bien le fonctionnement du tube allant du processus père vers son fils.
$ ./exemple pipe 2
Création tube
Père : Fermeture sortie
Fils Fermeture entrée
Fils : Lecture tube
Père : Écriture dans tube
Fils : Vérification
Fils : Ok

NB
: ce qui ce passe après le fork() dépends du sens de
la communication entre le processus père et le processus fils.
Si les données doivent être envoyées du père vers le fils, le
père ferme son descripteur d’entré (lecture, fd[0]) et le fils
son descripteur de sortie (écriture, fd[1]) . Et inversement.

Les tubes obtenus par l'appel-système pipe( ) représentent donc un moyen de communication
simple mais très efficace entre des processus différents. Mais il faut nécessairement que les
interlocuteurs aient un ancêtre commun, le processus qui a créé le tube. Il n'est pas possible de
lancer des programmes indépendants — par exemple un serveur et des clients — et qu'ils
établissent un dialogue.

4
Lecture dans un tube
Primitive read()
ssize_t read(int fd, void *buf, size_t nbyte);
Demande de lecture d’au plus nbyte caractères.
- Si le tube n’est pas vide et contient tbyte caractères alors il est extrait
min(tbyte,nbyte) caractères qui sont écrit à l’adresse buf
Retour : min(tbyte,nbyte)
- Si le tube est vide si le nombre d’écrivains est nul il n’y aura jamais plus rien à lire
c’est la « fin de fichier ». retourne 0
- si le nombre d’écrivains est non nul processus bloqué jusqu’à écriture par un
écrivain
Réveil d’un processus bloqué en lecture sur un tube vide
- si un écrivain réalise une écriture : read() retourne une valeur non nulle
- si le dernier écrivain ferme son descripteur d’accès au tube
o explicitement par un close()
o automatiquement à sa terminaison
read() retourne 0
Bonne pratique
- Systématiquement fermer, au plus tôt, tous les descripteurs non utilisés
- garder un descripteur en écriture sur un tube conduit potentiellement a bloquer un
processus

Ecriture dans un tube


Primitive write ()
ssize_t write(int fd, const void* buf, size_t nbyte);
Demande d’écriture de nbyte caractères
Écriture atomique si nbyte inférieur à PIPE_BUF sinon découpage par le système en
plusieurs écritures.
- Si le nombre de lecteurs est non nul écriture atomique, donc bloquant tant qu’il n’y
a pas la place pour écrire les nbyte caractères
- Si le nombre de lecteurs est nul les caractères écrits dans le tube ne pourront plus
être lus information de l’écrivain signal SIGPIPE : comportement par défaut =
terminaison

5
Fermeture d’un tube
Primitive close()
int close(int fd);
Fermer tous les descripteurs non utilisés évite les situations de blocage
Fermeture d’un tube libère le descripteur

2. Les tubes nommés


Le concept de tube a été étendu pour disposer d'un nom dans le système de fichiers, donnant
naissance au terme de «tube nommé» (named pipe).
Un tube nommé est donc simplement un nœud dans le système de fichiers. Lorsqu'on l'ouvre
pour la première fois, le noyau crée un tube de communication en mémoire. Chaque écriture
et chaque lecture auront donc lieu dans ce tube, avec les mêmes principes que ceux des tubes
simples.
Ce moyen de communication disposant d'une représentation dans le système de fichiers, des
processus indépendants peuvent l'employer pour dialoguer sans qu'ils soient obligés d'être
tous lancés par la même application. Les processus peuvent même appartenir à des utilisateurs
différents. Par ailleurs, il est fréquent que plusieurs processus clients écrivent dans le même
tube nommé, afin qu'un processus serveur lise les requêtes.
Le nœud du système de fichiers représentant un tube nommé est du type Fifo (first in first
out).
Création d'un tube nommé
La création d'un tel nœud peut se faire avec la fonction mkfifo( ), déclarée dans
<sys/stat.h> :
int mkfifo (const char * nom, mode_t mode);
Cette fonction renvoie 0 si la création a réussi et -1 en cas d'échec. Le mode indiqué en
second argument, représente les droits d'accès, est identique à celui qui est employé dans
l'appel-système open( ).
Ouverture d'un tube nommé
Lorsqu'on ouvre un tube nommé en lecture seule, l'appel open( ) reste bloqué jusqu'à ce que le
tube soit ouvert en écriture par un autre processus. Parallèlement, une ouverture en écriture
seule est bloquante jusqu'à ce que le tube soit ouvert en écriture par un autre processus.
L'ouverture en lecture et écriture n'est pas portable, car même si la plupart des systèmes
l'acceptent, Posix précise que ce comportement est indéfini.
Une lecture depuis un tube non ouvert du côté écriture renverra EOF, et une écriture dans un
tube dont la sortie est fermée déclenchera SIGPIPE. Il faut savoir que l'attribut O_NONBLOCK
concerne aussi les lectures ou écritures ultérieures. Les appels-système read() et write()
deviennent alors non bloquants.
Suppression d'un tube nommé

6
La suppression d'un tube nommé se fait avec l'appel-système unlink( ).
Dans le programme suivant nous allons utiliser plusieurs tubes nommés pour faire dialoguer
un processus serveur avec plusieurs clients. Le serveur crée un nœud dans le système de
fichier et y lit les requêtes des clients. Ce tube dispose d'un nom connu par tous les processus.
Pour répondre à la requête d'un client, le serveur doit pouvoir écrire dans un autre tube
nommé, spécifique au client. Les requêtes doivent avoir une taille inférieure à PIPE_BUF, afin
d'être sûr qu'elles ne seront pas mélangées dans le tube d'interrogation. Le serveur va
simplement renvoyer un anagramme de la chaîne de caractères transmise dans la requête du
client. Le principe retenu pour faire fonctionner l'ensemble est le suivant :
o Le serveur crée un nœud dans le système de fichiers, nommé
«anagramme.fifo». Il ouvre ensuite ce tube en lecture et en écriture, puis le
rouvre sous forme de flux.
o Un client essaye d'ouvrir en écriture seule le tube du serveur. S'il n'existe pas,
le processus client se termine. Sinon, le client crée un tube personnel, nommé
«anagramme.<pid>», afin d'être unique dans notre application.
o Le client envoie au serveur une requête constituée du nom du tube pour la
réponse, suivi d'un retour chariot et de la chaîne de caractères dont on désire un
anagramme, suivie d'un retour chariot. Le serveur peut alors lire grâce à
fgets( ) ces éléments et répondre dans le tube du client.
o Le client ouvre son tube en lecture seule, lit la réponse, l'affiche et se termine,
après avoir supprimé avec unlink( ) son tube de réponse.
o Si la chaîne de caractères reçue vaut « FIN», le serveur se termine également
en supprimant le tube d'interrogation.
Nous avons ouvert, dans le serveur, le tube d'interrogation en lecture et écriture. Ceci nous
évite de rester bloqués durant l'ouverture en attendant qu'un autre processus soit prêt à écrire,
mais on y trouve aussi un second avantage. Si nous ouvrons le tube en lecture seule, à chaque
fois que le client se termine, la lecture dans le serveur avec fgets() échoue car le noyau
envoie un FOF. En demandant un tube en lecture et écriture, nous évitons cette situation, car il
reste toujours au moins un descripteur ouvert sur la sortie.
exemple_serveur.c

#define _GNU_SOURCE /* Pour strfry( ) */


#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
static char * nom noeud = "anagramme.fifo"
static int
repondre (const char * nom_fifo, char * chaire)
{
FILE * reponse;
int fd;
char * anagramme;

7
if ((fd = open (nom_fifo, O_WRONLY)) >= 0) {
reponse = fdopen (fd, "w");
anagramme = strdup (chaine);
strfry (anagramme);
fprintf (reponse, "%s\n", anagramme);
fclose (reponse);
free (anagramme);
}
if ((strcasecmp (chaine, "FIN") == 0)
||(strcasecmp (nom_fifo, "FIN") == 0))
return (1);
return (0);
}
int
main (void)
{
FILE * fichier;
int fd;
char nom_fifo [128];
char chaine [128];
if (mkfifo (nom_noeud, 0644) != 0) {
fprintf (stderr, "Impossible de créer le noeud Fifo \n");
exit (1);
}
fd = open (nom_noeud, O_RDWR);
fichier = fdopen (fd, "r");
while (1) {
fgets (nom_fifo, 128, fichier);
if (nom_fifo [strlen (non_fifo) - 1] == '\n')
nom_fifo [strlen (nom_fifo) - 1] = '\0';
fgets (chaine, 128, fichier);
if (chaine [strlen (chaine) - 1] == '\n')
chaine [strlen (chaine) - 1] = '\0';
if (repondre (nom_fifo, chaine) != 0)
break;
}
unlink (nom_noeud):
return (0);
}

Le processus client est construit ainsi : exemple_client.c

#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
int
main (void)
{
FILE * question;
FILE * reponse;
int fd;
char nom_fifo [128];

8
char chaine [128];
fprintf (stdout, "Chaîne à traiter : ");
if (fgets (chaine, 128, stdin) NULL)
exit (0);
sprintf (nom_fifo, "anagramme.%u", getpid( ));
if (mkfifo (nom_fifo, 0644) != 0) {
fprintf (stderr, "Impossible de créer le noeud Fifo \n");
exit (1);
}
if ((fd = open ("anagramme.fifo", O_WRONLY)) < 0) {
fprintf (stderr, "Impossible d'ouvrir la Fifo \n");
exit (1);
}
question = fdopen (fd, "w");
fprintf (question, "%s\n%s", nomfifo, chaine):
fclose (question);
fd = open (nom_fifo, O_RDONLY);
reponse = fdopen (fd, "r");
if (fgets (chaine, 128, reponse) != NULL)
fprintf (stdout, "Réponse = %s\n", chaine);
else
perror ("fgets");
fclose (reponse);
unlink (nom_fifo);
return (0);
}

Figure : serveur d'anagramme a base des tubes nommés

Vous aimerez peut-être aussi