Frage Warum isst dieser Speicherfresser nicht wirklich die Erinnerung?


Ich möchte ein Programm erstellen, das eine Out-of-Memory (OOM) -Situation auf einem Unix-Server simuliert. Ich habe diesen super-einfachen Speicherfresser erschaffen:

#include <stdio.h>
#include <stdlib.h>

unsigned long long memory_to_eat = 1024 * 50000;
size_t eaten_memory = 0;
void *memory = NULL;

int eat_kilobyte()
{
    memory = realloc(memory, (eaten_memory * 1024) + 1024);
    if (memory == NULL)
    {
        // realloc failed here - we probably can't allocate more memory for whatever reason
        return 1;
    }
    else
    {
        eaten_memory++;
        return 0;
    }
}

int main(int argc, char **argv)
{
    printf("I will try to eat %i kb of ram\n", memory_to_eat);
    int megabyte = 0;
    while (memory_to_eat > 0)
    {
        memory_to_eat--;
        if (eat_kilobyte())
        {
            printf("Failed to allocate more memory! Stucked at %i kb :(\n", eaten_memory);
            return 200;
        }
        if (megabyte++ >= 1024)
        {
            printf("Eaten 1 MB of ram\n");
            megabyte = 0;
        }
    }
    printf("Successfully eaten requested memory!\n");
    free(memory);
    return 0;
}

Es isst so viel Speicher wie in memory_to_eat Das sind jetzt genau 50 GB RAM. Es reserviert Speicher um 1 MB und druckt genau den Punkt, wo es nicht mehr zuzuteilen, so dass ich weiß, welchen maximalen Wert es zu essen hat.

Das Problem ist, dass es funktioniert. Sogar auf einem System mit 1 GB physikalischem Speicher.

Wenn ich nach oben schaue, sehe ich, dass der Prozess 50 GB virtuellen Speicher und nur weniger als 1 MB residenten Speicher verbraucht. Gibt es eine Möglichkeit, einen Speicherfresser zu erschaffen, der ihn wirklich verbraucht?

Systemspezifikationen: Linux Kernel 3.16 (Debian) höchstwahrscheinlich mit overcommit aktiviert (nicht sicher, wie man es auscheckt) ohne Swap und virtualisiert.


147
2017-10-20 10:24


Ursprung


Antworten:


Wenn dein malloc() Implementierung fordert Speicher vom Systemkern an (über einen sbrk() oder mmap() (Systemaufruf), der Kernel merkt nur, dass Sie den Speicher angefordert haben und wo er in Ihrem Adressraum platziert werden soll. Diese Seiten werden derzeit noch nicht zugeordnet.

Wenn der Prozess anschließend auf Speicher innerhalb der neuen Region zugreift, erkennt die Hardware einen Segmentierungsfehler und warnt den Kernel vor der Bedingung. Der Kernel schaut dann die Seite in seinen eigenen Datenstrukturen nach und findet, dass Sie dort eine Nullseite haben sollten, so dass er in eine Nullseite mappt (möglicherweise zuerst eine Seite aus dem Seitencache entfernt) und von dem Interrupt zurückkehrt. Ihr Prozess erkennt nicht, dass irgendetwas davon passiert ist, die Kerneloperation ist vollkommen transparent (abgesehen von der kurzen Verzögerung, während der Kernel seine Arbeit macht).

Durch diese Optimierung kann der Systemaufruf sehr schnell zurückgegeben werden, und vor allem werden Ressourcen vermieden, die bei der Erstellung des Mappings an den Prozess gebunden werden müssen. Auf diese Weise können Prozesse größere Puffer reservieren, die sie unter normalen Umständen nie brauchen, ohne dass sie befürchten müssen, zu viel Speicher zu verbrauchen.


Also, wenn Sie einen Speicherfresser programmieren wollen, müssen Sie wirklich etwas mit dem Speicher tun, den Sie zuweisen. Dazu müssen Sie Ihrem Code nur eine Zeile hinzufügen:

int eat_kilobyte()
{
    if (memory == NULL)
        memory = malloc(1024);
    else
        memory = realloc(memory, (eaten_memory * 1024) + 1024);
    if (memory == NULL)
    {
        return 1;
    }
    else
    {
        //Force the kernel to map the containing memory page.
        ((char*)memory)[1024*eaten_memory] = 42;

        eaten_memory++;
        return 0;
    }
}

Beachten Sie, dass es vollkommen ausreichend ist, in ein einzelnes Byte auf jeder Seite zu schreiben (das 4096 Byte auf X86 enthält). Das liegt daran, dass die gesamte Speicherzuweisung vom Kernel an einen Prozess bei der Speicherseitengranularität erfolgt, was wiederum auf die Hardware zurückzuführen ist, die kein Paging bei kleineren Granularitäten zulässt.


218
2017-10-20 11:13



Alle virtuellen Seiten beginnen mit der Kopie-auf-Schreib-Verknüpfung, die auf die gleiche physikalische Nullseite abgebildet ist. Um physische Seiten zu verwenden, können Sie sie verschmutzen, indem Sie etwas auf jede virtuelle Seite schreiben.

Wenn Sie als root laufen, können Sie verwenden mlock(2) oder mlockall(2) Damit der Kernel die Seiten nach der Zuweisung verschaltet, ohne sie zu verschmutzen. (normale Nicht-Root-Benutzer haben a ulimit -l von nur 64kiB.)

Wie viele andere vorgeschlagen haben, scheint es, dass der Linux-Kernel den Speicher nicht wirklich reserviert, wenn Sie nicht darauf schreiben

Eine verbesserte Version des Codes, die das tut, was der OP wollte:

Dies behebt auch die fehlende Übereinstimmung der PrintF-Formatzeichenfolge mit den Typen von memory_to_eat und appeared_memory unter Verwendung von %zi zu drucken size_t ganze Zahlen. Die Speichergröße in kiB kann optional als Befehlszeilenarg angegeben werden.

Das chaotische Design, das globale Variablen verwendet und um 1k anstelle von 4k Seiten wächst, ist unverändert.

#include <stdio.h>
#include <stdlib.h>

size_t memory_to_eat = 1024 * 50000;
size_t eaten_memory = 0;
char *memory = NULL;

void write_kilobyte(char *pointer, size_t offset)
{
    int size = 0;
    while (size < 1024)
    {   // writing one byte per page is enough, this is overkill
        pointer[offset + (size_t) size++] = 1;
    }
}

int eat_kilobyte()
{
    if (memory == NULL)
    {
        memory = malloc(1024);
    } else
    {
        memory = realloc(memory, (eaten_memory * 1024) + 1024);
    }
    if (memory == NULL)
    {
        return 1;
    }
    else
    {
        write_kilobyte(memory, eaten_memory * 1024);
        eaten_memory++;
        return 0;
    }
}

int main(int argc, char **argv)
{
    if (argc >= 2)
        memory_to_eat = atoll(argv[1]);

    printf("I will try to eat %zi kb of ram\n", memory_to_eat);
    int megabyte = 0;
    int megabytes = 0;
    while (memory_to_eat-- > 0)
    {
        if (eat_kilobyte())
        {
            printf("Failed to allocate more memory at %zi kb :(\n", eaten_memory);
            return 200;
        }
        if (megabyte++ >= 1024)
        {
            megabytes++;
            printf("Eaten %i  MB of ram\n", megabytes);
            megabyte = 0;
        }
    }
    printf("Successfully eaten requested memory!\n");
    free(memory);
    return 0;
}

26
2017-10-20 10:31



Hier wird eine sinnvolle Optimierung vorgenommen. Die Laufzeit ist eigentlich nicht erwerben die Erinnerung, bis Sie sie benutzen.

Eine einfache memcpy wird ausreichen, um diese Optimierung zu umgehen. (Sie könnten das finden calloc optimiert immer noch die Speicherzuweisung bis zum Zeitpunkt der Verwendung.)


13
2017-10-20 10:50



Ich bin mir nicht sicher über dieses, aber die einzige Erklärung, die ich kann, ist, dass Linux ein Copy-on-Write-Betriebssystem ist. Wenn man anruft fork Beide Prozesse verweisen auf denselben physischen Speicher. Der Speicher wird nur kopiert, wenn ein Prozess tatsächlich in den Speicher geschrieben wird.

Ich denke hier, der tatsächliche physische Speicher wird nur zugewiesen, wenn man versucht, etwas darauf zu schreiben. Berufung sbrk oder mmap kann nur das Speicherbuch des Keys aktualisieren. Der tatsächliche RAM darf nur zugewiesen werden, wenn wir tatsächlich versuchen, auf den Speicher zuzugreifen.


6