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.

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 libresactive: le nombre de pages actives, donc utiliséesinactive: le nombre de pages inactives, donc pouvant être réclamées si le système a besoin de mémoirewired: le nombre de pages “branchées”, c’est-à-dire réservées par le noyau ou mlock(2) éesvnodepages: le nombre de pages contenant le contenu de fichiersvtextpages: 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 :
- La quantité totale de mémoire
- La quantité de mémoire utilisée
Ces deux points soulèvent deux questions :
- 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. - Qu’est-ce qu’on entend par “mémoire utilisée” ? La partie facile c’est de
considérer que
freeetinactivereprésentent de la mémoire disponible, et queactiveetwiredreprésentent de la mémoire utilisée. Et le cache,vnodepagesetvtextpages, 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
.