8

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

我对 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/meminfoMemFreeandSwapFree值中进行检查。
  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 杀手,但至少可以防止第一次写入内存时出现段错误。

4

2 回答 2

6

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

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

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

由系统管理员决定内存是否过度使用。在 Linux 中,该策略是非常可调的(参见例如man 5 proc/proc/sys/vm/overcommit_memory分配期间,进程不能做任何事情来影响内存过度使用策略
 

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

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

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

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

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

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

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

于 2018-02-03T01:35:08.313 回答
1

从评论中的讨论来看,似乎在呼吁

mlockall( MCL_CURRENT | MCL_FUTURE );

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

根据Linuxmlockall()手册页

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 是在假设内存过量使用可用的情况下开发的,所以像调用fork()after这样简单的事情mlockall()可能会遇到问题。

于 2018-02-02T15:41:09.497 回答