Почему этот лютик памяти действительно не ел память?

Я хочу создать программу, которая будет имитировать ситуацию с нехваткой памяти (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 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;
}

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

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

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

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

Ответ 1

Когда ваша реализация 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). Это связано с тем, что все выделение памяти из ядра для процесса выполняется на уровне детализации страницы памяти, что, в свою очередь, связано с оборудованием, которое не позволяет подкачки при меньших деталях.

Ответ 2

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

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

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

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

Это также устраняет несоответствия строки формата printf с типами memory_to_eat и eaten_memory, используя %zi для печати целых чисел size_t. Размер памяти для использования в kiB можно опционально указать в качестве командной строки arg.

Беспорядочный дизайн с использованием глобальных переменных и рост на 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;
}

Ответ 3

Здесь делается разумная оптимизация. Время выполнения фактически не приобретает память, пока вы ее не используете.

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

Ответ 4

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

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