Pièges de la mémoire partagée et bonnes pratiques concrètes
· Mis à jour le: · Go Komura · Mémoire partagée, IPC, Concurrence, C++, C#, Développement Windows
Trames d’image, résultats d’inspection, journaux chronologiques, données de carnet d’ordres, énormes buffers. Quand on veut échanger de grandes quantités de données à faible latence au sein d’une même machine, la mémoire partagée est plutôt séduisante.
Le hic, c’est qu’elle s’approche de vous avec le visage de « l’IPC rapide ». En réalité, la mémoire partagée est « une IPC qui réduit les copies, mais qui vous renvoie la responsabilité de la cohérence ».
- Rapide
- Flexible
- Mais le protocole, c’est vous qui le construisez
- Et quand ça casse, les symptômes sont spectaculaires
C’est à peu près ce lot de quatre éléments qu’on obtient.
Dans cet article, en gardant à l’esprit le file mapping de Windows et shm_open / mmap sous POSIX, nous passons en revue les points de blocage rencontrés en pratique avec la mémoire partagée, ainsi que la conception qui permet de réduire le taux d’incidents.
Que ce soit en C/C++ ou avec MemoryMappedFile en C#, l’essentiel reste quasiment identique.1
1. D’abord la conclusion (en une phrase)
Pour formuler les choses de façon assez brute, mais utile en pratique :
- La mémoire partagée est un mécanisme qui présente la même suite d’octets à plusieurs processus ; ce n’est pas la synchronisation elle-même23
- Ce qui est rapide, c’est l’échange de données volumineuses au sein d’une même machine. Pour de simples petits messages de contrôle, pipe / socket / named pipe / queue est très souvent plus simple
- Avec la mémoire partagée, être visible et pouvoir être lu en toute sécurité sont deux problèmes distincts
- Ne bâtissez pas votre conception sur
volatile. L’atomicité, l’ordre et l’attente doivent être traités séparément45 - Placer des pointeurs bruts, des
HANDLE, des descripteurs de fichiers,std::string,std::vector,std::mutexdirectement en mémoire partagée finit presque toujours par vous faire pleurer - Les données placées en mémoire partagée sont plus sûres si elles tendent vers des entiers de largeur fixe + une disposition explicite + un en-tête versionné
- Le simple fait de placer magic / version / size / state / generation / heartbeat dans un en-tête initial change considérablement la facilité d’investigation en cas d’incident
- Les difficultés de la mémoire partagée ne concernent pas la vitesse mais l’initialisation, la durée de vie, la récupération, les permissions et l’ABI
- Sous Windows, le squelette est
CreateFileMapping/OpenFileMapping/MapViewOfFile; sous POSIX, c’estshm_open/ftruncate/mmap63 - Le point de départ le moins sujet aux incidents est un ring buffer SPSC (single-producer single-consumer) ou un double buffer
En résumé : la mémoire partagée est rapide, mais si on l’utilise sans précaution, on attrape la maladie du « on dirait que ça se synchronise tout seul ». Éviter cela est le premier combat.
2. Ce que la mémoire partagée partage — et ce qu’elle ne partage pas
Pour le dire simplement, la mémoire partagée est un mécanisme qui mappe les mêmes pages physiques dans les espaces d’adressage virtuel de plusieurs processus.
Windows utilise un objet file mapping et des vues (views) ; POSIX fait un mmap d’un objet de mémoire partagée.273
Deux points comptent ici.
- Ce qui est partagé, c’est le contenu en octets, pas les adresses virtuelles elles-mêmes
- Être cohérent et être synchronisé sont deux choses différentes
La documentation Windows indique également que les vues créées à partir du même objet file mapping sont cohérentes à un instant donné. Mais cela ne veut pas dire que le lecteur peut toujours lire un enregistrement cohérent et entièrement à jour.8
Par exemple, même si le writer a l’intention d’écrire
length- puis
payload - puis le
ready flag
dans cet ordre, un reader qui lit sans aucune synchronisation peut voir le nouveau length combiné à l’ancien payload.
La mémoire partagée ne corrige pas cela automatiquement pour vous.
Donc ce que partage la mémoire partagée, ce sont des octets. Ce qu’elle ne partage pas, ce sont le sens, l’ordre, la notification d’achèvement et la politique de récupération. Tout cela, c’est à vous de le concevoir.
3. Où la mémoire partagée est adaptée — et où elle ne l’est pas
| Situation | Adéquation | Raison |
|---|---|---|
| Faire transiter de grandes trames ou buffers au sein d’une même machine | Adapté | Facile de réduire le nombre de copies |
| Valeurs de capteurs à haute fréquence, images, audio, données de carnet d’ordres, etc. | Adapté | Facile de viser une faible latence et un débit élevé |
| N’échanger que de petites commandes et réponses | Peu adapté | Le coût de synchronisation pour le contrôle est relativement lourd |
| Communiquer avec d’autres machines | Non adapté | La mémoire partagée suppose fondamentalement un hôte unique |
| Coexistence à long terme de langages et de versions différents | Difficile | Nécessite une conception d’ABI et de versionnement |
| Persistance également nécessaire | Selon l’objectif | Les mappings adossés à un fichier (file-backed) sont viables, mais les responsabilités de persistance et d’IPC ont tendance à se mélanger |
En pratique, la séparation contrôle par messagerie, données par mémoire partagée est très solide. Par exemple :
- Le processus UI notifie le processus worker « utilise la prochaine trame » via un event / pipe / socket
- La trame elle-même réside en mémoire partagée
Cette configuration a tendance à être paisible.
4. Les quatre choses à décider en premier
Lors de la conception d’une mémoire partagée, les quatre premières choses à décider sont les suivantes.
4.1 Séparer le plan de contrôle (control plane) et le plan de données (data plane)
Décidez à l’avance ce qui va dans la mémoire partagée.
- data plane : images, audio, séquences d’enregistrements, données en masse
- control plane : démarrage, arrêt, erreurs, reconnexion, réinitialisation, notifications
Le simple fait de séparer ces deux éléments simplifie considérablement le design côté mémoire partagée.
4.2 Restreindre le modèle de concurrence
- SPSC : 1 producteur / 1 consommateur
- MPSC : plusieurs writers / 1 consommateur
- SPMC : 1 writer / plusieurs readers
- MPMC : plusieurs writers / plusieurs readers
La difficulté augmente à peu près dans cet ordre. Se lancer directement en MPMC est plutôt téméraire. En général, les spectres de l’ordre mémoire finissent par apparaître.
4.3 Décider du propriétaire et de la durée de vie
- Qui crée
- Qui initialise
- Qui supprime
- Qui restaure quand un participant tombe en cours de route
Si ce point reste flou, l’air devient trouble à chaque ordre de démarrage ou redémarrage.
4.4 Décider de l’ABI et du versionnement
- Disposition (layout)
- Taille des types
- Alignement
- Zones réservées
- Version / feature flags
- Existence d’une compatibilité
La mémoire partagée n’est pas une question d’API mais d’ABI (interface binaire). Traiter cela à la légère mène au type d’incident désagréable où la compatibilité source existe, mais où seule l’exécution finit par casser.
5. Pièges courants
5.1 Ne pas synchroniser
C’est le plus fréquent.
« On regarde la même mémoire, donc si j’écris, ça doit pouvoir se lire. »
Ça peut effectivement se lire. Mais cela ne veut pas dire que ce sera lu au bon moment, dans la bonne unité, dans le bon ordre.
Que ce soit sous Windows ou POSIX, l’accès à la mémoire partagée est censé être combiné à un autre moyen de synchronisation. La documentation Windows précise elle aussi que l’accès à une vue partagée doit être coordonné via des mutex / sémaphores / events.2 La documentation POSIX indique elle aussi que l’accès à la mémoire partagée nécessite une synchronisation.9
5.2 Essayer de s’en sortir avec volatile
volatile n’est pas un sortilège qui sauve la conception d’une mémoire partagée.
Au minimum, l’atomicité et l’exclusion mutuelle sont des problèmes distincts.45
Par exemple, une conception qui place volatile bool ready; et le surveille par busy loop :
- gaspille le CPU
- rend floue la garantie d’ordre entre le payload et le flag ready
- n’est pas portable
- capte facilement des états intermédiaires
Et il n’en sort à peu près rien de bon.
De plus, WaitOnAddress sous Windows est destiné aux threads au sein d’un même processus.
Il est plus prudent de ne pas le considérer comme un mécanisme d’attente inter-processus.10
5.3 Laisser lire des états intermédiaires
Quand la mémoire partagée casse, l’allure du problème est assez banale.
- Seul l’en-tête est nouveau
- Seul le payload est ancien
- Seule la longueur est mise à jour
- La paire de deux champs est incohérente
Si vous vous contentez de mettre à jour atomiquement un seul scalaire, les choses restent relativement simples, mais si vous publiez un enregistrement composé de plusieurs champs, il faut une procédure de commit.
Typiquement, l’une des solutions suivantes :
- Protéger l’ensemble avec un mutex
- Utiliser un double buffer et basculer en dernier « l’index du buffer actuellement valide »
- Utiliser un ring buffer avec un state / sequence par slot
- Pour 1 writer / plusieurs readers, prendre des snapshots via un compteur de séquence
Même « poser le flag ready en dernier » reste une conception immature tant que vous n’avez pas décidé avec quel ordre mémoire ce flag est écrit et lu. Dans la mémoire partagée, le moment de publication est lui-même le protocole.
5.4 Placer des pointeurs ou des objets complexes tels quels
C’est un autre schéma fréquent.
- Pointeurs bruts
HANDLE- Descripteurs de fichiers
std::stringstd::vectorstd::unordered_mapstd::mutexCRITICAL_SECTION
Placer ces éléments directement en mémoire partagée et essayer de les utiliser depuis un autre processus. Un petit enfer commence en général.
La raison est simple : les adresses virtuelles et les ressources propres à un processus n’ont de sens que dans le contexte de ce processus. Pour les vues Windows aussi, mapper le même mapping dans un autre processus ne garantit pas que les adresses virtuelles coïncident.711
Donc, si des références sont nécessaires, l’approche de base consiste à les conserver comme des offsets depuis l’adresse de base.
typedef struct ShmRef {
uint64_t offset; // position relative au début du segment
uint32_t length;
uint32_t kind;
} ShmRef;
Ainsi, chaque processus peut résoudre base + offset vers sa propre adresse.
5.5 Casser l’ABI
La mémoire partagée est une promesse binaire, pas du code source. Autrement dit, chacune des différences suivantes compte.
- Taille de
int/long - Représentation de
bool - Type sous-jacent des
enum - Taille de
wchar_t - Différences 32 bits / 64 bits
#pragma pack- Différences de compilateur / de langage
- Alignement / padding
- Little-endian / big-endian
Au sein d’un même hôte, l’endianness est en général cohérente, mais il suffit d’ajouter un support ARM64 ou une toolchain mixte pour que des décalages parfaitement ordinaires apparaissent.
C’est pourquoi, pour les structures placées en mémoire partagée, nous recommandons fortement :
- Des entiers de largeur fixe comme
uint32_t/uint64_t - Des zones de padding / reserved explicites
- Un en-tête avec
version,header_size,record_size,total_size static_assert(sizeof(...))si nécessaire- Aucun objet non trivial
5.6 Races d’initialisation
La mémoire partagée casse facilement sur l’hypothèse que « celui qui l’a créée l’a forcément initialisée ».
Sous Windows, si CreateFileMapping tombe sur un nom existant, elle retourne l’objet existant, et GetLastError() indique ERROR_ALREADY_EXISTS.
Les pages initiales d’un mapping adossé au pagefile démarrent à zéro.8
Sous POSIX, un nouvel objet de mémoire partagée commence avec une longueur de 0, et obtient sa taille via ftruncate. Les octets nouvellement alloués sont initialisés à zéro. La création avec O_CREAT | O_EXCL est atomique.3
Sans connaître cette différence, si vous :
- l’utilisez immédiatement après l’ouverture
- n’avez pas de flag d’initialisation terminée
- laissez les participants s’initialiser en même temps
- ne vérifiez pas les incohérences de version
alors ça casse selon l’ordre de démarrage.
Au minimum, il vaut mieux placer ces états dans l’en-tête initial :
INITIALIZINGREADYBROKEN
Et seul le créateur initialise ; les rejoignants (joiners) attendent READY.
Cette seule discipline apaise considérablement les choses.
5.7 Ne pas penser à la récupération après crash
Que faire si le writer meurt en cours de mise à jour des données partagées ? Livrer en production avec ce point non défini, et le visage de tout le monde devient soudain grave lors d’un incident.
Un mutex Windows devient abandoned quand le thread propriétaire se termine sans le libérer, et les waiters reçoivent WAIT_ABANDONED. Cela signifie que la ressource partagée peut être dans un état indéterminé.12
Avec les mutex robustes POSIX aussi, quand le propriétaire meurt, EOWNERDEAD est retourné, et après avoir réparé l’état, on appelle pthread_mutex_consistent().1314
L’important, ici, c’est de ne pas « continuer tant bien que mal ». La récupération nécessite au moins l’un des éléments suivants :
- Un numéro de génération
- La dernière séquence commit avec succès
- Un heartbeat
- Un flag dirty / clean
- Un commit en deux phases de type journal
- Une procédure de réinitialisation complète en cas de corruption
5.8 False sharing et contention de ligne de cache
On dit souvent que la mémoire partagée est rapide. Mais si des compteurs très sollicités (hot) sont tassés dans la même ligne de cache, celle-ci fait des allers-retours entre CPU, et les performances se dégradent allègrement.
L’exemple classique :
- Le producer met à jour
write_index - Le consumer met à jour
read_index - Les deux se trouvent sur la même ligne de cache
Dans ce cas :
- Séparer les champs hot sur des lignes de cache distinctes
- Séparer les champs mis à jour fréquemment de ceux mis à jour rarement
- Viser un writer par ligne de cache
Cela seul change déjà beaucoup les choses. On entend souvent parler d’alignement sur 64 octets ; considérez cela comme « 64 octets est une valeur courante sur de nombreux CPU, pas une loi absolue ».
5.9 Prendre à la légère les noms, permissions et la sécurité
La mémoire partagée nommée est pratique, mais des noms et permissions traités à la légère provoquent des incidents.
Sous Windows :
- Il existe les espaces de noms
Global\etLocal\ - Créer un file mapping
Global\depuis l’extérieur de la session 0 nécessiteSeCreateGlobalPrivilege - Les noms d’objets partagent un espace de noms avec les events / sémaphores / mutex / waitable timers / jobs
Autrement dit :
- Vous le nommez
"Global\\MyApp"en pensant que le service et l’application de bureau pourront le partager - Mais ça échoue sur les permissions
- Et par-dessus le marché, un mutex portant le même nom a été créé avant, et vous obtenez
ERROR_INVALID_HANDLE
Le genre de boue bien typique de Windows qui remonte à la surface.
Côté POSIX aussi, traiter à la légère le mode ou l’umask de shm_open rend les choses visibles de façon inutilement large, ou au contraire impossibles à ouvrir.3
La mémoire partagée n’est pas « juste de la mémoire, donc sûre ». Depuis n’importe quel processus disposant de la permission de lecture, elle est plutôt visible sans détour. Si vous y placez des informations confidentielles, il faut y réfléchir dans le même contexte que la mémoire ordinaire : paging / swap / dumps / permissions.
5.10 Redimensionner et faire évoluer à la légère
« J’aimerais l’agrandir un peu plus tard » est une demande plutôt dangereuse pour la mémoire partagée.
- Un objet mapping Windows a une taille fixée à sa création8
- Sous POSIX aussi, sans garder
ftruncateetmmapcohérents, la longueur mappée par les participants finit par ne plus correspondre316
En pratique, il est plus sûr de rendre la taille immuable au sein d’une génération. Si une extension est nécessaire :
- Créer un segment avec une nouvelle version / un nouveau nom / une nouvelle génération
- Basculer les participants
- Fermer l’ancien segment
Cela réduit le taux d’incidents.
5.11 Faire tout passer, y compris les notifications, par la mémoire partagée
Un schéma courant :
- Écrire
ready = 1dans la mémoire partagée - L’autre côté fait
while (!ready) Sleep(1);
Ça marche au début. Mais ensuite, cela revient sous la forme de :
- CPU gaspillé
- Latence qui varie à cause de
Sleep(1) - Mises à jour manquées difficiles à remarquer
- Timeouts et notifications d’arrêt difficiles à écrire proprement
Il vaut mieux orienter la mémoire partagée vers le côté données, et déporter la notification vers des primitives que l’on peut attendre.
- Windows : event / sémaphore / mutex / named pipe, etc.217
- POSIX : sémaphores / mutex process-shared + condvar, etc.1819
5.12 Penser « avec ça, je peux aussi partager avec d’autres machines »
Il y a un moment où l’on est tenté de penser : si j’utilise un mapping adossé à un fichier et que je mappe un fichier partagé via le réseau, je pourrai peut-être aussi faire de la mémoire partagée entre machines.
C’est dangereux.
La documentation de CreateFileMapping sous Windows indique elle aussi que la cohérence n’est pas garantie pour les fichiers distants.
Si deux machines mappent la même page en écriture, chacune ne voit que ses propres écritures, et rien n’est fusionné lors de la mise à jour du disque.8
La mémoire partagée est fondamentalement un mécanisme à hôte unique. Pour franchir plusieurs machines, choisir directement socket / RPC / message broker préserve bien mieux votre santé mentale.
6. Bonnes pratiques
6.1 Séparer le plan de contrôle et le plan de données
Placez uniquement les données en masse dans la mémoire partagée ; déportez notifications et transitions d’état vers un canal séparé.
- Mémoire partagée : frame, sample, batch, snapshot
- Event / sémaphore / pipe / socket : ready, consumed, stop, error, reconnect
Cette séparation améliore la clarté de la conception, avant même d’améliorer la performance.
6.2 Placer un en-tête fixe en tête
Au minimum, nous recommandons fortement un en-tête initial comme celui-ci.
typedef struct SharedHeader {
uint32_t magic;
uint16_t abi_version;
uint16_t header_size;
uint32_t state; // 0=initializing, 1=ready, 2=broken
uint32_t flags;
uint64_t total_size;
uint64_t generation;
uint64_t heartbeat_ns;
uint64_t payload_offset;
uint64_t payload_size;
uint64_t write_seq;
uint64_t read_seq;
uint8_t reserved[64];
} SharedHeader;
Les points clés sont :
magicrejette les segments étrangers ou non initialisésabi_versionetheader_sizerejettent les différences de disposition (layout)staterejette les états en cours d’initialisationgenerationdétecte les recréationsheartbeatsurveille la vivacitéreservedlaisse une porte de sortie pour les extensions futures
Ce qui est pénible avec la mémoire partagée, c’est qu’« on voit mal ce qui se passe ». C’est précisément pour cela qu’on lui donne dès le départ des métadonnées d’observabilité.
6.3 Utiliser des références par offset
Conservez les références sous forme d’offsets, pas de pointeurs.
- Résoudre via
base + offset - Ajouter une vérification de plage sur
offset + length - Définir une valeur sentinelle pour les valeurs invalides
Cela seul réduit considérablement les incidents liés à une inadéquation d’adresse.
6.4 Restreindre le modèle de concurrence
La mémoire partagée devient brutalement plus difficile à mesure que les writers se multiplient. Donc, au départ, l’une de ces deux options est solide :
- Ring buffer SPSC
- Snapshot 1 writer / plusieurs readers
S’il faut plusieurs writers, les choses se passent en général mieux en réduisant le nombre de points de responsabilité de la cohérence, par exemple :
- Seul l’enqueue est lock-free / atomique
- Les mises à jour réelles des données sont concentrées sur un seul consumer
6.5 Expliciter le protocole de commit
Une conception où vous ne pouvez pas expliquer en mots « à partir de quel instant peut-on lire en toute sécurité » est dangereuse.
Pour un double buffer, par exemple, on définit le rituel de publication :
- Écrire dans le buffer non publié
- Finaliser le checksum et la longueur
- Basculer l’index du buffer actif avec une sémantique release
- Le reader lit l’index actif avec une sémantique acquire
- Après lecture, vérifier que l’index n’a pas changé
6.6 Fixer la taille par génération
Plutôt que de redimensionner en place, découper en générations comme
name = MyShm.v3abi_version = 3generation = 42
est plus facile à maintenir.
La mémoire partagée ne fait pas de « vérification de type à l’appel » comme le ferait une API. C’est pourquoi il est si important de ne pas casser un ABI une fois qu’il a été fixé.
6.7 Intégrer l’observabilité
Au minimum, disposer de ces éléments aide beaucoup.
- Heure de la dernière mise à jour
- Dernière séquence réussie
- Nombre de drops / d’overwrites
- Nombre d’incohérences de version
- Nombre d’attach / detach
- Dernier code d’erreur
- Heartbeat
Quand la mémoire partagée casse, les logs sont en général maigres. Ajouter vos propres compteurs rend la réponse aux incidents considérablement plus facile.
6.8 Écrire d’abord les tests de scénarios anormaux
Le chemin nominal seul ne suffit pas. Il faut au minimum examiner ceci :
- Terminaison forcée du writer en cours de mise à jour
- Le ring déborde à cause d’un reader qui accuse du retard
- Connexion avec une incohérence de version
- Mélange 32 bits / 64 bits
- Ouverture à travers des sessions différentes
- Permissions insuffisantes
- Un processus précédent redémarre en conservant une ancienne génération
- Cache miss / effets NUMA lors d’un transfert continu de données volumineuses
Pour la mémoire partagée, les tests de casse valent plus que les tests du chemin nominal.
7. Points à vérifier entre Windows et POSIX
| Aspect | Windows | POSIX |
|---|---|---|
| Création / ouverture | CreateFileMapping / OpenFileMapping / MapViewOfFile 6 |
shm_open / ftruncate / mmap 3 |
| Partage sans fichier disque | Mapping adossé au pagefile avec INVALID_HANDLE_VALUE 68 |
Objet de mémoire partagée POSIX + mmap 3 |
| Valeurs initiales | Les pages adossées au pagefile sont initialisées à zéro 8 | Les nouveaux objets démarrent avec une longueur de 0. Les octets nouvellement alloués sont initialisés à zéro 3 |
| Synchronisation | mutex / sémaphore / event / interlocked, etc. 25 | Mutex / condvar / sémaphore process-shared 2018 |
| À ne pas utiliser entre processus | CRITICAL_SECTION, WaitOnAddress 2110 |
Mutex / condvar laissés en PTHREAD_PROCESS_PRIVATE 2019 |
| Mort du propriétaire | WAIT_ABANDONED 12 |
Mutex robuste + EOWNERDEAD / pthread_mutex_consistent() 1314 |
| Suppression du nom | Disparaît quand le dernier handle / la dernière vue est libéré 28 | shm_unlink supprime le nom. L’objet persiste tant qu’il reste des références 2223 |
| Espace de noms / permissions | Global\ / Local\, ACL, SeCreateGlobalPrivilege 1524 |
mode, umask, espace de noms, O_CREAT|O_EXCL 3 |
Le MemoryMappedFile de C# est aussi, pour l’essentiel, un wrapper au-dessus du file mapping Windows.
Les bases ne changent donc pas :
- Ouvrir sous le même nom
- Utiliser un mutex / event séparé
- Lire les vues avec une disposition explicite
- Ne jamais placer directement de références d’objets
8. La checklist à consulter en premier
- La mémoire partagée est-elle vraiment nécessaire ? S’agit-il de données volumineuses sur le même hôte ?
- Le plan de contrôle et le plan de données sont-ils séparés ?
- Le modèle de concurrence peut-il être ramené à SPSC / 1 writer, plusieurs readers ?
- L’en-tête initial contient-il magic / version / size / state / generation / heartbeat ?
- Placez-vous un pointeur /
HANDLE/ descripteur de fichier / objet STL /std::mutex? - Existe-t-il un protocole de commit garantissant que les readers ne voient jamais d’états intermédiaires ?
- Un seul initialisateur est-il désigné ?
- Existe-t-il une procédure de récupération en cas de terminaison anormale ?
- Les noms et permissions sont-ils explicites ?
Global\est-il vraiment nécessaire ?- Supposez-vous un redimensionnement en place ?
- Avez-vous testé la mort du writer / le blocage du reader / l’incohérence de version / des permissions insuffisantes ?
9. Conclusion
Bien utilisée, la mémoire partagée est véritablement puissante. En particulier pour de grandes quantités de données au sein d’une seule machine, telles que :
- Images
- Audio
- Flux de capteurs
- Gros batches
- Snapshots à haute fréquence
elle est vraiment efficace.
Cependant, l’essence de la mémoire partagée est moins la « vitesse » qu’un transfert de responsabilité. En échange de moins de copies et de moins de messagerie médiée par le noyau, vous prenez en charge :
- La synchronisation
- La visibilité
- L’initialisation
- L’ABI
- La récupération
- Les permissions
- L’observabilité
Ainsi, pour votre première implémentation, la forme sûre est :
- Un ring buffer SPSC ou un double buffer
- Un en-tête initial fixe
- Des références par offset
- Une notification sur un canal séparé
- Avec version / generation / heartbeat
- Avec des tests de scénarios anormaux
En partant de cette forme, la mémoire partagée devient un outil plutôt bien élevé. La traiter dès le premier jour comme « une mémoire commune rapide où tout est permis », et elle cesse peu à peu d’être une application pour devenir de l’archéologie.
10. Ressources de référence
- Windows : bases du file mapping et de la mémoire partagée nommée682
- Windows : espace de noms / sécurité / synchronisation1524512
- POSIX :
shm_open,shm_unlink,mmap, synchronisation process-shared / robuste322162013 - .NET : présentation de
MemoryMappedFile1
-
Microsoft Learn, “Memory-Mapped Files” / Microsoft Learn, “MemoryMappedFile Class” ↩ ↩2 ↩3
-
Microsoft Learn, “/volatile (volatile Keyword Interpretation)” / Microsoft Learn, “volatile (C++)” ↩ ↩2
-
Microsoft Learn, “Interlocked Variable Access” / Microsoft Learn, “MemoryBarrier function” ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, “Creating Named Shared Memory” ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, “Scope of Allocated Memory” ↩ ↩2
-
Microsoft Learn, “CreateFileMappingA function” ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
man7.org, “POSIX Shared Memory”, diapositives de formation ↩
-
Microsoft Learn, “WaitOnAddress function” ↩ ↩2
-
Microsoft Learn, “MapViewOfFileEx function” / Microsoft Learn, “MapViewOfFile function” ↩
-
Microsoft Learn, “Mutex Objects” ↩ ↩2 ↩3
-
man7.org, “pthread_mutex_lock(3p)” / man7.org, “pthread_mutexattr_setrobust(3)” ↩ ↩2 ↩3
-
man7.org, “pthread_mutex_consistent(3)” / man7.org, “pthread_mutex_consistent(3p)” ↩ ↩2
-
Microsoft Learn, “Kernel object namespaces” ↩ ↩2 ↩3
-
Microsoft Learn, “Using Mutex Objects” ↩
-
man7.org, “sem_init(3)” / man7.org, “sem_init(3p)” ↩ ↩2
-
Microsoft Learn, “Critical Section Objects” ↩
-
man7.org, “shm_unlink(3p)” ↩ ↩2
-
man7.org, “shm_open(3)” (sémantique de shm_unlink) ↩
-
Microsoft Learn, “File Mapping Security and Access Rights” ↩ ↩2
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Checklist pour gérer les processus enfants en toute sécurité dans une application Windows
Pour gérer en toute sécurité les processus enfants dans une application Windows, l'important n'est pas l'API de lancement mais la concept...
Appeler une DLL C# Native AOT depuis C/C++
Nous faisons le point sur la publication d'une bibliothèque de classes C# en DLL native avec Native AOT, et sur l'appel depuis C/C++ des ...
Compatibilité descendante des interfaces DLL et COM — Tableau de décision : quels changements cassent les appelants
Quels changements apportés à une DLL ou à un composant COM cassent réellement leurs appelants ? Nous détaillons les trois niveaux de comp...
Versionner le schéma de base de données d'une application métier — pratiques de migration pour éviter que « chaque client ait une base différente »
Guide pratique pour versionner le schéma de base de données d'applications métier dont les bases sont dispersées chez chaque client. Impl...
CI/CD pratique pour les applications WinForms / WPF — Automatiser du build à la signature et à la distribution avec GitHub Actions
Guide pratique pour mettre en place le CI/CD des applications WinForms / WPF avec GitHub Actions. Couvre un YAML minimal de build+tests s...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Services liés à ce sujet
Cet article est directement lié aux services suivants.
Développement d'applications Windows
L'échange de données volumineuses et la conception d'une séparation des processus à l'aide de la mémoire partagée, du file mapping et de MemoryMappedFile sont des sujets directement liés au développement d'applications Windows.
Conseil technique et revue de conception
Le travail de conception qui réduit le taux d'incidents — stratégie de synchronisation, conception de l'ABI, stratégie de récupération, séparation entre plan de contrôle et plan de données — se prête bien au conseil technique et à la revue de conception.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- Une valeur écrite dans la mémoire partagée peut-elle être lue immédiatement et correctement par un autre processus ?
- Non, ce sont deux problèmes distincts. La mémoire partagée est un mécanisme qui présente la même suite d'octets à plusieurs processus ; ce n'est pas la synchronisation elle-même. Même si l'écrivain (writer) a l'intention d'écrire dans l'ordre length, payload puis ready flag, un lecteur (reader) qui lit sans aucune synchronisation peut voir un nouveau length combiné à un ancien payload. Que ce soit sous Windows ou POSIX, l'accès à la mémoire partagée est censé être combiné à un moyen de synchronisation tel qu'un mutex, un sémaphore ou un event.
- Peut-on placer des pointeurs, des std::string ou des HANDLE dans la mémoire partagée ?
- Mieux vaut s'en abstenir. Les adresses virtuelles et les ressources propres à un processus n'ont de sens que dans le contexte de ce processus, et mapper le même mapping dans un autre processus ne garantit pas que les adresses virtuelles coïncident. Il en va de même pour std::vector, std::mutex ou CRITICAL_SECTION. S'il faut des références, il est plus sûr de les conserver sous forme d'offset depuis l'adresse de base, et de faire tendre les données placées en mémoire partagée vers des entiers de largeur fixe, une disposition explicite et un en-tête versionné.
- L'utilisation de volatile rend-elle la synchronisation de la mémoire partagée inutile ?
- Non. volatile n'est pas un sortilège qui sauve la conception de la mémoire partagée : l'atomicité et l'exclusion mutuelle restent, au minimum, des problèmes distincts. Une conception qui place un volatile bool et le surveille par busy loop gaspille le CPU, rend floue la garantie d'ordre entre le payload et le ready flag, et capte facilement des états intermédiaires. De plus, WaitOnAddress sous Windows est destiné aux threads au sein d'un même processus ; il est plus sûr de ne pas le considérer comme un mécanisme d'attente inter-processus. Il vaut mieux déporter la notification vers des primitives que l'on peut attendre, comme un event ou un sémaphore.
- Que faut-il décider en premier lors de la conception d'une mémoire partagée ?
- Il y en a quatre. Séparer le plan de contrôle et le plan de données : le contrôle (démarrage, arrêt, notification) passe par la messagerie, tandis que les données elles-mêmes vont dans la mémoire partagée. Restreindre le modèle de concurrence (au départ, un ring buffer SPSC ou un double buffer est le moins sujet aux incidents). Décider du propriétaire et de la durée de vie : qui crée, qui initialise, qui supprime, qui restaure. Et concevoir l'ABI, y compris la disposition et la version. Le simple fait de placer magic, version, size, state, generation et heartbeat dans l'en-tête initial change considérablement la facilité d'investigation en cas d'incident.
Profil de l’auteur
Page de présentation de l’auteur de l’article.
Go Komura
Représentant de KomuraSoft LLC
Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.
Liens publics