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

Pet*_*etr 148 c linux memory virtual-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;
}
Run Code Online (Sandbox Code Playgroud)

它占用尽可能多的内存memory_to_eat,现在正好是50 GB的内存.它将内存分配1 MB并准确打印出无法分配更多内存的点,以便我知道它设置了哪个最大值.

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

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

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

cma*_*ter 219

当您的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;
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,写入每个页面中的单个字节(在X86上包含4096个字节)就足够了.这是因为从内核到进程的所有内存分配都是在内存页面粒度下完成的,而这又是因为硬件不允许以较小的粒度进行分页.

  • 也可以使用`mmap`和`MAP_POPULATE`提交内存(但请注意,手册页说"自Linux 2.6.23_以来,私有映射支持_MAP_POPULATE"). (5认同)
  • 这基本上是正确的,但我认为这些页面都是写时复制映射到一个归零的页面,而不是根本不存在于页表中。这就是为什么你必须写,而不仅仅是阅读每一页。此外,另一种耗尽物理内存的方法是锁定页面。例如调用`mlockall(MCL_FUTURE)`。(这需要 root,因为对于 Debian/Ubuntu 默认安装的用户帐户,`ulimit -l` 只有 64kiB。)我刚刚在 Linux 3.19 上尝试使用默认的 sysctl `vm/overcommit_memory = 0`,并且锁定页面使用交换/物理内存。 (2认同)
  • @cad虽然X86-64支持两种较大的页面大小(2 MiB和1 GiB),但Linux内核仍然对它们非常特殊.例如,它们仅用于显式请求,并且仅在系统已配置为允许它们时使用.此外,4 kiB页面仍然是可以映射内存的粒度.这就是为什么我不认为提到大页面会增加答案. (2认同)
  • @BillBarth对于硬件,你所谓的页面错误和段错误之间没有区别.硬件仅看到违反页表中规定的访问限制的访问,以及通过分段错误向内核调度的信号.然后,只有软件方面决定是否应该通过提供页面(更新页面表)来处理分段错误,或者是否应该将"SIGSEGV"信号传递给进程. (2认同)

Mag*_*sch 26

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

如果以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;
}
Run Code Online (Sandbox Code Playgroud)


Bat*_*eba 13

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

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

  • 你确定吗?我认为如果他的分配金额达到*virtual*memory的最大值,那么无论如何,malloc都会失败.malloc()如何知道没有人会使用内存?它不能,所以它必须调用sbrk()或其操作系统中的等价物. (2认同)
  • @doron这里没有编译器.这是Linux内核的行为. (2认同)

dor*_*ron 6

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

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