为什么这个内存吞噬者不真的吃内存?

Why doesn't this memory eater really eat memory?

我想创建一个程序来模拟 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 RAM。它按 1 MB 分配内存并准确打印未能分配更多内存的点,以便我知道它设法吃掉了哪个最大值。

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

当我检查 top 时,我发现该进程占用了 50 GB 的虚拟内存,而只有不到 1 MB 的常驻内存。有没有办法创建一个真正消耗它的内存吞噬者?

系统规格:Linux 内核 3.16 (Debian) 很可能启用了过度使用(不确定如何检查),没有交换和虚拟化。

这里正在进行合理的优化。在您使用内存之前,运行时实际上不会获取内存。

一个简单的memcpy就足以绕过这个优化。 (您可能会发现 calloc 仍在优化内存分配直到使用点。)

所有虚拟页面开始时都是写时复制映射到相同的零物理页面。要用完物理页面,您可以通过向每个虚拟页面写入内容来弄脏它们。

如果 运行 作为 root,您可以使用 mlock(2)mlockall(2) 让内核在分配页面时连接页面,而不必弄脏它们。 (普通非 root 用户的 ulimit -l 只有 64kiB。)

As many others suggested, it seems that the Linux kernel doesn't really allocate the memory unless you write to it

代码的改进版本,实现了 OP 的要求:

这也修复了 printf 格式字符串与 memory_to_eat 和 eaten_memory 类型不匹配的问题,使用 %zi 打印 size_t 整数。要使用的内存大小(以 kiB 为单位)可以选择指定为命令行参数。

使用全局变量并增加 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;
}

不确定这个,但我唯一能做的解释是 linux 是一个写时复制操作系统。当调用 fork 时,两个进程都指向相同的物理内存。只有在一个进程实际写入内存时才会复制内存。

我想在这里,实际的物理内存只有在尝试向其中写入内容时才会分配。调用 sbrkmmap 可能只会更新内核的内存簿。实际的RAM可能只有在我们实际尝试访问内存时才会分配。

当您的 malloc() 实现从系统内核请求内存时(通过 sbrk()mmap() 系统调用),内核只会记录您已请求内存和将其放置在您的地址 space 中的位置。 它实际上还没有映射那些页面

当进程随后访问新区域内的内存时,硬件会识别出分段错误并向内核发出警报。然后内核在它自己的数据结构中查找页面,发现那里应该有一个零页面,所以它映射到一个零页面(可能首先从页面缓存中驱逐一个页面)和 returns 来自中断。您的进程没有意识到任何这一切的发生,内核操作是完全透明的(除了内核工作时的短暂延迟)。

此优化允许系统调用 return 非常快,最重要的是,它避免了在进行映射时将任何资源提交给您的进程。这允许进程保留在正常情况下他们永远不需要的相当大的缓冲区,而不用担心占用太多内存。


所以,如果你想编写一个内存吞噬器,你绝对必须对你分配的内存做一些实际的事情。为此,您只需要在代码中添加一行:

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 个字节)。这是因为从内核到进程的所有内存分配都是在内存页面粒度上完成的,这反过来又是因为硬件不允许以更小的粒度进行分页。