1. Introduction
La mémoire partagée POSIX permet à plusieurs processus d'accéder à une même région mémoire.
Elle constitue l'un des mécanismes IPC les plus rapides, car les processus n'ont pas besoin de copier les données à travers un pipe ou une file de messages à chaque échange.
Le principe général est :
- créer ou ouvrir un objet de mémoire partagée avec shm_open() ;
- définir sa taille avec ftruncate() ;
- mapper l'objet dans l'espace d'adressage du processus avec mmap() ;
- lire ou écrire directement dans la zone mémoire ;
- supprimer le nom de l'objet avec shm_unlink() lorsqu'il n'est plus nécessaire.
2. Vue d'ensemble
Un objet de mémoire partagée POSIX est un objet noyau identifié par un nom.
Exemple :
Code: Select all
"/inside_shm"
Code: Select all
shm_open()
Ce descripteur peut ensuite être utilisé avec :
Code: Select all
ftruncate()
mmap()
fstat()
close()
Code: Select all
MAP_SHARED
3. Étapes générales
Le schéma classique est :
- 1. Créer ou ouvrir l'objet avec shm_open().
- 2. Définir sa taille avec ftruncate().
- 3. Mapper l'objet avec mmap().
- 4. Utiliser la zone mémoire.
- 5. Retirer le mapping avec munmap().
- 6. Fermer le descripteur avec close().
- 7. Supprimer le nom avec shm_unlink().
4. Création d'un objet de mémoire partagée
shm_open()
Prototype :
Code: Select all
int shm_open(
const char *name,
int oflag,
mode_t mode
);
Code: Select all
int fd = shm_open(
"/inside_shm",
O_CREAT | O_RDWR,
0666
);
En cas d'erreur :
Code: Select all
-1
5. Nom d'un objet POSIX
Le nom permet à plusieurs processus d'ouvrir le même objet.
Exemple :
Code: Select all
"/inside_shm"
Code: Select all
/
Code: Select all
shm_open("/inside_shm", ...);
6. Flags de shm_open()
Les flags importants sont :
- O_RDONLY ;
- O_RDWR ;
- O_CREAT ;
- O_EXCL ;
- O_TRUNC.
O_RDONLY
Ouvre l'objet en lecture seule.
Exemple :
Code: Select all
shm_open("/inside_shm", O_RDONLY, 0);
O_RDWR
Ouvre l'objet en lecture et en écriture.
Exemple :
Code: Select all
shm_open("/inside_shm", O_RDWR, 0);
O_CREAT
Crée l'objet s'il n'existe pas.
Exemple :
Code: Select all
shm_open(
"/inside_shm",
O_CREAT | O_RDWR,
0666
);
O_EXCL
Utilisé avec O_CREAT.
Si l'objet existe déjà, l'appel échoue.
Exemple :
Code: Select all
shm_open(
"/inside_shm",
O_CREAT | O_EXCL | O_RDWR,
0666
);
O_TRUNC
Permet de tronquer un objet existant.
Selon l'implémentation et les droits d'accès, l'objet peut être ramené à une taille nulle.
7. Permissions
Lors de la création, le paramètre mode définit les permissions.
Exemple :
Code: Select all
0666
8. Taille initiale de l'objet
Un nouvel objet créé par shm_open() possède initialement une taille nulle.
Il faut donc généralement l'agrandir avant de le mapper.
La fonction utilisée est :
Code: Select all
ftruncate()
9. ftruncate()
Prototype conceptuel :
Code: Select all
int ftruncate(
int fd,
off_t length
);
Code: Select all
if (ftruncate(fd, 4096) == -1) {
perror("ftruncate");
}
Code: Select all
4096
10. Pourquoi définir la taille avant mmap()
Le mapping doit référencer une zone réellement présente dans l'objet.
Si l'objet a une taille nulle et que le programme tente d'accéder à une zone qui n'existe pas, le comportement n'est pas valide.
Le schéma correct est donc :
Code: Select all
shm_open()
ftruncate()
mmap()
11. Mapping de la mémoire partagée
Une fois l'objet créé et dimensionné, il peut être mappé avec :
Code: Select all
mmap()
Code: Select all
void *addr = mmap(
NULL,
4096,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd,
0
);
12. MAP_SHARED
Le flag :
Code: Select all
MAP_SHARED
Il indique que les modifications effectuées dans le mapping doivent être visibles par les autres processus mappant le même objet.
Exemple :
Processus A écrit :
Code: Select all
strcpy(addr, "Inside The System");
Code: Select all
"Inside The System"
13. MAP_PRIVATE ne convient pas au partage
Avec :
Code: Select all
MAP_PRIVATE
Le mapping utilise un comportement privé basé notamment sur le copy-on-write.
Pour une mémoire réellement partagée entre processus, on utilise donc généralement :
Code: Select all
MAP_SHARED
14. Protections mémoire
Les protections sont choisies avec le paramètre prot de mmap().
Exemples :
Code: Select all
PROT_READ
Code: Select all
PROT_WRITE
Combinaison fréquente :
Code: Select all
PROT_READ | PROT_WRITE
15. Accès aux données
Une fois mmap() réussi, la zone peut être manipulée comme une zone mémoire classique.
Exemple :
Code: Select all
char *buffer = addr;
strcpy(buffer, "Inside The System");
Code: Select all
struct SharedData *data = addr;
data->counter++;
16. Le descripteur peut être fermé après mmap()
Une fois le mapping créé avec succès, il n'est généralement plus nécessaire de conserver le descripteur de fichier uniquement pour maintenir le mapping.
On peut donc faire :
Code: Select all
void *addr = mmap(
NULL,
size,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd,
0
);
close(fd);
Le descripteur et le mapping sont deux références différentes.
17. Récupérer la taille d'un objet existant
Lorsqu'un processus ouvre un objet déjà existant, il peut récupérer sa taille avec :
Code: Select all
fstat()
Code: Select all
struct stat sb;
if (fstat(fd, &sb) == -1) {
perror("fstat");
}
printf("Taille : %ld\n", (long)sb.st_size);
18. Exemple d'écriture
Code: Select all
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
int main(void)
{
const char *name = "/inside_shm";
const size_t size = 4096;
int fd = shm_open(
name,
O_CREAT | O_RDWR,
0666
);
if (fd == -1) {
perror("shm_open");
return EXIT_FAILURE;
}
if (ftruncate(fd, size) == -1) {
perror("ftruncate");
close(fd);
return EXIT_FAILURE;
}
char *addr = mmap(
NULL,
size,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd,
0
);
if (addr == MAP_FAILED) {
perror("mmap");
close(fd);
return EXIT_FAILURE;
}
close(fd);
strcpy(addr, "Inside The System");
munmap(addr, size);
return EXIT_SUCCESS;
}
19. Exemple de lecture
Code: Select all
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
int main(void)
{
const char *name = "/inside_shm";
int fd = shm_open(
name,
O_RDONLY,
0
);
if (fd == -1) {
perror("shm_open");
return EXIT_FAILURE;
}
struct stat sb;
if (fstat(fd, &sb) == -1) {
perror("fstat");
close(fd);
return EXIT_FAILURE;
}
char *addr = mmap(
NULL,
sb.st_size,
PROT_READ,
MAP_SHARED,
fd,
0
);
if (addr == MAP_FAILED) {
perror("mmap");
close(fd);
return EXIT_FAILURE;
}
close(fd);
printf("Message : %s\n", addr);
munmap(addr, sb.st_size);
return EXIT_SUCCESS;
}
20. Démapper la mémoire
Une région créée avec mmap() doit être retirée avec :
Code: Select all
munmap()
Code: Select all
int munmap(
void *addr,
size_t length
);
Code: Select all
munmap(addr, 4096);
21. Suppression de l'objet
shm_unlink()
Prototype :
Code: Select all
int shm_unlink(
const char *name
);
Code: Select all
shm_unlink("/inside_shm");
22. shm_unlink() ne détruit pas forcément immédiatement
Si un processus possède encore :
- un descripteur ouvert ;
- ou un mapping actif ;
Cela ressemble au comportement de :
Code: Select all
unlink()
23. Différence entre close(), munmap() et shm_unlink()
close()
Ferme le descripteur.
Code: Select all
close(fd);
munmap()
Supprime un mapping dans l'espace virtuel du processus.
Code: Select all
munmap(addr, size);
shm_unlink()
Supprime le nom de l'objet POSIX.
Code: Select all
shm_unlink("/inside_shm");
24. Partage entre plusieurs processus
Processus A :
Code: Select all
fd = shm_open("/inside_shm", O_CREAT | O_RDWR, 0666);
ftruncate(fd, 4096);
addrA = mmap(NULL, 4096,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd,
0);
Code: Select all
fd = shm_open("/inside_shm", O_RDWR, 0);
addrB = mmap(NULL, 4096,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd,
0);
Cependant, ils référencent les mêmes données partagées.
25. Les adresses virtuelles peuvent être différentes
Il n'est pas nécessaire que les deux processus obtiennent la même adresse virtuelle.
Exemple :
Code: Select all
Processus A :
0x7f1000000000
Processus B :
0x7f9000000000
Il ne faut donc généralement pas stocker de pointeurs absolus dans une structure partagée si plusieurs processus doivent les utiliser.
26. Éviter les pointeurs absolus dans la mémoire partagée
Exemple problématique :
Code: Select all
struct Shared {
char *buffer;
};
Code: Select all
buffer
Il est souvent préférable d'utiliser :
- des offsets ;
- des indices ;
- des structures entièrement contenues dans la zone partagée.
Code: Select all
struct Shared {
size_t buffer_offset;
};
27. Synchronisation nécessaire
La mémoire partagée permet à plusieurs processus d'accéder aux mêmes données.
Elle ne protège cependant pas automatiquement contre les accès concurrents.
Exemple :
Code: Select all
counter++;
Si plusieurs processus modifient simultanément counter, une race condition peut se produire.
Il faut donc utiliser un mécanisme de synchronisation.
28. Synchronisation possible
On peut utiliser :
- un sémaphore POSIX ;
- un mutex process-shared ;
- un sémaphore System V ;
- des opérations atomiques adaptées ;
- d'autres mécanismes de synchronisation.
Code: Select all
sem_wait(sem);
shared->counter++;
sem_post(sem);
29. Mémoire partagée + sémaphore POSIX
Un modèle fréquent consiste à utiliser :
- un objet shm_open() pour les données ;
- un sémaphore nommé avec sem_open() pour la synchronisation.
Code: Select all
sem_t *sem = sem_open(
"/inside_sem",
O_CREAT,
0666,
1
);
Code: Select all
sem_wait(sem);
strcpy(shared_memory, "Hello");
sem_post(sem);
30. Sémaphore non nommé directement dans la zone partagée
On peut aussi placer un sémaphore non nommé dans la mémoire partagée.
Exemple :
Code: Select all
struct Shared {
sem_t sem;
int counter;
};
Code: Select all
sem_init(
&shared->sem,
1,
1
);
Code: Select all
1
31. Exemple complet avec structure partagée
Code: Select all
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <semaphore.h>
struct Shared {
sem_t sem;
int counter;
};
int main(void)
{
int fd = shm_open(
"/shared_struct",
O_CREAT | O_RDWR,
0666
);
if (fd == -1) {
perror("shm_open");
return EXIT_FAILURE;
}
if (ftruncate(fd, sizeof(struct Shared)) == -1) {
perror("ftruncate");
close(fd);
return EXIT_FAILURE;
}
struct Shared *shared = mmap(
NULL,
sizeof(struct Shared),
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd,
0
);
if (shared == MAP_FAILED) {
perror("mmap");
close(fd);
return EXIT_FAILURE;
}
close(fd);
shared->counter = 0;
if (sem_init(&shared->sem, 1, 1) == -1) {
perror("sem_init");
munmap(shared, sizeof(struct Shared));
return EXIT_FAILURE;
}
sem_wait(&shared->sem);
shared->counter++;
sem_post(&shared->sem);
sem_destroy(&shared->sem);
munmap(shared, sizeof(struct Shared));
shm_unlink("/shared_struct");
return EXIT_SUCCESS;
}
32. Inspection sous Linux
Sous Linux, les objets de mémoire partagée POSIX sont généralement représentés dans un système de fichiers temporaire.
Ils sont souvent visibles dans :
Code: Select all
/dev/shm
Code: Select all
ls -l /dev/shm
Code: Select all
"/inside_shm"
Code: Select all
inside_shm
33. Le lien avec tmpfs
Sous Linux, /dev/shm est généralement basé sur :
Code: Select all
tmpfs
Ils restent néanmoins utilisés comme des objets IPC et non comme de simples fichiers classiques du programme.
34. Utilisation de fstat()
Comme shm_open() retourne un descripteur, les fonctions classiques travaillant sur des descripteurs peuvent souvent être utilisées.
Exemple :
Code: Select all
struct stat sb;
fstat(fd, &sb);
- la taille ;
- certaines métadonnées ;
- les permissions.
35. Redimensionnement
La taille d'un objet de mémoire partagée POSIX peut être modifiée avec :
Code: Select all
ftruncate()
Code: Select all
ftruncate(fd, 8192);
Il peut être nécessaire de :
- faire munmap() ;
- puis refaire mmap() avec la nouvelle taille.
36. Persistance
Un objet de mémoire partagée POSIX peut continuer à exister après la fin du processus qui l'a créé tant que son nom n'a pas été supprimé.
Cela permet à un autre processus de l'ouvrir ensuite.
Le programme doit donc gérer correctement :
Code: Select all
shm_unlink()
37. POSIX Shared Memory vs System V Shared Memory
System V
Fonctions principales :
Code: Select all
shmget()
shmat()
shmdt()
shmctl()
Exemple :
Code: Select all
int shmid;
POSIX
Fonctions principales :
Code: Select all
shm_open()
ftruncate()
mmap()
munmap()
shm_unlink()
Exemple :
Code: Select all
"/inside_shm"
38. Création : POSIX vs System V
System V
Code: Select all
int shmid = shmget(
key,
size,
IPC_CREAT | 0666
);
POSIX
Code: Select all
int fd = shm_open(
"/inside_shm",
O_CREAT | O_RDWR,
0666
);
ftruncate(fd, size);
39. Mapping : POSIX vs System V
System V
Code: Select all
void *addr = shmat(
shmid,
NULL,
0
);
POSIX
Code: Select all
void *addr = mmap(
NULL,
size,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd,
0
);
40. Suppression : POSIX vs System V
System V
Code: Select all
shmctl(
shmid,
IPC_RMID,
NULL
);
POSIX
Code: Select all
shm_unlink(
"/inside_shm"
);
41. POSIX Shared Memory vs Shared File Mapping
Un fichier classique peut également être utilisé comme backing store pour mmap().
Exemple :
Code: Select all
int fd = open(
"shared.bin",
O_CREAT | O_RDWR,
0666
);
Code: Select all
mmap(
NULL,
size,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd,
0
);
42. Différence avec un fichier classique
Avec un fichier classique :
- les données peuvent être destinées à rester sur le stockage ;
- le fichier possède un chemin classique dans le système de fichiers.
- l'objet est destiné principalement à l'IPC ;
- il possède un nom POSIX ;
- il peut être stocké dans un système temporaire comme tmpfs sous Linux.
43. Trois grandes techniques de mémoire partagée
On peut donc distinguer :
- System V Shared Memory ;
- shared file mappings ;
- POSIX Shared Memory.
44. Points communs entre les trois techniques
- Chaque processus possède son propre mapping dans son espace virtuel.
- Les adresses virtuelles peuvent être différentes.
- La synchronisation doit être gérée séparément.
- Les données peuvent être directement accessibles sans copie à chaque échange.
- Les mappings peuvent être utilisés pour partager des structures complexes.
45. Différences importantes
System V
Utilise :
Code: Select all
key_t
shmid
shmget()
shmat()
File mapping
Utilise un véritable fichier comme backing store.
Exemple :
Code: Select all
open()
mmap()
POSIX Shared Memory
Utilise un objet nommé avec :
Code: Select all
shm_open()
Code: Select all
ftruncate()
mmap()
46. Taille de la région
Avec System V, la taille du segment est définie lors de la création.
Avec POSIX Shared Memory, on peut utiliser :
Code: Select all
ftruncate()
Cela apporte une certaine flexibilité supplémentaire.
47. Synchronisation non fournie automatiquement
La mémoire partagée ne synchronise pas automatiquement les accès.
Deux processus peuvent écrire simultanément dans la même variable.
Exemple dangereux :
Code: Select all
shared->counter++;
Exemple :
Code: Select all
sem_wait(&shared->sem);
shared->counter++;
sem_post(&shared->sem);
48. Cohérence et visibilité
MAP_SHARED rend les modifications visibles entre les différents mappings de l'objet.
Cependant, cela ne remplace pas la synchronisation.
La visibilité de la mémoire et l'ordre logique des opérations doivent être gérés avec des primitives adaptées lorsque plusieurs processus accèdent simultanément aux données.
49. Avantages de POSIX Shared Memory
- API cohérente avec open(), close(), ftruncate() et mmap().
- Utilisation de noms lisibles.
- Très bonnes performances pour de gros volumes de données.
- Aucune copie explicite nécessaire entre producteurs et consommateurs.
- Facile à combiner avec les sémaphores POSIX.
- Taille modifiable avec ftruncate().
- Inspection facile sous Linux via /dev/shm.
50. Inconvénients
- La synchronisation doit être conçue séparément.
- Les pointeurs stockés directement dans la zone peuvent être invalides dans un autre processus.
- Une mauvaise gestion de la durée de vie peut laisser des objets persistants.
- Les erreurs de synchronisation peuvent provoquer des races difficiles à diagnostiquer.
- Pour de petits messages simples, une message queue ou un pipe peut être plus facile à utiliser.
51. Quand utiliser POSIX Shared Memory
Elle est particulièrement adaptée lorsque :
- les processus échangent beaucoup de données ;
- les performances sont importantes ;
- on veut éviter les copies fréquentes ;
- les processus doivent partager des structures ;
- on dispose déjà d'un mécanisme de synchronisation adapté.
52. Quand préférer une Message Queue
Une file de messages est souvent plus simple lorsque :
- les données sont naturellement organisées en messages ;
- le volume est relativement faible ;
- on souhaite des priorités ;
- on ne veut pas gérer directement une région mémoire partagée.
53. Quand préférer un pipe
Un pipe est souvent adapté lorsque :
- on souhaite un flux séquentiel simple ;
- les processus ont une relation parent/enfant ;
- aucune structure partagée complexe n'est nécessaire.
54. Exemple de cycle de vie complet
Code: Select all
int fd = shm_open(
"/inside_shm",
O_CREAT | O_RDWR,
0666
);
ftruncate(fd, 4096);
void *addr = mmap(
NULL,
4096,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd,
0
);
close(fd);
/* utilisation */
munmap(addr, 4096);
shm_unlink("/inside_shm");
55. Erreurs fréquentes
Oublier ftruncate()
Créer l'objet sans lui donner une taille correcte avant mmap().
Utiliser MAP_PRIVATE
Cela empêche d'obtenir le comportement de mémoire partagée attendu.
Oublier la synchronisation
Plusieurs processus peuvent modifier les mêmes données simultanément.
Stocker des pointeurs absolus
Les adresses virtuelles peuvent différer entre les processus.
Oublier shm_unlink()
L'objet peut rester présent après la fin du programme.
Utiliser la zone après munmap()
Une fois le mapping retiré, l'adresse n'est plus valide.
56. Points importants à retenir
- shm_open() crée ou ouvre un objet de mémoire partagée POSIX.
- shm_open() retourne un descripteur de fichier.
- Un nouvel objet possède initialement une taille nulle.
- ftruncate() permet de définir sa taille.
- mmap() permet de mapper l'objet dans l'espace virtuel.
- MAP_SHARED est utilisé pour partager les modifications.
- PROT_READ et PROT_WRITE contrôlent les accès.
- fstat() peut permettre de récupérer la taille d'un objet existant.
- close() ferme le descripteur mais ne détruit pas le mapping.
- munmap() retire le mapping.
- shm_unlink() supprime le nom de l'objet.
- L'objet peut rester valide tant que des références subsistent.
- Les différents processus peuvent obtenir des adresses virtuelles différentes.
- Il faut éviter de partager directement des pointeurs absolus.
- La synchronisation doit être ajoutée séparément.
- Les sémaphores POSIX se combinent très bien avec la mémoire partagée.
57. Résumé
POSIX Shared Memory permet à plusieurs processus de partager directement une région mémoire commune.
Un objet est créé ou ouvert avec shm_open(), qui retourne un descripteur de fichier.
La taille de l'objet est généralement définie avec ftruncate(), puis l'objet est mappé avec mmap() en utilisant MAP_SHARED.
Une fois le mapping établi, les processus peuvent lire et écrire directement dans la zone partagée.
Le descripteur peut être fermé sans détruire le mapping.
munmap() retire le mapping local, tandis que shm_unlink() supprime le nom de l'objet.
Contrairement aux files de messages, la mémoire partagée ne fournit pas automatiquement de structure d'échange ni de synchronisation. Il est donc courant de la combiner avec des sémaphores ou des mutex process-shared.
Par rapport à System V Shared Memory, l'interface POSIX s'intègre mieux au modèle classique Unix basé sur les descripteurs, ftruncate() et mmap().
POSIX Shared Memory est particulièrement efficace pour échanger de gros volumes de données entre processus avec un coût minimal de copie.
