Communication inter processus par ipc sous *nix

Communication inter processus par IPC (System V) sous *NIX

Introduction

Ce petit tutoriel vous présente différentes méthodes proposées par les systèmes *NIX pour permettre une communication entre processus locaux. Ces mécanismes offrent de bien meilleures performances et de nombreux avantages par rapport aux mécanismes classiques. Ils ne sont pas encore implémentés dans le noyau Windows, qui lui a surtout pris de l'avance dans la variété des mécanismes de synchronisation inter processus. Mais qui sait ? Peut être que la prochaine mouture réserve quelque surprise.

Il n'est pas nécessaire d'être un expert pour lire ce tutoriel. Il faut cependant avoir quelques notions sur le système UNIX ou linux. Si des expressions comme structure de données, primitive (ou appel système), processus, espace d'adressage, système de fichier ou pointeur ne vous disent rien, il est préférable d'aller d'abord se renseigner sur ces notions avant d'entamer la lecture de ce document.

Les méthodes de communication les plus connues sont les fichiers, les tubes et les sockets. Ces méthodes font appel au système de fichier. Ce qui signifie que les mécanismes mis en oeuvre pour la communication par ces méthodes peuvent être lourds et lents (en terme système, pas en terme humain, oeuf corse !).

Les IPC, ou Inter Process Communication, apparus avec SystemV d'UNIX et repris dans linux, ne sont pas des fichiers mais des éléments mémoires gérés par le noyau, donc avec de meilleures performances de gestion. Cependant, on verra que la philosophie UNIX, « Tout est fichier » est respectée par des méthodes de rattachement de ces objets au système de fichier.

Il existe 3 sortes d'IPC :
- Les files de messages
- Les segments de mémoire partagés
- Les sémaphores

Ce tutoriel s'attachera sur les deux premiers types, car il traite principalement de la communication inter processus, tandis que les sémaphores sont plutôt réservés à des mécanismes comme la synchronisation des processus. Ils peuvent être utilisés comme mécanisme de communication mais ce n'est pas leur rôle principal.

Les caractéristiques communes des IPC

Les IPC sont gérées par le noyau par l'intermédiaire de tables. C'est-à-dire qu'il existe trois tables système IPC, une pour chaque type.

Du point de vue interne, chaque objet de type IPC est identifié au sein du système par un identificateur (positif ou nul), qui joue un rôle similaire aux identificateurs de fichier. Ainsi, un processus qui souhaite utiliser un des ces objets devra en connaître l'identificateur.

Du point de vue externe, les IPC sont identifiés par une clé, qui joue le rôle des références pour les fichiers. Le type key_t, défini dans sys/types.h, est le type utilisé pour manipuler ces clés numériques.

Le fichier contient des déclarations de structures, de constantes symboliques et de fonctions communes pour leur manipulation.

Les principales constantes symboliques sont des commandes utilisées avec les primitives de manipulation.

Les primitives de création (*get) utilisent :

IPC_PRIVATE : elle permet la création d'un IPC personnel qui ne pourra être connu que depuis la filiation du processus créateur.

IPC_CREAT : elle permet la création d'une IPC si celui-ci n'existe pas déjà. Sinon il est retrouvé.

IPC_EXCL : utilisée en conjonction avec IPC_CREAT, elle permet l'émission d'une erreur si l'IPC existe déjà.

IPC_ALLOC : elle permet de retrouver un IPC. Celui-ci doit déjà exister. Sinon, il y a émission d'une erreur.

Les primitives d'opération (semop, msgrcv, msgsnd) utilisent :

IPC_NOWAIT : elle permet de mettre en oeuvre des opérations non bloquantes.

Les primitives de controle (*ctl) utilisent :

IPC_RMID : elle permet la suppression d'une IPC.

IPC_STAT : elle permet d'obtenir des informations sur une IPC.

IPC_SET : elle permet de modifier les caractéristiques d'une IPC.

La structure ipc_perm regroupe des informations sur une IPC :

struct ipc_perm
{
  uid_t uid;                  /*Propriétaire actuel de l'IPC. */
  gid_t gid;                  /* Groupe du propriétaire de l'IPC. */
  uid_t cuid;                 /* Créateurde l'IPC. */
  gid_t cgid;                 /* Groupe du créateur de l'IPC. */
  mode_t mode;           /* Droits d'accès à l'IPC (ces notions sont restreintes à r et w) . */
  key_t key;                /* Clé de l'IPC*/
  unsigned short seq;     /* Compteur d'utilisation de l'entrée IPC*/
};

Enfin, la fonction key_t ftok (const char *path, int id);

Cette fonction permet de générer une clé unique à partir de la référence path et de l'entier id.

Les fonctions shell de manipulation des IPC

- ipcs : elle permet la consultation des tables d'IPC.(ipcs --help pour connaître ses options).

Il existe un autre moyen de consulter les IPC, en passant par le pseudo système de fichier /proc et son pseudo répertoire sysvipc. D'autres caractéristiques des IPC sont regroupées dans le pseudo répertoire/proc/sys/kernel (entre parenthèse, la valeur par défaut sur mon système).

Pour les files de messages (cat /proc/sys/kernel/msg*):

msgmni (16): Nombre maximum d'IPC actuellement autorisé.

msgmax (8192): Taille maximum d'un message.

msgmnb (16384): Taille maximum en octets d'une file de messages.

Pour les sémaphores (cat /proc/sys/kernel/sem) :

sem : regroupe toutes les limites pour les sémaphores avec dans l'ordre

semmsl (250) : Nombre maximum de sémaphores par groupe de sémaphores.
semmns (32000) : Nombre maximum de sémaphores autorisés dans le système.

semopm (32) : Nombre maximum d'opérations multisémaphore par un appel semop ().
semmni (128) : Nombre maximum d'entrées dans la table de sémaphores.

Pour les segments de mémoire partagés (cat/proc/sys/kernel/shm*) :

shmall (2 097 152) : Nombre maximum de pages partageable dans le système

shmmax (33 554 452) : Taille maximum d'un segment de mémoire partagé.

shmmni (4096) : Nombre maximum de segment que l'on peutcréer.

- ipcrm : elle permet la suppression d'une entrée dans une des tables d'IPC. Les arguments optionnels -q, -m et -s permettent de spécifier la catégorie de l'objet que l'on cherche. Seul le créateur ou le propriétaire de l'IPC ainsi que le super utilisateur peuvent demander sa suppression.

Les files de messages

Il s'agit en quelque sorte d'une implémentation du principe de la boîte aux lettres. Cependant, elles autorisent des mécanismes de multiplexage, c'est-à-dire la possibilité d'envoyer des messages à plusieurs processus dans la même file. Ce que ne permet pas, par exemple, une socket simple.

Le fichier contient les déclarations de structures et de constantes symboliques spécifiques dont la structure msqid_ds,qui est la structure d'une entrée dans la table des files de messages. On accède à une structure msqid_ds par l'intermédiaire de la primitive msgctl ().

struct msqid_ds
{
  struct ipc_perm msg_perm;    /* Permissions sur la file. */
  struct msg     *msg_first;        /* Pointeur vers le premier message de la file */
  struct msg     *msg_last;         /*Pointeur vers le dernier message de la file */
  msglen_t        msg_cbytes;     /* Nombre d'octets actuellement dans la file. */
  msgqnum_t    msg_qnum;       /*Nombre de messages actuellement dans la file. */
  msglen_t        msg_qbytes;     /* Nombre maximum d'octets autorisés dans la file. */
  pid_t               msg_lspid;       /*pid du dernier msgsnd (). */
  pid_t               msg_lrpid;       /*pid du dernier msgrcv (). */
  time_t             msg_stime;      /*Date du dernier msgsnd (). */
  time_t             msg_rtime;      /*Date du dernier msgrcv (). */
  time_t             msg_ctime;      /*Date du dernier changement msgctl (). */
}

La structure msg n'est pas en principe utilisée par les applications utilisateur :

struct msg
{
  struct msg *msg_next ;             /*Pointeur sur le message suivant */
  long msg_type ;                       /*Type du message */
  ushort msg_ts ;             /*Taille du message (hors msg_type) */
  short msg_spot ;                      /*Adresse du texte du message */
}

Enfin la structure générique d'un message :

struct msg_buf
{
  long mtype;      /*type du message */
  char mtext[1] ; /* Texte du message */
}

Cette structure n'est pas directement utilisable. Il faut en général définir une structure spécifique à l'application basée sur ce modèle.Le champ mtype est obligatoire et doit être un entier strictement positif. C'est ce type qui permet à un processus de choisir une catégorie de message dans la file et donc le multiplexage. La suite de la structure peut être n'importe quoi, à la seule condition que ce soit des objets contigus (pas de pointeurs).

Les quatre primitives de manipulations sont :

int msgget (key_t key, int msgflg);

int msgctl (int msqid, int cmd, structmsqid_ds *buf);

ssize_tmsgrcv (int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

int msgsnd (int msqid, const void *msgp,size_t msgsz, int msgflg);

- La primitive msgget () permet de créer ou de retrouver une file de message à partir de sa clé. Le champ msgflg est une combinaison des constantes symboliques avec les droits d'accès.

- La primitive msgctl () permet d'accéder ou de modifier les caractéristiques d'une entrée de la table, ou de supprimer une entrée si cmd est égal à IPC_RMID. L'entrée est identifiée par son msqid.

L'option IPC_STAT permet de ramener l'entête de la file, dans laquelle le champ msg_perm.mode permet de savoir si des lecteurs ou des écrivains sont bloqués :

-> MSG_RWAIT : des lecteurs sont en attente.

-> MSG_WWAIT : des écrivains sont en attente.

L'option IPC_SET permet de modifier les caractéristiques de l'entrée.

-> Pour modifier msg_perm.uid, msg_perm.gid ou msg_perm.mode, il faut un euid identique à msg_perm_cuid ou msg_perm_uid ou SU.

-> Pour modifier msg_perm.qbytes, il faut un euid identique à SU.

L'option IPC_RMID permet de supprimer une file de message. Il faut cependant avoir un euid identique à SU.

- La primitive msgsnd () permet à un processus d'envoyer un message dans la file.

Si msgflg contient IPC_NOWAIT, l'appel est non bloquant, alors que par défaut cet appel est bloquant en mode interruptible. Une autre solution pour éviter le mode bloquant est d'utiliser un système de délai (timeout).

Exemple :

MyAlarm (int sig) { return ; /* Juste pour provoquer la sortie du mode bloquant */ }

MyWriter () {
    alarm(3) ; /* délai d'attente de 3 secondes */
    ret =msgsnd(..., 0) ; /* 0 n'est pas IPC_NOWAIT */
    if (ret != 0 ) {
        switch(errno) {
            case EINTR : /* timeout réalisé */
                ...
        }
    }
}

- La primitive msgrcv () permet de retirer un message de la file en fonction de son type. Comme pour msgsnd (), IPC_NOWAIT rend l'appel non bloquant.

msgsz spécifie la taille attendue du message. Si msgflg contient MSG_NOERROR, alors l'appel ne génère pas d'erreur si le message retiré est de taille supérieur à la taille demandée, il est alors tronqué, et la partie non lue est perdue. Dans le cas où MSG_NOERROR n'est pas utilisé, le message n'est pas lu, il reste dans la file et E2BIG est retourné dans errno.

msgtyp :

Si il vaut0, alors le premier message est lu.

Si il estsupérieur à 0, alors le premier message de type msgtype est lu

Si il estinférieur à 0, alors le premier message dont le type est inférieur ou égal à |msgtyp| est lu.

Les segments de mémoire partagés

Un soucis des mécanismes de communication classique (fichiers, tubes, etc.), lorsqu'on échange de grande quantités de données est qu'ils nécessitent le transfert des données depuis l'espace d'adressage du processus émetteur vers l'espace noyau, puis de l'espace noyau vers l'espace d'adressage du processus destinataire, voir même par le système de fichier. Perte de temps. L'un des avantages des segments de mémoire partagés est que les processus vont partager des pages mémoires directement par l'intermédiaire de leur espace d'adressage. Gain de temps. Cependant, l'utilisation de ces espaces partagés peut entraîner des effets de bords si les processus ne se synchronisent pas (signal, sémaphore, etc.). D'autre part, un segment a une existence indépendante des processus. Ainsi si tout les processus utilisant un segment se terminent, un autre processus, connaissant la clé du segment pourra demander l'attachement (mapping) du segment dans son espace d'adressage et l'utiliser à son tour.

Le fichier sys/shm.h contient les déclarations de structures et de constantes symboliques spécifiques dont la structure shmid_ds, qui est la structure d'une entrée dans la table des segments de mémoire partagés. On accède à une structure shmid _ds par l'intermédiaire de la primitive shmctl().

struct shmid_ds
{
  struct ipc_perm    shm_perm; /*Permissions sur le segment. */
  size_t             shm_segsz;                  /* Taille du segment en octets. */
  pid_t              shm_lpid;                     /* pid de la dernière opération. */
  pid_t              shm_cpid;                    /* pid du créateur. */
  shmatt_t           shm_nattch;               /* Nombre d'attachements. */
  timestruc_t        shm_atim;                 /* Date du dernier attachement shmat (). */
  timestruc_t        shm_dtim;                 /* Date du dernier détachement shmdt (). */
  timestruc_t        shm_ctim;                 /* Date du dernier chagement shmctl (). */
}

Les quatre primitives de manipulations sont :

int shmget (key_t key, size_t size, int shmflg);

void *shmat(int shmid, const void *shmaddr, int shmflg);

int shmdt (const void *shmaddr);

int shmctl (int shmid, int cmd, struct shmid_ds*buf);

- Le fonctionnement de shmget est identique à celui de msgget (). Le paramètre size spécifie la taille du segment.

L'option IPC_PRIVATE permet de créer un IPC qui ne sera partagé que par la filiation du processus créateur (les enfants n'auront pas besoin de faire un ftok () pour retrouver cet IPC).

- La primitive shmat () est la primitive d'attachement (mapping) ou d'association du segment identifié par shmid à l'adresse shmadr de l'espace d'adressage du processus demandeur. Après un appel réussi à cette primitive, le processus pourra lire ou écrire dans le segment de la même manière qu'à n'importe quel endroit de son espace d'adressage. La valeur de retour est l'adresse effective où l'attachement a été réalisé (l'adresse du premier octet utilisable) ou -1 en cas d'échec. Le paramètre shmadr doit respecter certaines règles :

-> Ne pas entrer en conflit, immédiatement ou plus tard (du fait de l'augmentation de la pile ou du déplacement du point de rupture), avec des adresses déjà utilisées ou susceptible de l'être.

-> Ne pas violer la forme générale d'une adresse imposée par le système : les segments commencent à des limites de pages et ont des adresses alignées.

Si shmadr est NULL, alors l'adresse est choisie par le système. C'est la solution qui garanti la meilleure portabilité. Si shmadr est non NULL, alors l'adresse est choisie par l'utilisateur. Le système se chargera d'ajuster cette valeur si nécessaire.

L'option SHM_RND permet l'attachement à l'adresse la plus proche dont la valeur est multiple de la constante système SHMLBA.

L'option SHM_RDONLY permet l'attachement d'un segment en lecture seule. Un signal SIGSEGV sera levé si le processus tente une écriture dans le segment ainsi attaché.

Il existe également deux autres options SHM_LOCK et SHM_UNLOCK, utilisable uniquement par le super utilisateur qui empêche ou autorise le segment d'être swappé.

Un shmat () n'est pas utile lors d'un fork () car les IPC sont hérités.

Un segment de mémoire partagé est toujours initialisé à 0.

- La primitive shmdt () permet le détachement d'un segment de mémoire qui a été attaché par un appel à shmat (). Comme dit plus haut, le détachement d'un segment de tous les processus ne provoque pas sa destruction.

- La primitive shmctl () permet de réaliser différentes opérations de contrôle sur un segment. Les valeurs possibles pour cmd sont :

IPC_RMID : suppression du segment identifié par shmid. Cette destruction est différée si le segment est encore attaché à d'autres processus.

IPC_STAT : demande d'information sur le segment par le remplissage de la structure pointée par buf.

IPC_SET : demande de modification des caractéristiques de l'entrée identifiée par shmid avec les informations contenues dans la structure pointée par buf. Les seuls champs modifiables sont shm_perm.uid, shm_perm.gid et shm_perm.mode.

Après l'attachement d'un segment de mémoire partagé, l'adresse retourné par shmat () est un pointeur vers le premier octet du segment. Cependant plusieurs demandes d'attachement par plusieurs processus sur le même segment ne retournent pas la même adresse, puisque les espaces d'adressage des processus sont différents et même dans le cas d'un attachement multiple sur le même processus. Par conséquent lors de l'utilisation d'une structure chaînée (comme une liste simple ou double) dans un segment, le chaînage doit être fait en adressage relatif (par rapport au début du segment ou les éléments chaînés les uns par rapport aux autres) et non en adressage absolu. Les autres types de structures de données s'utilisent de manière classique.

Conclusion

Voilà donc les mécanismes de communication inter processus proposés par les implémentations conformes à SYSTEM V. J'ai passé sous silence les sémaphores, parce que le texte est suffisamment long et ceux qui sont intéressés par cet aspect du système feront comme moi, ils se renseigneront par eux-mêmes et d'autre part, le mécanisme de communication par sémaphore est plutôt rudimentaire. Les sémaphores, proposés au départ par Dijkstra, servent plus à des mécanismes comme la synchronisation de processus ou le partage de ressources par exclusion mutuelle des processus qu'à la communication de données. Ce texte ne prétend pas être exhaustif et est loin d'être complet sur le sujet.

Pour illustrer ce texte, j'ai réalisé, pour comprendre moi aussi, un programme, présentant l'utilisation des files de messages et des segments de mémoire partagés. Il est disponible dans la partie « Sources » de CS.

Vos commentaires et critiques sont les bienvenues, dans la mesure où elles sont constructives.

Ce document intitulé « Communication inter processus par ipc sous *nix » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous