Communication interprocessus

Le Chapitre !!processus, «Processus»!! traitait de la création de processus et montrait comment il est possible d’obtenir le code de sortie d’un processus fils. Il s’agit de la forme la plus simple de communication entre deux processus, mais en aucun cas de la plus puissante. Les mécanismes présentés au Chapitre !!processus!! ne fournissent aucun moyen au processus parent pour communiquer avec le fils excepté via les arguments de ligne de commande et les variables d’environnement, ni aucun moyen pour le processus fils de communiquer avec son père, excepté par le biais de son code de sortie. Aucun de ces mécanismes ne permet de communiquer avec le processus fils pendant son exécution, ni n’autorise une communication entre les processus en dehors de la relation père-fils.

Ce chapitre présente des moyens de communication interprocessus qui dépassent ces limitations. Nous présenterons différentes façons de communiquer entre père et fils, entre des processus « sans liens » et même entre des processus s’exécutant sur des machines distinctes.

La communication interprocessus (interprocess communication, IPC) consiste à transférer des données entre les processus. Par exemple, un navigateur Internet peut demander une page à un serveur, qui envoie alors les données HTML. Ce transfert utilise des sockets dans une connexion similaire à celle du téléphone. Dans un autre exemple, vous pourriez vouloir imprimer les noms des fichiers d’un répertoire en utilisant une commande du type ls | lpr. Le shell crée un processus ls et un processus lpr distincts et connecte les deux au moyen d’un tube (ou pipe) représenté par le symbole “|“. Un tube permet une communication à sens unique entre deux processus. Le processus ls écrit les données dans le tube et le processus lpr les lit à partir du tube.

Dans ce chapitre, nous traiterons de cinq types de communication interprocessus:

  • La mémoire partagée permet aux processus de communiquer simplement en lisant ou écrivant dans un emplacement mémoire prédéfini.
  • La mémoire mappée est similaire à la mémoire partagée, excepté qu’elle est associée à un fichier.
  • Les tubes permettent une communication séquentielle d’un processus à l’autre.
  • Les files FIFO sont similaires aux tubes excepté que des processus sans lien peuvent communiquer car le tube reçoit un nom dans le système de fichiers.
  • Les sockets permettent la communication entre des processus sans lien, pouvant se trouver sur des machines distinctes.

Ces types d’IPC diffèrent selon les critères suivants:

  • Ils restreignent ou non la communication à des processus liés (processus ayant un ancêtre commun), à des processus partageant le même système de fichiers ou à tout ordinateur connecté à un réseau.
  • Un processus communiquant n’est limité qu’à la lecture ou qu’à l’écriture de données.
  • Le nombre de processus pouvant communiquer.
  • Les processus qui communiquent sont synchronisés par l’IPC − par exemple, un processus lecteur s’interrompt jusqu’à ce qu’il y ait des données à lire.

Dans ce chapitre, nous ne traiteront pas des IPC ne permettant qu’une communication limitée à un certain nombre de fois, comme communiquer en utilisant la valeur de sortie du fils.

Mémoire partagée

Une des méthodes de communication interprocessus les plus simples est d’utiliser la mémoire partagée. La mémoire partagée permet à deux processus ou plus d’accéder à la même zone mémoire comme s’ils avaient appelé malloc et avaient obtenu des pointeurs vers le même espace mémoire. Lorsqu’un processus modifie la mémoire, tous les autres processus voient la modification.

Communication locale rapide

La mémoire partagée est la forme de communication interprocessus la plus rapide car tous les processus partagent la même mémoire. L’accès à cette mémoire partagée est aussi rapide que l’accès à la mémoire non partagée du processus et ne nécessite pas d’appel système ni d’entrée dans le noyau. Elle évite également les copies de données inutiles.

Comme le noyau ne coordonne pas les accès à la mémoire partagée, vous devez mettre en place votre propre synchronisation. Par exemple, un processus ne doit pas effectuer de lecture avant que des données aient été écrites et deux processus ne doivent pas écrire au même emplacement en même temps. Une stratégie courante pour éviter ces conditions de concurrence est d’utiliser des sémaphores, ce dont nous parlerons dans la prochaine section. Nos programmes d’illustration, cependant, ne montrent qu’un seul processus accédant à la mémoire, afin de se concentrer sur les mécanismes de la mémoire partagée et éviter d’obscurcir le code avec la logique de synchronisation.

Le modèle mémoire

Pour utiliser un segment de mémoire partagée, un processus doit allouer le segment. Puis, chaque processus désirant accéder au segment doit l’attacher. Après avoir fini d’utiliser le segment, chaque processus le détache. À un moment ou à un autre, un processus doit libérer le segment.

La compréhension du modèle mémoire de Linux aide à expliquer le processus d’allocation et d’attachement. Sous Linux, la mémoire virtuelle de chaque processus est divisée en pages. Chaque processus conserve une correspondance entre ses adresses mémoire et ces pages de mémoire virtuelle, qui contiennent réellement les données. Même si chaque processus dispose de ses propres adresses, plusieurs tables de correspondance peuvent pointer vers la même page, permettant le partage de mémoire. Les pages mémoires sont examinées de plus près dans la Section 8.8, « La Famille mlock: Verrouiller la Mémoire Physique », du Chapitre 8, « Appels Système Linux ».

L’allocation d’un nouveau segment de mémoire partagée provoque la création de nouvelles pages de mémoire virtuelle. Comme tous les processus désirent accéder au même segment de mémoire partagée, seul un processus doit allouer un nouveau segment de mémoire partagée. Allouer un segment existant ne crée pas de nouvelles pages mais renvoie l’identifiant des pages existantes. Pour qu’un processus puisse utiliser un segment de mémoire partagée, il doit l’attacher, ce qui ajoute les correspondances entre sa mémoire virtuelle et les pages partagées du segment. Lorsqu’il en a terminé avec le segment, ces correspondances sont supprimées. Lorsque plus aucun processus n’a besoin d’accéder à ces segments de mémoire partagée, un processus exactement doit libérer les pages de mémoire virtuelle.

Tous les segments de mémoire partagée sont alloués sous forme de multiples entiers de la taille de page du système, qui est le nombre d’octets dans une page mémoire. Sur les systèmes Linux, la taille de page est de 4 Ko mais vous devriez vous baser sur la valeur renvoyée par getpagesize.

Allocation

Un processus alloue un segment de mémoire partagée en utilisant shmget (« SHared Memory GET », obtention de mémoire partagée). Son premier paramètre est une clé entière qui indique le segment à créer. Des processus sans lien peuvent accéder au même segment partagé en spécifiant la même valeur de clé. Malheureusement, d’autres processus pourraient avoir choisi la même valeur de clé fixée, ce qui provoquerait un conflit. Utiliser la constante spéciale IPC_PRIVATE comme valeur de clé garantit qu’un nouveau segment mémoire est créé.

Le second paramètre indique le nombre d’octets du segment. Comme les segments sont alloués en utilisant des pages, le nombre d’octets effectivement alloués est arrondi au multiple de la taille de page supérieur.

Le troisième paramètre est un ou binaire entre des indicateurs décrivant les options demandées à shmget. Voici ces indicateurs:

  • IPC_CREAT − Cet indicateur demande la création d’un nouveau segment. Cela permet la création d’un nouveau segment tout en spécifiant une valeur de clé.
  • IPC_EXCL − Cet indicateur, toujours utilisé avec IPC_CREAT, provoque l’échec de shmget si la clé de segment spécifiée existe déjà. Donc, cela permet au processus appelant d’avoir un segment « exclusif ». Si cette option n’est pas précisée et que la clé d’un segment existant est utilisée, shmget renvoie le segment existant au lieu d’en créer un nouveau.
  • Indicateurs de mode − Cette valeur est constituée de 9~bits indiquant les permissions du propriétaire, du groupe et des autres utilisateurs pour contrôler l’accès au segment. Les bits d’exécution sont ignorés. Une façon simple de spécifier les permissions est d’utiliser les constantes définies dans <sys/stat.h> et documentées dans la page de manuel de section 2 de stat1). Par exemple, S_IRUSR et S_IWUSR spécifient des permissions de lecture et écriture pour le propriétaire du segment de mémoire partagée et S_IROTH et S_IWOTH spécifient des permissions de lecture et écriture pour les autres utilisateurs.

L’appel suivant à shmget crée un nouveau segment de mémoire partagée (ou accède à un segment existant, si shm_key est déjà utilisé) qui peut être lu et écrit par son propriétaire mais pas par les autres utilisateurs.

int segment_id = shmget (shm_key, getpagesize (),
                         IPC_CREAT | S_IRUSR | S_IWUSR);

Si l’appel se passe bien, shmget renvoie un identifiant de segment. Si le segment de mémoire partagée existe déjà, les permissions d’accès sont vérifiées et le système s’assure que le segment n’est pas destiné à être détruit.

Attachement et détachement

Pour rendre le segment de mémoire partagée disponible, un procesus doit utiliser shmat (« SHared Memory ATtach », attachement de mémoire partagée) en lui passant l’identifiant du segment de mémoire partagée SHMID renvoyé par shmget. Le second argument est un pointeur qui indique où vous voulez que le segment soit mis en correspondance dans l’espace d’adressage de votre processus ; si vous passez NULL, Linux sélectionnera une adresse disponible. Le troisième argument est un indicateur, qui peut prendre une des valeurs suivantes:

  • SHM_RND indique que l’adresse spécifiée par le second paramètre doit être arrondie à un multiple inférieur de la taille de page. Si vous n’utilisez pas cet indicateur, vous devez aligner le second argument de shmat sur un multiple de page vous-même.
  • SHM_RDONLY indique que le segment sera uniquement lu, pas écrit.

Si l’appel se déroule correctement, il renvoie l’adresse du segment partagé attaché. Les processus fils créés par des appels à fork héritent des segments partagés attachés; il peuvent les détacher s’ils le souhaitent.

Lorsque vous en avez fini avec un segment de mémoire partagée, le segment doit être détaché en utilisant shmdt (« SHared Memory DeTach », Détachement de Mémoire Partagée) et lui passant l’adresse renvoyée par shmat. Si le segment n’a pas été libéré et qu’il s’agissait du dernier processus l’utilisant, il est supprimé. Les appels à exit et toute fonction de la famille d‘exec détachent automatiquement les segments.

Contrôler et libérer la mémoire partagée

L’appel shmctl (« SHared Memory ConTroL », contrôle de la mémoire partagée) renvoie des informations sur un segment de mémoire partagée et peut le modifier. Le premier paramètre est l’identifiant d’un segment de mémoire partagée.

Pour obtenir des informations sur un segment de mémoire partagée, passez IPC_STAT comme second argument et un pointeur vers une struct shmid_ds.

Pour supprimer un segment, passez IPC_RMID comme second argument et NULL comme troisième argument. Le segment est supprimé lorsque le dernier processus qui l’a attaché le détache.

Chaque segment de mémoire partagée devrait être explicitement libéré en utilisant shmctl lorsque vous en avez terminé avec lui, afin d’éviter de dépasser la limite du nombre total de segments de mémoire partagée définie par le système. L’invocation de exit et exec détache les segments mémoire mais ne les libère pas.

Consultez la page de manuel de shmctl pour une description des autres opérations que vous pouvez effectuer sur les segments de mémoire partagée.

Programme exemple

Le programme du Listing !!shm!! illustre l’utilisation de la mémoire partagée.

Listing (shm.c) Utilisation de la Mémoire Partagée

#include <stdio.h>
#include <sys/shm.h>
#include <sys/stat.h>
int main ()
{
  int segment_id;
  char* shared_memory;
  struct shmid_ds shmbuffer;
  int segment_size;
  const int shared_segment_size = 0x6400;
  /* Alloue le segment de mémoire partagée. */
  segment_id = shmget (IPC_PRIVATE, shared_segment_size,
                       IPC_CREAT | IPC_EXCL | S_IRUSR | S_IWUSR);
  /* Attache le segment de mémoire partagée. */
  shared_memory = (char*) shmat (segment_id, 0, 0);
  printf ("mémoire partagée attachée à l'adresse %p\n", shared_memory);
  /* Détermine la taille du segment. */
  shmctl (segment_id, IPC_STAT, &shmbuffer);
  segment_size = shmbuffer.shm_segsz;
  printf ("taille du segment : %d\n", segment_size);
  /* Écrit une chaîne dans le segment de mémoire partagée. */
  sprintf (shared_memory, "Hello, world.");
  /* Détache le segment de mémoire partagée. */
  shmdt (shared_memory);
  /* Réattache le segment de mémoire partagée à une adresse différente. */
  shared_memory = (char*) shmat (segment_id, (void*) 0x5000000, 0);
  printf ("mémoire partagée réattachée à l'adresse %p\n", shared_memory);
  /* Affiche la chaîne de la mémoire partagée. */
  printf ("%s\n", shared_memory);
  /* Détache le segment de mémoire partagée. */
  shmdt (shared_memory);
 
  /* Libère le segment de mémoire partagée.  */
  shmctl (segment_id, IPC_RMID, 0);
 
  return 0;
}

Débogage

La commande ipcs donne des informations sur les possibilités de communication interprocessus, y compris les segments de mémoire partagée. Utilisez l’option -m pour obtenir des informations sur la mémoire partagée. Par exemple, ce code illustre le fait qu’un segment de mémoire partagée, numéroté 1627649, est utilisé:

% ipcs -m

−−− Segments de mémoire partagée −−−
clé        shmid     propriétaire perms     octets   nattch état
0x00000000 1627649   user      640       25600     0

Si ce segment de mémoire avait été oublié par erreur par un programme, vous pouvez utiliser la commande ipcrm pour le supprimer.

% ipcrm shm 1627649

Avantages et inconvénients

Les segments de mémoire partagée permettent une communication bidirectionnelle rapide entre n’importe quel nombre de processus. Chaque utilisateur peut à la fois lire et écrire, mais un programme doit définir et suivre un protocole pour éviter les conditions de concurrence critique comme écraser des informations avant qu’elles ne soient lues. Malheureusement, Linux ne garantit pas strictement l’accès exclusif même si vous créez un nouveau segment partagé avec IPC_PRIVATE.

De plus, lorsque plusieurs processus utilisent un segment de mémoire partagée, ils doivent s’arranger pour utiliser la même clé.

Sémaphores de processus

Nous l’avons évoqué dans la section précédente, les processus doivent se coordonner pour accéder à la mémoire partagée. Comme nous l’avons dit dans la Section !!semthreads!!, « Sémaphores pour les Threads », du Chapitre !!threads!!, « Threads », les sémaphores sont des compteurs qui permettent la synchronisation de plusieurs threads. Linux fournit une implémentation alternative distincte de sémaphores qui peuvent être utilisés pour synchroniser les processus (appelés sémaphores de processus ou parfois sémaphores System~V). Les sémaphores de processus sont instanciés, utilisés et libérés comme les segments de mémoire partagée. Bien qu’un seul sémaphore soit suffisant pour quasiment toutes les utilisations, les sémaphores de processus sont regroupés en ensembles.

Tout au long de cette section, nous présentons les appels système relatifs aux sémaphores de processus, en montrant comment implémenter des sémaphores binaires isolés les utilisant.

Instanciation et libération

Les appels semget et semctl instancient et libèrent des sémaphores, ils sont analogues à shmget et shmctl pour la mémoire partagée. Invoquez semget avec une clé correspondant à un ensemble de sémaphores, le nombre de sémaphores dans l’ensemble et des indicateurs de permissions, comme pour shmget; la valeur de retour est un identifiant d’ensemble de sémaphores. Vous pouvez obtenir l’identifiant d’un ensemble de sémaphores existant en passant la bonne valeur de clé; dans ce cas, le nombre de sémaphores peut être à zéro.

Les sémaphores continuent à exister même après que tous les processus les utilisant sont terminés. Le dernier processus à utiliser un ensemble de sémaphores doit le supprimer explicitement afin de s’assurer que le système d’exploitation ne tombe pas à court de sémaphores. Pour cela, invoquez semctl avec l’identifiant de l’ensemble de sémaphores, le nombre de sémaphores qu’il contient, IPC_RMID en troisième argument et n’importe quelle valeur d‘union semun comme quatrième argument (qui est ignoré). L’identifiant d’utilisateur effectif du processus appelant doit correspondre à celui de l’instanciateur du sémaphore (ou l’appelant doit avoir les droits root). Contrairement aux segments de mémoire partagée, la suppression d’un jeu de sémaphores provoque sa libération immédiate par Linux.

Le listing !!semalldeall!! présente les fonctions d’allocation et de libération d’un sémaphore binaire.

Listing (sem_all_deall.c) Allouer et libérer un sémaphore binaire

#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
 
/* Nous devons définir l'union semun nous-mêmes. */
 
union semun {
   int val;
   struct semid_ds *buf;
   unsigned short int *array;
   struct seminfo *%%__%%buf;
};
 
/* Obtient l'identifiant d'un sémaphore binaire, l'alloue si nécessaire. */
 
int binary_semaphore_allocation (key_t key, int sem_flags)
{
   return semget (key, 1, sem_flags);
}
 
 
/* Libère un sémaphore binaire. Tous les utilisateurs doivent avoir fini de 
    s'en servir. Renvoie -1 en cas d'échec. */
 
int binary_semaphore_deallocate (int semid)
{
   union semun ignored_argument;
   return semctl (semid, 1, IPC_RMID, ignored_argument);
}

Initialisation des sémaphores

L’instanciation et l’initialisation des sémaphores sont deux opérations distinctes. Pour initialiser un sémaphore, utilisez semctl en lui passant zéro comme second argument et SETALL comme troisième paramètre. Pour le quatrième argument, vous devez créer un objet union semun et faire pointer son champ array vers un tableau de valeurs unsigned short. Chaque valeur est utilisée pour initialiser un sémaphore dans l’ensemble.

Le listing !!seminit!! présente une fonction initialisant un sémaphore binaire.

Listing (sem_init.c) Initialiser un sémaphore binaire

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
 
/* Nous devons définir l'union semun nous-mêmes. */
 
union semun {
   int val;
   struct semid_ds *buf;
   unsigned short int *array;
   struct seminfo *%%__%%buf;
};
 
/* Initialise un sémaphore binaire avec une valeur de 1. */
 
int binary_semaphore_initialize (int semid)
{
   union semun argument;
   unsigned short values[1];
   values[0] = 1;
   argument.array = values;
   return semctl (semid, 0, SETALL, argument);
}

Opérations d'attente et de réveil

Chaque sémaphore contient une valeur positive ou nulle et supporte des opérations d’attente et de réveil. L’appel système semop implémente les deux opérations. Son premier paramètre est l’identifiant d’un ensemble de sémaphores. Son second paramètre est un tableau d’éléments struct sembuf qui définit les opérations que vous voulez accomplir. Le troisième paramètre est la taille de ce tableau.

Les champs de la struct sembuf sont les suivants:

  • sem_num est le numéro du sémaphore dans l’ensemble sur lequel est effectuée l’opération.
  • sem_op est un entier spécifiant l’opération à accomplir.
    Si sem_op est un entier positif, ce chiffre est ajouté à la valeur du sémaphore

immédiatement.
Si sem_op est un nombre négatif, la valeur absolue de ce chiffre est soustraite de la valeur du sémaphore. Si cela devait rendre la valeur du sémaphore négative, l’appel est bloquant jusqu’à ce que la valeur du sémaphore atteigne la valeur absolue de sem_op (par le biais d’incrémentations effectuées par d’autres processus).
Si sem_op est à zéro, l’opération est bloquante jusqu’à ce que la valeur atteigne zéro.

  • sem_flg est un indicateur. Positionnez-le à IPC_NOWAIT pour éviter que l’opération ne soit bloquante; au lieu de cela, l’appel à semop échoue si elle devait l’être. Si vous le positionnez à SEM_UNDO, Linux annule automatiquement l’opération sur le sémaphore lorsque le processus se termine.

Le listing !!sempv!! illustre les opérations d’attente et de réveil pour un sémaphore binaire.

Listing (sem_pv.c) Attente et réveil pour un sémaphore binaire

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
 
   /* Se met en attente sur un sémaphore binaire. Bloque jusqu'à ce que la 
      valeur du sémaphore soit positive, puis le décrémente d'une unité. */
int binary_semaphore_wait (int semid)
{
  struct sembuf operations[1];
  /* Utilise le premier (et unique) sémaphore.  */
  operations[0].sem_num = 0;
  /* Décrémente d'une unité. */
  operations[0].sem_op = -1;
  /* Autorise l'annulation. */
  operations[0].sem_flg = SEM_UNDO;
 
  return semop (semid, operations, 1);
}
 
/* Envoie un signal de réveil à un sémaphore binaire : incrémente sa valeur
   d'une unité. Sort de la fonction immédiatement. */
int binary_semaphore_post (int semid)
{
  struct sembuf operations[1];
  /* Utilise le premier (et unique) sémaphore. */
  operations[0].sem_num = 0;
  /* Incrémente d'une unité. */
  operations[0].sem_op = 1;
  /* Autorise l'annulation. */
  operations[0].sem_flg = SEM_UNDO;
 
  return semop (semid, operations, 1);
}

Passer l’indicateur SEM_UNDO permet de traiter le problème de la fin d’un processus alors qu’il dispose de ressources allouées via un sémaphore. Lorsqu’un processus se termine, volontairement ou non, la valeur du sémaphore est automatiquement ajustée pour « annuler » les actions du processus sur le sémaphore. Par exemple, si un processus qui a décrémenté le sémaphore est tué, la valeur du sémaphore est incrémentée.

Débogage des sémaphores

Utilisez la commande ipcs -s pour afficher des informations sur les ensembles de sémaphores existants. Utilisez la commande ipcrm sem pour supprimer un ensemble de sémaphores depuis la ligne de commande. Par exemple, pour supprimer l’ensemble de sémaphores ayant l’identifiant 5790517, utilisez cette commande:

% ipcrm sem 5790517

Mémoire mappée

La mémoire mappée permet à différents processus de communiquer via un fichier partagé. Bien que vous puissiez concevoir l’utilisation de mémoire mappée comme étant à celle d’un segment de mémoire partagée avec un nom, vous devez être conscient qu’il existe des différences techniques. La mémoire mappée peut être utilisée pour la communication interprocessus ou comme un moyen pratique d’accéder au contenu d’un fichier.

La mémoire mappée crée une correspondance entre un fichier et la mémoire d’un processus. Linux divise le fichier en fragments de la taille d’une page puis les copie dans des pages de mémoire virtuelle afin qu’elles puissent être disponibles au sein de l’espace d’adressage d’un processus. Donc le processus peut lire le contenu du fichier par le biais d’accès mémoire classiques. Cela permet un accès rapide aux fichiers.

Vous pouvez vous représenter la mémoire mappée comme l’allocation d’un tampon contenant la totalité d’un fichier, la lecture du fichier dans le tampon, puis (si le tampon est modifié) l’écriture de celui-ci dans le fichier. Linux gère les opérations de lecture et d’écriture à votre place.

Il existe d’autres utilisations des fichiers de mémoire mappée que la communication interprocessus. Quelques unes d’entre elles sont traitées dans la Section 5.3.5, « Autres utilisations de mmap ».

Mapper un fichier ordinaire

Pour mettre en correspondance un fichier ordinaire avec la mémoire d’un processus, utilisez l’appel mmap (« Memory MAPped », Mémoire mappée, prononcez « em-map »). Le premier argument est l’adresse à laquelle vous désirez que Linux mette le fichier en correspondance au sein de l’espace d’adressage de votre processus; la valeur NULL permet à Linux de choisir une adresse de départ disponible. Le second argument est la longueur de l’espace de correspondance en octets. Le troisième argument définit la protection de l’intervalle d’adresses mis en correspondance. La protection consiste en un ou binaire entre PROT_READ, PROT_WRITE et PROT_EXEC, correspondant aux permissions de lecture, d’écriture et d’exécution, respectivement. Le quatrième argument est un drapeau spécifiant des options supplémentaires. Le cinquième argument est un descripteur de fichier pointant vers le fichier à mettre en correspondance, ouvert en lecture. Le dernier argument est le déplacement, par rapport au début du fichier, à partir duquel commencer la mise en correspondance. Vous pouvez mapper tout ou partie du fichier en mémoire en choisissant le déplacement et la longueur de façon appropriée.

Le drapeau est un ou binaire entre ces contraintes:

  • MAP_FIXED − Si vous utilisez ce drapeau, Linux utilise l’adresse que vous demandez pour mapper le fichier plutôt que de la considérer comme une indication. Cette adresse doit être alignée sur une page.
  • MAP_PRIVATE − Les écritures en mémoire ne doivent pas être répercutées sur le fichier mis en correspondance, mais sur une copie privée du fichier. Aucun autre processus ne voit ces écritures. Ce mode ne doit pas être utilisé avec MAP_SHARED.
  • MAP_SHARED − Les écritures sont immédiatement répercutées sur le fichier mis en correspondance. Utilisez ce mode lorsque vous utilisez la mémoire mappée pour l’IPC. Ce mode ne doit pas être utilisé avec MAP_PRIVATE.

Si l’appel se déroule avec succès, la fonction renvoie un pointeur vers le début de la mémoire. S’il échoue, la fonction renvoie MAP_FAILED.

Programmes exemples

Examinons deux programmes pour illustrer l’utilisation des régions de mémoire mappée pour lire et écrire dans des fichiers. Le premier programme, le Listing !!mmapwrite!!, génère un nombre aléatoire et l’écrit dans un fichier mappé en mémoire. Le second programme, le listing !!mmapread!!, lit le nombre, l’affiche et le remplace par le double de sa valeur. Tous deux prennent en argument de ligne de commande le nom du fichier à mettre en correspondance avec la mémoire.

Listing (mmap-write.c) Écrit un nombre aléatoire dans un fichier mappé

#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#define FILE_LENGTH 0x100
 
/* Renvoie un nombre aléatoire compris dans l'intervalle [low,high]. */
 
int random_range (unsigned const low, unsigned const high)
{
  unsigned const range = high - low + 1;
  return low + (int) (((double) range) * rand () / (RAND_MAX + 1.0));
}
 
int main (int argc, char* const argv[])
{
  int fd;
  void* file_memory;
  /* Initialise le générateur de nombres aléatoires.  */
  srand (time (NULL));
 
  /* Prépare un fichier suffisamment long pour contenir le nombre. */
  fd = open (argv[1], O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
  lseek (fd, FILE_LENGTH+1, SEEK_SET);
 
  write (fd, "", 1);
  lseek (fd, 0, SEEK_SET);
 
 /* Met en correspondance le fichier et la mémoire. */
  file_memory = mmap (0, FILE_LENGTH, PROT_WRITE, MAP_SHARED, fd, 0);
  close (fd);
  /* Ecrit un entier aléatoire dans la zone mise en correspondance. */
  sprintf((char*) file_memory, "%d\n", random_range (-100, 100));
  /* Libère la mémoire (facultatif car le programme se termine). */
  munmap (file_memory, FILE_LENGTH);
  return 0;
}

Le programme mmap-write ouvre le fichier, le créant s’il n’existe pas. Le second argument de open indique que le fichier est ouvert en lecture et écriture. Comme nous ne connaissons pas la taille du fichier, nous utilisons lseek pour nous assurer qu’il est suffisamment grand pour stocker un entier puis nous nous replaçons au début du fichier.

Le programme met en correspondance le fichier et la mémoire puis ferme le fichier car il n’est plus utile. Il écrit ensuite un entier aléatoire dans la mémoire mappée, et donc dans le fichier, puis libère la mémoire. L’appel munmap n’est pas nécessaire car Linux supprimerait automatiquement la mise en correspondance à la fin du programme.

Listing (mmap-read.c) Lit un entier depuis un fichier mis en correspondance avec la mémoire et le multiplie par deux

#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#define FILE_LENGTH 0x100
 
int main (int argc, char* const argv[])
{
  int fd;
  void* file_memory;
  int integer;
 
  /* Ouvre le fichier. */
  fd = open (argv[1], O_RDWR, S_IRUSR | S_IWUSR);
  /* Met en correspondance le fichier et la mémoire. */
  file_memory = mmap (0, FILE_LENGTH, PROT_READ | PROT_WRITE,
                      MAP_SHARED, fd, 0);
  close (fd);
 
  /* Lit l'entier, l'affiche et le multiplie par deux. */
  sscanf (file_memory, "%d", &integer);
  printf ("valeur : %d\n", integer);
  sprintf ((char*) file_memory, "%d\n", 2 * integer);
  /* Libère la mémoire (facultatif car le programme se termine). */
  munmap (file_memory, FILE_LENGTH);
 
  return 0;
}

Le programme mmap-read lit le nombre à partir du fichier puis y écrit son double. Tout d’abord, il ouvre le fichier et le met en correspondance en lecture/écriture. Comme nous pouvons supposer que le fichier est suffisamment grand pour stocker un entier non signé, nous n’avons pas besoin d’utiliser lseek, comme dans le programme précédent. Le programme lit la valeur à partir de la mémoire en utilisant sscanf puis formate et écrit le double de la valeur en utilisant sprintf.

Voici un exemple de l’exécution de ces programmes d’exemple. Il utilise le fichier /tmp/integer-file.

% ./mmap-write /tmp/integer-file
% cat /tmp/integer-file
42
% ./mmap-read /tmp/integer-file
valeur : 42
% cat /tmp/integer-file
84

Remarquez que le texte 42 a été écrit dans le fichier sur le disque sans jamais appeler write et à été lu par la suite sans appeler read. Notez que ces programmes de démonstration écrivent et lisent l’entier sous forme de chaîne (en utilisant sprintf et sscanf) dans un but d’exemple uniquement − il n’y a aucune raison pour que le contenu d’un fichier mis en correspondance avec la mémoire soit au format texte. Vous pouvez lire et écrire de façon binaire dans un fichier mis en correspondance avec la mémoire.

Accès partagé à un fichier

Des processus distincts peuvent communiquer en utilisant des régions de la mémoire mises en correspondance avec le même fichier. Passez le drapeau MAP_SHARED afin que toute écriture dans une telle région soit immédiatement répercutée sur le disque et visible par les autres processus. Si vous n’indiquez pas ce drapeau, Linux pourrait placer les données écrites dans un tampon avant de les transférer dans le fichier.

Une alternative est de forcer Linux à intégrer les changements effectués en mémoire dans le fichier en appelant msync. Ses deux premiers paramètres définissent une région de la mémoire mise en correspondance avec un fichier, comme pour munmap. Le troisième paramètre peut prendre les valeurs suivantes:

  • MS_ASYNC − La mise à jour est planifiée mais pas nécessairement exécutée avant la fin de la fonction.
  • MS_SYNC − La mise à jour est immédiate; l’appel à msync est bloquant jusqu’à ce qu’elle soit terminée. MS_SYNC et MS_ASYNC ne peuvent être utilisés simultanément.
  • MS_INVALIDATE − Toutes les autres mises en correspondance avec le fichier sont invalidées afin de prendre en compte les modifications.

Par exemple, pour purger un fichier partagé mis en correspondance à l’adresse mem_addr et d’une longueur de mem_length octets, effectuez cet appel:

msync (mem_addr, mem_length, MS_SYNC | MS_INVALIDATE);

Comme pour les segments de mémoire partagée, les utilisateurs de régions de mémoire mises en correspondance avec un fichier doivent établir et suivre un protocole afin d’éviter les conditions de concurrence critique. Par exemple, un sémaphore peut être utilisé pour éviter que plus d’un processus n’accède à la région de la mémoire en même temps. Vous pouvez également utiliser fcntl pour placer un verrou en lecture ou en écriture sur le fichier, comme le décrit la Section 8.3, « fcntl : Verrous et Autres Opérations sur les Fichiers », du Chapitre 8.

Mises en correspondance privées

Passer MAP_PRIVATE à mmap crée une région en mode copie à l’écriture. Toute écriture dans la région n’est répercutée que dans la mémoire du processus; les autres processus qui utilisent une mise en correspondance sur le même fichier ne voient pas les modifications. Au lieu d’écrire directement sur une page partagée par tous les processus, le processus écrit sur une copie privée de la page. Toutes les lectures et écritures ultérieures du processus utilisent cette page.

Autres utilisations de mmap

L’appel mmap peut être utilisé dans d’autres buts que la communication interprocessus. Une utilisation courante est de le substituer à read et write. Par exemple, plutôt que de charger explicitement le contenu d’un fichier en mémoire, un programme peut mettre le fichier en correspondance avec la mémoire et l’analyser via des lectures en mémoire. Pour certains programmes, cette façon de faire est plus pratique et peut également être plus rapide que des opérations d’entrées/sorties explicites sur le fichier.

Une technique puissante utilisée par certains programmes consiste à fabriquer des structures de données (des instances struct ordinaires, par exemple) dans un fichier mis en correspondance avec la mémoire. Lors d’une invocation ultérieure, le programme remet le fichier en correspondance avec la mémoire et les structures de données retrouvent leur état précédent. Notez cependant que les pointeurs de ces structures de données seront invalides à moins qu’ils ne pointent vers des adresses au sein de la même région de mémoire et que le fichier soit bien mis en correspondance à la même adresse mémoire qu’initialement.

Une autre technique utile est de mettre le fichier spécial /dev/zero en correspondance avec la mémoire. Ce fichier, décrit dans la Section 6.5.2, « /dev/zero », du Chapitre 6, « Périphériques », se comporte comme s’il était un fichier de taille infinie rempli d’octets à zéro. Un programme ayant besoin d’une source d’octets à zéro peut appeler mmap pour le fichier /dev/zero. Les écritures sur /dev/zero sont ignorées, donc la mémoire mise en correspondance peut être utilisée pour n’importe quelle opération. Les distributeurs de mémoire personnalisés utilisent souvent /dev/zero pour obtenir des portions de mémoire préinitialisées.

Tubes

Un tube est un dispositif de communication qui permet une communication à sens unique. Les données écrites sur l’« extrémité d’écriture » du tube sont lues depuis l’« extrémité de lecture ». Les tubes sont des dispositifs séquentiels; les données sont toujours lues dans l’ordre où elles ont été écrites. Typiquement, un tube est utilisé pour la communication entre deux threads d’un même processus ou entre processus père et fils.

Dans un shell, le symbole | crée un tube. Par exemple, cette commande provoque la création par le shell de deux processus fils, l’un pour ls et l’autre pour less:

% ls | less

Le shell crée également un tube connectant la sortie standard du processus ls avec l’entrée standard de less. Les noms des fichiers listés par ls sont envoyés à less dans le même ordre que s’ils étaient envoyés directement au terminal.

La capacité d’un tube est limitée. Si le processus écrivain écrit plus vite que la vitesse à laquelle le processus lecteur consomme les données, et si le tube ne peut pas contenir de données supplémentaires, le processus écrivain est bloqué jusqu’à ce qu’il y ait à nouveau de la place dans le tube. Si le lecteur essaie de lire mais qu’il n’y a plus de données disponibles, il est bloqué jusqu’à ce que ce ne soit plus le cas. Ainsi, le tube synchronise automatiquement les deux processus.

Créer des tubes

Pour créer un tube, appelez la fonction pipe. Passez lui un tableau de deux entiers. L’appel à pipe stocke le descripteur de fichier en lecture à l’indice zéro et le descripteur de fichier en écriture à l’indice un. Par exemple, examinons ce code:

int pipe_fds[2];
int read_fd;
int write_fd;
 
pipe (pipe_fds);
read_fd = pipe_fds[0];
write_fd = pipe_fds[1];

Les données écrites via le descripteur write_fd peuvent être relues via read_fd.

Communication entre processus père et fils

Un appel à pipe crée des descripteurs de fichiers qui ne sont valides qu’au sein du processus appelant et de ses fils. Les descripteurs de fichiers d’un processus ne peuvent être transmis à des processus qui ne lui sont pas liés; cependant, lorsqu’un processus appelle fork, les descripteurs de fichiers sont copiés dans le nouveau processus. Ainsi, les tubes ne peuvent connecter que des processus liés.

Dans le programme du listing !!pipe!!, un fork crée un nouveau processus fils. Le fils hérite des descripteurs de fichiers du tube. Le père écrit une chaîne dans le tube et le fils la lit. Le programme exemple convertit ces descripteurs de fichiers en flux FILE* en utilisant fdopen. Comme nous utilisons des flux plutôt que des descripteurs de fichiers, nous pouvons utiliser des fonctions d’entrées/sorties de la bibliothèque standard du C de plus haut niveau, comme printf et fgets.

Listing (pipe.c) Utiliser un tube pour communiquer avec un processus fils

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
 
/* Écrit COUNT fois  MESSAGE vers STREAM, avec une pause d'une seconde
   entre chaque. */
 
void writer (const char* message, int count, FILE* stream)
{
  for (; count > 0; −count) {
    /* Écrit le message vers le flux et purge immédiatement. */
    fprintf (stream, "%s\n", message);
    fflush (stream);
    /* S'arrête un instant. */
    sleep (1);
  }
}
 
/* Lit des chaînes aléatoires depuis le flux
   aussi longtemps que possible.  */
 
void reader (FILE* stream)
{
  char buffer[1024];
  /* Lit jusqu'à ce que l'on atteigne la fin du flux. fgets lit jusqu'à
     ce qu'une nouvelle ligne ou une fin de fichier survienne. */
  while (!feof (stream)
         && !ferror (stream)
         && fgets (buffer, sizeof (buffer), stream) != NULL)
    fputs (buffer, stdout);
}
 
int main ()
{
  int fds[2];
  pid_t pid;
 
  /* Crée un tube. Les descripteurs de fichiers pour les deux bouts du tube
     sont placés dans fds. */
  pipe (fds);
  /* Crée un processus fils. */
  pid = fork ();
  if (pid == (pid_t) 0) {
    FILE* stream;
    /* Nous sommes dans le processus fils. On ferme notre copie de
       l'extrémité en écriture du descripteur de fichiers. */
    close (fds[1]);
    /* Convertit le descripteur de fichier de lecture en objet FILE
       et lit à partir de celui-ci. */
    stream = fdopen (fds[0], "r");
    reader (stream);
    close (fds[0]);
  }
  else {
    /* Nous sommes dans le processus parent. */
    FILE* stream;
    /* Ferme notre copie de l'extrémité en lecture
       du descripteur de fichier. */
    close (fds[0]);
    /* Convertit le descripteur de fichier d'écriture en objet FILE
       et y écrit des données. */
    stream = fdopen (fds[1], "w");
    writer ("Coucou.", 5, stream);
    close (fds[1]);
  }
 
  return 0;
}

Au début de la fonction main, fds est déclaré comme étant un tableau de deux entiers. L’appel à pipe crée un tube et place les descripteurs en lecture et en écriture dans ce tableau. Le programme crée alors un processus fils. Après avoir fermé l’extrémité en lecture du tube, le processus père commence à écrire des chaînes dans le tube. Après avoir fermé l’extrémité en écriture du tube, le processus fils lit les chaînes depuis le tube.

Notez qu’après l’écriture dans la fonction writer, le père purge le tube en appelant fflush. Dans le cas contraire, les données pourraient ne pas être envoyées dans le tube immédiatement.

Lorsque vous invoquez la commande ls | less, deux divisions de processus ont lieu: une pour le processus fils ls et une pour le processus fils less. Ces deux processus héritent des descripteurs de fichier du tube afin qu’il puissent communiquer en l’utilisant. Pour faire communiquer des processus sans lien, utilisez plutôt des FIFO, comme le décrit la Section 5.4.5, « FIFO ».

Rediriger les flux d'entrée, de sortie et d'erreur standards

Souvent, vous aurez besoin de créer un processus fils et de définir l’extrémité d’un tube comme son entrée ou sa sortie standard. Grâce à la fonction dup2, vous pouvez substituer un descripteur de fichier à un autre. Par exemple, pour rediriger l’entrée standard vers un descripteur de fichier fd, utilisez ce code:

dup2 (fd, STDIN_FILENO);

La constante symbolique STDIN_FILENO représente le descripteur de fichier de l’entrée standard, qui a la valeur~0. L’appel ferme l’entrée standard puis la rouvre comme une copie de fd de façon à ce que les deux puissent être utilisés indifféremment. Les descripteurs de fichiers substitués partagent la même position dans le fichier et le même ensemble d’indicateurs de statut de fichier. Ainsi, les caractères lus à partir de fd ne sont pas relus à partir de l’entrée standard.

Le programme du listing !!dup2!! utilise dup2 pour envoyer des donnée d’un tube vers la commande sort2). Une fois le tube créé, le programme se divise. Le processus parent envoie quelques chaînes vers le tube. Le processus fils connecte le descripteur de fichier en lecture du tube à son entrée standard en utilisant dup2. Il exécute ensuite le programme sort.

Listing (dup2.c) Rediriger la sortie d’un tube avec dup2

#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
 
int main ()
{
  int fds[2];
  pid_t pid;
 
  /* Crée un tube. Les descripteurs de fichiers des deux extrémités
     du tube sont placées dans fds. */
  pipe (fds);
  /* Crée un processus fils. */
  pid = fork ();
  if (pid == (pid_t) 0) {
    /* Nous sommes dans le processus fils. On ferme notre copie du
       descripteur de fichier en écriture. */
    close (fds[1]);
    /* Connexion de l'extrémité en lecture à l'entrée standard. */
    dup2 (fds[0], STDIN_FILENO);
    /* Remplace le processus fils par le programme "sort". */
    execlp ("sort", "sort", 0);
  }
  else {
    /* Processus père. */
    FILE* stream;
    /* Ferme notre copie de l'extrémité en lecture du descripteur. */
    close (fds[0]);
    /* Convertit le descripteur de fichier en écriture en objet FILE, et
       y écrit. */
    stream = fdopen (fds[1], "w");
    fprintf (stream, "C'est un test.\n");
    fprintf (stream, "Coucou.\n");
    fprintf (stream, "Mon chien a des puces.\n");
    fprintf (stream, "Ce programme est excellent.\n");
    fprintf (stream, "Un poisson, deux poissons.\n");
    fflush (stream);
    close (fds[1]);
    /* Attend la fin du processus fils. */
    waitpid (pid, NULL, 0);
  }
 
  return 0;
}

popen et pclose

Une utilisation courante des tubes est d’envoyer ou de recevoir des données depuis un programme en cours d’exécution dans un sous-processus. Les fonctions popen et pclose facilitent cette pratique en éliminant le besoin d’appeler pipe, fork, dup2, exec et fdopen.

Comparez le listing !!popen!!, qui utilise popen et pclose, à l’exemple précédent (Listing !!dup2!!).

Listing (popen.c) Exemple d’utilisation de popen

#include <stdio.h>
#include <unistd.h>
 
int main ()
{
  FILE* stream = popen ("sort", "w");
  fprintf (stream, "C'est un test.\n");
  fprintf (stream, "Coucou.\n");
  fprintf (stream, "Mon chien a des puces.\n");
  fprintf (stream, "Ce programme est excellent.\n");
  fprintf (stream, "Un poisson, deux poissons.\n");
  return pclose (stream);
}

L’appel à popen crée un processus fils exécutant la commande sort, ce qui remplace les appels à pipe, fork, dup2 et execlp. Le second argument, “w”, indique que ce processus désire écrire au processus fils. La valeur de retour de popen est l’extrémité d’un tube; l’autre extrémité est connectée à l’entrée standard du processus fils. Une fois que le processus père a terminé d’écrire, pclose ferme le flux du processus fils, attend la fin du processus et renvoie son code de retour.

Le premier argument de popen est exécuté comme s’il s’agissait d’une commande shell, dans un processus exécutant /bin/sh. Le shell recherche les programmes à exécuter en utilisant la variable d’environnement PATH de la façon habituelle. Si le deuxième argument est “r”, la fonction renvoie le flux de sortie standard du processus fils afin que le père puisse lire la sortie. Si le second argument est “w”, la fonction renvoie le flux d’entrée standard du processus fils afin que le père puisse envoyer des données. Si une erreur survient, popen renvoie un pointeur nul.

Appelez pclose pour fermer un flux renvoyer par popen. Après avoir fermé le flux indiqué, pclose attend la fin du processus fils.

FIFO

Une file premier entré, premier sorti (first-in, first-out, FIFO) est un tube qui dispose d’un nom dans le système de fichiers. Tout processus peut ouvrir ou fermer la FIFO; les processus raccordés aux extrémités du tube n’ont pas à avoir de lien de parenté. Les FIFO sont également appelés canaux nommés.

Vous pouvez créer une FIFO via la commande mkfifo. Indiquez l’emplacement où elle doit être créée sur la ligne de commande. Par exemple, créez une FIFO dans /tmp/fifo en invoquant ces commandes:

% mkfifo /tmp/fifo
% ls -l /tmp/fifo
prw-rw-rw-    1 samuel users 0 Jan 16 14:04 /tmp/fifo

Le premier caractère affiché par ls est p ce qui indique que le fichier est en fait une FIFO (canal nommé, named pipe). Dans une fenêtre, lisez des données depuis la FIFO en invoquant cette commande:

% cat < /tmp/fifo

Dans une deuxième fenêtre, écrivez dans la FIFO en invoquant cela:

% cat > /tmp/fifo

Puis, saisissez du texte. À chaque fois que vous appuyez sur Entrée, la ligne de texte est envoyé dans la FIFO et apparaît dans la première fenêtre. Fermez la FIFO en appuyant sur Ctrl+D dans la seconde fenêtre. Supprimez la FIFO avec cette commande:

% rm /tmp/fifo

Créer une FIFO

Pour créer une FIFO par programmation, utilisez la fonction mkfifo. Le premier argument est l’emplacement où créer la FIFO; le second paramètre spécifie les permissions du propriétaire du tube, de son groupe et des autres utilisateurs, comme le décrit le Chapitre 10, « Sécurité », Section 10.3, « Permissions du Système de Fichiers ». Comme un tube doit avoir un lecteur et un écrivain, les permissions doivent comprendre des autorisations en lecture et en écriture. Si le tube ne peut pas être créé (par exemple, si un fichier possédant le même nom existe déjà), mkfifo renvoie -1. Incluez <sys/types.h> et <sys/stat.h> si vous appelez mkfifo.

Accéder à une FIFO

L’accès à une FIFO se fait de la même façon que pour un fichier ordinaire. Pour communiquer via une FIFO, un programme doit l’ouvrir en écriture. Il est possible d’utiliser des fonction d’E/S de bas niveau (open, write, read, close, etc. listées dans l’Appendice B, « E/S de Bas Niveau ») ou des fonctions d’E/S de la bibliothèque C (fopen, fprintf, fscanf, fclose, etc.).

Par exemple, pour écrire un tampon de données dans une FIFO en utilisant des routines de bas niveau, vous pourriez procéder comme suit:

int fd = open (fifo_path, O_WRONLY);
write (fd, data, data_length);
close (fd);

Pour lire une chaîne depuis la FIFO en utilisant les fonctions d’E/S de la bibliothèque C, vous pourriez utiliser ce code:

FILE* fifo = fopen (fifo_path, "r");
fscanf (fifo, "%s", buffer);
fclose (fifo);

Une FIFO peut avoir plusieurs lecteurs ou plusieurs écrivains. Les octets de chaque écrivain sont écrits de façon atomique pour une taille inférieure à PIPE_BUF (4Ko sous Linux). Les paquets d’écrivains en accès simultanés peuvent être entrelacés. Les mêmes règles s’appliquent à des lectures concurrentes.

Différences avec les canaux nommés Windows

Les tubes des systèmes d’exploitation Win32 sont très similaires aux tubes Linux (consultez la documentation de la bibliothèque Win32 pour plus de détails techniques). Les principales différences concernent les canaux nommés, qui, sous Win32, fonctionnent plus comme des sockets. Les canaux nommés Win32 peuvent connecter des processus d’ordinateurs distincts connectés via un réseau. Sous Linux, ce sont les sockets qui sont utilisés pour ce faire. De plus, Win32 permet de multiples connexions de lecteur à écrivain sur un même canal nommé sans que les données ne soient entrelacées et les tubes peuvent être utilisés pour une communication à double sens3).

Sockets

Un socket est un dispositif de communication bidirectionnel pouvant être utilisé pour communiquer avec un autre processus sur la même machine ou avec un processus s’exécutant sur d’autres machines. Les sockets sont la seule forme de communication interprocessus dont nous traiterons dans ce chapitre qui permet la communication entre processus de différentes machines. Les programmes Internet comme Telnet, rlogin, FTP, talk et le World Wide Web utilisent des sockets.

Par exemple, vous pouvez obtenir une page depuis un serveur Web en utilisant le programme Telnet car tous deux utilisent des sockets pour la communication via le réseau4). Pour ouvrir une connexion vers un serveur Web dont l’adresse est www.codesourcery.com, utilisez la commande telnet www.codesourcery.com 80. La constante magique 80 demande une connexion au serveur Web de www.codesourcery.com plutôt qu’à un autre processus. Essayez d’entrer GET / une fois la connexion établie. Cela envoie un message au serveur Web à travers le socket, celui-ci répond en envoyant la source HTML de la page d’accueil puis en fermant la connexion − par exemple:

% telnet www.codesourcery.com 80
Trying 206.168.99.1...
Connected to merlin.codesourcery.com (206.168.99.1).
Escape character is "^]".
GET /
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
...

Concepts relatifs aux sockets

Lorsque vous créez un socket, vous devez indiquer trois paramètres: le style de communication, l’espace de nommage et le protocole.

Un style de communication contrôle la façon dont le socket traite les données transmises et définit le nombre d’interlocuteurs. Lorsque des données sont envoyées via le socket, elles sont découpées en morceaux appelés paquets. Le style de communication détermine comment sont gérés ces paquets et comment ils sont envoyés de l’émetteur vers le destinataire.

  • Le style connexion garantit la remise de tous les paquets dans leur ordre d’émission. Si des paquets sont perdus ou mélangés à cause de problèmes dans le réseau, le destinataire demande automatiquement leur retransmission à l’émetteur.
    Un socket de type connexion ressemble à un appel téléphonique: les adresses de l’émetteur et du destinataire sont fixées au début de la communication lorsque la connexion est établie.
  • Le style datagramme ne garantit pas la remise ou l’ordre d’arrivée des paquets. Des paquets peuvent être perdus ou mélangés à cause de problèmes dans le réseau. Chaque paquet doit être associé à sa destination et il n’y a aucune garantie quant à sa remise. Le système ne garantit que le « meilleur effort » (best effort), des paquets peuvent donc être perdus ou être remis dans un ordre différent de leur émission.
    Un socket de style datagramme se comporte plus comme une lettre postale. L’émetteur spécifie l’adresse du récepteur pour chaque message.

L’espace de nommage d’un socket spécifie comment les adresses de socket sont écrites. Une adresse de socket identifie l’extrémité d’une connexion par socket. Par exemple, les adresses de socket dans « l’espace de nommage local » sont des noms de fichiers ordinaires. Dans « l’espace de nommage Internet », une adresse de socket est composée de l’adresse Internet (également appelée adresse IP) d’un hôte connecté au réseau et d’un numéro de port. Le numéro de port permet de faire la distinction entre plusieurs sockets sur le même hôte.

Un protocole spécifie comment les données sont transmises. Parmi ces protocoles, on peut citer TCP/IP; les deux protocoles principaux utilisés pour Internet, le protocole réseau AppleTalk; et le protocole de communication locale d’UNIX. Toutes les combinaisons de styles, d’espace de nommage et de protocoles ne sont pas supportées.

Appels système

Les sockets sont plus flexibles que les techniques de communication citées précédemment. Voici les appels systèmes utiles lors de l’utilisation de sockets:

  • socket − Crée un socket.
  • close − Détruit un socket.
  • connect − Crée une connexion entre deux sockets.
  • bind − Associe un socket serveur à une adresse.
  • listen − Configure un socket afin qu’il accepte les connexions.
  • accept − Accepte une connexion et crée un nouveau socket pour celle-ci.

Les sockets sont représentés par des descripteurs de fichiers.

Créer et détruire des sockets

Les fonctions socket et close créent et détruisent des sockets, respectivement. Lorsque vous créez un socket, spécifiez ses trois propriétés: espace de nommage, style de communication et protocole. Pour le paramètre d’espace de nommage, utilisez les constantes commençant par PF_ (abréviation de « protocol family », famille de protocoles). Par exemple, PF_LOCAL ou PF_UNIX spécifient l’espace de nommage local et PF_INET correspond à l’espace de nommage Internet. Pour le paramètre du style de communication, utilisez les constantes commençant par SOCK_. SOCK_STREAM demande un socket de style connexion, SOCK_DGRAM un socket de style datagramme.

Le troisième paramètre, le protocole, spécifie le mécanisme de bas niveau utilisé pour transmettre et recevoir des données. Chaque protocole est valide pour une combinaison d’espace de nommage et de type de communication particulière. Comme il y a souvent un protocole adapté pour chaque paire, indiquer 0 sélectionne généralement le bon protocole. Si l’appel à socket se déroule correctement, il renvoie un descripteur de fichier pour le socket. Vous pouvez lire ou écrire sur un socket en utilisant read, write, etc. comme avec les autres descripteurs de fichiers. Lorsque vous en avez fini avec le socket, appelez close pour le supprimer.

Appeler connect

Pour créer une connexion entre deux sockets, le client appelle connect, en lui passant l’adresse d’un socket serveur auquel se connecter. Un client est un processus initiant une connexion et un serveur est un processus en attente de connexions. Le client appelle connect pour initier une connexion depuis un socket local, dont le descripteur est passé en premier argument, vers le socket serveur spécifié par le deuxième argument. Le troisième argument est la longueur, en octets, de la structure d’adresse pointée par le second argument. Les formats d’adresse de sockets diffèrent selon l’espace de nommage du socket.

Envoyer des informations

Toutes les techniques valides pour écrire dans un descripteur de fichier le sont également pour écrire dans un socket. Reportez-vous à l’Appendice B pour une présentation des fonctions d’E/S de bas niveau Linux et de certains des problèmes ayant trait à leur utilisation. La fonction send, qui est spécifique aux descripteur de fichiers socket, est une alternative à write avec quelques choix supplémentaires; reportez-vous à sa page de manuel pour plus d’informations.

Serveurs

Le cycle de vie d’un serveur consiste à créer un socket de type connexion, le lier à une adresse, appeler listen pour permettre au socket d’accepter des connexions, appeler accept régulièrement pour accepter les connexions entrantes, puis fermer le socket. Les données ne sont pas lues et écrites directement via le socket serveur; au lieu de cela, chaque fois qu’un programme accepte une nouvelle connexion, Linux crée un socket séparé utilisé pour le transfert de données via cette connexion. Dans cette section, nous introduirons bind, listen et accept.

Une adresse doit être liée au socket serveur en utilisant bind afin que les clients puissent le trouver. Son premier argument est le descripteur de fichier du socket. Le second argument est un pointeur vers une structure d’adresse de socket; son format dépend de la famille d’adresse du socket. Le troisième argument est la longueur de la structure d’adresse en octets. Une fois qu’une adresse est liée à un socket de type connexion, il doit invoquer listen pour indiquer qu’il agit en tant que serveur. Son premier argument est le descripteur de fichier du socket. Le second spécifie combien de connexions peuvent être mises en file d’attente. Si la file est pleine, les connexions supplémentaires seront refusées. Cela ne limite pas le nombre total de connexions qu’un serveur peut gérer; cela limite simplement le nombre de clients tentant de se connecter qui n’ont pas encore été acceptés.

Un serveur accepte une demande de connexion d’un client en invoquant accept. Son premier argument est le descripteur de fichier du socket. Le second pointe vers une structure d’adresse de socket, qui sera renseignée avec l’adresse de socket du client. Le troisième argument est la longueur, en octets, de la structure d’adresse de socket. Le serveur peut utiliser l’adresse du client pour déterminer s’il désire réellement communiquer avec le client. L’appel à accept crée un nouveau socket pour communiquer avec le client et renvoie le descripteur de fichier correspondant. Le socket serveur original continue à accepter de nouvelles connexions de clients. Pour lire des données depuis un socket sans le supprimer de la file d’attente, utilisez recv. Cette fonction prend les mêmes arguments que read ainsi qu’un argument FLAGS supplémentaire. Passer la valeur MSG_PEEK permet de lire les données sans les supprimer de la file d’attente.

Sockets locaux

Les sockets mettant en relation des processus situés sur le même ordinateur peuvent utiliser l’espace de nommage local représenté par les constantes PF_LOCAL et PF_UNIX. Ils sont appelés sockets locaux ou sockets de domaine UNIX. Leur adresse de socket, un nom de fichier, n’est utilisée que lors de la création de connexions.

Le nom du socket est spécifié dans une struct sockaddr_un. Vous devez positionner le champ sun_family à AF_LOCAL, qui représente un espace de nommage local. Le champ sun_path spécifie le nom de fichier à utiliser et peut faire au plus 108 octets de long. La longueur réelle de la struct sockaddr_un doit être calculée en utilisant la macro SUN_LEN. Tout nom de fichier peut être utilisé, mais le processus doit avoir des autorisations d’écriture sur le répertoire, qui permettent l’ajout de fichiers. Pour se connecter à un socket, un processus doit avoir des droits en lecture sur le fichier. Même si différents ordinateurs peuvent partager le même système de fichier, seuls des processus s’exécutant sur le même ordinateur peuvent communiquer via les sockets de l’espace de nommage local.

Le seul protocole permis pour l’espace de nommage local est 0.

Comme il est stocké dans un système de fichiers, un socket local est affiché comme un fichier. Par exemple, remarquez le s du début:

% ls -l /tmp/socket
srwxrwx−x    1 user group 0 Nov 13 19:18 /tmp/socket

Appelez unlink pour supprimer un socket local lorsque vous ne l’utilisez plus.

Un exemple utilisant les sockets locaux

Nous allons illustrer l’utilisation des sockets au moyen de deux programmes. Le programme serveur, listing !!socketserver!!, crée un socket dans l’espace de nommage local et attend des connexions. Lorsqu’il reçoit une connexion, il lit des messages au format texte depuis celle-ci et les affiche jusqu’à ce qu’elle soit fermée. Si l’un de ces messages est « quit », le programme serveur supprime le socket et se termine. Le programme socket-server prend en argument de ligne de commande le chemin vers le socket.

Listing (socket-server.c) Serveur utilisant un socket local

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
 
/* Lit du texte depuis le socket et l'affiche. Continue jusqu'à ce que
   le socket soit fermé. Renvoie une valeur différente de zéro si le client
   a envoyé un message "quit", zéro sinon. */
 
int server (int client_socket)
{
  while (1) {
    int length;
    char* text;
 
    /* Commence par lire la longueur du message texte depuis le socket. 
       Si read renvoie zéro, le client a fermé la connexion. */
    if (read (client_socket, &length, sizeof (length)) == 0)
      return 0;
    /* Alloue un tampon pour contenir le texte. */
    text = (char*) malloc (length);
 
    /* Lit le texte et l'affiche. */
    read (client_socket, text, length);
    printf ("%s\n", text);
    /* Si le client a envoyé le message "quit", c'est fini.  */
    if (!strcmp (text, "quit")) {
      /* Libère le tampon. */
      free (text);
      return 1;
    }
    /* Libère le tampon. */
    free (text);
  }
}
 
int main (int argc, char* const argv[])
{
const char* const socket_name = argv[1];
int socket_fd;
struct sockaddr_un name;
int client_sent_quit_message;
 
/* Crée le socket. */
socket_fd = socket (PF_LOCAL, SOCK_STREAM, 0);
/* Indique qu'il s'agit d'un serveur. */
name.sun_family = AF_LOCAL;
strcpy (name.sun_path, socket_name);
bind (socket_fd, (struct sockaddr *) &name, SUN_LEN (&name));
/* Se met en attente de connexions. */
listen (socket_fd, 5);
 
/* Accepte les connexions de façon répétée, lance un server() pour traiter
   chaque client. Continue jusqu'à ce qu'un client
    envoie un message "quit". */
  do {
    struct sockaddr_un client_name;
    socklen_t client_name_len;
    int client_socket_fd;
 
    /* Accepte une connexion. */
    client_socket_fd = 
      accept (socket_fd, (struct sockaddr *) &client_name, &client_name_len);
    /* Traite la connexion. */
    client_sent_quit_message = server (client_socket_fd);
    /* Ferme notre extrémité. */
    close (client_socket_fd);
  }
  while (!client_sent_quit_message);
 
  /* Supprime le fichier socket. */
  close (socket_fd);
  unlink (socket_name);
 
  return 0;
}

Le programme client, listing !!socketclient!!, se connecte à un socket local et envoie un message. Le chemin vers le socket et le message sont indiqués sur la ligne de commande.

Listing (socket-client.c) Client utilisant un socket local

#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
 
/* Écrit TEXT vers le socket indiqué par le descripteur SOCKET_FD. */
 
void write_text (int socket_fd, const char* text)
{
  /* Écrit le nombre d'octets de la chaîne, y compris l'octet
     nul de fin. */
  int length = strlen (text) + 1;
  write (socket_fd, &length, sizeof (length));
  /* Écrit la chaîne. */
  write (socket_fd, text, length);
}
 
int main (int argc, char* const argv[])
{
  const char* const socket_name = argv[1];
  const char* const message = argv[2];
  int socket_fd;
  struct sockaddr_un name;
 
  /* Crée le socket. */
  socket_fd = socket (PF_LOCAL, SOCK_STREAM, 0);
  /* Stocke le nom du serveur dans l'adresse du socket. */
  name.sun_family = AF_LOCAL;
  strcpy (name.sun_path, socket_name);
  /* Se connecte au socket. */
  connect (socket_fd, (struct sockaddr*) &name, SUN_LEN (&name));
  /* écrit le texte de la ligne de commande vers le socket. */
  write_text (socket_fd, message);
  close (socket_fd);
  return 0;
}

Avant que le client n’envoie le message, il envoie sa longueur contenue dans la variable entière length. De même, le serveur lit la longueur du texte en lisant une variable entière depuis le socket. Cela permet au serveur d’allouer un tampon de taille adéquate pour contenir le message avant de le lire.

Pour tester cet exemple, démarrez le programme serveur dans une fenêtre. Indiquez le chemin vers le socket − par exemple, /tmp/socket.

% ./socket-server /tmp/socket

Dans une autre fenêtre, lancez plusieurs fois le client en spécifiant le même socket ainsi que des messages à envoyer au client:

% ./socket-client /tmp/socket "Coucou."
% ./socket-client /tmp/socket "Ceci est un test."

Le programme serveur reçoit et affiche les messages. Pour fermer le serveur, envoyez le message « quit » depuis un client:

% ./socket-client /tmp/socket "quit"

Le programme serveur se termine alors.

Sockets internet

Les sockets de domaine UNIX ne peuvent être utilisés que pour communiquer entre deux processus s’exécutant sur le même ordinateur. Les sockets Internet, par contre, peuvent être utilisés pour connecter des processus s’exécutant sur des machines distinctes connectées par un réseau.

Les sockets connectant des processus via Internet utilisent l’espace de nommage Internet représenté par PF_INET. Le protocole le plus courant est TCP/IP. L’Internet Protocol (IP), un protocole de bas niveau transporte des paquets sur Internet, en les fragmentant et les réassemblant si nécessaire. Il ne garantit que le « meilleur effort » de remise, des paquets peuvent donc disparaître ou être mélangés durant le transport. Tous les ordinateurs participants sont définis en utilisant une adresse IP unique. Le Transmission Control Protocol (TCP), qui s’appuie sur IP, fournit un transport fiable orienté connexion. Il permet d’établir des connexions semblables aux connexions téléphoniques entre des ordinateurs et assure que les données sont remises de façon fiable et dans l’ordre.

Noms DNS
Comme il est plus facile de se souvenir de noms que de numéros, le Domain Name Service (DNS, Service de Nom de Domaine) associe un nom comme www.codesourcery.com à l’adresse IP d’un ordinateur. Le DNS est implémenté par une hiérarchie mondiale de serveurs de noms, mais vous n’avez pas besoin de comprendre les protocoles employés par le DNS pour utiliser des noms d’hôtes Internet dans vos programmes.

Les adresses de sockets Internet comprennent deux parties: un numéro de machine et un numéro de port. Ces informations sont stockées dans une variable de type struct sockaddr_in. Positionnez le champ sin_family à AF_INET pour indiquer qu’il s’agit d’une adresse de l’espace de nommage Internet. Le champ sin_addr stocke l’adresse Internet de la machine cible sous forme d’une adresse IP entière sur 32 bits. Un numéro de port permet de faire la distinction entre plusieurs sockets d’une machine donnée. Comme des machines distinctes peuvent stocker les octets de valeurs multioctets dans des ordres différents, utilisez htons pour représenter le numéro de port dans l’ordre des octets défini par le réseau. Consultez la page de manuel de ip pour plus d’informations.

Pour convertir des noms d’hôtes, des adresses en notation pointée (comme 10.0.0.1) ou des noms DNS (comme www.codesourcery.com) adresse IP sur 32 bits, vous pouvez utiliser gethostbyname. Cette fonction renvoie un pointeur vers une structure struct hostent; le champ h_addr contient l’IP de l’hôte. Reportez-vous au programme exemple du Listing !!socketinet!!.

Le listing !!socketinet!! illustre l’utilisation de sockets de domaine Internet. Le programme rapatrie la page d’accueil du serveur Web dont le nom est passé sur la ligne de commande.

Listing (socket-inet.c) Lecture à partir d’un serveur WWW

#include <stdlib.h>
#include <stdio.h>
#include <netinet/in.h>
#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
 
/* Affiche le contenu de la page d'accueil du serveur correspondant
   au socket. */
 
void get_home_page (int socket_fd)
{
  char buffer[10000];
  ssize_t number_characters_read;
  /* Envoie la commande HTTP GET pour la page d'accueil. */
  sprintf (buffer, "GET /\n");
  write (socket_fd, buffer, strlen (buffer));
 
  /* Lit à partir du socket. L'appel à read peut ne pas renvoyer toutes
  les données en une seule fois, on continue de lire jusqu'à ce qu'il
  n'y ait plus rien. */
  while (1) {
    number_characters_read = read (socket_fd, buffer, 10000);
    if (number_characters_read == 0)
      return;
    /* Ecrit les données vers la sortie standard. */
    fwrite (buffer, sizeof (char), number_characters_read, stdout);
  }
}
 
int main (int argc, char* const argv[])
{
  int socket_fd;
  struct sockaddr_in name;
  struct hostent* hostinfo;
 
  /* Crée le socket. */
  socket_fd = socket (PF_INET, SOCK_STREAM, 0);
  /* Place le nom du serveur dans l'adresse du socket. */
  name.sin_family = AF_INET;
  /* Convertit la chaîne en adresse IP sur 32 bits. */
  hostinfo = gethostbyname (argv[1]);
  if (hostinfo == NULL)
    return 1;
  else
    name.sin_addr = *((struct in_addr *) hostinfo->h_addr);
  /* Les serveurs Web utilisent le port 80. */
  name.sin_port = htons (80);
 
  /* Se connecte au serveur Web. */
  if (connect (socket_fd, (struct sockaddr*) &name, 
                                   sizeof (struct sockaddr_in)) == -1) {
    perror ("connect");
    return 1;
  }
  /* Récupère la page d'accueil du serveur. */
  get_home_page (socket_fd);
  return 0;
}

Ce programme lit le nom du serveur Web à partir de la ligne de commande (pas l’URL − c’est-à-dire sans le « http:// »). Il appelle gethostbyname pour traduire le nom d’hôte en adresse IP numérique puis connecte un socket de type connexion (TCP) au port 80 de cet hôte. Les serveurs Web parlent l’Hypertext Transport Protocol (HTTP), donc le programme émet la commande HTTP GET et le serveur répond en envoyant le texte correspondant à la page d’accueil.

Numéros de ports standards
Par convention, les serveurs Web attendent des connexions sur le port 80. La plupart des services réseau Internet sont associés à un numéro de port standard. Par exemple, les serveurs Web sécurisés utilisant SSL attendent les connexions sur le port 443 et les serveurs de mail (qui parlent SMTP) utilisent le port 25.
Sur les systèmes GNU/Linux, les associations entre les noms des services, les protocoles et les numéros de port standards sont listés dans le fichier /etc/services. La première colonne est le nom du protocole ou du service. La seconde colonne indique le numéro de port et le type de connexion: tcp pour le mode orienté connexion et udp pour le mode datagramme.
Si vous implémentez des services réseau personnalisés utilisant des sockets Internet, utilisez des numéros de ports supérieurs à 1024.

Par exemple, pour rapatrier la page d’accueil du site Web www.codesourcery.com, invoquez la commande suivante:

% ./socket-inet www.codesourcery.com
<html>
  <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
...

Couples de sockets

Comme nous l’avons vu précédemment, la fonction pipe crée deux descripteurs de fichiers chacune étant l’extrémité d’un tube. Les tubes sont limités car les descripteurs de fichiers doivent être utilisés par des processus liés et car la communication est unidirectionnelle. La fonction socketpair crée deux descripteurs de fichiers pour deux sockets connectés sur le même ordinateur. Ces descripteurs de fichiers permettent une communication à double sens entre des processus liés. Ses trois premiers paramètres sont les mêmes que pour l’appel socket: ils indiquent le domaine, le type de connexion et le protocole. Le dernier paramètre est un tableau de deux entiers, qui sera renseigné avec les descripteurs de fichiers des deux sockets, comme pour pipe. Lorsque vous appelez socketpair, vous devez utiliser PF_LOCAL comme domaine.

1) Ces bits de permissions sont les mêmes que ceux utilisés pour les fichiers. Ils sont décrits dans la Section 10.3, « Permissions du Système de Fichiers ».
2) sort lit des lignes de texte depuis l’entrée standard, les trie par ordre alphabétique et les affiche sur la sortie standard.
3) Notez que seul Windows NT permet la création de canaux nommés; les programmes pour Windows 9x ne peuvent créer que des connexions client.
4) Habituellement, vous utilisez telnet pour vous connecter à un serveur Telnet pour une identification à distance. Mais vous pouvez également utiliser telnet pour vous connecter à un autre type de serveur et lui envoyer des commandes directement.
 
livre/chap5/communication_interprocessus.txt · Dernière modification: 2011/05/30 18:19 par jaaf
 
Recent changes RSS feed Creative Commons License Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki Hosted by TuxFamily