Kann Linux „keinen ausreichenden Arbeitsspeicher haben“?

Kann Linux „keinen ausreichenden Arbeitsspeicher haben“?

Ich habe im Internet mehrere Posts von Leuten gesehen, die sich offenbar darüber beschwerten, dass ein gehosteter VPS unerwartet Prozesse beendete, weil sie zu viel RAM verwendeten.

Wie ist das möglich? Ich dachte, alle modernen Betriebssysteme bieten „unendlichen RAM“, indem sie einfach Disk-Swap für alles verwenden, was über den physischen RAM hinausgeht. Ist das richtig?

Was kann passieren, wenn ein Prozess „aufgrund von zu wenig RAM beendet“ wird?

Antwort1

Was kann passieren, wenn ein Prozess „aufgrund von zu wenig RAM beendet“ wird?

Es wird manchmal behauptet, dass Linux standardmäßig niemals Anfragen nach mehr Speicher von Anwendungscode ablehnt -- z malloc(). B. 1 Dies ist in der Tat nicht wahr; die Standardeinstellung verwendet eine Heuristik, bei der

Offensichtliche Überbelegungen des Adressraums werden abgelehnt. Wird für ein typisches System verwendet. Es stellt sicher, dass eine ernsthaft wilde Zuweisung fehlschlägt, während Überbelegungen zugelassen werden, um die Swap-Nutzung zu reduzieren.

Aus [linux_src]/Documentation/vm/overcommit-accounting(alle Zitate stammen aus dem 3.11-Baum). Was genau als „ernsthaft wilde Zuweisung“ gilt, wird nicht explizit angegeben, daher müssten wir die Quelle durchgehen, um die Details zu ermitteln. Wir könnten auch die experimentelle Methode in Fußnote 2 (unten) verwenden, um zu versuchen, eine gewisse Widerspiegelung der Heuristik zu erhalten – basierend darauf ist meine anfängliche empirische Beobachtung, dass Sie unter idealen Umständen (== das System ist im Leerlauf) etwa die Hälfte Ihres RAM zuweisen dürfen, wenn Sie keinen Swap haben, und wenn Sie Swap haben, etwa die Hälfte Ihres RAM plus Ihren gesamten Swap erhalten. Das ist mehr oder wenigerpro Vorgang(aber beachten Sie diese GrenzeIstdynamisch und kann sich aufgrund des Staates ändern, siehe einige Beobachtungen in Fußnote 5).

Die Hälfte Ihres RAM plus Swap ist ausdrücklich der Standardwert für das Feld „CommitLimit“ in /proc/meminfo. Dies bedeutet es – und beachten Sie, dass es eigentlich nichts mit dem gerade besprochenen Limit (aus [src]/Documentation/filesystems/proc.txt) zu tun hat:

CommitLimit:Basierend auf dem Überbelegungsverhältnis ('vm.overcommit_ratio') ist dies die Gesamtmenge an Speicher, die derzeit zugewiesen werden kann.auf dem System.Dieses Limit wird nur eingehalten, wenn die strikte Overcommit-Abrechnung aktiviert ist (Modus 2 in „vm.overcommit_memory“). Das CommitLimit wird mit der folgenden Formel berechnet: CommitLimit = („vm.overcommit_ratio“ * Physischer RAM) + Swap. Auf einem System mit 1 GB physischem RAM und 7 GB Swap mit einem „vm.overcommit_ratio“ von 30 würde sich beispielsweise ein CommitLimit von 7,3 GB ergeben.

Das zuvor zitierte Overcommit-Accounting-Dokument besagt, dass der Standardwert vm.overcommit_ratio50 beträgt. Wenn Sie also sysctl vm.overcommit_memory=2, können Sie vm.covercommit_ratio (mit sysctl) anpassen und die Konsequenzen sehen. 3 Der Standardmodus, wenn CommitLimitnicht erzwungen wird und nur „offensichtliche Überbelegungen des Adressraums abgelehnt werden“, ist, wenn vm.overcommit_memory=0.

Während die Standardstrategie eine heuristische Begrenzung pro Prozess hat, die die „wirklich wilde Zuweisung“ verhindert, lässt sie dem System als Ganzes die Freiheit, ernsthaft wild zu werden, was die Zuweisung betrifft. 4 Das bedeutet, dass es irgendwann keinen Speicher mehr haben kann und einigen Prozessen über dieOOM-Killer.

Was tötet der OOM-Killer? Nicht unbedingt den Prozess, der Speicher angefordert hat, als keiner vorhanden war, da dies nicht unbedingt der wirklich schuldige Prozess ist und, was noch wichtiger ist, nicht unbedingt der Prozess, der das System am schnellsten aus dem Problem herausholt, in dem es steckt.

Dies ist ein Zitat ausHierdas wahrscheinlich eine 2.6.x-Quelle zitiert:

/*
 * 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 vernünftige Begründung zu sein. Ohne jedoch forensisch zu werden, scheint Nr. 5 (die Redundanz von Nr. 1 ist) in Bezug auf die Implementierung schwer zu verkaufen, und Nr. 3 ist Redundanz von Nr. 2. Es könnte also sinnvoll sein, dies auf Nr. 2/3 und Nr. 4 zu reduzieren.

Ich habe eine aktuelle Quelle (3.11) durchgesehen 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.
 */

Hier ist etwas ausführlicheres zu Nr. 2:„Das Ziel ist, die Aufgabe zu beenden, die den meisten Speicher verbraucht, um nachfolgende OOM-Fehler zu vermeiden.“und implizit #4 ("Wir wollen so wenig Prozesse wie möglich beenden (eins)).

Wenn Sie den OOM-Killer in Aktion sehen möchten, lesen Sie Fußnote 5.


1 Eine Wahnvorstellung, von der mich Gilles glücklicherweise befreit hat, siehe Kommentare.


2 Hier ist ein einfaches Stück C, das zunehmend größere Speicherblöcke anfordert, um zu bestimmen, 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 kein C können, können Sie dies kompilieren gcc virtlimitcheck.c -o virtlimitcheckund dann ausführen ./virtlimitcheck. Das ist völlig harmlos, da der Prozess den angeforderten Speicherplatz nicht nutzt – d. h., er nutzt nie wirklich RAM.

Auf einem 3.11 x86_64-System mit 4 GB System und 6 GB Swap scheitere ich bei ~7400000 kB; die Zahl schwankt, also ist vielleicht der Status ein Faktor. Dies liegt zufällig nahe an in CommitLimit, /proc/meminfoaber die Änderung über vm.overcommit_ratiomacht keinen Unterschied. Auf einem 3.6.11 32-Bit ARM 448 MB System mit 64 MB Swap scheitere ich jedoch bei ~230 MB. Das ist interessant, da im ersten Fall die Menge fast doppelt so groß ist wie die RAM-Menge, während sie im zweiten etwa 1/4 davon beträgt – was stark darauf hindeutet, dass die Swap-Menge ein Faktor ist. Dies wurde bestätigt, indem Swap auf dem ersten System ausgeschaltet wurde, als die Fehlerschwelle auf ~1,95 GB sank, ein sehr ähnliches Verhältnis wie bei der kleinen ARM-Box.

Aber gilt das wirklich pro Prozess? Es scheint so. Das kurze Programm unten fordert einen benutzerdefinierten Speicherblock an und wartet, wenn es erfolgreich ist, darauf, dass Sie die Eingabetaste drücken – auf diese Weise können Sie mehrere Instanzen gleichzeitig ausprobieren:

#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 hier nicht streng um die RAM- und Swap-Menge geht, unabhängig von der Nutzung – Beobachtungen zu den Auswirkungen des Systemzustands finden Sie in Fußnote 5.


3 CommitLimit bezeichnet den Adressraum, der fürdas Systemwenn vm.overcommit_memory = 2. Vermutlich sollte dann der Betrag, den Sie zuordnen können, dieser abzüglich des bereits zugewiesenen Betrags sein, was anscheinend das Committed_ASFeld ist.

#include <unistd.h>Ein möglicherweise interessantes Experiment, das dies demonstriert, besteht darin, am Anfang von virtlimitcheck.c (siehe Fußnote 2) und fork()direkt vor der Schleife ein hinzuzufügen while(). Es gibt keine Garantie dafür, dass dies wie hier beschrieben ohne eine mühsame Synchronisierung funktioniert, aber es besteht eine gute Chance, dass es funktioniert, Ihre Ergebnisse können abweichen:

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

Das ist sinnvoll – wenn Sie sich tmp.txt im Detail ansehen, können Sie sehen, dass die Prozesse ihre immer größeren Zuweisungen abwechselnd vornehmen (das ist einfacher, wenn Sie die PID in die Ausgabe einfügen), bis einer offensichtlich genug beansprucht hat, sodass der andere versagt. Der Gewinner kann sich dann alles bis CommitLimitminus schnappen Committed_AS.


4 An dieser Stelle sei erwähnt, falls Sie sich mit virtueller Adressierung und Demand Paging noch nicht auskennen: Was die Überbelegung überhaupt erst möglich macht, ist die Tatsache, dass der Kernel den Userland-Prozessen überhaupt keinen physischen Speicher zuweist -- es istvirtueller Adressraum. Wenn ein Prozess beispielsweise 10 MB für etwas reserviert, wird dies als Folge von (virtuellen) Adressen angelegt, aber diese Adressen entsprechen noch nicht dem physischen Speicher. Wenn auf eine solche Adresse zugegriffen wird, führt dies zu einemSeitenfehlerund dann versucht der Kernel, es in den realen Speicher abzubilden, damit ein realer Wert gespeichert werden kann. Prozesse reservieren normalerweise viel mehr virtuellen Speicherplatz, als sie tatsächlich abrufen, wodurch der Kernel den RAM möglichst effizient nutzen kann. Der physische Speicher ist jedoch immer noch eine begrenzte Ressource, und wenn er vollständig in den virtuellen Adressraum abgebildet wurde, muss ein Teil des virtuellen Adressraums gelöscht werden, um RAM freizugeben.


5 Zuersteine Warnung: Wenn Sie dies mit versuchen vm.overcommit_memory=0, speichern Sie unbedingt zuerst Ihre Arbeit und schließen Sie alle kritischen Anwendungen, da das System für ca. 90 Sekunden eingefroren wird und einige Prozesse abstürzen!

Die Idee ist, eineGabelbombeDabei wird nach 90 Sekunden eine Zeitüberschreitung festgestellt, wobei die Forks Speicherplatz zuweisen und einige von ihnen große Datenmengen in den RAM schreiben und dabei die ganze Zeit über 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 zunächst mit sysctl vm.overcommit_memory=2– Sie erhalten wahrscheinlich etwas 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 Fork-Bombe nicht sehr weit. Beachten Sie, dass die Zahl in „sagt N Forks“ nicht die Gesamtzahl der Prozesse ist, sondern die Anzahl der Prozesse in der Kette/dem Zweig, die zu diesem führen.

Versuchen 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. B.:

> 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 Prozesse konnten 1 GB nicht zuordnen – was zeigt, dass die Heuristik für overcommit_memory = 0 ist.Istvom Status betroffen. Wie viele Prozesse gab es? Wenn man sich das Ende von tmp.txt ansieht, sind es wahrscheinlich > 100.000. Wie kann ich nun tatsächlich 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 wiederum Sinn macht, da ich zu der Zeit ca. 3 GB RAM frei hatte und 6 GB Swap.

Sehen Sie sich anschließend Ihre Systemprotokolle an. Sie sollten sehen, dass der OOM-Killer (unter anderem) Scores meldet. Dies bezieht sich vermutlich auf oom_badness.

Antwort2

Das passiert Ihnen nicht, wenn Sie immer nur 1 GB Daten in den Speicher laden. Was passiert, wenn Sie viel, viel mehr laden? Ich arbeite beispielsweise oft mit riesigen Dateien, die Millionen von Wahrscheinlichkeiten enthalten, die in R geladen werden müssen. Dies erfordert etwa 16 GB RAM.

Wenn ich den obigen Prozess auf meinem Laptop ausführe, beginnt dieser wie verrückt zu swappen, sobald meine 8 GB RAM voll sind. Das wiederum verlangsamt alles, weil das Lesen von der Festplatte viel langsamer ist als das Lesen aus dem RAM. Was ist, wenn ich einen Laptop mit 2 GB RAM und nur 10 GB freiem Speicherplatz habe? Sobald der Prozess den gesamten RAM belegt hat, füllt er auch die Festplatte, weil er in den Swap schreibt und ich keinen RAM und keinen Speicherplatz mehr zum Swap habe (aus genau diesem Grund beschränken die Leute den Swap eher auf eine dedizierte Partition als auf eine Swap-Datei). Hier kommt der OOM-Killer ins Spiel und beginnt, Prozesse zu beenden.

Dem System kann also tatsächlich der Arbeitsspeicher ausgehen. Darüber hinaus können Systeme mit starkem Swapping schon lange vorher unbrauchbar werden, einfach aufgrund der langsamen E/A-Vorgänge aufgrund des Swappings. Generell möchte man Swapping so weit wie möglich vermeiden. Selbst auf High-End-Servern mit schnellen SSDs gibt es einen deutlichen Leistungsabfall. Auf meinem Laptop, der ein klassisches Laufwerk mit 7200 U/min hat, macht jedes signifikante Swapping das System im Wesentlichen unbrauchbar. Je mehr es swappt, desto langsamer wird es. Wenn ich den störenden Prozess nicht schnell beende, bleibt alles hängen, bis der OOM-Killer eingreift.

Antwort3

Prozesse werden nicht beendet, wenn kein RAM mehr vorhanden ist, sondern wenn sie auf folgende Weise betrogen wurden:

  • Der Linux-Kernel erlaubt es Prozessen im Allgemeinen, eine größere Menge an virtuellem Speicher zuzuweisen (d. h. zu reservieren), als tatsächlich verfügbar ist (Teil des RAM + der gesamte Swap-Bereich).
  • solange die Prozesse nur auf eine Teilmenge der von ihnen reservierten Seiten zugreifen, läuft alles reibungslos.
  • Wenn ein Prozess nach einiger Zeit versucht, auf eine ihm gehörende Seite zuzugreifen, aber keine Seiten mehr frei sind, tritt eine Speichersituation auf.
  • Der OOM-Killer wählt einen der Prozesse aus (nicht notwendigerweise den, der eine neue Seite angefordert hat) und beendet ihn einfach, um den virtuellen Speicher wiederherzustellen.

Dies kann sogar passieren, wenn das System nicht aktiv auslagert, beispielsweise wenn der Swap-Bereich mit Speicherseiten schlafender Daemons gefüllt ist.

Dies passiert nie bei Betriebssystemen, die den Speicher nicht überbelegen. Bei ihnen wird kein zufälliger Prozess beendet, aber der erste Prozess, der virtuellen Speicher anfordert, obwohl dieser erschöpft ist, gibt malloc (oder ähnliches) fälschlicherweise zurück. So erhält er die Chance, die Situation richtig zu handhaben. Bei diesen Betriebssystemen kann es jedoch auch passieren, dass dem System der virtuelle Speicher ausgeht, obwohl noch freier RAM vorhanden ist, was ziemlich verwirrend ist und im Allgemeinen missverstanden wird.

Antwort4

Um noch einen weiteren Gesichtspunkt aus den anderen Antworten hinzuzufügen: Viele VPS hosten mehrere virtuelle Maschinen auf einem bestimmten Server. Jede einzelne VM hat eine bestimmte Menge an RAM für den eigenen Gebrauch. Viele Anbieter bieten „Burst-RAM“ an, bei dem sie RAM über die ihnen zugewiesene Menge hinaus verwenden können. Dies ist nur für die kurzfristige Verwendung gedacht, und diejenigen, die diesen Wert über einen längeren Zeitraum überschreiten, können bestraft werden, indem der Host Prozesse beendet, um die verwendete RAM-Menge zu verringern, damit andere nicht unter der Überlastung des Host-Rechners leiden.

verwandte Informationen