cours/content/progsys/8_IPC/index.md
2018-11-21 17:58:59 +01:00

16 KiB

title date categories tags
Les communications inter processus 2018-10-23
Programmation système
cours
C
mutex
sémaphore
threads

Les communications inter processus, ou IPC (pour Inter Process Communication) comprennent un ensemble de mécanismes permettant à des processus concurrents de Communiquer.

Il existe trois mécanisme :

  • les files de messages (message queue)
  • la mémoire partagée
  • les sémaphores

Et deux API différentes

  • System V IPC datant de 1983. C'est l'API historique, plus compliquer à utiliser, elle ne se base pas sur les descripteurs de fichiers.
  • IPC POSIX datant de 2004 et également normée dans SUSv4. C'est une API moderne et disponible dans le noyau Linux à partis de la version 2.6.10

Les files de messages

C'est un mécanisme utilisé pour envoyer / recevoir des données entre processus suivant un mécanisme d'expéditeur / destinataire. Les communication sont asynchrones : l'expéditeur n'est pas contraint d'attendre la réception du message par le destinataire pour continuer son exécution.

Les informations envoyées à une file de message ne sont pas destinée à un processus en particulier.

C'est un mécanisme proche des tubes, seulement les données ne sont pas lues comme un flux d'octets.

Exemple de code

Ouvrir une file de message

mqd_t mq_open(const char *name, int flags);
mqd_t mq_open(const char *name, int flags, mode_t mode, struct mq_attr *attr);

En cas d'échec, retourne -1 avec errno positionné, sinon un descripteur de file de message

  • name : nom de la file de message, il doit commencé par le caractère / et ne pas en contenir d'autres.
  • flag et mode : ce sont exactement les mêmes choses que pour open()
  • attr : attributs de la file de messages comme le nombre maximal de messages à stocker, la taille maximale d'un message ...

Envoyer un message

int mq_send(mqd_t mqdes, const char *buffer, size_t len, unsigned int prio);
int mq_timedsend(mqd_t mqdes, const char *buffer,
                              size_t len, unsigned int prio,
                              const struct timespec *abs_timeout);

Renvoie 0 en cas de succès, sinon -1 et errno est positionné. Si la file de messages et pleine, ces deux appels sont bloquant jusqu'à libération d'une place.mq_timedsend() échouera si le message n'as pu être mis dans la file après le temps donné par *abs_timeout

Les messages seront dépilés dans l'ordre décroissant de priorité (0 en dernier)

Lire un message

ssize_t mq_receive(mqd_t mqdes, char *buffer, size_t len, unsigned int *prio);
ssize_t mq_timedreceive(mqd_t mqdes, char *buffer,
                                     size_t len, unsigned int *prio,
                                     const struct timespec *abs_timeout);

Renvoie le nombre d'octets du message reçu ou 0 (et errno positionné) en cas d'échec.

Le buffer doit être assez grand pour contenir le plus grand message que la file pourrait envoyé. La dimension des messages peut être obtenu avec mq_getattr() et modifiée avec mq_setattr().

Tout comme la version temporisée de mq_send(), mqtimedreceive() échouera si le message n'est pas lu avant un temps donné par *abs_timeout.

Fermeture / suppression d'une liste

// fermeture d'une file
int mq_close(mqd_t mqdes);

// suppression d'une liste
int mq_unlink(mqd_t mqdes);

Ces deux fonctions retourne 0 en cas de succès, -1 et positionne errno en cas d'échec.

Une file est persistante et reste disponible pour les autres processus après fermeture.

Tout comme unlink() pour les fichiers, la suppression d'une file de message sera effective seulement lorsque plus aucun processus ne l'utilisera.

Envoi d'un message, exemple complet

#include <fcntl.h>
#include <mqueue.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h> 
int main (int argc, char **argv) {
    mqd_t mq;
    int priority;
    if (argc != 4) { 
        fprintf(stderr, "Usage: %s queue priority message\n", argv[0]);
        exit(EXIT_FAILURE); 
    }
    if (sscanf(argv[2], "%d", &priority) != 1) {
        fprintf(stderr, "Invalid priority: %s\n", argv[2]); 
        exit(EXIT_FAILURE);
    }
    if ((mq = mq_open(argv[1], O_WRONLY | O_CREAT, 0644, NULL)) == (mqd_t) -1) {
        perror("Opening message queue failed"); 
        exit(EXIT_FAILURE); 
    }
    if (mq_send(mq, argv[3], strlen(argv[3]), priority) == -1) {
        perror("Unable to send message to queue");
        exit(EXIT_FAILURE);
    }
    if (mq_close(mq) == -1) {
        perror("Unable to close queue");
        exit(EXIT_FAILURE);
    }
    return EXIT_SUCCESS;
}

exemple de lecture

#include <fcntl.h>
#include <mqueue.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h> 
int main (int argc, char * argv[]) {
    int n;
    mqd_t mq;
    struct mq_attr attr;
    char * buffer = NULL;
    unsigned int priority;
    if (argc != 2) {
        fprintf(stderr, "Usage: %s queue\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    if ((mq = mq_open(argv[1], O_RDONLY)) == (mqd_t) -1) {
        perror("Opening message queue failed");
        exit(EXIT_FAILURE);
    }
    if (mq_getattr(mq, &attr) != 0) {
        perror("Unable to get message queue attributes");
        exit(EXIT_FAILURE);
    }
    if ((buffer = (char *) malloc(attr.mq_msgsize)) == NULL) {
        perror("Unable to allocate memory");
        exit(EXIT_FAILURE);
    }
    if ((n = mq_receive(mq, buffer, attr.mq_msgsize, &priority)) < 0) {
        perror("Unable to receive message from queue");
        exit(EXIT_FAILURE);
    }
    if (mq_close(mq) == -1) {
        perror("Unable to close queue");
        exit(EXIT_FAILURE);
    }
    fprintf(stdout, "[%d] %s\n", priority, buffer);
    free(buffer);
    return EXIT_SUCCESS;
}

Compilation et exécution

Afin de compiler un programme C faisant appel à des files de messages, il est important de rajouter le paramètre -lrt.

$ gcc -Wall -lrt mq-send.c -o mq-send
$ ./mq-send /plop 10 "Msg 10" 
$ ./mq-send /plop 20 "Msg 20" 
$ ./mq-send /plop 80 "Msg 30"
$ gcc -Wall -lrt mq-recv.c -m mq-revc
$ ./mq-recv /plop
[30] Msg 30
$ ./mq-recv /plop
[20] Msg 20
$ ./mq-recv /plop
[10] Msg 10

Mémoire partagée

Une zone de mémoire vive est accédée par plusieurs processus afin de partager des données. Celà consiste à ouvrir un segment de mémoire avec shm_open() (1) puis de le projeter dans l'espace mémoire d'un processus avec mmap() (2)

Les zones de mémoire partagées sont stockées dans le répertoire /dev/shm/

La mémoire partagée est plus rapide que les files de messages, elle est aussi moins limitée (les files de messages nécessite deux appels systèmes distinct)

Schéma mémoire partagée

Projection de mémoire

#include <sys/mman.h>
void *mmap(void *addr, size_t length,
                       int protect, int flags, int fd, off_t offset);

mmap() crée une projection de la mémoire partagés dans l'espace d'adressage virtuel du processus appelant. L'adresse de démarrage de la nouvelle projection est indiqué par *addr, length indiquant la taille de la zone mémoire projetée.

L'appel renvoi un pointeur sur la zone de mémoire et MAP_FAILED et positionne errno en cas d'échec.

  • protect : défini le type d'accès autorisé : PROT_EXEC, PROT_WRITE, PROT_READ ou PROT_NONE
  • flag : Défini le type de partage par exemple MAP_PRIVATE (toute modification dans cet espace ne sera pas visible par les autres processus projetant la zone) ou MAP_SHARED (modifications visibles par tous les autres processus).
  • fd : descripteur de fichier.
  • offset : décalage dans le fichier pointé par fd.

Exemple d'accès à un segment de mémoire partagée.

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>

int main (int argc, char **argv) {
    int fd;
    long int *counter;
    if (argc != 2) {
        fprintf(stderr, "Usage: %s segment\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    if ((fd = shm_open(argv[1], O_RDWR | O_CREAT, 0600)) == -1) {
        perror("Opening shared memory segment failed");
        exit(EXIT_FAILURE);
    }
    if (ftruncate(fd, sizeof(long int)) != 0) {
        perror("Unable to truncate shared memory segment");
        exit(EXIT_FAILURE);
    }
    counter = mmap(NULL, sizeof(long int), PROT_READ | PROT_WRITE, MAP_SHARED,fd, 0);
    if (counter == MAP_FAILED) {
        perror("Unable to map shared memory segment")
        exit(EXIT_FAILURE);
    }
    for(long int i=0; i< 1000000000; 1++) {
        (*counter)++;
    }
    fprintf(stdout, "counter=%ld\n",
    return EXIT_SUCCESS;
}

Compilation et exécution

$ gcc -Wall -lrt shm-inc.c -o shm-inc
$ ./shm-inc /foobar
counter=1000000000
$ ./shm-inc /foobar
counter=2000000000
$ ls -l /dev/shm
-rw------- 1 user group 8 Nov 15 00:00 foobar
$ cat /dev/shm/foobar
5w
$ hexdump /dev/shm/foobar
0000000 9400 7735 0000 0000
0000008
$ echo "7*16^7 + 7*16^6 + 3*16^5 + 5*16^4 + 9*16^3 + 4*16^2 + 0*16^1 + 0*16^0" | bc
2000000000

Nous pouvons vérifié que la valeur donné par counter correspond bien à ce qui se trouve dans notre segment de mémoire partagée contenu dans /dev/shm/foobar.

Problème d'accès à une section critique

Exécutons maintenant notre processus deux fois en même temps, une fois en avant plan et une fois en arrière plan (en prenant soin de supprimer notre segment partagé avant)

$ rm -f /dev/shm/foobar
$ ./shm-inc /foobar & ./shm-inc /foobar
counter=1004416125

On voit bien que la valeur retournée n'est pas celle attendu, tout comme les problèmes de variables partagées dans les processus légers.

Les sémaphores

Il permettent de synchroniser les processus entre eux et un accès concurrent aux ressources partagées. Il se présente sous la forme d'un entier strictement positif. Seule deux opérations sont possibles sur les sémaphore :

  • l'incrémentation de sa valeur V() (du néerlandais Verghotenm - incrémenter, référence à l'inventeur des sémaphores E. Djikstra)
  • la décrémentation de sa valeur P() (proberen - tester). Si le sémaphore est égal à 0, celui-ci est bloquant jusqu'à ce que sa valeur devienne supérieure à 0.

Dans la norme POSIX, les sémaphores peuvent être de deux types : nommés ou anonymes

Sémaphore anonyme

Comme son nom l'indique, un sémaphore anonyme n'as pas de nom. Il est aussi appelés sémaphore en mémoire. Il peut etre partagé entre plusieurs thread d'un même processus via des variables globales. Mais aussi par plusieurs processus via un espace de mémoire partagée (via l'utilisation de segment de mémoire partagée via shm_open()

Création destruction

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int shared);

Renvoie 0 en cas de succès, sinon -1 et positionne errno

  • pshared indique si le sémaphore sera partagé entre des threads (0) et dans se cas sera situé dans une variable visible par tous ceux du processus (variable globale ou allouée sur le tas) ou entre processus ( NON NULL) et donc situé dans une zone de mémoire partagée.
  • value spécifie la valeur initiale du processus.
#include <semaphore.h>
int sem_destroy(sem_t *sem);

Renvoie 0 en cas du succès, sinon -1 et errno est positionné.

Sémaphore nommé

Il est identifié par un nom commençant par le caractère / suivi d'un ou plusieurs caractères. Deux (ou plus) processus utilisent le même sémaphore nommé en passant le même nom à sem_open()

include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>

sem_t *sem_open(const char *name, int flags);
sem_t *sem_open(const char *name, int flags, mode_t mode, unsigned int value);

Renvoie l'adresse su nouveau sémaphore en cas de succès, sinon SEM_FAILED et positionne errno.

  • name défini le nom du sémaphore
  • mode et flags correspondent aux mêmes paramètre que l'appel système open()
  • value défini la valeur initiale du sémaphore.
#include <semaphore.h>
int sem_close(sem_t *sem);

Renvoie 0 en cas de succès, sinon -1 et positionne errno.

Opération sur les sémaphores

Les opération suivantes sont valables sur les deux types de sémaphores.

Décrémenter le sémaphore

#include <semaphore.h>
int sem_wait(sem_t *sem);

Retourne 0 en vas de succès, sinon -1 avec errno positionné. Comme indiqué plus haut, si le sémaphore est égal à 0, cet appel est bloquant en attendant son incrémentation.

Incrémenter le sémaphore

#include <semaphore.h>
int sem_post(sem_t *sem);

Retourne 0 en cas de succès, sinon -1 et positionne errno.

Sémaphore et mutex, quelle différences?

Les mutex sont utilisé pour sérialiser l'accès à une section de code devant être exécutées par plusieurs threads. Un mutex autorisera alors un seul thread à accéder à la ressource, forçant les autres threads à attendre la libération de celle-ci.

Les sémaphores restreignent le nombre d'utilisation maximale d'une ressource partagée. Les threads demandent alors l'accès à la ressource, décrémentant ainsi le sémaphore puis signalent lorsqu'ils on finit, incrémentant ainsi le sémaphore.

Exemple de code utilisant les sémaphores

#include <errno.h>
#include <fcntl.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char **argv) {
    int i;
    sem_t *sem;
    if (argc != 2) {
        fprintf(stderr, "Usage: %s sem_name\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    sem = sem_open(argv[1], O_RDWR | O_CREAT, 0666, 1);
    if (sem == SEM_FAILED) {
        perror("Unable to open semaphore");
        exit(EXIT_FAILURE);
    }
    fprintf(stdout, "[%d] Sempahore %s created\n", getpid(), argv[1]);
    for (i = 0; i < 3; i ++) {
        fprintf(stdout, "[%d] waiting...\n", getpid());
        sem_wait(sem);
        fprintf(stdout, "\t[%d] semaphore locked\n", getpid());
        sleep(4);
        fprintf(stdout, "\t[%d] semaphore released\n", getpid());
        sem_post(sem);
        sleep(2);
    }
    return EXIT_SUCCESS;
}

Compilation et exécution

Comme pour les files de message ou la mémoire partagée, il faut utiliser l'argument -lrt du compilateur gcc. Il est aussi nécessaire d'utiliser -pthread.

$ gcc -pthread -lrt -Wall sem-lock.c -o sem-lock
$ ./sem-lock foobar & ./sem-lock foobar ./sem-lock foobar
$ ls -l /dev/shm
-rw-r--r-- 1 user group 32 Nov 15 00:00 sem.foobar

Comme pour la mémoire partagée, le sémaphore est créé dans le répertoire /dev/shm/, mais son nom commence par sem.. Dans l'exemple, le nom ne commence pas par un /, ce qui ne semble pas posé de problèmes à l'exécution (est-ce une bonne pratique plutôt qu'un impératif?)

Sortie de l'exécution

[16597] Sempahore foobar created
[16597] waiting...
[16597] semaphore locked
[16599] Sempahore foobar created
[16599] waiting...
[16600] Sempahore foobar created
[16600] waiting...
[16597] semaphore released
[16599] semaphore locked
[16597] waiting...
[16599] semaphore released
[16600] semaphore locked
[16599] waiting...
[16600] semaphore released
[16597] semaphore locked
[16600] waiting...
[16597] semaphore released
[16599] semaphore locked
[16597] waiting...
[16599] semaphore released
[16600] semaphore locked
[16599] waiting...
[16600] semaphore released
[16597] semaphore locked
[16600] waiting...
[16597] semaphore released
[16599] semaphore locked
[16599] semaphore released
[16600] semaphore locked
[16600] semaphore released

Bibliophraphie