为什么这种记忆食物真的不会记忆?
我想创建一个程序来模拟Unix服务器上的内存不足(OOM)情况。 我创造了这个超级简单的记忆食器:
#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 GB。 它将内存分配1 MB,并精确地打印出未能分配更多内容的位置,以便我知道它设法使用哪个最大值。
问题在于它的工作原理。 即使在具有1 GB物理内存的系统上。
当我查看顶部时,我发现该进程可以消耗50GB的虚拟内存,并且只有不到1MB的驻留内存。 有没有办法创建一个真正消耗它的记忆食器?
系统规格:Linux核心3.16(Debian)最有可能启用overcommit(不知道如何检查出来),没有交换和虚拟化。
当你的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;
}
}
请注意,写入每个页面中的单个字节(其中包含X86上的4096个字节)就足够了。 这是因为从内核到进程的所有内存分配都是以内存页面粒度完成的,这反过来又是因为硬件不允许以较小粒度进行分页。
所有的虚拟页面开始写时复制映射到相同的归零物理页面。 要使用物理页面,可以通过向每个虚拟页面写入内容来使其变脏。
如果以root身份运行,可以使用mlock(2)
或mlockall(2)
让内核在分配时连接页面,而不必弄脏它们。 (正常的非root用户有一个只有64kiB的ulimit -l
。)
和其他人一样,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 ramn", 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 ramn", megabytes);
megabyte = 0;
}
}
printf("Successfully eaten requested memory!n");
free(memory);
return 0;
}
这里正在做出明智的优化。 在您使用它之前,运行时实际上并不会获取内存。
一个简单的memcpy
就足以绕过这个优化。 (你可能会发现calloc
仍然会优化内存分配直到使用点。)