virli/tutorial/3/oom.md

6.6 KiB

\newpage

Gestion de la mémoire

Linux a une gestion de la mémoire bien particulière1 : 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.

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 :

```bash 42sh$ cat /proc/self/oom_score 666 42sh$ cat /proc/1/oom_score 0 ```

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 :

```bash ps -A | while read pid _ _ comm; do echo $(cat /proc/$pid/oom_score) $comm; done | sort -h ```

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 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 ?

```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 ```

:::::

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 threads 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 :

```c uint64_t ret; read(, &ret, sizeof(ret)); ```

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.


  1. 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. ↩︎