148 votes

Pourquoi n ' t ce mangeur de mémoire vraiment manger mémoire ?

Je veux créer un programme qui va vous permettre de simuler une mémoire (OOM) situation sur un serveur Unix. J'ai créé ce super-mémoire simple eater:

#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;
}

Il mange autant de mémoire que défini dans l' memory_to_eat qui maintenant est exactement de 50 GO de RAM. Il alloue de la mémoire de 1 MO et gravures exactement le point où il ne parvient pas à allouer plus, pour que je sache à qui la valeur maximale, il a réussi à en manger.

Le problème est qu'il fonctionne. Même sur un système avec 1 GO de mémoire physique.

Quand je vérifie dessus je vois que le processus mange 50 GO de mémoire virtuelle et seulement moins de 1 MO de mémoire résidente. Est-il un moyen de créer une mémoire eater qui fait vraiment la consommer?

Les spécifications du système: Linux kernel 3.16 (Debian) sont les plus susceptibles à la surexploitation activé (pas sûr de la façon de le vérifier) avec pas de swap et virtualisés.

219voto

cmaster Points 7460

Quand votre malloc() de la mise en œuvre des demandes de la mémoire du noyau du système (via un sbrk() ou mmap() appel système), le noyau ne fait une remarque que vous avez demandé la mémoire et où il doit être placé à l'intérieur de votre espace d'adressage. Il ne fait pas de carte ces pages encore.

Lorsque le processus par la suite accède à la mémoire au sein de la nouvelle région, le matériel reconnaît une erreur de segmentation et alertes le noyau de l'état. Le noyau, puis regarde la page dans ses propres structures de données, et trouve que vous devriez avoir un zéro page là, donc c'cartes dans un zéro page (éventuellement première expulsion d'une page à partir de la page-cache) et les retours à partir de l'interruption. Votre processus ne se rendent pas compte que tout cela est arrivé, les grains de l'opération est parfaitement transparent (sauf pour le court délai, alors que le noyau fait son travail).

Cette optimisation permet l'appel système pour revenir très vite, et, plus important encore, il évite toutes les ressources pour être commis à votre processus lorsque la correspondance est établie. Ce processus permet de réserver plutôt tampons de grande taille qu'ils n'ont jamais besoin dans des circonstances normales, sans crainte d'engloutir trop de mémoire.


Donc, si vous voulez programmer une mémoire mangeur, vous devez absolument le faire réellement quelque chose avec de la mémoire à allouer. Pour cela, il vous suffit d'ajouter une seule ligne de code:

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;
    }
}

Notez qu'il est tout à fait suffisante pour écrire sur un seul octet à l'intérieur de chaque page (qui contient 4096 octets sur X86). C'est parce que l'allocation de la mémoire à partir du noyau d'un processus est fait à la mémoire de la page de granularité, qui est, à son tour, en raison du matériel qui ne permet pas de pagination dans les petits niveaux de granularité.

26voto

Magisch Points 2913

Toutes les pages virtuelles de commencer la copie sur écriture mappé à la même mise à zéro de la page physique. Pour utiliser les pages physiques, vous pouvez sale par écrit quelque chose à chaque page virtuelle.

Si en cours d'exécution en tant que root, vous pouvez utiliser mlock(2) ou mlockall(2) d'avoir le noyau fil des pages lorsqu'ils sont attribués, sans avoir à se salir. (normal les utilisateurs non-root ont un ulimit -l seulement 64kiB.)

Comme beaucoup d'autres ont suggéré, il semble que le noyau Linux n'a pas vraiment d'allouer de la mémoire, sauf si vous y écrire

Une version améliorée de code qui fait ce que l'OP a vouloir:

Cela résout aussi le format de printf inadéquation avec les types de memory_to_eat et eaten_memory, à l'aide de %zi imprimer size_t des entiers. La taille de la mémoire de manger, de kiB, peut éventuellement être spécifié en ligne de commande arg.

Le désordre de la conception à l'aide de variables globales, et une croissance de 1k au lieu de 4k pages, est inchangée.

#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;
}

13voto

Bathsheba Points 23209

Une optimisation raisonnable faite ici. Le runtime ne fait pas réellement acquérir la mémoire jusqu'à ce que vous l’utilisez.

Un simple sera suffisante pour contourner cette optimisation. (Si vous pensez que optimise encore sur l’allocation de mémoire jusqu’au point d’utilisation.)

6voto

doron Points 10296

Pas sûr de cela, mais la seule explication que je puisse les choses, c'est que linux est un copie-écriture du système d'exploitation. Quand on appelle fork les deux processus pointer vers le même physiquement la mémoire. La mémoire est seulement une fois copié à un processus réellement ÉCRIT à la mémoire.

Je pense qu'ici, la mémoire physique réelle n'est attribuée que lorsque l'on essaie d'écrire quelque chose. Appelant sbrk ou mmap peut très bien ne mettre à jour le noyau du livre de mémoire-garder. La réelle de la RAM ne peut être allouée que lorsque nous tentons de le faire accéder à la mémoire.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X