Я хочу создать программу, которая будет имитировать ситуацию нехватки памяти (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 ГБ физической памяти.
Когда я проверяю top, я вижу, что процесс съедает 50 ГБ виртуальной памяти и только менее 1 МБ резидентной памяти. Есть ли способ создать пожиратель памяти, который действительно потребляет его?
Спецификации системы: ядро Linux 3.16 ( Debian ), скорее всего, с включенной функцией overcommit (не знаю, как это проверить), без подкачки и виртуализации.
источник
sysctl -w vm.overcommit_memory=2
как root; см. mjmwired.net/kernel/Documentation/vm/overcommit-accounting . Обратите внимание, что это может иметь другие последствия; в частности, очень большие программы (например, ваш веб-браузер) могут не запускать вспомогательные программы (например, программа чтения PDF).Ответы:
Когда ваша
malloc()
реализация запрашивает память у системного ядра (черезsbrk()
илиmmap()
системный вызов), ядро только отмечает, что вы запросили память и где она должна быть размещена в вашем адресном пространстве. На самом деле он еще не отображает эти страницы .Когда процесс впоследствии обращается к памяти в новом регионе, оборудование распознает ошибку сегментации и предупреждает ядро об этом состоянии. Затем ядро просматривает страницу в своих собственных структурах данных и обнаруживает, что у вас должна быть нулевая страница, поэтому она отображается на нулевой странице (возможно, сначала удаляя страницу из кеша страниц) и возвращается из прерывания. Ваш процесс не осознает, что все это произошло, работа ядра совершенно прозрачна (за исключением небольшой задержки, пока ядро выполняет свою работу).
Эта оптимизация позволяет системному вызову возвращаться очень быстро, и, что наиболее важно, она позволяет избежать выделения каких-либо ресурсов для вашего процесса при сопоставлении. Это позволяет процессам резервировать довольно большие буферы, которые им никогда не нужны в нормальных условиях, не опасаясь сожрать слишком много памяти.
Итак, если вы хотите запрограммировать пожиратель памяти, вам действительно нужно что-то делать с выделенной вам памятью. Для этого вам нужно всего лишь добавить одну строку в ваш код:
Обратите внимание, что вполне достаточно написать один байт на каждой странице (который содержит 4096 байт в X86). Это связано с тем, что все выделение памяти от ядра процессу выполняется с гранулярностью страниц памяти, что, в свою очередь, связано с аппаратным обеспечением, которое не допускает разбиение на страницы при меньших гранулярностях.
источник
mmap
иMAP_POPULATE
(хотя обратите внимание, что на странице руководства написано, что « MAP_POPULATE поддерживается только для частных сопоставлений, начиная с Linux 2.6.23 »).mlockall(MCL_FUTURE)
. (Для этого требуется root, посколькуulimit -l
для учетных записей пользователей при установке Debian / Ubuntu по умолчанию используется только 64 КБ.) Я только что попробовал это в Linux 3.19 с sysctl по умолчаниюvm/overcommit_memory = 0
, и заблокированные страницы используют swap / физическую RAM.malloc()
возвращает один из его вызововnull
. Это явно недостаток этого подхода к управлению памятью. Тем не менее, уже наличие отображений копирования при записи (например, динамических библиотек и т. Д.fork()
) Делает невозможным для ядра знать, сколько памяти на самом деле потребуется. Таким образом, если бы он не перегружал память, вам бы не хватило картографической памяти задолго до того, как вы фактически использовали всю физическую память.SIGSEGV
сигнал должен быть доставлен процессу.Все виртуальные страницы начинаются копированием при записи, сопоставленными с одной и той же обнуленной физической страницей. Чтобы использовать физические страницы, вы можете испачкать их, записав что-то на каждую виртуальную страницу.
Если вы работаете от имени пользователя root, вы можете использовать
mlock(2)
илиmlockall(2)
заставить ядро подключать страницы, когда они выделены, без необходимости их пачкать. (обычные пользователи безulimit -l
полномочий root имеют размер только 64 КБ.)Улучшенная версия кода, которая делает то, что хотел OP:
Это также исправляет несоответствие строки формата printf с типами memory_to_eat и eaten_memory, используемыми
%zi
для печатиsize_t
целых чисел. Объем используемой памяти в килобайтах необязательно может быть указан в командной строке arg.Грязный дизайн с использованием глобальных переменных и ростом на 1 Кб вместо 4 Кб страниц не изменился.
источник
Здесь проводится разумная оптимизация. Среда фактически не приобретает память , пока вы не используете его.
Простого
memcpy
будет достаточно, чтобы обойти эту оптимизацию. (Вы можете обнаружить, что этоcalloc
оптимизирует распределение памяти до момента использования.)источник
malloc
выделяет на границах страницы то, что мне кажется довольно вероятным.calloc
использует mmap (MAP_ANONYMOUS), дающий обнуленные страницы, поэтому он не дублирует работу ядра по обнулению страниц.Не уверен насчет этого, но единственное объяснение, которое я могу понять, это то, что linux - это операционная система копирования при записи. При вызовеfork
оба процесса указывают на одну и ту же физическую память. Память копируется только один раз, когда один процесс действительно ЗАПИСЫВАЕТ в память.Я думаю здесь, фактическая физическая память выделяется только тогда, когда кто-то пытается что-то записать в нее. Звонок
sbrk
илиmmap
вполне может только обновить память ядра памяти. Фактическая RAM может быть выделена только тогда, когда мы действительно пытаемся получить доступ к памяти.источник
fork
не имеет к этому никакого отношения. Вы бы увидели то же поведение, если бы загрузили Linux с этой программой, как/sbin/init
. (т.е. PID 1, первый процесс пользовательского режима). Тем не менее, у вас была правильная общая идея с копированием при записи: до тех пор, пока вы не испачкаете их, все вновь выделенные страницы будут копироваться при записи на одну и ту же нулевую страницу.