为什么这种记忆食物真的没有记忆?

我想创build一个程序来模拟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 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 GB的内存。 它分配1MB的内存,并打印出分配更多错误的点,以便我知道它设法吃的最大值。

问题是,它的工作。 即使在具有1 GB物理内存的系统上。

当我查看顶部时,我看到该进程吃了50 GB的虚拟内存,只有不到1 MB的驻留内存。 有没有办法build立一个真正消耗它的记忆食器?

系统规格:Linux内核3.16( Debian )最有可能启用overcommit(不知道如何检查出来),没有交换和虚拟化。

当你的malloc()实现从系统内核(通过sbrk()mmap()系统调用)请求内存时,内核只会记下你已经请求了内存,并且将它放在你的地址空间中。 它实际上并没有映射这些页面

当进程随后访问新区域内的内存时,硬件会识别出分段故障并向内核发出警报。 内核然后在它自己的数据结构中查找页面,发现你应该有一个零页面,所以它映射到一个零页(可能首先从页面caching中逐出页面),并从中断返回。 你的进程没有意识到发生了这种情况,内核操作是完全透明的(除了内核工作时的短暂延迟)。

此优化允许系统调用很快返回,最重要的是,它避免了在映射时将任何资源提交到您的进程。 这允许进程保留在正常情况下从不需要的相当大的缓冲区,而不用担心会吞噬太多的内存。


所以,如果你想编程一个记忆食客,你绝对必须真正做你分配的内存的东西。 为此,您只需在代码中添加一行代码即可:

 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 。)

正如其他人所build议的那样,除非写入内核,否则Linux内核似乎并不真正分配内存

代码的改进版本,这是OP所要做的:

这还修复了printf格式string与memory_to_eat和eaten_memorytypes不匹配,使用%zi打印size_t整数。 在kiB中吃的内存大小可以select性地指定为一个命令行arg。

使用全局variables的混乱devise,并增长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 ,两个进程都指向相同的物理内存。 只有一个进程实际写入内存时才会复制内存。

我想在这里,实际的物理内存只有在试图写入内容时才会被分配。 调用sbrkmmap可能只会更新内核的内存book-keep。 实际的RAM只能在我们实际访问内存的时候分配。