Почему этот пожиратель памяти на самом деле не ест память?


Я хочу создать программу, которая будет имитировать ситуацию нехватки памяти (OOM) на сервере Unix. Я создал этот супер-простой пожиратель памяти:

#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 ramn", 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 ramn");
            megabyte = 0;
        }
    }
    printf("Successfully eaten requested memory!n");
    free(memory);
    return 0;
}

он съедает столько памяти, сколько определено в memory_to_eat который теперь составляет ровно 50 ГБ оперативной памяти. Он выделяет память на 1 МБ и печатает именно ту точку, где он не может выделить больше, так что я знаю, какое максимальное значение ему удалось съесть.

проблема в том, что он работает. Даже на системе с 1 ГБ физической память.

когда я проверяю top, я вижу, что процесс съедает 50 ГБ виртуальной памяти и только менее 1 МБ резидентной памяти. Есть ли способ создать пожирателя памяти, который действительно потребляет его?

системные характеристики: Ядро Linux 3.16 (Debian) скорее всего, с включенным overcommit (не уверен, как это проверить) без подкачки и виртуализации.

4 148

4 ответа:

когда malloc() реализация запрашивает память из ядра системы (через sbrk() или mmap() системный вызов), ядро только делает заметку, что вы запросили память и где она должна быть размещена в вашем адресном пространстве. на самом деле он еще не отображает эти страницы.

когда процесс впоследствии обращается к памяти в новом регионе, аппаратное обеспечение распознает ошибку сегментации и предупреждает ядро об этом условии. Ядро тогда выглядит вверх по странице в своих собственных структурах данных и обнаруживает, что у вас должна быть нулевая страница, поэтому она отображается на нулевой странице (возможно, сначала вытесняя страницу из кэша страниц) и возвращается из прерывания. Ваш процесс не понимает, что все это произошло, операция ядра совершенно прозрачна (за исключением короткой задержки, пока ядро выполняет свою работу).

эта оптимизация позволяет системному вызову возвращаться очень быстро, и, самое главное, она позволяет избежать каких-либо ресурсов, чтобы быть совершенные в процессе, когда выполняется съемка. Это позволяет процессам резервировать довольно большие буферы, которые им никогда не нужны при нормальных обстоятельствах, не опасаясь поглощения слишком большого объема памяти.


Итак, если вы хотите запрограммировать пожирателя памяти, вам абсолютно необходимо что-то сделать с выделенной памятью. Для этого вам нужно всего лишь добавить одну строку в ваш код:

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

обратите внимание, что для записи в один байт вполне достаточно внутри каждой страницы (которая содержит 4096 байт на X86). Это связано с тем, что все выделение памяти из ядра в процесс выполняется при гранулярности страницы памяти, что, в свою очередь, связано с аппаратным обеспечением, которое не позволяет выполнять подкачку при меньших гранулярностях.

все виртуальные страницы начинаются с копирования на запись, сопоставленные одной и той же обнуленной физической странице. Чтобы использовать физические страницы, вы можете загрязнить их, написав что-то на каждой виртуальной странице.

если работает как root, вы можете использовать mlock(2) или mlockall(2) чтобы ядро подключало страницы, когда они выделяются, без необходимости их загрязнять. (обычный не-root пользователей ulimit -l только 64kiB.)

как и многие другие, предложил, кажется, что ядро Linux не действительно выделите память, если вы не пишете в нее

улучшенная версия кода, которая делает то, что OP хотел:

это также исправляет несоответствие строки формата printf с типами memory_to_eat и eaten_memory, используя %zi печати size_t целых чисел. Размер памяти, который нужно съесть, в КИБ, можно выборочно определить как арг командной строки.

грязный дизайн, использующий глобальные переменные, и растущий на 1k вместо 4K страниц, является не менявшийся.

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

здесь проводится разумная оптимизация. Среда выполнения на самом деле не приобрести память, пока вы используете его.

простой memcpy будет достаточно, чтобы обойти эту оптимизацию. (Вы можете найти, что calloc по-прежнему оптимизирует выделение памяти до точки использования.)

не уверен в этом, но единственное объяснение, которое я могу сделать, это то, что linux-это операционная система для копирования на запись. Когда кто-то звонит fork оба процесса указывают на одну и ту же физическую память. Память копируется только один раз, когда один процесс фактически записывает в память.

Я думаю, что здесь фактическая физическая память выделяется только тогда, когда кто-то пытается что-то записать в нее. Звоню sbrk или mmap вполне может обновить только Книгу памяти ядра. Фактическая оперативная память может быть выделена только тогда, когда мы фактически пытаемся получить доступ к памяти.