samedi 1 mars 2014

Linux: Mémoire occupée par un processus




On peut obtenir des informations sur chaque processus Linux en allant fouiner dans /proc. Chaque processus a une arborescence propre dans /proc/[pid].On peut ainsi obtenir des informations sur l'occupation et l'activité mémoire de ce processus.

Prenons un exemple. Lançons un processus qui ne fait rien d'autre que dormir. Ce processus exécute un binaire créé par édition de liens dynamique.

 

 

 

/proc/[pid]/status

fa $ ./memory &
[1] 3522
fa $

Regardons son état (résultat tronqué)
fa $ cat /proc/3522/status
Name:    memory
...
Pid:    3522
...
VmSize:        2128 kB
...
VmRSS:         312 kB
...

fa $

On apprend ici que la taille cumulée des régions mémoires valides dans l'espace d'adressage de ce processus est de 2128 KB. Et qu'il occupe actuellement 312 KB de mémoire. Les  2128 KB - 312 KB soit 1816 KB "manquants" correspondent à des pages qui n'ont pas encore été référencées au cours de l'exécution de ce programme.

/proc/[pid]/maps

On peut regarder de manière plus détaillée la disposition de l'espace d'adressage de ce processus. Là encore le résultat est volontairement tronqué.
fa $ cat /proc/3522/maps
08048000-0804b000 r-xp 00000000 08:01 260689     /home/fa/memory
0804b000-0804c000 r--p 00002000 08:01 260689     /home/fa/memory
0804c000-0804d000 rw-p 00003000 08:01 260689     /home/fa/memory
....
bfc74000-bfc95000 rw-p 00000000 00:00 0          [stack]

fa $

Pour chaque région (chaque plage d'adresses définie) on trouve:
  • une adresse de début
  • une adresse de fin (l'adresse du premier caractère après la région)
  • des indicateurs déterminant si la région est accessible en lecture (r) écriture (w), exécution(x) et si elle est privée à ce processus (p) ou partagée (s).
  • un déplacement (offset) par rapport au début du fichier si cette région projette (mappe) un fichier.
  • le numéro (08:01) de périphérique où est stocké le fichier
  • le numéro d'inode (260689) du fichier
  • le chemin d'accès au fichier
Ici on a trois régions correspondant dans l'ordre au segment de code, à un segment de données en lecture seule, et aux données statiques du programmes.
Si on prend la région de code elle s'étend de l'adresse 08048000 à l'adresse0804B000. Elle a donc une taille de 3 pages (B - 8 = 3).

Il est à noter que la région de code est considérée comme "mappée" de manière privative... Tiens? Ne m'a-t-on pas dit, redit etc à l'envie que le code était partagé entre processus issus d'un même binaire?

Oui. Le code est partagé mais ça ne se voit pas!  En fait, si un peu comme on le verra plus bas. La région de code est considérée comme "privée". Ça permet par exemple de placer le processus sous contrôle d'un débogueur (gdb quoi) qui peut modifier le code pour insérer un point d'arrêt sans que ce point d'arrêt soit appliqué à tous les processus partagent ce code, ce qui serait le cas si la région de code était "mappée - partagée".

/proc/[pid]/smaps

On peut obtenir plus de détails sur ces régions grâce au fichier /proc/[pid]/smaps. La sortie est là encore tronquée pour se limiter aux champs qui nous intéressent:

fa $ cat /proc/3522/smaps
08048000-0804b000 r-xp 00000000 08:01 260689     /home/fa/memory
Size:                 12 kB
Rss:                  12 kB
Pss:                  12 kB

Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:        12 kB
Private_Dirty:         0 kB
...

fa $

On retrouve en première ligne la description de région obtenue en regardant /proc/[pid]/maps.Puis:
  • Size: la taille de la région virtuelle
  • Rss : la taille résidente en mémoire pour cette région
  • Pss : la taille résidente rapportée au prorata des régions partagent une page physique
La taille résidente compte toutes les pages physiques associées à cette région. Ici visiblement, les 3 pages ont été accédées et ont donc été chargées en mémoire depuis le fichier binaire exécutable.
Si on lance un autre processus depuis le même binaire exécutable, on obtient pour le nouveau processus:

fa $ cat/proc/3826/smaps
08048000-0804b000 r-xp 00000000 08:01 260689     /home/fa/memory
Size:                 12 kB
Rss:                  12 kB
Pss:                   6 kB
Shared_Clean:         12 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         0 kB

...
fa $

RSS versus PSS

On trouve donc la même valeur pour le champ RSS.  Si on prend une collection de processus, par exemple, tous les processus Oracle, ou toutes les JVM, sur un système et que l'on additionne "bêtement" leurs champs RSS... on peut se retrouver avec une valeur de somme qui dépasse la taille de la mémoire physique!

Pas d'histoire de "swap" ici, on parle bien de mémoire résidente donc présente dans la RAM du système. L'approche qui consiste à ajouter des champs RSS, ignore le fait que les pages physiques accessibles depuis une région d'un processus peuvent être "partagées", accessibles depuis une autre région d'un autre du processus, voire du même processus.

Si l'on regarde attentivement, le résultat pour le premier processus, les 3 pages étaient comptabilisées aussi sous le champ : "Private_Clean", à savoir pages non modifiées en mémoire (leur contenu est identique à ce qui se trouve sur disque dans le fichier) et référencées uniquement par cette région.

Le résultat pour le 2ème processus  montre 3 pages comptabilisées sous le champ: "Shared_Clean". D'ailleurs si on ré-exécute la commande sur le premier processus, les 3 pages ne sont plus vues comme "Private_Clean" mais comme "Shared_Clean".

Comme exécute deux fois le même binaire exécutable, l'OS (feignant mais surtout efficace) réutilise les pages déjà chargées. La mémoire résidente pour cette région de code vue de chaque processus est légitimement de 12KB, mais  il n'y a pas 24 KB occupés en mémoire mais seulement 12KB . Le champ RSS est donc valide pour un processus mais doit être considéré avec circonspection quand on regarde plusieurs processus.

PSS

Le champ PSS est là pour remédier à ce problème. On va regarder page par page si chacune des pages est référencée par une seule ou plusieurs régions, et n'imputer au processus que la taille de la page au prorata du nombre de références vers cette page.

D'ailleurs, on voit que le champ PSS du deuxième processus n'est pas de 12KB mais seulement de 6KB. Si l'on regarde à nouveau le fichiers "smaps" du premier processus, on voit qu'il est passé à 6KB aussi.

On a donc bien 6KB + 6KB, 12 KB chargés en mémoire.

Vérification de partage

dans un système Linux, on peut s'amuser à vérifier le partage des pages. A chaque processus est associé un fichier /proc/[pid]/pagemap.
Voir http://lxr.free-electrons.com/source/Documentation/vm/pagemap.txt

Pour une fois il ne s'agit pas d'un fichier "ASCII" dans /proc, mais d'un fichier binaire. Chaque entrée de 64 bits décrit une page (par défaut  4KB) de l'espace d'adressage virtuel du processus. Sachant qu'une entrée fait donc 8 bytes, et que l'adresse du premier octet de  notre région est d'après le fichier maps: 08048000

La page correspondante a donc le numéro  0x08048 . En faisant la conversion en décimal,  en multipliant par 8 et en utilisant  la commande od, on obtient:

fa $ od -A d -j 262720 -N 8 -t x8 /proc/3832/pagemap
0262720 a600000000008614

fa $

La page physique correspondant à notre première page de code est ldonc la page N° 0x8614. On va donc pouvoir vérifier si cette page est effectivement partagée ou non. Comment? Linux fournit un fichier /proc/kpagecount qui fournit ce genre d'information.On reprend la valeur 8614, on la convertit en décimal, on multiplie par 8, et hop:

fa $ sudo od -A d -j 274592 -N 8 -t x8 /proc/kpagecount
0274592 0000000000000002

fa $

On peut bien sûr vérifier sur l'autre processus et s'amuser à répéter l'opération avec 3, 4, ...6, ... 12 processus ou plus.

Le fichier kpagecount est documenté avec le fichier pagemap. Il faut être super-utilisateur pour pouvoir y accéder.

"Anomalies"

Poursuivons notre expérimentation, en chargeant un nouvel exemplaire de notre programme.

fa $ memory &
08048000-0804b000 r-xp 00000000 08:01 260689     /home/fa/memory
Size:                 12 kB
Rss:                  12 kB
Pss:                   3 kB
...

fa $

Les deux instances précédentes soient aussi leur PSS passer  de 6KB à 3KB!
Euh?!
12 / 3 ne font-ils pas 4? Oui, presque, mais en fait les calculs arrondissent cette valeur à l'entier inférieur.  Si on lance 6 exemplaires de notre processus, on va retrouver 6 processus ayant pour leur région de code un PSS de 1KB. Et si on en lance 12, on va trouver un PSS de 0!

Si vous voulez regarder le détail du calcul il est disponible pour le noyau 3.13 ici:
http://lxr.free-electrons.com/source/fs/proc/task_mmu.c#L483
 

Problème d'échelle!

Supposons maintenant que j'ai un système avec beaucoup de processus (disons entre 1 500 et 45 000). Et que je veuille accéder à l'intégralité des champs PSS de toutes les régions mémoires de tous mes processus...

Par exemple:

fa $ time cat /proc/[0-9]*/smaps >/dev/null
fa $

La commande ci-dessus est évidemment peu utile, mais si je fais "time" de cette commande, cela me donnera  une idée du cout induit. C'est aussi pas très futé, mais si je veux calculer les PSS de mes processus, il semblerait que ce soit ma seule source d'information.

Sur 1 500 processus, représentant environ 800 000 régions mémoire, la commande ci-dessus consomme environ 20 secondes de temps CPU système. Sur 45 000 processus, j'ai mesuré des temps de l'ordre de 47 secondes!

L'explication se trouve là : http://lxr.free-electrons.com/source/fs/proc/task_mmu.c#L483!

Pour chaque région le système va parcourir l'ensemble des pages et déterminer pour chaque page quel est son compteur de partage... Le temps de calcul est donc proportionnel au nombre de pages accessibles depuis les régions des processus.

Évidemment la question qui vient à l'esprit est : "Mais pourquoi faire ce calcul lorsqu'on accède à cette information au lieu de maintenir un compteur PSS par région"?

En fait, il est probable que l'on ait peu de choix!  Dans une même région, une première page peut-être partagée par deux processus, et la suivante par 4 ou 12... Maintenir un compteur par région semble donc inutile. Un compteur par page... et bien, c'est ce que fait déjà le système.

Conclusion?

Pfiouh.

RSS: intéressant mais le cumul sur plusieurs processus n'a pas (forcément) de sens
PSS: information sémantiquement plus juste, mais les biais de calcul peuvent être déroutants et le cout d'accès peut devenir prohibitif.

Comme d'habitude, quand on a des outils à sa disposition, on a toujours intérêt à bien les considérer pour connaitre leurs limites et savoir dans quelles conditions ils sont utiles et au delà de quelle limite, ils nous désinforment plutôt qu'ils ne nous informent.




1 commentaire: