C'est possible, bien qu'il existe des problèmes de cohérence du cache spécifiques à l'architecture que vous devrez peut-être prendre en compte. Certaines architectures ne permettent tout simplement pas d'accéder simultanément à la même page à partir de plusieurs adresses virtuelles sans perte de cohérence. Ainsi, certaines architectures gèreront cette amende, d'autres non.
Modifié pour ajouter :AMD64 Architecture Programmer's Manual vol. 2, Programmation système, section 7.8.7 Modification du type de mémoire, indique :
Une page physique ne doit pas avoir différents types de capacité de mise en cache qui lui sont attribués via différents mappages virtuels ; ils doivent être tous d'un type pouvant être mis en cache (WB, WT, WP) ou tous d'un type non pouvant être mis en cache (UC, WC, CD). Sinon, cela peut entraîner une perte de cohérence du cache, entraînant des données obsolètes et un comportement imprévisible.
Ainsi, sur AMD64, il devrait être sûr de mmap()
le même fichier ou la même région de mémoire partagée, tant que le même prot
et flags
sont utilisés; cela devrait amener le noyau à utiliser le même type pouvant être mis en cache pour chacun des mappages.
La première étape consiste à toujours utiliser une sauvegarde de fichier pour les cartes mémoire. Utilisez mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_NORESERVE, fd, 0)
afin que les mappages ne réservent pas d'échange. (Si vous oubliez cela, vous rencontrerez les limites d'échange beaucoup plus tôt que vous n'atteindrez les limites réelles réelles pour de nombreuses charges de travail.) La surcharge supplémentaire causée par la sauvegarde d'un fichier est absolument négligeable.
Modifié pour ajouter :l'utilisateur strcmp a souligné que les noyaux actuels n'appliquent pas la randomisation de l'espace d'adressage aux adresses. Heureusement, cela est facile à résoudre, en fournissant simplement des adresses générées aléatoirement à mmap()
au lieu de NULL
. Sur x86-64, l'espace d'adressage utilisateur est de 47 bits et l'adresse doit être alignée sur la page ; vous pouvez utiliser par ex. Xorshift* pour générer les adresses, puis masquer les bits indésirables :& 0x00007FFFFE00000
donnerait des adresses 47 bits alignées sur 2097152 octets, par exemple.
Étant donné que le support est vers un fichier, vous pouvez créer un deuxième mappage vers le même fichier, après avoir agrandi le fichier de support à l'aide de ftruncate()
. Ce n'est qu'après une période de grâce appropriée - lorsque vous savez qu'aucun thread n'utilise plus le mappage (peut-être utiliser un compteur atomique pour garder une trace de cela ?) - que vous démappez le mappage d'origine.
En pratique, lorsqu'un mappage doit être agrandi, vous agrandissez d'abord le fichier de sauvegarde, puis essayez mremap(mapping, oldsize, newsize, 0)
pour voir si le mappage peut être agrandi, sans déplacer le mappage. Ce n'est qu'en cas d'échec du remappage sur place que vous devez passer au nouveau mappage.
Modifié pour ajouter :vous voulez certainement utiliser mremap()
au lieu d'utiliser simplement mmap()
et MAP_FIXED
pour créer un mappage plus grand, car mmap()
démappe (atomiquement) tous les mappages existants, y compris ceux appartenant à d'autres fichiers ou régions de mémoire partagée. Avec mremap()
, vous obtenez une erreur si le mappage agrandi chevauche des mappages existants ; avec mmap()
et MAP_FIXED
, tous les mappages existants que le nouveau mappage chevauche sont ignorés (non mappés).
Malheureusement, je dois admettre que je n'ai pas vérifié si le noyau détecte les collisions entre les mappages existants, ou s'il suppose simplement que le programmeur est au courant de ces collisions -- après tout, le programmeur doit connaître l'adresse et la longueur de chaque mappage, et devrait donc savoir si le mappage entrerait en collision avec un autre existant. Modifié pour ajouter :les noyaux de la série 3.8 le font, renvoyant MAP_FAILED
avec errno==ENOMEM
si la cartographie agrandie entrerait en collision avec des cartes existantes. Je m'attends à ce que tous les noyaux Linux se comportent de la même manière, mais je n'ai aucune preuve, à part les tests sur 3.8.0-30-generic sur x86_64.
Notez également que sous Linux, la mémoire partagée POSIX est implémentée à l'aide d'un système de fichiers spécial, généralement un tmpfs monté à /dev/shm
(ou /run/shm
avec /dev/shm
étant un lien symbolique). Le shm_open()
et. al sont implémentés par la bibliothèque C. Au lieu d'avoir une grande capacité de mémoire partagée POSIX, j'utiliserais personnellement un tmpfs spécialement monté pour une utilisation dans une application personnalisée. Si ce n'est pour rien d'autre, les contrôles de sécurité (utilisateurs et groupes capables d'y créer de nouveaux "fichiers") sont beaucoup plus faciles et plus clairs à gérer.
Si le mappage est, et doit être, anonyme, vous pouvez toujours utiliser mremap(mapping, oldsize, newsize, 0)
pour essayer et redimensionnez-le ; cela peut échouer.
Même avec des centaines de milliers de mappages, l'espace d'adressage 64 bits est vaste et les cas d'échec rares. Ainsi, bien que vous deviez également gérer le cas d'échec, il ne doit pas nécessairement être rapide . Modifié pour modifier:sur x86-64, l'espace d'adressage est de 47 bits et les mappages doivent commencer à une limite de page (12 bits pour les pages normales, 21 bits pour les pages énormes 2M et 30 bits pour les pages énormes 1G), il n'y a donc que 35, 26 ou 17 bits disponibles dans l'espace d'adressage pour les mappages. Ainsi, les collisions sont plus fréquentes, même si des adresses aléatoires sont suggérées. (Pour les mappages 2M, 1 024 cartes ont eu une collision occasionnelle, mais pour 65 536 cartes, la probabilité d'une collision (échec de redimensionnement) était d'environ 2,3 %.)
Modifié pour ajouter :l'utilisateur strcmp a souligné dans un commentaire que par défaut Linux mmap()
renverra des adresses consécutives, auquel cas la croissance du mappage échouera toujours à moins qu'il ne s'agisse du dernier, ou qu'une carte n'ait été démappée juste là.
L'approche que je connais fonctionne sous Linux est compliquée et très spécifique à l'architecture. Vous pouvez remapper le mappage d'origine en lecture seule, créer un nouveau mappage anonyme et y copier l'ancien contenu. Vous avez besoin d'un SIGSEGV
gestionnaire (SIGSEGV
signal émis pour le thread particulier qui essaie d'écrire dans le mappage désormais en lecture seule, c'est l'un des rares SIGSEGV
récupérables situations sous Linux même si POSIX n'est pas d'accord) qui examine l'instruction qui a causé le problème, la simule (en modifiant le contenu du nouveau mappage à la place), puis ignore l'instruction problématique. Après une période de grâce, lorsqu'il n'y a plus de threads accédant à l'ancien mappage, désormais en lecture seule, vous pouvez supprimer le mappage.
Toute la méchanceté est dans le SIGSEGV
gestionnaire, bien sûr. Non seulement il doit être capable de décoder toutes les instructions machine et de les simuler (ou du moins celles qui écrivent en mémoire), mais il doit également être en attente si le nouveau mappage n'a pas encore été complètement copié. C'est compliqué, absolument non portable et très spécifique à l'architecture... mais possible.
Cela a été ajouté dans le noyau 5.7 en tant que nouveau drapeau à mremap(2) appelé MREMAP_DONTUNMAP. Cela laisse le mappage existant en place après le déplacement des entrées de la table des pages.
Voir https://github.com/torvalds/linux/commit/e346b3813067d4b17383f975f197a9aa28a3b077#diff-14bbdb979be70309bb5e7818efccacc8
Oui, vous pouvez le faire.
mremap(old_address, old_size, new_size, flags)
supprime l'ancien mappage uniquement de la taille "old_size". Ainsi, si vous passez 0 comme "old_size", cela ne démappera rien du tout.
Attention :cela fonctionne comme prévu uniquement avec les mappages partagés, donc un tel mremap() doit être utilisé sur une région précédemment mappée avec MAP_SHARED. C'est en fait tout cela, c'est-à-dire que vous n'avez même pas besoin d'un mappage sauvegardé sur fichier, vous pouvez utiliser avec succès Combinaison "MAP_SHARED | MAP_ANONYMOUS" pour les drapeaux mmap(). Certains systèmes d'exploitation très anciens peuvent ne pas prendre en charge "MAP_SHARED | MAP_ANONYMOUS", mais sous Linux, vous êtes en sécurité.
Si vous essayez cela sur une région MAP_PRIVATE, le résultat serait à peu près similaire à memcpy(), c'est-à-dire qu'aucun alias de mémoire ne sera créé. Mais il utilisera toujours les machines CoW. D'après votre question initiale, il n'est pas clair si vous avez besoin d'un alias ou si la copie CoW convient également.
MISE À JOUR :pour que cela fonctionne, vous devez également spécifier le drapeau MREMAP_MAYMOVE évidemment.