\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 . 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](#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` ?
```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 *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 :
```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.