167 lines
6.6 KiB
Markdown
167 lines
6.6 KiB
Markdown
\newpage
|
||
|
||
Gestion de la mémoire
|
||
=====================
|
||
|
||
Linux a une gestion de la mémoire bien particulière[^vm-overcommit] : en effet,
|
||
par défaut, `malloc(3)` ne retournera jamais `NULL`. En se basant sur
|
||
l'euristique qu'un bloc mémoire demandé ne sera pas utilisé directement et que
|
||
de nombreux process ne feront pas un usage total des blocs qu'ils ont alloués,
|
||
le noyau permet d'allouer plus de mémoire qu'il n'y en a réellement
|
||
disponible. La mémoire est ainsi utilisée de manière plus efficace.
|
||
|
||
[^vm-overcommit]: Cela dépend de la valeur de `/proc/sys/vm/overcommit_memory`,
|
||
généralement 0. Voir
|
||
<https://www.kernel.org/doc/html/latest/vm/overcommit-accounting.html>.
|
||
|
||
Mais évidemment, cela peut donner lieu à des situations où, au moment où un
|
||
processus se met à utiliser un nouveau bloc de mémoire (reçu du noyau d'un
|
||
appel précédent à `malloc(3)`) qu'il n'utilisait pas jusque là, le noyau se
|
||
trouve dans l'impossibilité d'attribuer un bloc physiquement disponible, car il
|
||
n'y en a tout simplement plus (y compris via le swap).
|
||
|
||
Puisque le noyau ne peut pas honorer sa promesse et qu'il n'a plus la
|
||
possibilité de retourner `NULL` au programme qui réclamme sa mémoire (il s'agit
|
||
sans doute d'une simple assignation de variable à ce stade), il faut trouver
|
||
une solution si l'on veut pouvoir continuer l'exécution du programme.
|
||
|
||
Le noyau pourrait choisir de suspendre l'exécution du processus tant qu'il n'y
|
||
a pas de nouveau bloc mémoire disponible... mais, à moins qu'un processus se
|
||
termine ou libère de la mémoire, on risque de se retrouver face à une situation
|
||
de faillite où tous les processus seraient suspendus, sans garantie qu'une
|
||
solution se produise à un moment donné.
|
||
|
||
Pour être certain de récupérer de la mémoire, le noyau n'a pas d'autre solution
|
||
que de tuer un processus. L'issue la plus simple est de tuer le processus qui
|
||
est en train d'accéder à la plage de mémoire que le noyau ne peut pas
|
||
honorer. Pour autant, ce n'est pas une raison pour tuer ce processus, car il
|
||
est peut-être vital pour le système (peut-être est-ce `init` qui est en train
|
||
de gérer le lancement d'un nouveau daemon). On dit alors que le noyau est
|
||
*Out-Of-Memory*.
|
||
|
||
Pour se sortir d'une telle situation, et après avoir tenté de vider les caches,
|
||
il lance son arme ultime pour retrouver au plus vite de la mémoire :
|
||
l'*Out-Of-Memory killer*.
|
||
|
||
|
||
## Trouver un coupable
|
||
|
||
Tous les processus, au cours de leur exécution, disposent d'un score permettant
|
||
au système de savoir à tout moment quel processus apporterait le plus de gain à
|
||
être éliminé, si la mémoire venait à manquer.
|
||
|
||
Lorsqu'une situation d'OOM est déclarée, le noyau tue le processus avec le
|
||
score le plus élevé à ce moment là.
|
||
|
||
Pour connaître le score actuel d'un processus, on affiche le contenu du fichier
|
||
`oom_score` dans son dossier de `/proc` :
|
||
|
||
<div lang="en-US">
|
||
```bash
|
||
42sh$ cat /proc/self/oom_score
|
||
666
|
||
42sh$ cat /proc/1/oom_score
|
||
0
|
||
```
|
||
</div>
|
||
|
||
Le score est établi entre 0 et 1000.
|
||
|
||
Le but étant de récupérer le plus vite possible, le plus de mémoire possible,
|
||
le score est établi selon la quantité de mémoire que le processus occupe.
|
||
|
||
Voici un script pour visualiser les programmes ayant les plus gros scores sur
|
||
votre système :
|
||
|
||
<div lang="en-US">
|
||
```bash
|
||
ps -A | while read pid _ _ comm; do
|
||
echo $(cat /proc/$pid/oom_score) $comm;
|
||
done | sort -h
|
||
```
|
||
</div>
|
||
|
||
A priori, la dernière ligne montre le processus ayant le plus de chance d'être
|
||
tué en cas de passage proche de l'OOM-killer.
|
||
|
||
|
||
::::: {.exercice}
|
||
|
||
#### À vous de jouer {-}
|
||
|
||
Continuons l'exercice précédent où nous avions [fixé les
|
||
limites](#Fixer-des-limites) de mémoire que pouvaient réserver les processus de
|
||
notre groupe. Que se passe-t-il alors si `memhog` dépasse la quantité de
|
||
mémoire autorisée au sein du `cgroup` ?
|
||
|
||
<div lang="en-US">
|
||
```bash
|
||
42sh$ echo 512M > /sys/fs/cgroup/memory/MYNAME/memory.limit_in_bytes
|
||
42sh$ ./monitor MYNAME memhog 500
|
||
................ # OK
|
||
42sh$ ./monitor MYNAME memhog 700
|
||
..............Killed
|
||
```
|
||
</div>
|
||
|
||
:::::
|
||
|
||
Eh oui, l'OOM-killer passe également lorsqu'un `cgroup` atteint la limite de
|
||
mémoire qui lui est réservé. Dans ce cas évidemment, les processus pris en
|
||
compte sont ceux contenus dans le `cgroup`.
|
||
|
||
|
||
## Esquiver l'OOM killer ?
|
||
|
||
Le passage de l'OOM killer relevant parfois de la roulette russe, il peut être
|
||
intéressant, pour certains processus, de vouloir faire en sorte qu'ils aient
|
||
moins de chance d'arriver en tête du classement, même s'ils occupent beaucoup
|
||
de mémoire. On pourrait par exemple vouloir que des services importants ou des
|
||
programmes contenant notre travail en cours (potentiellement non-enregistré !)
|
||
ne soient pas ciblés à moins de ne plus avoir d'autre choix.
|
||
|
||
Le sujet étant très épineux, et aucune solution ou algorithme n'ayant
|
||
réellement démontré sa supériorité pour évincer la tâche idéale, nous avons la
|
||
possibilité de modifier le score à la hausse ou à la baisse.
|
||
|
||
Le fichier `oom_score_adj` peut contenir un valeur entre -1000 et +1000, cette
|
||
valeur vient s'ajouter au score du processus à chaque prochain calcul. Une
|
||
valeur négative va faire réduire le score et réduire d'autant les chances que
|
||
le processus soit ciblé, tandis qu'une valeur positive va augmenter le score et
|
||
donc accroître les chances que le processus soit ciblé.
|
||
|
||
La valeur spéciale de `-1000` fait que le processus ne sera pas considéré comme
|
||
une cible potentielle. Au même titre que les *thread*s du noyau ou le processus
|
||
`init` du système.
|
||
|
||
|
||
## Être notifié sur l'état de la mémoire
|
||
|
||
Au sein d'un *cgroup* *memory*, les fichiers `memory.oom_control` (v1) ou
|
||
`memory.events` (v2) peuvent être utilisés afin de recevoir une notification du
|
||
noyau avant que l'OOM-killer ne s'attaque à un processus de ce groupe.
|
||
|
||
L'idée est de créer un *eventfd* avec `eventfd(2)`, d'ouvrir le fichier
|
||
`memory.oom_control` ou `memory.events` du groupe pour lequel on veut être
|
||
notifié, pour obtenir un *file descriptor* ; enfin écrire dans le fichier
|
||
`cgroup.event_control` le numéro de l'*eventfd* puis du *file descriptor* sur
|
||
`memory.oom_control`, séparé par une espace.
|
||
|
||
On attend ensuite la notification avec :
|
||
|
||
<div lang="en-US">
|
||
```c
|
||
uint64_t ret;
|
||
read(<fd of eventfd()>, &ret, sizeof(ret));
|
||
```
|
||
</div>
|
||
|
||
D'autres notifications peuvent être mises en place, selon le même principe sur
|
||
d'autres fichiers, notamment `memory.usage_in_bytes`, pour être prévenu dès
|
||
lors que l'on franchit dans un sens ou dans l'autre un certain palier. Le palier
|
||
qui nous intéresse est à indiquer comme troisième argument à
|
||
`cgroup.event_control`.
|
||
|
||
Dans la version 2 des *cgroups*, il est même possible d'utiliser `inotify` pour
|
||
surveiller les changements.
|