首页 文章

为什么这个记忆食者真的不吃记忆?

提问于
浏览
148

我想创建一个程序来模拟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的内存 . 它将内存分配1 MB并准确打印出无法分配更多内存的点,以便我知道它设置了哪个最大值 .

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

当我检查顶部时,我看到该进程占用50 GB的虚拟内存,并且只有不到1 MB的驻留内存 . 有没有办法创建一个真正消耗它的记忆食者?

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

4 回答

  • 26

    当您的 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个字节)就足够了 . 这是因为从内核到进程的所有内存分配都是在内存页面粒度下完成的,而这又是因为硬件不允许以较小的粒度进行分页 .

  • 6

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

    如果以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;
    }
    
  • 13

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

    一个简单的_1797627就足以绕过这种优化 . (您可能会发现 calloc 仍然优化了内存分配,直到使用点 . )

  • 219

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

    我想在这里,实际的物理内存只在一个人试图写东西时分配 . 调用 sbrkmmap 可能只会更新内核的内存簿 . 实际的RAM只能在我们实际尝试访问内存时分配 .

相关问题