web-dev-qa-db-fra.com

Utilisation de fichiers pour la mémoire partagée IPC

Dans mon application, il existe un processus qui écrit des données dans un fichier, puis, en réponse à la réception d'une demande, enverra (certaines) de ces données via le réseau au processus demandeur. La base de cette question est de voir si nous pouvons accélérer la communication lorsque les deux processus se trouvent sur le même hôte. (Dans mon cas, les processus sont Java, mais je pense que cette discussion peut s'appliquer plus largement.)

Il existe quelques projets qui utilisent les MappedByteBuffers renvoyés par FileChannel.map () de Java pour partager la mémoire IPC entre les JVM sur le même hôte (voir Chronicle Queue, Aeron IPC, etc.).

Une approche pour accélérer la communication entre le même hôte consisterait à faire en sorte que mon application utilise l'une de ces technologies pour fournir le chemin de demande-réponse pour la communication avec le même hôte, soit en conjonction avec le mécanisme existant pour écrire dans le fichier de données, soit en fournissant un moyen unifié de communication et d'écriture dans le fichier.

Une autre approche consisterait à permettre au processus demandeur d'avoir un accès direct au fichier de données.

J'ai tendance à privilégier la deuxième approche - en supposant qu'elle soit correcte - car elle serait plus facile à mettre en œuvre et semble plus efficace que de copier/transmettre une copie des données pour chaque requête (en supposant que nous n'ayons pas remplacé le mécanisme d'écriture existant au fichier).

Essentiellement, j'aimerais comprendre ce qui se passe exactement lorsque deux processus ont accès au même fichier et l'utilisent pour communiquer, en particulier Java (1.8) et Linux (3.10)).

D'après ma compréhension, il semble que si deux processus ont le même fichier ouvert en même temps, la "communication" entre eux se fera essentiellement via la "mémoire partagée".

Notez que cette question n'est pas concernée par l'implication sur les performances de l'utilisation d'un MappedByteBuffer ou non - il semble très probable que l'utilisation de tampons mappés et la réduction de la copie et des appels système réduiront la surcharge par rapport à la lecture et à l'écriture du fichier, mais que peut nécessiter des modifications importantes de l'application.

Voici ma compréhension:

  1. Lorsque Linux charge un fichier à partir du disque, il copie le contenu de ce fichier sur des pages en mémoire. Cette région de mémoire est appelée le cache de page. Pour autant que je sache, il le fait indépendamment de la méthode Java) (FileInputStream.read (), RandomAccessFile.read (), FileChannel.read (), FileChannel.map ()) ou La méthode native est utilisée pour lire le fichier (obseved avec "free" et la surveillance de la valeur "cache").
  2. Si un autre processus tente de charger le même fichier (alors qu'il réside toujours dans le cache), le noyau le détecte et n'a pas besoin de recharger le fichier. Si le cache de page est plein, les pages seront expulsées - les pages sales étant réécrites sur le disque. (Les pages sont également réécrites s'il y a un vidage explicite sur le disque, et périodiquement, avec un thread du noyau).
  3. Avoir déjà un (gros) fichier dans le cache est une amélioration significative des performances, bien plus que les différences basées sur les méthodes Java) que nous utilisons pour ouvrir/lire ce fichier.
  4. Si un fichier est chargé en utilisant l'appel système mmap (C) ou via FileChannel.map () (Java), essentiellement les pages du fichier (dans le cache) sont chargées directement dans l'espace d'adressage du processus. En utilisant d'autres méthodes pour ouvrir un fichier, le fichier est chargé dans des pages qui ne se trouvent pas dans l'espace d'adressage du processus, puis les différentes méthodes pour lire/écrire ce fichier copient certains octets de/vers ces pages dans un tampon dans l'espace d'adressage du processus . Il y a un avantage évident en termes de performances à éviter cette copie, mais ma question ne concerne pas les performances.

Donc, en résumé, si je comprends bien - bien que le mappage offre un avantage en termes de performances, il ne semble pas qu'il offre des fonctionnalités de "mémoire partagée" que nous n'obtenons pas déjà uniquement par la nature de Linux et du cache de page.

Alors, faites-moi savoir ce que je comprends.

Merci.

3

Essentiellement, j'essaie de comprendre ce qui se passe lorsque deux processus ont le même fichier ouvert en même temps, et si l'on peut l'utiliser pour offrir en toute sécurité et de manière performante une communication entre les processus.

Si vous utilisez des fichiers normaux en utilisant les opérations read et write (c'est-à-dire sans les mapper en mémoire), les deux processus ne partagent aucune mémoire.

  • La mémoire de l'espace utilisateur dans les objets Java Buffer associés au fichier n'est PAS partagée entre les espaces d'adressage.
  • Lorsqu'un appel système write est effectué, les données sont copiées depuis les pages d'un processus de l'espace d'adressage vers les pages de l'espace noyau. (Ces pourraient être des pages dans le cache de pages. Cela est spécifique au système d'exploitation.)
  • Lorsqu'un appel système read est effectué, les données sont copiées des pages de l'espace noyau vers les pages de l'espace d'adressage des processus de lecture.

Cela doit être fait de cette façon. Si le système d'exploitation partageait les pages associées au lecteur et à l'écrivain traitant les tampons derrière leur dos, alors ce serait un trou de sécurité/fuite d'informations:

  • Le lecteur pourrait voir des données dans l'espace d'adressage de l'écrivain qui n'avaient pas encore été écrites via write(...), et ne le seraient peut-être jamais.
  • Le rédacteur pourrait voir les données que le lecteur a (hypothétiquement) écrites dans sa mémoire tampon de lecture.
  • Il ne serait pas possible de résoudre le problème en utilisant intelligemment la protection de la mémoire, car la granularité de la protection de la mémoire est une page par rapport à la granularité de read(...) et write(...) qui est aussi peu qu'un simple octet.

Bien sûr: vous pouvez utiliser en toute sécurité la lecture et l'écriture de fichiers pour transférer des données entre deux processus. Mais vous auriez besoin de définir un protocole qui permette au lecteur de savoir combien de données l'écrivain a écrit. Et le lecteur sachant quand l'écrivain a écrit quelque chose pourrait impliquer un sondage; par exemple. pour voir si le fichier a été modifié.

Si vous regardez cela uniquement en termes de copie de données dans le "canal" de communication

  • Avec les fichiers mappés en mémoire, vous copiez (sérialisez) les données des objets du tas d'application vers le tampon mappé, et une deuxième fois (désérialisez) du tampon mappé vers les objets du tas d'application.

  • Avec les fichiers ordinaires, il y a deux copies supplémentaires: 1) du tampon des processus d'écriture (non mappé) vers les pages de l'espace noyau (par exemple dans le cache de page), 2) des pages de l'espace du noyau vers le tampon des processus de lecture (non mappé) .

L'article ci-dessous explique ce qui se passe avec la lecture/écriture conventionnelle et le mappage mémoire. (C'est dans le contexte de la copie d'un fichier et de la "copie zéro", mais vous pouvez l'ignorer.)

Référence:

0
Stephen C

Ma question est, sur Java (1.8) et Linux (3.10), les MappedByteBuffers sont-ils vraiment nécessaires pour implémenter la mémoire partagée IPC, ou un accès à un fichier commun fournirait-il la même fonctionnalité?

Cela dépend de pourquoi vous voulez implémenter IPC de mémoire partagée.

Vous pouvez clairement implémenter IPC sans mémoire partagée; par exemple. sur les prises. Donc, si vous ne le faites pas pour des raisons de performances, il n'est pas du tout nécessaire de faire de la mémoire partagée IPC!

La performance doit donc être à la base de toute discussion.

L'accès à l'aide de fichiers via les API classiques io ou nio Java ne fournit pas de fonctionnalités ou de performances de mémoire partagée.

La principale différence entre les E/S de fichiers ordinaires ou les E/S de socket et la mémoire partagée IPC est que la première nécessite que les applications effectuent explicitement des appels système read et write pour envoyer et recevoir messages. Cela implique des appels système supplémentaires et le noyau copie les données. De plus, s'il y a plusieurs threads, vous avez besoin soit d'un "canal" séparé entre chaque paire de threads, soit de quelque chose pour multiplexer plusieurs "conversations" sur un canal partagé. Ce dernier peut conduire le canal partagé à devenir un goulot d'étranglement de concurrence.

Notez que ces frais généraux sont orthogonaux au cache de page Linux.

En revanche, avec IPC implémenté en utilisant la mémoire partagée, il n'y a pas d'appels système read et write, ni d'étape de copie supplémentaire. Chaque "canal" peut simplement utiliser une zone distincte du tampon mappé. Un thread dans un processus écrit des données dans la mémoire partagée et il est presque immédiatement visible pour le deuxième processus.

La mise en garde est que les processus doivent 1) se synchroniser et 2) implémenter des barrières de mémoire pour s'assurer que le lecteur ne voit pas les données périmées. Mais ceux-ci peuvent tous deux être mis en œuvre sans appels système.

Dans le nettoyage, la mémoire partagée IPC utilisant des fichiers mappés en mémoire >> est << plus rapide que l'utilisation de fichiers ou de sockets conventionnels, et c'est pourquoi les gens le font.


Vous avez également demandé implicitement si la mémoire partagée IPC peut être implémentée sans fichiers mappés en mémoire.

  • Un moyen pratique serait de créer un fichier mappé en mémoire pour un fichier qui vit dans un système de fichiers à mémoire uniquement; par exemple. un "tmpfs" sous Linux.

    Techniquement, c'est toujours un fichier mappé en mémoire. Cependant, vous n'encourez pas de surcharge de vidage des données sur le disque et vous évitez le problème de sécurité potentiel des données privées IPC qui se retrouvent sur le disque.

  • Vous pourriez en théorie implémenter un segment partagé entre deux processus en procédant comme suit:

    • Dans le processus parent, utilisez mmap pour créer un segment avec MAP_ANONYMOUS | MAP_SHARED.
    • Fork des processus enfants. Ceux-ci finiront tous de partager le segment entre eux et avec le processus parent.

    Cependant, implémenter cela pour un processus Java serait ... difficile. AFAIK, Java ne prend pas en charge cela.

Référence:

0
Stephen C

Il convient de mentionner trois points: les performances, les modifications simultanées et l'utilisation de la mémoire.

Vous avez raison de dire que MMAP offrira généralement un avantage de performance par rapport aux E/S basées sur fichier. En particulier, l'avantage de performance est significatif si le code exécute beaucoup de petits IO à un point arbitraire du fichier.

pensez à changer le N-ième octet: avec mmap buffer[N] = buffer[N] + 1, et avec un accès basé sur des fichiers, vous avez besoin (au moins) de 4 appels système + vérification des erreurs:

   seek() + error check
   read() + error check
   update value
   seek() + error check
   write + error check

Il est vrai que le nombre réel de IO (sur le disque)) sera probablement le même.

Le deuxième point mérite de noter l'accès simultané. Avec les E/S basées sur les fichiers, vous devez vous soucier des accès simultanés potentiels. Vous devrez émettre un verrouillage explicite (avant la lecture) et déverrouiller (après l'écriture), pour empêcher deux processus d'accéder de manière incorrecte à la valeur en même temps. Avec la mémoire partagée, les opérations atomiques peuvent éliminer le besoin d'un verrou supplémentaire.

Le troisième point est l'utilisation réelle de la mémoire. Dans les cas où la taille des objets partagés est importante, l'utilisation de la mémoire partagée peut permettre à un grand nombre de processus d'accéder aux données sans allouer de mémoire supplémentaire. Si les systèmes sont limités par la mémoire ou le système qui doit fournir des performances en temps réel, cela pourrait être le seul moyen d'accéder aux données.

0
dash-o