Cet article présente en détail le gestionnaire de mémoire virtuelle de Solaris 8, les commandes qui permettent de visualiser la configuration et la consommation mémoire, ainsi que les paramètres du noyau sur lesquels il est possible de jouer.

Concepts et définitions

Mémoire physique

La mémoire physique s’obtient simplement par la commande : prtconf | grep Memory ou prtdiag | grep Memory.

Page mémoire La mémoire est découpée en pages, la page étant l’unité d’allocation élémentaire gérée par le système d’exploitation. La taille d’une page sous Solaris est généralement de 8 Ko. Elle est donnée par la commande pagesize. Jusqu’à Solaris 8 (inclus), cette taille de page est fixe, et unique. A partir de Solaris 9, il est possible de disposer de plusieurs tailles de pages, allant jusqu’à 16 Go. Le kernel maintient une structure de données pour chaque page physique existant dans le système, ainsi qu’une série de listes chaînées pour accélérer les traitements (ie, une liste chaînée de l’ensemble des pages physiques libres).

Mémoire virtuelle

La mémoire virtuelle est un mécanisme permettant d’agrandir l’espace d’adressage (c’est-à-dire l’espace mémoire qu’il est capable de gérer, ou adresser) des processus du système, en leur donnant l’impression qu’ils ont à disposition beaucoup plus de mémoire qu’il n’en est réellement physiquement disponible. Cela peut se faire notamment étendant la mémoire avec de l’espace disque. Le gestionnaire de mémoire virtuelle utilise une table appelée Hardware Address Translation (HAT) pour traduire les adresses virtuelles en adresses physiques (et réciproquement). Le gestionnaire de mémoire virtuelle permet également de protéger l’espace mémoire du kernel de tous les processus utilisateurs, et d’isoler ceux des différents processus les uns des autres. Notons que toute la couche de gestion de la mémoire virtuelle est (par définition et par implémentation) entièrement transparente pour les processus du système (à l’exception du kernel). En particulier, la mémoire virtuelle est vue par les processus comme un espace d’adressage linéaire et continu, même si, physiquement, la mémoire réelle peut ne pas l’être (ce qui est souvent le cas).

Mémoire résidente

La mémoire résidente est la mémoire physique occupée, c’est-à-dire l’ensemble des pages virtuelles actuellement présentes en mémoire physique.

Mémoire partagée

Une page de mémoire partagée est une page qui peut être adressée simultanément dans l’espace d’adressage de plusieurs processus. Les deux utilisations principales de ce mécanisme sont d’une part les bibliothèques partagées, et d’autre part les InterProcess Communication (IPC) de type Shared Memory (shm).

Pagination

Le mécanisme de pagination (on-demand paging) va de pair avec la mémoire virtuelle : puisque la mémoire virtuelle présentée au système est plus grande que la mémoire physique réelle, il est nécessaire de pouvoir déplacer des données de la mémoire physique vers le disque (page out), et réciproquement (page in).

Architecture d’un processus

Il est difficile de parler de mémoire virtuelle sans présenter rapidement la façon dont un processus est agencé en mémoire. Bien entendu, puisqu’il s’agit de la vision qu’en a le processus, on ne parle que d’adresses virtuelles, jamais physiques.

Segments

Lorsqu’un processus est lancé, le kernel utilise les informations contenues dans l’exécutable ELF (Executable and Linking Format) pour mapper les différentes sections du programme dans l’espace mémoire du processus. Cette image mémoire est découpée en partitions logiques appelées segments. En pratique, un segment est un bloc d’adresses contiguës. Parmi les segments que l’on va typiquement retrouver dans un processus, on peut citer text (code exécutable), data (incluant notamment le heap, ou tas), stack (pile utilisée pour les appels de fonctions), ainsi que des segments pour toutes les bibliothèques externes chargées ou encore les fichiers qui sont ouverts par le processus. Chaque segment d’un processus est décrit par une structure seg. Cette structure pointe (par l’intermédiaire de la structure segvn_data) sur deux nouvelles structures : vp (vnode, virtual file node), et amp (anon_map, anonymous pages mapping).

Vnodes et pages anonymes

Selon l’origine et la nature des données, elles peuvent être décrites par deux types de structures en mémoire. Les vnodes correspondent à des fichiers mappés en mémoire (exécutables, bibliothèques), et ne sont en fait qu’une couche d’abstraction vis-à-vis du filesystem. On peut donc notamment toujours lier un vnode à un inode sur un filesystem. D’autre part, ces données n’ont pas vocation à évoluer. Les pages sur lesquelles pointent ces structures sont décrites par un couple (vnode, offset), c’est-à-dire au final un fichier (inode) et un offset dans le fichier. Les pages anon, elles, sont utilisées soit pour des données initialement lues dans un fichier mais qui ont été modifiées, soit pour des données qui ont été créées au cours de la vie du processus (stack, heap, …). En d’autres termes, elles servent pour des données qui sont appelées à être modifiées. Elles sont anonymes en ce sens qu’elles ne correspondent pas à un fichier nommé.

Architecture détaillée

Le diagramme ci-dessous présente un peu plus en détail les structures composant un processus et le cheminement suivi pour accéder au backing store (cf section suivante). On notera que dans un segment, la partie segvn_data est composée de deux pointeurs, vp qui se réfère à une structure vnode, et amp qui se réfère à un tableau de pointeurs sur des structures anon.

Processus

Le swap

Le terme swap étant utilisé dans plusieurs sens, il est souvent difficile de comprendre ce qu’il recouvre vraiment. Le plus souvent, on utilise ce terme pour parler du swapfs, mais aussi parfois pour parler de l’ensemble des devices et fichiers de swap, ce qui ne revient pas tout à fait au même.

Device de swap

Puisque la mémoire virtuelle utilise du disque pour simuler une plus grande quantité de mémoire que ce qui est réellement présent sur le système, elle a besoin de zones sur disques dédiées à cette fonction. Un device de swap est donc un device du système intégralement dédié à la mémoire virtuelle (ie, /dev/vx/dsk/swapvol). Ce device servira à la couche swapfs.

Fichier de swap

De même que l’on a des devices dédiés au swap, on peut avoir des fichiers sur des filesystems fournissant la même fonctionnalité. Ces fichiers serviront également à la couche swapfs.

Backing store

Le mécanisme de pagination présenté plus haut implique que l’on sorte des pages de la mémoire physique pour libérer de l’espace. Les processus auxquels ces pages appartiennent auront probablement besoin de les récupérer tôt ou tard, et il faut donc stocker ces pages quelque part. Ce quelque part est appelé backing store, et n’est pas le même pour les pages liées à un vnode et pour les pages anonymes. Pour les vnodes, il s’agit de données lues dans un fichier et qui n’ont pas changé (ie, le code d’un exécutable). Quand on veut libérer une telle page, on se contente donc de marquer la page physique comme libre, tout en conservant la structure associée (en particulier le couple vnode et offset). On retrouvera donc les données dans les fichiers où elles ont été lues initialement, et le backing store est le filesystem lui-même. Pour une page anonyme, le kernel utilise une couche intermédiaire (appelée anon layer), qui sert d’interface avec le swapfs. Ce swapfs est le backing store des pages anonymes, et utilise un mécanisme interne de nommage lui aussi à base de vnodes. C’est également la couche swapfs qui gère l’allocation du backing store et la pagination des pages anonymes.

Swapfs

Comme on vient de le voir, le swapfs est le backing store des pages anonymes, c’est-à-dire de toutes les pages mémoires sauf les données statiques lues dans des fichiers. Cet espace est en fait un espace de swap virtuel, qui inclut bien sûr tous les devices et fichiers de swap présents, mais aussi une partie de la mémoire physique. Cette notion peut paraître surprenante : si on doit sortir une page de la mémoire physique, on n’a aucun intérêt à la swapper en mémoire physique! En fait, c’est le design du gestionnaire de mémoire qui veut ça : pour toute page anonyme en mémoire, on doit impérativement avoir une page correspondante (réservée ou allouée) dans le swapfs. Cela permet d’avoir un mécanisme d’allocation mémoire plus propre, qui contrôle mieux les possibilités d’erreurs dues au manque de ressources : impossible d’allouer de la mémoire si on n’a pas les ressources pour swapper les pages correspondantes, et on évite ainsi le cas où le kernel veut sortir une page de la mémoire physique mais n’a plus de place en backing store. L’utilisation de RAM dans le swapfs permet ainsi à un système de fonctionner, même sans espace physique (fichier ou device) de swap. Par ailleurs, l’espace physique du swapfs est toujours réservé/alloué en priorité par rapport à l’espace en RAM. Cette dernière n’est donc utilisée qu’après saturation du swap physique. La quantité de RAM présente dans le swap est égale au total de la mémoire physique, auquel on soustrait d’une part toutes les pages allouées au noyau (statiquement ou non), et d’autre part la portion de mémoire physique que le swap ne peut pas utiliser (typiquement 1/8e).

Tmpfs

Le filesystem /tmp est alloué sur l’espace de swap, c’est-à-dire sur le swapfs. Il s’agit donc d’un filesystem résidant en mémoire virtuelle, et utilisant swapfs et non ufs pour allouer les blocs de données. En conséquence, tout fichier créé dans /tmp réduit donc directement l’espace de swapfs disponible.

Swapping vs paging

Le swapping est une fonctionnalité quasiment inutilisée par Solaris, sauf dans des cas très critiques, qui consiste à sortir d’un seul coup l’intégralité d’un processus de la mémoire physique. Solaris utilise quasiment exclusivement le paging (champs pi et po de vmstat – pour page in et page out), et non le swapping (champs si et so, visibles via vmstat –S).

Réservation et allocation de swap

Une zone de swap est réservée quand elle a été bloquée pour servir de backup store à une page anonyme mais que les données n’y ont encore jamais été écrites. Elle est allouée à partir du moment où les données y ont été écrites. En pratique, cela ne change pas grand-chose puisque l’espace est considéré comme occupé de toute façon.

Cache filesystem

Le système gère dynamiquement la taille du cache utilisé pour les I/O sur les filesystems. En Solaris 8, ce cache est limité à une taille maximale exprimée en pourcentage de la mémoire physique (12% par défaut), mais nous ne disposons pas d’outils permettant de mesure le niveau réel d’utilisation de la mémoire à un instant donné.

Libération de mémoire

On l'a vu, selon les cas, la libération d’une page mémoire va ou non générer une écriture dans le swapfs. Intéressons-nous maintenant aux algorithmes utilisés par le gestionnaire de mémoire pour chercher les pages à libérer.

Least Recently Used (LRU)

Cet algorithme va scanner les pages dès que la mémoire libre tombe en dessous d’un certain seuil (lotfree), et libérer les pages les plus anciennes.

Priority paging

Cet algorithme est une variante du précédent, utilisé sous Solaris 2.6 et 7, et qui introduit un seuil supplémentaire (cachefree = 2 x lotsfree) qui ne libère que les pages utilisées pour cacher des données liées au filesystem et qui n’ont pas été modifiées récemment.

Cyclical Page Cache

Cet outil n’est pas un algorithme de libération de mémoire, mais le système de cache du filesystem introduit dans Solaris 8. Précédemment, ce cache était présent dans la mémoire virtuelle et était donc en compétition avec les processus pour obtenir des ressources mémoire. Il est maintenant isolé, et n’est plus en compétition qu’avec lui-même : une grosse activité sur un filesystem ne va réclamer que des pages déjà attribuées à cette fonctionnalité. En conséquence, le priority paging n’est plus disponible (et n’a plus lieu d’exister) sous Solaris 8. Cela signifie également qu’avant Solaris 8, il était impossible de différencier un manque de mémoire d’une grosse activité sur les filesystems.

Mémoire, swap et commandes Solaris

top

Le champ SIZE correspond à la taille totale du process (code, données et pile), et RES correspond à la partie du process résidant actuellement en mémoire physique. Ces deux valeurs sont exprimées en Ko.

swap –s

Cette commande retourne l’espace utilisé dans le swapfs, en le découpant en mémoire allouée et en mémoire réservée.

ps

La Virtual Memory Size (VSZ) de ps, obtenue par exemple avec ps –edf –o vsz,args ne représente pas la mémoire réellement utilisée par le processus, car la VSZ inclut la mémoire non allouée du processus, ainsi que les bibliothèques et les segments de mémoire partagée. Le champ SZ de ps est identique à VSZ, mais exprimée en nombre de pages (dont la taille, souvent 8192 octets, peut être obtenue par la commande pagesize).

kstat –n system_pages (ou netstat –k system_pages)

Retourne les valeurs de différents compteurs intégrés au kernel :

# kstat -n system_pages module: unix instance: 0 name: system_pages class: pages availrmem 218099 cachefree 3886 crtime 60.2351946 desfree 1943 desscan 25 econtig 318242816 fastscan 8192 freemem 8987 kernelbase 268435456 lotsfree 3886 minfree 971 nalloc 28961713 nalloc_calls 9932 nfree 28869606 nfree_calls 8803 nscan 0 pagesfree 8987 pageslocked 30622 pagestotal 248721 physmem 251879 pp_kernel 27806 slowscan 100 snaptime 421095.3073906

La plupart de ces valeurs sont détaillées dans la section suivante. Parmi les autres, on retiendra :

  • pp_kernel : taille du noyau (en pages)
  • physmem : mémoire physique utilisable (excluant notamment les pages statiques du noyau)
  • pageslocked : pages verrouillées (non swappables)
  • freemem : mémoire physique disponible (en pages)

Découpage du swapfs : disque et mémoire physique

La quantité d’espace sur disque présente dans le swap est simple à mesurer (swap –l), et un swap –s accompagné une bête soustraction permet d’en déduire directement la quantité de RAM présente dans le swap. Cette quantité de mémoire physique représente en fait l’intégralité de la mémoire physique moins trois éléments : les pages statiques allouées au noyau au boot de la machine (qui n’apparaissent même pas dans la mémoire physique disponible physmem), le reste de l’espace alloué au kernel (inclut dans physmem et explicité dans pp_kernel), et la réserve de mémoire physique que le swap ne peut pas utiliser (définie par minfree). On a donc :

  • mémoire statiquement allouée au noyau = mémoire physique totale – physmem
  • swap total en RAM = physmem – pp_kernel - minfree

Kernel tunables sous Solaris 8

Swap

  • swapfs_reserve est la quantité de mémoire virtuelle que l’on réserve pour les processus d’uid 0. Par défaut, le minimum entre 1/16e de la RAM et 4 Mo.
  • swapfs_minfree est la quantité de mémoire physique que l’on souhaite garder pour le reste du système. En dessous de ce seuil de RAM disponible, le système ne pourra plus utiliser la RAM comme backing store. Par défaut, le maximum entre 2 Mo et 1/8e de la RAM.

Pagination

Attention : il est généralement préférable de laisser le système gérer lui-même ces paramètres et de ne pas les forcer dans le fichier /etc/system. Cela est encore plus vrai pour les systèmes capable de gérer la dynamic reconfiguration, qui recalculent normalement ces paramètres à la volée en cas de modification du hardware présent, sauf quand ils sont forcés dans le /etc/system.

  • lotsfree est le seuil initial (comptée en pages) en dessous duquel le système va commencer à chercher des pages à libérer. Par défaut, le maximum entre 512 ko et 1/64e de la RAM. Ce paramètre est dynamique.
  • desfree est la quantité de mémoire libre (comptée en pages) que l’on souhaite avoir à tout instant sur le système. Quand on atteint ce seuil, le système ne libère plus la mémoire utilisée par le noyau. Par défaut, lotfree/2.
  • minfree est la quantité de mémoire libre (comptée en pages) minimale acceptable. En dessous de ce seuil, le système se concentre sur la libération de mémoire, et cesse d’allouer de la mémoire pour les processus utilisateurs. Par défaut, desfree/2.
  • throttlefree est le niveau de mémoire (comptée en pages) à partir duquel les demandes d’allocation mémoire sont mises en sommeil, même s’il y a de la mémoire disponible. Par défaut, minfree.
  • pageout_reserve est le nombre de pages mémoires à réserver exclusivement pour les threads de pagination et d’ordonnancement (pageout et scheduler). En dessous de ce seuil, les allocations sont refusées pour tous les autres processus. Par défaut, throttlefree/2.
  • pages_pp_maximum définit le nombre minimal de pages qui ne doivent pas être verrouillées (une page verrouillée ne peut pas être swappée). En dessous de ce seuil, les requêtes de verrouillage sont refusées.
  • tune_t_minarmem est la mémoire résidente minimale (comptée en pages) à maintenir pour éviter des deadlocks. Le système se réserve cette quantité de mémoire, et elle n’apparaît pas quand il détermine la quantité maximale de mémoire disponible. Par défaut, 25 pages.
  • fastscan est le nombre maximal de pages que le système va regarder (en vue de les libérer) par seconde. Par défaut, le minimum entre 64 Mo et la moitié de la RAM.
  • slowscan est le nombre minimal de pages que le système va regarder par seconde. Par défaut, le minimum entre 100 pages et 1/20e de la RAM.
  • min_percent_cpu est le pourcentage CPU minimal que le daemon pageout peut consommer. Par défaut, 4%.
  • handspreadpages est un paramètre spécifique à l’algorithme de libération des pages mémoires. Cet algorithme agit en deux temps : d’abord, la mémoire est parcourue et des pages sont marquées comme inutilisées. Simultanément et un tout petit peu après, une seconde passe vérifie si les pages sont toujours marquées comme inutilisées, et si oui, elles sont libérées. Ce paramètre définit l’intervalle entre les deux processus, en pages : si le premier processus est en train de regarder la page N, le second en est à la page (N – handspreadpages). Par défaut, fastscan.
  • pages_before_pager participe à la définition d’un seuil en dessous duquel les pages libérées après un I/O (dans le système de pagination) sont libérées immédiatement, au lieu d’être conservées pour une éventuelle réutilisation par le même processus. Ce seuil est égal à (lotsfree + pages_before_pager). Par défaut, 200 pages.
  • maxpgio est la taille de la queue des requêtes d’I/O dans le système de pagination, c’est-à-dire le nombre de requêtes simultanées de pagination qui peuvent être en attente d’exécution. Par défaut, 40.

Tmpfs

  • tmpfs:tmpfs_maxkmem définit la quantité maximale (en octets) de mémoire kernel que tmpfs peut utiliser pour ses structures de données (eg, répertoires). Par défaut, le maximum entre une page et 4% de la RAM.
  • tmpfs:tmpfs_minfree est la quantité minimale de swap que tmpfs doit laisser au reste du système. Par défaut, 256 pages.

File system

  • segmap_percent définit le pourcentage maximal de la mémoire physique qui peut être utilisé comme cache pour les accès au filesystem. Par défaut, 12%.