Que peut-il se passer si un processus est "tué en raison d'une faible RAM"?
On dit parfois que Linux par défaut ne refuse jamais les demandes de mémoire supplémentaire à partir du code d'application - par exemple malloc()
. 1 Ce n'est pas vrai en fait; la valeur par défaut utilise une heuristique selon laquelle
Les surcharges évidentes d'espace d'adressage sont refusées. Utilisé pour un système typique. Il garantit qu'une allocation sérieusement sauvage échoue tout en permettant un sur-engagement pour réduire l'utilisation du swap.
De [linux_src]/Documentation/vm/overcommit-accounting
(toutes les citations proviennent de l'arbre 3.11). Ce qui compte exactement comme une «allocation sérieusement sauvage» n'est pas rendu explicite, il nous faudrait donc passer par la source pour déterminer les détails. Nous pourrions également utiliser la méthode expérimentale de la note de bas de page 2 (ci-dessous) pour essayer d'obtenir une certaine réflexion de l'heuristique - sur cette base, mon observation empirique initiale est que dans des circonstances idéales (== le système est inactif), si vous ne le faites pas '' Si vous avez un swap, vous serez autorisé à allouer environ la moitié de votre RAM, et si vous avez un swap, vous obtiendrez environ la moitié de votre RAM plus la totalité de votre swap. C'est plus ou moins par processus (mais notez que cette limite est dynamique et susceptible de changer en raison de l'état, voir quelques observations dans la note 5).
La moitié de votre RAM plus swap est explicitement la valeur par défaut pour le champ "CommitLimit" dans /proc/meminfo
. Voici ce que cela signifie - et notez que cela n'a rien à voir avec la limite qui vient d'être discutée (à partir de [src]/Documentation/filesystems/proc.txt
):
CommitLimit: basé sur le ratio de sur-engagement ('vm.overcommit_ratio'), il s'agit de la quantité totale de mémoire actuellement disponible pour être allouée sur le système. Cette limite n'est respectée que si la comptabilité stricte de sur-validation est activée (mode 2 dans 'vm.overcommit_memory'). Le CommitLimit est calculé avec la formule suivante: CommitLimit = ('vm.overcommit_ratio' * RAM physique) + Swap Par exemple, sur un système avec 1G de RAM physique et 7G de swap avec un 'vm.overcommit_ratio' de 30, cela donnerait un CommitLimit de 7.3G.
Le document de comptabilité de surengagement précédemment cité indique que la valeur par défaut vm.overcommit_ratio
est 50. Donc, si vous sysctl vm.overcommit_memory=2
, vous pouvez ensuite ajuster vm.covercommit_ratio (avec sysctl
) et voir les conséquences. 3 Le mode par défaut, lorsqu'il CommitLimit
n'est pas appliqué et que «les surcharges évidentes d'espace d'adressage sont refusées», est lorsque vm.overcommit_memory=0
.
Bien que la stratégie par défaut ait une limite heuristique par processus empêchant «l'allocation sérieusement sauvage», elle laisse le système dans son ensemble libre de devenir sérieusement sauvage, en termes d'allocation. 4 Cela signifie qu'à un moment donné, il peut manquer de mémoire et devoir déclarer faillite à certains processus via le tueur OOM .
Qu'est-ce que le tueur OOM tue? Pas nécessairement le processus qui demandait de la mémoire quand il n'y en avait pas, car ce n'est pas nécessairement le processus vraiment coupable, et plus important encore, pas nécessairement celui qui éliminera le plus rapidement le système du problème dans lequel il se trouve.
Ceci est cité à partir d' ici qui cite probablement une source 2.6.x:
/*
* oom_badness - calculate a numeric value for how bad this task has been
*
* The formula used is relatively simple and documented inline in the
* function. The main rationale is that we want to select a good task
* to kill when we run out of memory.
*
* Good in this context means that:
* 1) we lose the minimum amount of work done
* 2) we recover a large amount of memory
* 3) we don't kill anything innocent of eating tons of memory
* 4) we want to kill the minimum amount of processes (one)
* 5) we try to kill the process the user expects us to kill, this
* algorithm has been meticulously tuned to meet the principle
* of least surprise ... (be careful when you change it)
*/
Ce qui semble être une justification décente. Cependant, sans devenir médico-légal, le n ° 5 (qui est redondant du n ° 1) semble être une mise en œuvre difficile à vendre, et le n ° 3 est redondant du n ° 2. Il pourrait donc être judicieux de considérer cela comme réduit aux # 2/3 et # 4.
J'ai parcouru une source récente (3.11) et j'ai remarqué que ce commentaire avait changé entre-temps:
/**
* oom_badness - heuristic function to determine which candidate task to kill
*
* The heuristic for determining which task to kill is made to be as simple and
* predictable as possible. The goal is to return the highest value for the
* task consuming the most memory to avoid subsequent oom failures.
*/
Ceci est un peu plus explicitement à propos de # 2: "Le but est de [tuer] la tâche consommant le plus de mémoire pour éviter les échecs OOM ultérieurs", et par implication # 4 ( "nous voulons tuer le nombre minimum de processus ( un ) ) .
Si vous voulez voir le tueur OOM en action, voir la note de bas de page 5.
1 Une illusion dont Gilles m'a heureusement débarrassé, voir commentaires.
2 Voici un bit simple de C qui demande des morceaux de mémoire de plus en plus grands pour déterminer quand une demande de plus échouera:
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#define MB 1 << 20
int main (void) {
uint64_t bytes = MB;
void *p = malloc(bytes);
while (p) {
fprintf (stderr,
"%lu kB allocated.\n",
bytes / 1024
);
free(p);
bytes += MB;
p = malloc(bytes);
}
fprintf (stderr,
"Failed at %lu kB.\n",
bytes / 1024
);
return 0;
}
Si vous ne connaissez pas C, vous pouvez le compiler gcc virtlimitcheck.c -o virtlimitcheck
, puis l'exécuter ./virtlimitcheck
. Il est complètement inoffensif, car le processus n'utilise aucun de l'espace qu'il demande - c'est-à-dire qu'il n'utilise jamais vraiment de RAM.
Sur un système 3.11 x86_64 avec un système de 4 Go et 6 Go de swap, j'ai échoué à ~ 7400000 ko; le nombre fluctue, donc peut-être que l'état est un facteur. C'est par hasard proche de l' CommitLimit
entrée /proc/meminfo
, mais la modification de ce via vm.overcommit_ratio
ne fait aucune différence. Sur un système 3.6.11 32 bits ARM 448 Mo avec 64 Mo de swap, cependant, j'échoue à ~ 230 Mo. Ceci est intéressant car dans le premier cas, la quantité est presque le double de la quantité de RAM, alors que dans le second, elle est d'environ 1/4 - ce qui implique fortement la quantité de swap est un facteur. Cela a été confirmé en désactivant le swap sur le premier système, lorsque le seuil de défaillance est tombé à environ 1,95 Go, un rapport très similaire à la petite boîte ARM.
Mais est-ce vraiment par processus? Il semble être. Le programme court ci-dessous demande un morceau de mémoire défini par l'utilisateur, et s'il réussit, attend que vous frappiez retour - de cette façon, vous pouvez essayer plusieurs instances simultanées:
#include <stdio.h>
#include <stdlib.h>
#define MB 1 << 20
int main (int argc, const char *argv[]) {
unsigned long int megabytes = strtoul(argv[1], NULL, 10);
void *p = malloc(megabytes * MB);
fprintf(stderr,"Allocating %lu MB...", megabytes);
if (!p) fprintf(stderr,"fail.");
else {
fprintf(stderr,"success.");
getchar();
free(p);
}
return 0;
}
Attention, cependant, il ne s'agit pas strictement de la quantité de RAM et de swap quelle que soit l'utilisation - voir la note de bas de page 5 pour les observations sur les effets de l'état du système.
3 CommitLimit
fait référence à la quantité d'espace d'adressage autorisée pour le système lorsque vm.overcommit_memory = 2. On peut supposer que le montant que vous pouvez allouer devrait être ce moins ce qui est déjà engagé, ce qui est apparemment le Committed_AS
champ.
Une expérience potentiellement intéressante démontrant cela consiste à ajouter #include <unistd.h>
en haut de virtlimitcheck.c (voir référence 2), et fork()
juste avant la while()
boucle. Cela n'est pas garanti de fonctionner comme décrit ici sans une synchronisation fastidieuse, mais il y a de fortes chances que cela fonctionne, YMMV:
> sysctl vm.overcommit_memory=2
vm.overcommit_memory = 2
> cat /proc/meminfo | grep Commit
CommitLimit: 9231660 kB
Committed_AS: 3141440 kB
> ./virtlimitcheck 2&> tmp.txt
> cat tmp.txt | grep Failed
Failed at 3051520 kB.
Failed at 6099968 kB.
Cela a du sens - en regardant tmp.txt en détail, vous pouvez voir que les processus alternent leurs allocations de plus en plus grandes (c'est plus facile si vous jetez le pid dans la sortie) jusqu'à ce que l'un, évidemment, ait suffisamment revendiqué que l'autre échoue. Le gagnant est alors libre de tout récupérer jusqu'à CommitLimit
moins Committed_AS
.
4 Il convient de mentionner, à ce stade, si vous ne comprenez pas déjà l'adressage virtuel et la pagination de la demande, que ce qui rend possible un engagement excessif, c'est que ce que le noyau alloue aux processus de l'espace utilisateur n'est pas du tout de la mémoire physique - c'est espace d'adressage virtuel . Par exemple, si un processus réserve 10 Mo pour quelque chose, il est présenté comme une séquence d'adresses (virtuelles), mais ces adresses ne correspondent pas encore à la mémoire physique. Lorsqu'une telle adresse est accessible, cela entraîne un défaut de pagepuis le noyau tente de le mapper sur la mémoire réelle afin qu'il puisse stocker une valeur réelle. Les processus réservent généralement beaucoup plus d’espace virtuel qu’ils n’y ont réellement accès, ce qui permet au noyau d’utiliser la RAM le plus efficacement possible. Cependant, la mémoire physique est toujours une ressource limitée et lorsque tout cela a été mappé à l'espace d'adressage virtuel, un certain espace d'adressage virtuel doit être éliminé pour libérer de la RAM.
5 D'abord un avertissement : si vous essayez ceci avec vm.overcommit_memory=0
, assurez-vous d'enregistrer votre travail d'abord et fermez toutes les applications critiques, car le système sera gelé pendant ~ 90 secondes et certains processus vont mourir!
L'idée est d'exécuter une bombe fork qui expire après 90 secondes, les fourches allouant de l'espace et certaines écrivant de grandes quantités de données dans la RAM, tout en se rapportant à stderr.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/time.h>
#include <errno.h>
#include <string.h>
/* 90 second "Verbose hungry fork bomb".
Verbose -> It jabbers.
Hungry -> It grabs address space, and it tries to eat memory.
BEWARE: ON A SYSTEM WITH 'vm.overcommit_memory=0', THIS WILL FREEZE EVERYTHING
FOR THE DURATION AND CAUSE THE OOM KILLER TO BE INVOKED. CLOSE THINGS YOU CARE
ABOUT BEFORE RUNNING THIS. */
#define STEP 1 << 30 // 1 GB
#define DURATION 90
time_t now () {
struct timeval t;
if (gettimeofday(&t, NULL) == -1) {
fprintf(stderr,"gettimeofday() fail: %s\n", strerror(errno));
return 0;
}
return t.tv_sec;
}
int main (void) {
int forks = 0;
int i;
unsigned char *p;
pid_t pid, self;
time_t check;
const time_t start = now();
if (!start) return 1;
while (1) {
// Get our pid and check the elapsed time.
self = getpid();
check = now();
if (!check || check - start > DURATION) return 0;
fprintf(stderr,"%d says %d forks\n", self, forks++);
// Fork; the child should get its correct pid.
pid = fork();
if (!pid) self = getpid();
// Allocate a big chunk of space.
p = malloc(STEP);
if (!p) {
fprintf(stderr, "%d Allocation failed!\n", self);
return 0;
}
fprintf(stderr,"%d Allocation succeeded.\n", self);
// The child will attempt to use the allocated space. Using only
// the child allows the fork bomb to proceed properly.
if (!pid) {
for (i = 0; i < STEP; i++) p[i] = i % 256;
fprintf(stderr,"%d WROTE 1 GB\n", self);
}
}
}
Compilez ceci gcc forkbomb.c -o forkbomb
. Tout d'abord, essayez avec sysctl vm.overcommit_memory=2
- vous obtiendrez probablement quelque chose comme:
6520 says 0 forks
6520 Allocation succeeded.
6520 says 1 forks
6520 Allocation succeeded.
6520 says 2 forks
6521 Allocation succeeded.
6520 Allocation succeeded.
6520 says 3 forks
6520 Allocation failed!
6522 Allocation succeeded.
Dans cet environnement, ce type de bombe à fourche ne va pas très loin. Notez que le nombre dans "dit N fourches" n'est pas le nombre total de processus, c'est le nombre de processus dans la chaîne / branche menant à celui-ci.
Maintenant, essayez avec vm.overcommit_memory=0
. Si vous redirigez stderr vers un fichier, vous pouvez effectuer une analyse grossière par la suite, par exemple:
> cat tmp.txt | grep failed
4641 Allocation failed!
4646 Allocation failed!
4642 Allocation failed!
4647 Allocation failed!
4649 Allocation failed!
4644 Allocation failed!
4643 Allocation failed!
4648 Allocation failed!
4669 Allocation failed!
4696 Allocation failed!
4695 Allocation failed!
4716 Allocation failed!
4721 Allocation failed!
Seuls 15 processus n'ont pas pu allouer 1 Go, ce qui montre que l'heuristique pour overcommit_memory = 0 est affectée par l'état. Combien de processus y avait-il? En regardant la fin de tmp.txt, probablement> 100 000. Maintenant, comment peut-on réellement utiliser le 1 Go?
> cat tmp.txt | grep WROTE
4646 WROTE 1 GB
4648 WROTE 1 GB
4671 WROTE 1 GB
4687 WROTE 1 GB
4694 WROTE 1 GB
4696 WROTE 1 GB
4716 WROTE 1 GB
4721 WROTE 1 GB
Huit - ce qui a encore un sens, car à l'époque j'avais environ 3 Go de RAM libre et 6 Go de swap.
Jetez un oeil à vos journaux système après avoir fait cela. Vous devriez voir les scores des rapports du tueur OOM (entre autres); on peut supposer que cela concerne oom_badness
.