【问题标题】:Malloc on linux without overcommittinglinux上的malloc没有过度使用
【发布时间】:2018-07-13 02:18:31
【问题描述】:

如何在不过度使用的情况下在 Linux 上分配内存,以便 malloc 在没有可用内存且进程在访问时不会随机崩溃时实际返回 NULL

我对 malloc 工作原理的理解:

  1. 分配器检查空闲列表是否有空闲内存。如果是,则分配内存。
  2. 如果否,则从内核分配新页面。这将是可能发生过度使用的地方。然后返回新的内存。

因此,如果有一种方法可以从内核获取由物理内存立即支持的内存,那么分配器可以使用该方法而不是获取过度使用的页面,并在内核拒绝提供更多内存时返回 NULL

有没有办法做到这一点?

更新:

我知道这不能完全保护进程免受 OOM 杀手的影响,因为如果分数不好,它仍然会在内存不足的情况下被杀死,但这不是我担心的。

更新 2: Nominal Animal 的评论让我有了以下使用mlock的想法:

void *malloc_without_overcommit(size_t size) {
    void *pointer = malloc(size);
    if (pointer == NULL) {
        return NULL;
    }
    if (mlock(pointer, size) != 0) {
        free(pointer);
        return NULL;
    }

    return pointer;
}

但是由于所有的系统调用,这可能很慢,所以这可能应该在分配器实现级别完成。而且它还阻止了使用交换。

更新 3:

遵循 John Bollingers 的新想法:

  1. 检查是否有足够的内存可用。据我了解,这必须在 /proc/meminfoMemFreeSwapFree 值中进行检查。
  2. 仅当有足够的可用空间(加上额外的安全余量)时,才分配内存。
  3. 使用getpagesize 找出页面大小,并在每个页面大小的内存中写入一个字节,以便它得到物理内存(RAM 或交换)的支持。

我还更仔细地查看了mmap(2) 并发现了以下内容:

MAP_NORESERVE

不要为此映射保留交换空间。保留交换空间时,可以保证可以修改映射。如果没有保留交换空间,如果没有可用的物理内存,可能会在写入时获得 SIGSEGV。另请参见 proc(5) 中对文件 /proc/sys/vm/overcommit_memory 的讨论。在 2.6 之前的内核中,这个标志只对私有可写有效

这是否意味着使用 ~MAP_NORESERVE 进行映射将完全保护进程免受 OOM 杀手的攻击?如果是这样,这将是一个完美的解决方案,只要有一个malloc 实现,它可以直接在mmap 之上工作。 (也许是 jemalloc?)

更新 4: 我目前的理解是~MAP_NORESERVE 不会保护 OOM 杀手,但至少可以防止第一次写入内存时出现段错误。

【问题讨论】:

  • @NominalAnimal 如果没有 [overcommit],虚拟内存将限制为总 RAM。 可用交换空间也会增加可用虚拟内存。
  • mlock(pointer, size) 可能无法使用 - mlock() 将锁定页面,而您仍在使用 malloc()。您还必须尝试以某种方式跟踪需要解锁的页面,因为munlock() 也对整个页面进行操作。
  • @FSMaxB free() 不必“回馈”任何东西。一旦将堆内存分配给您的进程,您的进程通常会永远保留它。 Linux 上的标准堆例程在底层确实使用了混合模式分配器,但是,较大的分配可能会通过专用的 mmap() 调用来满足,而较小的分配可能会使用 sbrk()/brk()-obtained RAM 或 @987654343 @ 记忆。 Linux 的混合模式分配器确实使解决您的特定问题变得更加困难。
  • 如果可能,您可以通过将 sysctl vm.overcommit_memory 设置为 2 来禁用整个系统的过度使用。
  • 我明确不想在整个系统中关闭过度使用。 -- 那有什么意义呢?内存过量使用是整个系统的问题。您无法在每个进程的基础上有效地避免它,因为即使您的进程的分配在没有 ovecommit 的情况下成功,任何进程的下一次分配可能会使系统进入过度使用状态,从而影响您的进程任何其他。

标签: c linux memory-management memory-overcommitment


【解决方案1】:

如何在不过度使用的情况下在 Linux 上分配内存

那是loaded question,或者至少是不正确的。该问题基于不正确的假设,这使得回答所述问题充其量是无关紧要的,最坏的情况是误导。

Memory overcommitment 是一个系统范围的策略——因为它决定了有多少虚拟内存可供进程使用——而不是进程可以自己决定的东西。

由系统管理员决定内存是否过度使用。在 Linux 中,该策略是相当可调的(参见例如 man 5 proc 中的 /proc/sys/vm/overcommit_memory在此期间进程无能为力 会影响内存过度使用策略的分配
 

OP 似乎也有兴趣让他们的进程免受 Linux 中的内存不足杀手(OOM 杀手)的影响。 (Linux中的OOM杀手是一种用于缓解内存压力的技术,通过杀死进程,从而将它们的资源释放回系统。)

这也是一种错误的方法,因为 OOM 杀手是一个启发式进程,其目的不是“惩罚或杀死行为不端的进程”,而是保持系统运行。这个工具在 Linux 中也非常可调,系统管理员甚至可以调整每个进程在高内存压力情况下被杀死的可能性。除了进程使用的内存量,在内存不足的情况下OOM杀手是否会杀死它并不取决于进程;这也是由系统管理员管理的政策问题,而不是流程本身。
 

我假设 OP 试图解决的实际问题是如何编写能够动态响应内存压力的 Linux 应用程序或服务,而不是仅仅死亡(由于 SIGSEGV 或 OOM 杀手)。这个问题的答案是你不需要——你让系统管理员担心什么对他们来说是重要的,在他们的工作量中——除非你的应用程序或服务是一个使用大量的应用程序或服务和大量的内存,因此很可能在高内存压力期间被不公平地杀死。 (特别是如果数据集足够大,需要启用比其他方式更大的交换量,从而导致更高的交换风暴风险和迟到但太强的 OOM 杀手。)

解决方案,或者至少是可行的方法,是对关键部分(甚至整个应用程序/服务,如果它适用于不应交换到磁盘的敏感数据)进行内存锁定,或者使用带有专用后备文件的内存映射。 (对于后者,here 是我在 2011 年写的一个例子,它操作了一个 TB 大小的数据集。)

OOM 杀手仍然可以杀死进程,并且仍然会发生 SIGSEGV(由于内核无法提供 RAM 支持的库函数的内部分配),除非所有应用程序都锁定到 RAM,但是至少服务/进程不再是不公平的目标,只是因为它使用大量内存。

可以捕获 SIGSEGV 信号(当没有内存可用于支持虚拟内存时会发生这种情况),但到目前为止,我还没有看到可以保证代码复杂性和所需维护工作的用例。
 

总之,对所述问题的正确答案是不,不要那样做

【讨论】:

    【解决方案2】:

    从 cmets 中的讨论来看,它似乎在呼唤

    mlockall( MCL_CURRENT | MCL_FUTURE );
    

    进程启动时满足malloc()在系统实际无法提供内存时返回NULL的要求。

    the Linux mlockall() man page:

    mlockall() 和 munlockall()

    mlockall() 锁定所有映射到该地址空间的页面 调用过程。这包括代码、数据和堆栈的页面 段,以及共享库、用户空间内核数据、共享 内存和内存映射文件。所有映射的页面都保证 调用成功返回时驻留在RAM中;页面是 保证在以后解锁之前一直保留在 RAM 中。

    flags 参数被构造为一个或多个的按位或 以下常量:

       MCL_CURRENT Lock all pages which are currently mapped into the
                   address space of the process.
    
       MCL_FUTURE  Lock all pages which will become mapped into the address
                   space of the process in the future.  These could be, for
                   instance, new pages required by a growing heap and stack
                   as well as new memory-mapped files or shared memory
                   regions.
    
       MCL_ONFAULT (since Linux 4.4)
                   Used together with MCL_CURRENT, MCL_FUTURE, or both.
                   Mark all current (with MCL_CURRENT) or future (with
                   MCL_FUTURE) mappings to lock pages when they are faulted
                   in.  When used with MCL_CURRENT, all present pages are
                   locked, but mlockall() will not fault in non-present
                   pages.  When used with MCL_FUTURE, all future mappings
                   will be marked to lock pages when they are faulted in,
                   but they will not be populated by the lock when the
                   mapping is created.  MCL_ONFAULT must be used with either
                   MCL_CURRENT or MCL_FUTURE or both.
    

    如果已指定 MCL_FUTURE,则稍后的系统调用(例如, mmap(2)、sbrk(2)、malloc(3)),如果会导致 锁定字节超过允许的最大值(见下文)。在相同的 在这种情况下,堆栈增长同样可能失败:内核将拒绝 堆栈扩展并向进程传递 SIGSEGV 信号。

    请注意,以这种方式使用mlockall() 可能会产生其他意想不到的后果。 Linux 的开发假设内存过量使用可用,因此在mlockall() 之后调用fork() 这样简单的操作可能会遇到问题。

    【讨论】:

    • 但这似乎再次与 RAM 相关联。如果所有 OP 想要避免过度使用,那么这是一个主要的过度杀伤力。过度使用与 RAM 无关(正如您在 cmets 中正确所说的那样)。
    • 嗯,阅读手册页的部分内容让我想知道通常允许锁定的限制是什么。也许 mlock 毕竟不是最好的方法。
    • @AnT 但这似乎又与 RAM 相关联。 当然,它与 RAM 相关联。 OP 的要求是 malloc() 在实际没有可用 RAM 时返回 NULL
    • @FSMaxB 允许锁定的限制通常是什么我猜这是一个可调整的参数,默认为 RAM 的 50%。
    • @Andrew Henle:但他在哪里说的?首先,最初的问题显然是关于过度使用和过度使用。其次,如果他们在仅 RAM 的设置中工作,那么锁定问题就没有实际意义了。如果他们在启用交换的设置中工作,那么malloc 和“RAM 可用”之间根本没有任何联系。在 cmets 中,OP 显然在谈论 RAM+swap。
    猜你喜欢
    • 2010-10-10
    • 1970-01-01
    • 2013-12-12
    • 2019-07-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多