Was kann passieren, wenn ein Prozess "wegen zu wenig RAM abgebrochen" wird?
Es wird manchmal gesagt, dass Linux standardmäßig niemals Anfragen nach mehr Speicher aus dem Anwendungscode ablehnt - z malloc()
. 1 Dies ist in der Tat nicht wahr; Standardmäßig wird eine Heuristik verwendet
Offensichtliche Überlastungen des Adressraums werden abgelehnt. Wird für ein typisches System verwendet. Es stellt sicher, dass eine ernsthafte Wild-Allocation fehlschlägt, und ermöglicht gleichzeitig eine Überbeanspruchung, um die Swap-Nutzung zu reduzieren.
Von [linux_src]/Documentation/vm/overcommit-accounting
(alle Anführungszeichen stammen aus dem 3.11-Baum). Was genau als "ernsthafte wilde Zuordnung" gilt, wird nicht explizit angegeben, daher müssten wir die Quelle durchgehen, um die Details zu bestimmen. Wir könnten auch die experimentelle Methode in Fußnote 2 (unten) verwenden, um zu versuchen, eine Reflexion der Heuristik zu erhalten. Meine erste empirische Beobachtung ist, dass unter idealen Umständen (== das System ist im Leerlauf), wenn Sie nicht " Wenn Sie keinen Swap haben, können Sie ungefähr die Hälfte Ihres Arbeitsspeichers zuweisen. Wenn Sie Swap haben, erhalten Sie ungefähr die Hälfte Ihres Arbeitsspeichers plus den gesamten Swap. Das ist mehr oder weniger pro Prozess (Beachten Sie jedoch , diese Grenze ist dynamisch und Änderungen vorbehalten wegen der staatlichen finden einige Beobachtungen in Fußnote 5).
Die Hälfte Ihres Arbeitsspeichers plus Swap ist explizit die Standardeinstellung für das Feld "CommitLimit" in /proc/meminfo
. Das bedeutet Folgendes - und beachten Sie, dass dies eigentlich nichts mit dem soeben diskutierten Grenzwert (von [src]/Documentation/filesystems/proc.txt
) zu tun hat :
CommitLimit: Basierend auf dem Overcommit-Verhältnis ('vm.overcommit_ratio') ist dies die Gesamtmenge des Arbeitsspeichers, die derzeit auf dem System verfügbar ist. Dieses Limit wird nur eingehalten, wenn die strikte Überlastungsabrechnung aktiviert ist (Modus 2 in 'vm.overcommit_memory'). Das CommitLimit wird mit der folgenden Formel berechnet: CommitLimit = ('vm.overcommit_ratio' * Physical RAM) + Swap Zum Beispiel würde es auf einem System mit 1G physischem RAM und 7G Swap mit einem 'vm.overcommit_ratio' von 30 ergeben ein CommitLimit von 7.3G.
Das zuvor zitierte Dokument zur Überlastungsabrechnung gibt an, dass der Standardwert vm.overcommit_ratio
50 ist. Wenn Sie dies also sysctl vm.overcommit_memory=2
tun, können Sie vm.covercommit_ratio (mit sysctl
) anpassen und die Konsequenzen anzeigen . 3 Der Standardmodus ist, wenn er CommitLimit
nicht erzwungen wird und nur "offensichtliche Überschreibungen des Adressraums abgelehnt werden", wenn vm.overcommit_memory=0
.
Während die Standardstrategie ein heuristisches Pro-Prozess-Limit hat, das die "ernsthafte wilde Allokation" verhindert, kann das System als Ganzes ernsthaft wild und allokationsweise werden. 4 Dies bedeutet, dass irgendwann nicht mehr genügend Arbeitsspeicher zur Verfügung steht und über den OOM-Killer Konkurs für einen oder mehrere Prozesse angemeldet werden muss .
Was tötet der OOM-Killer? Nicht unbedingt der Prozess, der nach Speicher gefragt hat, als es keinen gab, da dies nicht unbedingt der wirklich schuldige Prozess ist, und was noch wichtiger ist, nicht unbedingt der Prozess, der das System am schnellsten von dem Problem befreit, in dem es sich befindet.
Dies wird hier zitiert , wo wahrscheinlich eine 2.6.x-Quelle zitiert wird:
/*
* 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)
*/
Das scheint eine anständige Begründung zu sein. Ohne forensisch zu werden, scheint jedoch # 5 (die redundant zu # 1 ist) eine schwierige Verkaufsimplementierung zu sein, und # 3 ist redundant zu # 2. Es könnte also sinnvoll sein, dies auf # 2/3 und # 4 zu reduzieren.
Ich habe mich in einer kürzlich erschienenen Quelle (3.11) umgesehen und festgestellt, dass sich dieser Kommentar in der Zwischenzeit geändert hat:
/**
* 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.
*/
Dies ist ein wenig mehr explizit über # 2: „Das Ziel ist es , [töten] die Aufgabe , die meisten Speicherraubend nachfolgende oom Ausfälle zu vermeiden“ , und durch Implikation # 4 ( „wir wollen die minimale Menge an Prozesse töten ( eine ) ) .
Wenn Sie den OOM-Killer in Aktion sehen möchten, lesen Sie Fußnote 5.
1 Eine Täuschung, die Gilles mich dankbar loswurde, siehe Kommentare.
2 Hier ist ein einfaches Bit von C, das nach immer größeren Speicherblöcken fragt, um festzustellen, wann eine Anforderung nach mehr fehlschlägt:
#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;
}
Wenn Sie C nicht kennen, können Sie es kompilieren gcc virtlimitcheck.c -o virtlimitcheck
und dann ausführen ./virtlimitcheck
. Es ist völlig harmlos, da der Prozess keinen der benötigten Speicherplätze belegt - dh, er verwendet nie wirklich RAM.
Auf einem 3,11 x 86_64-System mit 4 GB System und 6 GB Swap schlug ich bei ~ 7400000 kB fehl. Die Anzahl schwankt, also ist der Zustand vielleicht ein Faktor. Dies ist zufällig nahe der CommitLimit
in /proc/meminfo
, aber diese durch Modifikation vm.overcommit_ratio
keinen Unterschied machen. Auf einem 3.6.11 32-Bit ARM 448 MB System mit 64 MB Swap scheitere ich jedoch bei ~ 230 MB. Dies ist interessant, da im ersten Fall die Menge fast doppelt so groß ist wie der RAM-Speicher, während im zweiten Fall etwa 1/4 der Swap-Größe eine Rolle spielt. Dies wurde durch Ausschalten von Swap auf dem ersten System bestätigt, als die Ausfallschwelle auf ~ 1,95 GB sank, ein sehr ähnliches Verhältnis wie bei der kleinen ARM-Box.
Aber ist das wirklich pro Prozess? Es scheint zu sein. Das kurze Programm unten fragt nach einem benutzerdefinierten Speicherblock und wartet bei Erfolg darauf, dass Sie die Eingabetaste drücken. Auf diese Weise können Sie mehrere Instanzen gleichzeitig ausführen:
#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;
}
Beachten Sie jedoch, dass es unabhängig von der Verwendung nicht ausschließlich um die Größe des Arbeitsspeichers und des Auslagerungsspeichers geht. In Fußnote 5 finden Sie Hinweise zu den Auswirkungen des Systemzustands.
3 CommitLimit
bezieht sich auf die Menge an Adressraum, die für das System zulässig ist , wenn vm.overcommit_memory = 2. Vermutlich sollte die Menge, die Sie zuweisen können, minus der bereits festgeschriebenen Menge sein, was anscheinend das Committed_AS
Feld ist.
Ein potenziell interessantes Experiment, das dies demonstriert, ist das Hinzufügen #include <unistd.h>
von oben in virtlimitcheck.c (siehe Fußnote 2) und fork()
rechts vor der while()
Schleife. Es ist nicht garantiert, dass dies ohne langwierige Synchronisierung wie hier beschrieben funktioniert, aber es besteht eine gute Chance, dass dies der Fall ist, 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.
Dies ist sinnvoll. Wenn Sie sich die Datei tmp.txt im Detail ansehen, können Sie sehen, dass die Prozesse ihre immer größeren Zuordnungen abwechseln (dies ist einfacher, wenn Sie die PID in die Ausgabe werfen), bis einer offensichtlich genug behauptet hat, dass der andere fehlschlägt. Dem Gewinner steht es dann frei, alles bis zum CommitLimit
Minus zu greifen Committed_AS
.
4 An dieser Stelle ist zu erwähnen, dass, wenn Sie die virtuelle Adressierung und das Anfordern von Paging noch nicht verstehen, die Möglichkeit einer Überbindung in erster Linie darin besteht, dass der Kernel Userland-Prozessen überhaupt keinen physischen Speicher zuweist, sondern diesen virtueller Adressraum . Wenn ein Prozess beispielsweise 10 MB für etwas reserviert, ist dies eine Folge von (virtuellen) Adressen, aber diese Adressen entsprechen noch nicht dem physischen Speicher. Wenn auf eine solche Adresse zugegriffen wird, führt dies zu einem Seitenfehlerund dann versucht der Kernel, es auf den realen Speicher abzubilden, so dass es einen realen Wert speichern kann. Prozesse reservieren in der Regel viel mehr virtuellen Speicherplatz als sie tatsächlich in Anspruch nehmen, wodurch der Kernel den Arbeitsspeicher am effizientesten nutzen kann. Der physische Speicher ist jedoch immer noch eine begrenzte Ressource, und wenn er vollständig dem virtuellen Adressraum zugeordnet wurde, muss ein Teil des virtuellen Adressraums entfernt werden, um RAM freizugeben.
5 Zuerst eine Warnung : Wenn Sie dies mit versuchen vm.overcommit_memory=0
, stellen Sie sicher, dass Sie Ihre Arbeit zuerst speichern und alle kritischen Anwendungen schließen, da das System für ~ 90 Sekunden eingefroren wird und ein Prozess abstürzt!
Die Idee ist, eine Gabelbombe zu starten , deren Zeit nach 90 Sekunden abläuft, wobei die Gabeln Speicherplatz zuweisen und einige von ihnen große Datenmengen in den RAM schreiben, während sie an stderr berichten.
#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);
}
}
}
Kompilieren Sie dies gcc forkbomb.c -o forkbomb
. Versuchen Sie es zuerst mit sysctl vm.overcommit_memory=2
- Sie werden wahrscheinlich etwas bekommen wie:
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.
In dieser Umgebung kommt diese Art von Gabelbombe nicht sehr weit. Beachten Sie, dass die Anzahl in "sagt N Gabeln" nicht die Gesamtzahl der Prozesse ist, sondern die Anzahl der Prozesse in der Kette / dem Zweig, die zu diesem Prozess führen.
Probieren Sie es jetzt mit vm.overcommit_memory=0
. Wenn Sie stderr in eine Datei umleiten, können Sie anschließend eine grobe Analyse durchführen, z.
> 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!
Nur 15 verarbeitet fehlgeschlagen 1 GB zuzuteilen - zeigt , daß die Heuristik für overcommit_memory = 0 wird von Zustand betroffen. Wie viele Prozesse gab es? Mit Blick auf das Ende von tmp.txt, wahrscheinlich> 100.000. Wie darf man nun eigentlich die 1 GB nutzen?
> 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
Acht - was wieder Sinn macht, da ich zu der Zeit ~ 3 GB RAM frei und 6 GB Swap hatte.
Sehen Sie sich danach Ihre Systemprotokolle an. Sie sollten sehen, dass der OOM-Killer (unter anderem) Punkte meldet. vermutlich bezieht sich dies auf oom_badness
.