En tant qu’utilisateur d’ i3 , j’aime afficher les informations telles que le niveau de batterie, la température du processeur ou la mémoire (RAM) consommée en bas de mon écran.

Capture d’écran de i3status-again

Le programme i3status ne supporte pas l’affichage de la mémoire sur OpenBSD, et i3status-rust, une alternative écrite en Rust, n’est carrément compatible qu’avec Linux.
J’ai donc décidé de développer ma propre barre , en Rust, et en développant le bloc qui affiche la mémoire j’ai compris que calculer la mémoire utilisée était une tâche plus difficile que prévue. Lors de ma première tentative, j’ai d’ailleurs égaré 22 Gio de mémoire.

La mémoire sur OpenBSD

Avant de détailler les différentes approches possibles pour calculer la mémoire, il faut comprendre un peu comment elle est gérée par le noyau OpenBSD.
Premièrement il faut savoir si on parle de l’ensemble de la mémoire (hw.physmem), ou seulement de la mémoire accessible à l’utilisateur (hw.usermem), c’est-à-dire hw.physmem moins la partie que se réserve le noyau pour fonctionner. Je choisis de me baser sur hw.physmem, l’ensemble de la mémoire incluant ce que se réserve le noyau mais excluant la partie que se garde le firmware et à laquelle le système n’a pas accès.

Pour donner un exemple : sur une machine possédant 32 Gio de mémoire, on peut imaginer que hw.physmem vaille 31,75 Gio et hw.usermem 31,10 Gio. Cela signifie que le firmware de la machine garde 250 Mio et que le noyau OpenBSD se réserve 650 Mio.

La structure uvmexp pour comptabiliser la mémoire

Maintenant qu’on sait de quoi on parle, voyons comment OpenBSD gère les données liées à la mémoire. En utilisant sysctl(2) pour récupérer les informations sur la mémoire, on récupère une structure uvmexp . Voici les champs qui nous intéressent :

  • pagesize: la taille d’une page mémoire, en octets (exemple 4096, soit 4 Kio)
  • npages: le nombre total de pages dans la mémoire (plus de 8,1 millions sur une machine avec 31,75 Gio de RAM)
  • free: le nombre de pages libres
  • active: le nombre de pages actives, donc utilisées
  • inactive: le nombre de pages inactives, donc pouvant être réclamées si le système a besoin de mémoire
  • wired: le nombre de pages “branchées”, c’est-à-dire réservées par le noyau ou mlock(2) ées
  • vnodepages: le nombre de pages contenant le contenu de fichiers
  • vtextpages: le nombre de pages contenant le contenu de programmes exécutables (sous-ensemble de vnodepages)

Avant de continuer il peut être utile d’expliquer ce que sont les caches “vnode” et “vtext”, et à quoi ils servent. Le principe est simple, lorsqu’un fichier est lu sur le disque, il est stocké dans le cache pour gagner du temps la prochaine fois qu’un utilisateur souhaitera le lire à nouveau.

Exemple concret

Imaginons que j’utilise less pour lire un fichier appelé “fichier.txt”. Le système va charger le contenu du programme less dans le cache vtext, si l’exécutable pèse 144 Kio ça représente 36 pages de 4 Kio stockées dans le cache (je simplifie, en vérité seules les instructions sont stockées). Ensuite il stocke fichier.txt dans le cache vnode, s’il pèse 4 Kio ça représente 1 page stockée dans le cache. Ça c’était juste la partie système, mais fichier.txt est stocké une seconde fois en mémoire, sur le tas (la zone mémoire où les programmes allouent dynamiquement leur données), pour que less y accède, ce qui représente encore 1 page utilisée.
Les pages stockées dans le cache vtext sont comptabilisées dans le champ vtextpages (et dans vnodepages puisque vtextpages en est un sous-ensemble). La page stockée dans le cache vnode est comptabilisée dans vnodepages. Et enfin la page stockée sur le tas est comptabilisée dans active.

Lorsque less termine son exécution, le système récupère la page comptabilisée dans active qui devient free, puisqu’elle est à nouveau disponible. Par contre, le contenu du binaire less et de fichier.txt est gardé en cache. Si j’ai à nouveau besoin d’exécuter less ou de lire fichier.txt plus tard, le système n’aura pas besoin de les lire depuis le disque, il pourra y accéder rapidement depuis le cache.

On peut toutefois faire une distinction : pendant que less s’exécutait les pages en cache étaient utilisées, mais maintenant qu’il a terminé les pages ne sont plus utilisées, elles sont là “au cas où”.
C’est ce “au cas où” qui rend la mesure délicate.

Les différentes approches pour compter la mémoire

C’est là que ça devient intéressant. Quand on veut afficher un simple pourcentage comme dans la capture d’écran en haut de l’article pour indiquer combien de mémoire est utilisée, il faut connaître :

  1. La quantité totale de mémoire
  2. La quantité de mémoire utilisée

Ces deux points soulèvent deux questions :

  1. La quantité totale, c’est plutôt hw.physmem ou hw.usermem ? Il n’y a pas de bonne ou mauvaise réponse à cette question, ça dépend si le développeur considère qu’il faut compter la mémoire réservée par le noyau ou non. Ce n’est pas de la mémoire que l’utilisateur pourra récupérer même s’il ferme tous ses programmes, donc on peut être tenté de préférer hw.usermem. D’un autre côté l’utilisateur peut trouver incohérent d’avoir une quantité totale de mémoire plus petite alors que la mémoire du noyau lui appartient aussi, donc on peut être tenté d’utiliser hw.physmem.
    J’ai choisi que dans mon programme la quantité totale était hw.physmem, mais ça ne signifie pas que c’est la bonne réponse, c’est simplement un choix.
  2. Qu’est-ce qu’on entend par “mémoire utilisée” ? La partie facile c’est de considérer que free et inactive représentent de la mémoire disponible, et que active et wired représentent de la mémoire utilisée. Et le cache, vnodepages et vtextpages, c’est de la mémoire disponible ou utilisée ? Techniquement le cache peut être réclamé si le système a besoin de mémoire donc on pourrait considérer cette mémoire comme disponible, mais d’un autre côté une partie du cache est activement utilisée, donc on pourrait considérer le cache comme de la mémoire occupée.

Approche 1 : le cache est de la mémoire utilisée

Lors de ma première tentative en développant le bloc qui affiche la mémoire dans mon programme, j’ai oublié de comptabiliser le cache, pensant que free incluait toute la mémoire disponible, cache ou non. Voici le calcul :

total_memory = npages  * pagesize
free_memory = (free + inactive) * pagesize
used_memory = total_memory - free_memory

C’était involontaire, mais en ne comptant que les pages free et inactive comme disponibles, je comptais le cache comme de la mémoire utilisée. J’ai mis un moment à comprendre pourquoi mon programme affichait 80% de mémoire utilisée. En utilisant un système léger comme OpenBSD et un gestionnaire de fenêtres minimaliste comme i3, consommer 25 Gio de RAM avec seulement un navigateur Web et deux émulateurs de terminal ouverts, ça paraît impossible.
À force de creuser je me suis rendu compte que les caches vnode et vtext pesaient 22 Gio. N’ayant pas le détail de ce qui était activement utilisé ou non j’ai supposé que la majorité du cache était là au cas où, vue mon utilisation sur le moment je ne pouvais pas utiliser activement 22 Gio de fichiers et programmes.

En tant qu’utilisateur de mon propre programme cette approche ne me convenait pas, car elle donnait l’impression que ma mémoire était presque pleine alors qu’en réalité la majorité du cache était certainement inutilisée. Indiquer 80% de mémoire utilisée était trompeur.

Approche 2 : le cache est de la mémoire disponible

N’étant pas satisfait avec la première approche, j’ai décidé d’inverser mon calcul et de considérer le cache comme de la mémoire disponible.

total_memory = npages  * pagesize
used_memory = (active + wired) * pagesize
free_memory = total_memory - used_memory

Cette fois, seules les pages active et wired étaient considérées comme de la mémoire utilisée, le reste était de la mémoire disponible.
À noter que ce n’est pas parfait non plus, si je reprends mon exemple avec less qui lit un fichier, les caches vnodes et vtext pendant l’exécution du programme seront considérés comme de la mémoire disponible, alors qu’en réalité cette mémoire est utilisée.
Avec un exemple aussi petit on ne s’en rend pas compte, mais si j’utilisais Emacs (dont exécutable fait ~8 Mio) pour ouvrir un fichier de 1 Gio, ça ferait une vraie différence dans la mémoire, et ne pas la compter est aussi un problème.

Conclusion

Il n’y a pas de bonne réponse entre ces deux approches, les deux sont imparfaites. J’ai considéré que la seconde approche était la moins mauvaise et surtout la plus proche de la vérité. Si un jour OpenBSD permet de savoir quelle quantité de cache est utilisée activement, j’améliorerai mon programme.

Et les autres, comment font-ils ?
Waybar , une barre utilisable avec Sway (une alternative à i3 qui fonctionne avec Wayland au lieu de X11), a la même approche que moi. Le programme utilise hw.physmem pour compter la mémoire totale et il compte les caches vnode et vtext comme de la mémoire disponible.
Le projet i3status a reçu une proposition d’un contributeur pour gérer la mémoire sur OpenBSD, et cette fois c’est un peu différent : il considère que la mémoire totale est hw.usermem, et que la mémoire disponible est free + les caches, sans inactive.

Cette expérience m’aura permis de plonger dans le noyau OpenBSD, comprendre comment il gère la mémoire, et également d’avoir enfin la quantité de mémoire affichée sur mon écran.
Mon programme, i3status-again , est prévu pour être portable sur plusieurs systèmes d’exploitation, je verrai comment les autres gèrent ce sujet de la mémoire.


Comprendre comment votre système gère ses ressources fait partie de mon quotidien.
Si vos serveurs méritent le même niveau d’attention, découvrez mes services ou contactez-moi .