537 lines
16 KiB
Markdown
537 lines
16 KiB
Markdown
---
|
|
title: " Les communications inter processus"
|
|
date: 2018-10-23
|
|
categories: ["Programmation système", "Cours"]
|
|
tags: ["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
|
|
|
|
```c
|
|
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
|
|
|
|
```c
|
|
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
|
|
|
|
```c
|
|
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
|
|
|
|
```c
|
|
// 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
|
|
|
|
```c
|
|
#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
|
|
|
|
```c
|
|
#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`.
|
|
|
|
```shell
|
|
$ 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)
|
|
|
|

|
|
|
|
### Projection de mémoire
|
|
|
|
```c
|
|
#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.
|
|
|
|
```c
|
|
#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
|
|
|
|
```shell
|
|
$ 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)
|
|
|
|
```c
|
|
$ 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
|
|
|
|
```c
|
|
#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.
|
|
|
|
```c
|
|
#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()`
|
|
|
|
|
|
```c
|
|
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.
|
|
|
|
```c
|
|
#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
|
|
|
|
```c
|
|
#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
|
|
|
|
```c
|
|
#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
|
|
|
|
```c
|
|
#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`.
|
|
|
|
```shell
|
|
$ 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
|
|
|
|
```shell
|
|
[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
|
|
|
|
- [Page Wikipedia][l_w_mq] sur les file de messages
|
|
- [Page Wikipedia][l_w_shm] sur la mémoire partagée.
|
|
- [Les sémaphores][l_sem_ulouvain] Cours sur l'Université Catholique de Louvain
|
|
(Belgique) par O. Bonaventure, G. Detal, C. Paasch.
|
|
- [Page Wikipedia][l_w_sem] sur les sémaphores.
|
|
- [Page de manuel][l_u_sem] sur le site d'Ubuntu (et en français) sur
|
|
l'implémentation en C des sémaphores.
|
|
|
|
[l_w_mq]:https://fr.wikipedia.org/wiki/File_d%27attente_de_message
|
|
[l_sem_ulouvain]:https://sites.uclouvain.be/SystInfo/notes/Theorie/html/Threads/coordination.html
|
|
[l_w_shm]:https://fr.wikipedia.org/wiki/M%C3%A9moire_partag%C3%A9e_(communication_inter-processus)
|
|
[l_w_sem]:https://fr.wikipedia.org/wiki/S%C3%A9maphore_(informatique)
|
|
[l_u_sem]:http://manpages.ubuntu.com/manpages/precise/fr/man7/sem_overview.7.html
|
|
|