Outils personnels
Vous êtes ici : Accueil C & C++ Programmation système Les IPC système V

Les IPC système V

Par Eric Salice - Dernière modification 18/04/2008 22:40
Contributeurs : Benjamin Poulain (Ikipou)
GPL

Les IPC sont les mécanismes de communication inter-processus. Les IPC présenté ici sont les file de message, la mémoire partagée, et les sémaphores.

Les IPC (Inter Process Communication) system V sont des mécanismes de communication/synchronisation inter-processus qui ne sont pas identifiés par des descripteurs de fichiers et ne peuvent donc se manipuler en utilisant les appels système relatifs aux fichiers ou les fonctions de la bibliothèque standard d'E/S.

Il existe 3 types d'IPC system V:

  • Les files de messages qui permettent à des processus de s'échanger des données en permettant de faire coïncider une lecture à une écriture contrairement aux tubes.
  • La mémoire partagée qui permet à plusieurs processus de partager une zone mémoire.
  • Les sémaphores qui offrent un mécanisme de synchronisation entre plusieurs processus pour gérer, par exemple, l'accès concurrent à un segment de mémoire partagée.

Lors de chaque création d'un IPC, le noyau lui associe un identificateur (un numéro) commun à tous les processus utilisant l'IPC. On ne peut pas connaître a priori cet identificateur et, de plus, lors de la suppression de l'IPC cet identificateur pourra être réattribué lors d'une création d'IPC ultérieure.

Les clés

Seul un descendant d'un processus qui a créé l'IPC peut hériter directement de cet identificateur. Pour qu'un processus qui ne descend pas du créateur puisse communiquer via l'IPC, il faudrait donc que le processus créateur lui transmette l'identificateur par un autre canal de communication (fichier, tube, ...) ce qui n'est pas très pratique.

Le système des IPC prévoit donc un autre mécanisme pour identifier une IPC de façon indépendante de l'identificateur utilisé par le noyau et permettre à plusieurs processus de l'utiliser: les clés.

Une clé est un objet du type keyt_t défini dans <sys/ipc>, il s'agit en général d'un entier. Les concepteurs d'applications utilisant des IPC pour communiquer peuvent choisir arbitrairement des valeurs pour les clés mais il faut s'assurer que d'autres applications utilisant des IPC n'utilisent
pas les mêmes valeurs de clé.

Il existe une fonction permettant de générer une clé à partir d'une référence à un fichier et d'un entier:

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok (char * pathname, int proj);

Les concepteurs d'applications utiliseront souvent le chemin du programme principal de l'application comme valeur de pathname. L'entier proj servira à différencier plusieurs IPC d'une même application.

La fonction ftok() peut fournir la même valeur de clé pour des valeurs de pathname et proj différentes. Ce risque est toutefois très réduit.

Des IPC de type différent (file de message, sémaphore ou segment de mémoire partagée) peuvent avoir la même clé et le même identificateur.

Sous Linux, il existe un appel système unique permettant de manipuler les IPC: l'appel ipc() qui ne sera pas détaillé ici. Les fonctions de la librairie C relatives aux IPC utilisent cet appel système.

Les files de messages

Une file de messages est une liste de blocs de données (les messages) gérée comme une file. Un processus qui utilise la file peut ajouter un message à la fin de la file ou extraire un message de la file. L'extraction ne se fait pas nécessairement au début. Les messages sont en effet dotés d'un type (une valeur entière) et un processus peut demander de n'extraire que les messages d'un type donné ou d'un type inférieur à un type donné et ceci par ordre croissant de type.

Les messages d'une file sont stockés dans des zones mémoire du noyau. Le nombre de file dans le système est limité par la constante MSGMNI. La taille d'une file est limitée par la constante MSGMNB. La taille d'un message est limitée par la constante MSGMAX. Ces constantes sont définies dans
<sys/msg.h>.

Obtenir la file à partir d'une clé

L'obtention de l'identificateur d'une file de message existante assoiée à une clé (où la création d'une telle file de message) s'effectue par la fonction

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
int msgget(key_tkey,int msgflg);

key est la clé. On peut utiliser la valeur IPC_PRIVATE (clé de valeur nulle) pour créer une nouvelle file sans utiliser de clé. Le système génèrera la clé à la création de la file. Ceci est surtout pratique quand on ne veut utiliser la file que dans le processus où elle est créée et ses descendants.

Le paramètre msgflg permet d'indiquer s'il faut créer la file si elle n'existe pas ainsi que les droits d'accès à la file s'il y a création. Pour créer la file, il faut faire un ou (|) entre la constante IPC_CREAT et une valeur entière représentant les droits d'accès. Seuls les 9 bits de poids faible de cette valeur peuvent être non nuls. Il représentent les permissions en lecture/écriture/exécution (non utilisé) de la file pour le créateur, le groupe du créateur et les autre utilisateurs de manière analogue aux permissions sur les fichiers.

En général, on utilise un nombre de 3 chiffres octaux. Si on utilise IPC_CREAT et que la file existe déjà, l'identificateur de cette file est retourné mais on peut faire un ou (|) avec la constante IPC_EXCL pour que la fonction retourne plutôt une erreur.

Cette fonction retourne l'identificateur de la file (existante ou créée) qui est un entier positif et -1 en cas d'erreur.

Ajouter un message sur la file

L'envoi d'un message s'effectue via la fonction

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd (int msqid, const void *msgp, size_t msgsz, int msgflg);

msqid est l'identificateur de la file de message.

msgp est l'adresse du premier octet d'une zone mémoire contenant le message.

msgz est sa taille.

msgflg peut valoir 0 auquel cas l'appel est bloquant (s'il n'y a pas de place dans la file) ou IPC_NOWAIT et l'appel est non bloquant et retourne une erreur s'il n'y a pas de place dans la file.

Les premiers octets de la zone contenant le message sont interprétés comme un entier long et indiquent le type du message. Cet entier doit être positif et ses octets ne doivent pas être pris en compte dans l'indication de la taille (msgz).

Si l'appel réussit, la valeur retournée est 0, en cas d'échec -1 (par exemple en cas d'accès en écriture interdit).

En général, le pointeur msgp pointera sur une structure de la forme:

struct msg {
long type;
...//Données du message
};

Attention, s'il y a des pointeurs dans les données du message, ce sont les valeurs des pointeurs qui feront partie du message et pas les objets pointés. Les pointeurs n'auront dès lors aucun sens dans un autre processus, car l'espace d'adressage y est différent.

La zone mémoire contenant le message sera recopiée dans une zone mémoire du noyau.

Lire d'un message sur la file

La lecture d'un message dans la file s'effectue par la fonction

#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

msqid est l'identificateur de la file.

msgp est l'adresse d'une zone mémoire destinée à accueillir le message. Les premiers octets de cette zone seront remplis par un entier long correspondant au type du message.

msgsz est la taille de la zone pointée par msgp.

msgtyp indique le type de message à lire:

  • 0: premier message de la file.
  • > 0: premier message de ce type.
  • < 0: le premier message du plus petit type inférieur ou égal à la valeur absolue de msgtyp.

msgflg peut-être 0 où une combinaison à l'aide d'un ou binaire des valeurs

  • IPC_NOWAIT: appel non bloquant s'il n'y a pas de message du type demandé et la fonction retourne un erreur.
  • MSG_NOERROR: si le message est plus grand que msgsz, le message est tronqué à cette taille.

La fonction retourne le nombre d'octets écrits dans la zone pointée par msgp ou -1 en cas d'erreur.

Configurer la file

La fonction

#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

permet de récupérer ou modifier la configuration de la file.

msqid est l'identificateur de la file de message.

cmd peut prendre les valeurs suivantes:

  • IPC_STAT: la structure buf sera remplie avec la configuration de la file.
  • IPC_SET: certains paramètres de la configuration de la file seront remplacés par les valeurs de champs de la structure buf.
  • IPC_RMID: supprime la file.

buf est une structure destinée à recevoir ou à fixer les paramètres de la file.

La structure msqid_ds contient (entre autres) les champs suivant

struct msqid_ds{
struct ipc_perm msg_perm;
unsigned short msg_cbytes;//Taille actuelle en octets.
unsigned short msg_qnum;//Nbre de messages.
unsigned short msg_qbytes; //Taille maximum en octets.
unsigned short msg_lspid;//PID du dernier émetteur.
unsigned short msg_lrpid;//PID du dernier lecteur.
time_t msg_stime;//Moment du dernier envoi.
time_t msg_rtime;//Moment de la dernière lecture.
...
};

La structure struct ipc_perm contient les champs suivants:

unsigned short uid;//UID du propriétaire.
unsigned short gid; //GID du groupe propriétaire
unsigned short cuid;//UID du créateur.
unsigned short cgid;//UID du groupe créateur.
unsigned short mode;//permissions (3 X 3 bits).
key_tkey; // clé.

msgctl() retourne 0 en cas de succès, -1 en cas d'erreur.

Remarques:

  • Seul le propriétaire ou le créateur peut changer la configuration de la file.
  • Avec IPC_SET comme valeur de cmd, on ne peut fixer que les valeurs des champs msg_qbyte (qui doit rester inférieur à MSGMNB), uid, gid, mode de msg_perm.
  • Lors de la suppression, les processus bloqués en attente d'une lecture/écriture sont débloqués et retournent une erreur.

Attention, si une file est supprimée alors qu'il existe encore des processus l'utilisant et que ceux-ci ne sont pas bloqués dans une opération de lecture/écriture, si une file de même identificateur est recréée avant que ces processus n'aient lu/écrit, ils ne se rendront pas compte de la suppression de la file.

La commande ipcs -q permet d'afficher des informations sur les files de messages présentes sur le système. Ces informations sont:

  • la clé
  • l'identificateur
  • le propriétaire
  • les droits d'accès
  • la taille en octets
  • le nombres de messages dans la file

Exemple

Fichier client_serveur.h:

#include <stdlib.h> 
#include <unistd.h>

#define FICHIER "serveur"
#define PROJET 1

typedef struct
{
long type;
int a, b;
pid_t pid;
} question_t;
typedef struct
{
long type;
int res;
} reponse_t;

#endif

Le serveur:

#include "client_serveur.h"

int msqid;

void handler(int sig)
{
msgctl(msqid, IPC_RMID, NULL);
exit(EXIT_SUCCESS);
}

int main()
{
key_t key;
question_t question;
reponse_t reponse;

key = ftok(FICHIER, PROJET);
if (key == -1){
perror("Problème pour obtenir la clé");
exit(EXIT_FAILURE);
}

msqid = msgget(key, 0666 | IPC_CREAT | IPC_EXCL);
if (msqid == -1) {
perror("Problème pour obtenir la file");
exit(1);
}

signal(SIGTERM, handler);

while(1){
msgrcv(msqid, &question, sizeof(question_t)-sizeof(long), 1, 0);
reponse.type = question.pid;
reponse.res = question.a + question.b;
msgsnd(msqid, &reponse, sizeof(reponse_t)-sizeof(long), 0);
}

return EXIT_SUCCESS;
}

Le client:

#include <time.h>
#include "client_serveur.h"

int main()
{
srand(time(NULL));

key_t key;
pid_t moi;
question_t question;
reponse_t reponse;
int msqid;

key = ftok(FICHIER, PROJET);
if (key == -1){
perror("Problème pour obtenir la clé");
exit(EXIT_FAILURE);
}

msqid = msgget(key, 0);
if (msqid == -1){
perror("Problème pour obtenir la file");
}

moi = getpid();
question.type = 1;
question.pid = moi;

int i;
for(i=0; i<10; i++){
question.a = rand() % 100;
question.b = rand() % 100;
msgsnd(msqid, &question, sizeof(question_t)-sizeof(long), 0);
msgrcv(msqid, &reponse, sizeof(reponse_t)-sizeof(long), moi, 0);
printf("%d + %d = %d\n", question.a, question.b, reponse.res);
}

return EXIT_SUCCESS;
}

Le fichier "serveur" doit exister pour pouvoir déterminer la clé.

L'exécution du programme peut donner ceci:

$ ./serveur &
[1] 6036
$ ./clie
7 + 49 = 56
73 + 58 = 131
30 + 72 = 102
44 + 78 = 122
23 + 9 = 32
40 + 65 = 105
92 + 42 = 134
87 + 3 = 90
27 + 29 = 56
40 + 12 = 52

La mémoire partagée

Le mécanisme de la mémoire partagée permet à plusieurs processus d'accéder directement à une même zone de mémoire physique. Il n'y a pas d'appel système pour écrire/lire dans une zone de mémoire partagée car cette zone fait partie de l'espace d'adressage des processus qui l'utilisent. Ce mécanisme est donc très performant (car on ne doit pas passer par le noyau pour chaque communication entre processus, on évite ainsi des context switch).

Identifier la mémoire partagée à partir de la clé

L'obtention de l'identificateur d'une zone de mémoire partagée à partir d'une clé s'effectue par la fonction

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

Les paramètres key et shmflg sont équivalents aux deux paramètres de msgget().

Le paramètre size est la taille désirée de la zone. Pour la création d'une zone, cette taille doit être comprise entre les valeurs SHMMIN et SHMMAX (<sys/shm.h>). La taille de la zone doit toujours être un multiple de la taille d'une page (PAGE_SIZE), la taille size demandée sera arrondie au multiple de PAGE_SIZE supérieure, au besoin. Si la zone existe déjà, on doit indiquer une valeur inférieure ou égale à sa taille (on utilisera en général 0).

La fonction retourne l'identificateur de la zone mémoire ou -1 en cas d'échec.

Attacher la mémoire partagé au processus

Une fois un identificateur obtenu, on doit l'attacher à l'espace mémoire du processus. Ceci est réalisé par la fonction

#include<sys/types.h>
#include<sys/shm.h>
void *shmat(int shmid,const void*shmaddr, int shmflg);

shmid est l'identificateur de la zone mémoire.

shmaddr est l'adresse où l'on veut placer la zone. On utilisera en général NULL pour laisser le système choisir cette adresse.

shmflg est 0 ou un ou binaire entre différentes valeurs dont SHM_RDONLY pour attacher la zone en lecture seule.

La fonction retourne l'adresse du premier octet de la zone ou -1 en cas d'erreur.

Détacher la mémoire partagé du processus

Pour "détacher" une zone de mémoire quand on n'en a plus l'usage, on utilise la fonction

#include<sys/types.h>
#include <sys/shm.h>
int shmdt (const void *shmaddr);

Cette fonction prend comme paramètre l'adresse d'attachement.

Elle retourne 0 si elle réussit et -1 en cas d'échec.

Les fonctions exec() et exit() détachent les zones mémoire utilisée par les processus.

Configurer la zone de mémoire partagée

La fonction

#include<sys/ipc.h>
#include<sys/shm.h>
int shmctl(int shmid,int cmd, struct shmid_ds *buf);

Permet de consulter ou modifier les paramètres de la la mémoire partagée. Les valeurs de cmd sont les mêmes que pour msgctl. La structure shmid_ds possède entre autres les champs suivants

struct shmid_ds {
struct ipc_perm shm_perm; //Droits d'accès
int shm_segsz; //Taille
time_t shm_atime; //Moment du dernier attachement
time_t shm_dtime; //Moment du dernier détachement
time_t shm_ctime; //Moment du dernier changement
unsigned short shm_cpid; //PID du dernier attaché
unsigned short shm_lpid; //PID du dernier detaché
short shm_nattch; //Nombre d'attachement.
...
};
La suppression du segment en utilisant la valeur IPC_RMID pour le paramètre commande ne supprime pas directement la zone de mémoire mais postpose sa suppression à son détachement par le dernier processus l'utilisant. Aucun processus ne pourra toutefois plus attacher la zone

de mémoire.

La principale difficulté avec les zones mémoire partagées est l'accès concurrent par plusieurs processus. Ce problème sera réglé par l'utilisation de sémaphores (voir plus loin).

La commande ipcs -m permet de visualiser les zones de mémoire partagée présentes sur le système. Elle indique la clé, l'identificateur, le propriétaire, les droits d'accès, la taille, le nombre d'attachements des zones de mémoire partagée.

Exemple

Un programme écrit les entiers de 0 à 9 dans une zone de mémoire partagée. Un autre programme les lit et les affiche. La clé est générée à partir du fichier du premier programme (passé comme paramètre au second).

Programme 1:

#include <stdio.h> 
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#define NB_ENTIERS 10

int main(int argc, char **argv)
{
key_t cle;
int id_zone;
int *tab;
cle = ftok (argv[0], 1);
id_zone = shmget(cle, NB_ENTIERS * sizeof(int), IPC_CREAT | 0666);
tab = (int *)shmat(id_zone, NULL, 0);

int i;
for ( i = 0; i < NB_ENTIERS; i++ )
tab[i] = i;
shmdt(tab);

return EXIT_SUCCESS;
}

Programme 2:

#include <stdio.h> 
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define NB_ENTIERS 10


int main(int argc, char **argv)
{
key_t cle;
int id_zone;
int *tab;
int i;
cle = ftok (argv[1], 1);
id_zone = shmget(cle, NB_ENTIERS * sizeof(int), 0);
tab = (int *)shmat(id_zone, NULL, 0);
for ( i = 0; i < NB_ENTIERS; i++ )
printf("%d\n", tab[i]);
shmdt(tab);
shmctl(id_zone, IPC_RMID, 0);

return EXIT_SUCCESS;
}

L'exécution des deux programmes donne ceci:

$ ./shm1 & 
[1] 6071
$ ./shm2 shm1
0
1
2
3
4
5
6
7
8
9

Les sémaphores

Les sémaphores des IPC système V s'apparentent aux sémaphore Posix utilisés avec les threads. Il s'agit de compteurs auxquels ont peut ajouter ou soustraire une valeur de façon atomique. La soustraction étant bloquante si la valeur du sémaphore est inférieure à la valeur que l'on veut soustraire.

Les IPC système V permettent de manipuler des ensembles de sémaphores en réalisant sur ceux-ci un ensemble d'opérations de manière atomique. En pratique on se limite à des ensembles d'un seul sémaphore. Le nombre d'ensemble de sémaphores et le nombre de sémaphores par ensemble sont bornés par des constantes définies dans <sys/sem.h>.

Obtention d'un ensemble de sémaphore

Pour obtenir l'identificateur d'un ensemble de sémaphores ou pour le créer, on utilise la fonction

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);

Les paramètres key et semflag ont le même rôle que dans msgget() et shmget(). Le paramètre nsems indique le nombre de sémaphores de l'ensemble. Ce nombre peut-être nul si on veut obtenir l'identificateur d'un ensemble existant. Les sémaphores de l'ensemble sont numérotés de
0 à nsems -1.

La fonction retourne l'identificateur de l'ensemble ou -1 en cas d'erreur.

Initialisation du sémaphore

L'initialisation des valeurs sémaphore s'effectue par la fonction

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl (int semid, int semno, int cmd, ...);

semid est l'identificateur de l'ensemble de sémaphores concerné.

semno est le n° du sémaphore de l'ensemble.

cmd peut prendre différentes valeurs. Pour l'initialisation des sémaphores, les valeurs à utiliser sont SETVAL et SETALL. Il faut alors indiquer un 4ème argument à la fonction de type union semun

L'union semun est définie comme suit

unionsemun{
int val;
unsigned short *array;
...
};

Si cmd à la valeur SETVAL, la valeur du semaphore semno est fixée à la valeur du champs val du 4ème argument.

Si cmd à la valeur SETALL, les valeurs du tableau array servent à initialiser les valeurs des sémaphores. Le paramètre semno est alors ignoré.

Les valeurs GETVAL et GETALL permettent de récupérer

  • la valeur du sémaphore semno dans le champ val du 4ème argument de type union semun pour GETVAL
  • la valeur de tous les sémaphores dans le tableau array du 4ème argument de type union semun pour GETALL

Il y a d'autres valeurs possibles pour cmd mais elles ne seront pas décrites ici sauf IPC_RMID qui sera abordée plus loin.

semctl() retourne la valeur de du champ val pour GETVAL et 0 pour les autres valeurs de cmd évoquées ici en cas d'appel réussi. -1 en cas d'échec.

Supprimer un ensemble de sémaphore

La suppresion d'un ensemble de sémaphores s'effectue via la fonction semctl avec la valeur IPC_RMID comme valeur de l'argument cmd.

L'ensemble de sémaphores est détruit immédiatemment et les fonctions semop() bloquées se terminent en retournant un code d'erreur.

Opérations sur les sémaphores

Les opérations sur les sémaphores s'effectuent par la fonction

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop (int semid, struct sembuf *sops, unsigned nsops);

semid est l'identificateur de l'ensemble.

sops et un pointeur vers un tableau de stuctures sembuf qui indiquent les opérations à effectuer sur l'ensemble de sémaphores.

nsops est le nombre d'éléments du tableau pointé par sops.

Une structure sembuf possède les champs suivants:

short sem_num;
short sem_op;
short sem_flg;

sem_num est le numéro du sémaphore de l'ensemble concerné.

sem_op est la valeur ajoutée au sémaphore (elle peut être négative). La valeur 0 a une signification particulière, elle bloque l'appel jusqu'à ce que la valeur de sémaphore soit nulle.

sem_flag peut prendre la valeur

  • IPC_NOWAIT: l'appel n'est pas bloquant
  • SEM_UNDO: le noyau mémorise l'opération effectuée pour pouvoir faire l'inverse à la terminaison du processus. En fait le noyau retient la somme des valeurs sem_op pour pouvoir ajouter l'opposé à la valeur du sémaphore à la terminaison du processus.

L'utilisation de cette option est vivement conseillée.

Toutes ces opérations sont réalisées atomiquement. Si une seule d'entre elles ne peut être réalisée, aucune ne le sera et l'appel pourra bloquer jusqu'à ce que toutes les opérations soient possibles.

La valeur de retour de semop est 0 si l'appel réussit ou -1 en cas d'erreur.

Commandes

La commande ipcs -x permet de visualiser les ensembles de sémaphores présents sur le système.

Actions sur le document