【问题标题】:How to get malloc/calloc to fail if request exceeds free physical memory (i.e., don't use swap)如果请求超过可用物理内存(即不使用交换),如何让 malloc/calloc 失败
【发布时间】:2023-03-17 09:33:01
【问题描述】:

malloc/calloc 显然使用交换空间来满足超出可用内存的请求。这几乎使系统挂起,因为磁盘使用指示灯一直亮着。发生在我身上之后,我并不确定为什么,我编写了以下 5 行测试程序来检查这确实是系统挂起的原因,

/* --- test how many bytes can be malloc'ed successfully --- */
#include <stdio.h>
#include <stdlib.h>
int main ( int argc, char *argv[] ) {
  unsigned int nmalloc = (argc>1? atoi(argv[1]) : 10000000 ),
               size    = (argc>2? atoi(argv[2]) : (0) );
  unsigned char *pmalloc = (size>0? calloc(nmalloc,size):malloc(nmalloc));
  fprintf( stdout," %s malloc'ed %d elements of %d bytes each.\n",
    (pmalloc==NULL? "UNsuccessfully" : "Successfully"),
    nmalloc, (size>0?size:1) );
  if ( pmalloc != NULL ) free(pmalloc);
  } /* --- end-of-function main() --- */

如果您的两个命令行参数的乘积超过物理内存,那确实会挂起系统。最简单的解决方案是 malloc/calloc 自动失败的某种方式。更难且不可移植的是编写一个 popen() 的 free 命令的小包装器,解析输出,并且只有在可用的“空闲”内存可以满足请求时才调用 malloc/calloc,也许有一点安全因素内置。

有没有更简单、更便携的方法来实现这一点? (显然类似于这个问题can calloc or malloc be used to allocate ONLY physical memory in OSX?,但我希望得到某种“是”的答案。)

                                                                                                                                                                                                                                                                                          = 决定遵循 Tom 的 /proc/meminfo 建议。也就是说,与其 popen()'ing "free",不如直接解析现有且易于解析的 /proc/meminfo 文件。然后,表单的单行宏

#define noswapmalloc(n) ( (n)

完成工作。如下所示的 memfree() 并不像我想要的那样可移植,但如果/当需要时,可以轻松透明地被更好的解决方案替换,而现在不是。

#include <stdio.h>
#include <stdlib.h>
#define _GNU_SOURCE                     /* for strcasestr() in string.h */
#include <string.h>
char    *strcasestr();                  /* non-standard extension */

/* ==========================================================================
 * Function:    memfree ( memtype )
 * Purpose:     return number of Kbytes of available memory
 *              (as reported in /proc/meminfo)
 * --------------------------------------------------------------------------
 * Arguments:   memtype (I)     (char *) to null-terminated, case-insensitive
 *                              (sub)string matching first field in
 *                              /proc/meminfo (NULL uses MemFree)
 * --------------------------------------------------------------------------
 * Returns:     ( int )         #Kbytes of memory, or -1 for any error
 * --------------------------------------------------------------------------
 * Notes:       o
 * ======================================================================= */
/* --- entry point --- */
int     memfree ( char *memtype ) {
  /* ---
   * allocations and declarations
   * ------------------------------- */
  static char memfile[99] = "/proc/meminfo"; /* linux standard */
  static char deftype[99] = "MemFree";  /* default if caller passes null */
  FILE  *fp = fopen(memfile,"r");       /* open memfile for read */
  char  memline[999];                   /* read memfile line-by-line */
  int   nkbytes = (-1);                 /* #Kbytes, init for error */
  /* ---
   * read memfile until line with desired memtype found
   * ----------------------------------------------------- */
  if ( memtype == NULL ) memtype = deftype; /* caller wants default */
  if ( fp == NULL ) goto end_of_job;    /* but we can't get it */
  while ( fgets(memline,512,fp)         /* read next line */
  !=      NULL ) {                      /* quit at eof (or error) */
    if ( strcasestr(memline,memtype)    /* look for memtype in line */
    !=   NULL ) {                       /* found line with memtype */
      char *delim = strchr(memline,':'); /* colon following MemType */
      if ( delim != NULL )              /* NULL if file format error? */
        nkbytes = atoi(delim+1);        /* num after colon is #Kbytes */
      break; }                          /* no need to read further */
    } /* --- end-of-while(fgets()!=NULL) --- */
  end_of_job:                           /* back to caller with nkbytes */
    if ( fp != NULL ) fclose(fp);       /* close /proc/meminfo file */
    return ( nkbytes );                 /* and return nkbytes to caller */
  } /* --- end-of-function memfree() --- */

#if defined(MEMFREETEST)
int     main ( int argc, char *argv[] ) {
  char  *memtype = ( argc>1? argv[1] : NULL );
  int   memfree();
  printf ( " memfree(\"%s\") = %d Kbytes\n Have a nice day.\n",
        (memtype==NULL?" ":memtype), memfree(memtype) );
  } /* --- end-of-function main() --- */
#endif

【问题讨论】:

  • 你可以对你的进程允许使用的内存量设置一个进程限制。
  • ulimit 可从 shell 获得,但它只调用 getrlimitsetrlimit,您可以从 C 程序中调用它们。 getrlimit 应该让您看到有效的限制。您还可以查看/proc/meminfo,了解有关您的处理器物理内存的信息。
  • 看看swapoff 命令。
  • @rici 谢谢,rici。我也没有意识到这一点。它确实有效! -- 我刚试了一下,swapoff -a 后,如果请求超过可用物理内存,测试程序立即失败。 (get/setrlimit 似乎是最便携的,并且可以在程序中透明地实现,不会影响系统的其余部分。)

标签: c linux malloc


【解决方案1】:

malloc/calloc 显然使用交换空间来满足超出可用内存的请求。

嗯,不。

Malloc/calloc 使用虚拟内存。 “虚拟”意味着它不是真实的——它是由虚假和谎言构成的人为制造的幻觉。你的整个进程都建立在这些人为构建的幻象之上——线程是虚拟 CPU,套接字是虚拟网络连接,C 语言实际上是“C 抽象机”的规范,进程是虚拟计算机(实现语言的抽象机器)。

你不应该往魔法幕后看。你不应该知道物理内存的存在。系统没有挂起 - 错觉只是变慢了,但这没关系,因为 C 抽象机器没有说明任何事情应该花费多长时间,也没有提供任何性能保证。

更重要的是;由于错觉,软件起作用了。它不会因为没有足够的物理内存而崩溃。失败意味着软件需要无限长的时间才能成功完成,而“无限长的时间”比“因为交换空间而变慢”要差很多数量级。

如果请求超过可用物理内存(即不使用交换),如何让 malloc/calloc 失败

如果你要探寻魔法幕后,你需要仔细定义你的目标。

例如,假设您的进程有 123 MiB 的代码,而当前有 1000 MiB 的可用物理 RAM;但是(因为代码在虚拟内存中)只有一小部分代码正在使用真实 RAM(其余代码在磁盘上,因为操作系统/可执行加载程序使用内存映射文件来避免浪费真实 RAM,直到它真的需要)。您决定分配 1000 MiB 的内存(并且因为创建错觉的操作系统不是很好,不幸的是这会导致分配 1000 MiB 的实际 RAM)。接下来,您执行更多代码,但您执行的代码尚未在实际内存中,因此操作系统必须将代码从磁盘上的文件中提取到物理 RAM 中,但是您消耗了所有物理 RAM,因此操作系统必须将一些数据发送到交换空间。

再举一个例子,假设您的进程有 1 MiB 的代码和 1234 MiB 的数据,这些数据经过仔细分配以确保所有内容都适合物理内存。然后启动一个完全不同的进程,它为其代码和数据分配 6789 MiB 的内存;因此操作系统会将您进程的所有数据发送到交换空间,以满足您无法控制的其他进程。

编辑

这里的问题是提供这种错觉的操作系统不是很好。当你用malloc()calloc()分配大量虚拟内存时;操作系统应该能够使用一小块真实内存来欺骗你,避免消耗大量的真实内存。特别是(对于在普通硬件上运行的大多数现代操作系统);操作系统应该能够用一个充满零的页面填充巨大的虚拟内存区域,该页面被多次映射(在许多虚拟地址上)为“只读”,因此分配大量虚拟内存几乎不需要物理RAM(直到您写入虚拟内存,导致操作系统分配满足修改所需的最少物理内存)。当然,如果您最终确实写入了所有分配的虚拟内存,那么您最终会耗尽物理内存并使用一些交换空间;但这可能会逐渐发生,而不是一次全部发生 - 分散在很长一段时间内的许多微小延迟比单个巨大延迟更不容易被注意到。

考虑到这一点;我很想尝试使用mmap(..., MAP_ANONYMOUS, ...) 而不是(实施不佳的)malloc()calloc()。这可能意味着您必须处理分配的虚拟内存不能保证初始化为零的可能性,但是(取决于您使用内存的目的)这可能很容易解决。

【讨论】:

  • 谢谢,Brendan,但您在澄清术语时并没有像 Tom 和 rici 在之前的 cmets 中那样提供实际的解决方案。而且我知道“挂起”可能有点夸张,尽管这个词经常被用来表示非常慢。 “崩溃”(您也使用过)是失败的明确术语。在我的测试用例中,我不得不在机器响应任何击键之前花费大约五分钟。 (顺便说一下,如果请求超过物理内存,“交换”也是明确的。虚拟可以用于较小的请求,但交换空间必须用于较大的请求。)
  • @JohnForkosh:你可能是对的——我添加了一个潜在的解决方案。 :-)
  • 谢谢,布伦丹 - 我添加了一个潜在的“检查”:)
  • @rici:您是说(“托管”部分)C 库不是操作系统的一部分;然后在下一句中说“Linux malloc 实现”,就好像 malloc 实现是 Linux 的一部分(而不仅仅是发行版的一部分)?您可以看到这是一个“理论上正确”的区别,通常没有实际区别(例如,“作为 GNU/Linux 的一部分提供的 GNU 的 libC”,或“Gentoo 的与 Gentoo 一起提供的 LibC 包”,或者......)。跨度>
  • @rici: 你是不是要让我相信一个操作系统只包含一个内核而没有其他任何东西(没有引导加载程序、没有 GUI 或 shell、没有库、没有创建/管理存储设备的工具, ...)?
【解决方案2】:

扩展我对原始问题的评论:

如果要禁用交换,请使用 swapoff 命令 (sudo swapoff -a)。我通常以这种方式运行我的机器,以避免它在 Firefox 执行不应该执行的操作时冻结。您可以使用setrlimit()(或ulimit 命令)来设置最大VM 大小,但这并不能适当地补偿其他一些突然决定成为内存占用者的进程(见上文)。

即使您选择上述选项之一,您也应该阅读此答案的其余部分,以了解如何避免在第一次调用 calloc() 时进行不必要的初始化。


至于您的精确测试工具,事实证明您正在触发 GNU calloc() 优化的不幸异常。

这是我对另一个答案的评论(现已删除),但严格来说并不准确:

我检查了默认 gnu/linux malloc 库的 glibc 源代码,并确认 calloc() 通常不会手动清除刚刚被 mmap 处理的内存。而malloc() 根本不会触及内存。

事实证明,我错过了 calloc 优化的一个例外。由于 GNU malloc 实现初始化 malloc 系统的方式,对callocfirst 调用总是使用memset() 将新分配的存储设置为0。对calloc() 的所有其他调用都会通过通过整个 calloc 逻辑,这避免了在新映射的存储上调用 memset()

因此,对测试程序的以下修改显示出完全不同的行为:

#include <stdio.h>
#include <stdlib.h>
int main ( int argc, char *argv[] ) {
  /* These three lines were added */
  void* tmp = calloc(1000, 1); /* force initialization */
  printf("Allocated 1000 bytes at %p\n", tmp);
  free(tmp);
  /* The rest is unchanged */
  unsigned int nmalloc = (argc>1? atoi(argv[1]) : 10000000 ),
               size    = (argc>2? atoi(argv[2]) : (0) );
  unsigned char *pmalloc = (size>0? calloc(nmalloc,size):malloc(nmalloc));
  fprintf( stdout," %s malloc'ed %d elements of %d bytes each.\n",
    (pmalloc==NULL? "UNsuccessfully" : "Successfully"),
    nmalloc, (size>0?size:1) );
  if ( pmalloc != NULL ) free(pmalloc);
}

请注意,如果您将MALLOC_PERTURB_ 设置为非零值,则它将用于初始化 malloc()'d 块,并强制将 calloc()'d 块初始化为 0。在测试中使用以下。

在下文中,我使用/usr/bin/time 来显示执行期间的页面错误数。注意小错误的数量,这是操作系统在匿名 mmap'd 区域中对以前未引用的页面进行零初始化的结果(以及其他一些事件,例如映射已经存在于 Linux 页面缓存中的页面)。还要查看驻留集的大小,当然还有执行时间。

$ gcc -Og -ggdb -Wall -o mall mall.c

$ # A simple malloc completes instantly without page faults
$ /usr/bin/time ./mall 4000000000
Allocated 1000 bytes at 0x55b94ff56260
 Successfully malloc'ed -294967296 elements of 1 bytes each.
0.00user 0.00system 0:00.00elapsed 100%CPU (0avgtext+0avgdata 1600maxresident)k
0inputs+0outputs (0major+61minor)pagefaults 0swaps

$ # Unless we tell malloc to initialise memory
$ MALLOC_PERTURB_=35 /usr/bin/time ./mall 4000000000
Allocated 1000 bytes at 0x5648c2436260
 Successfully malloc'ed -294967296 elements of 1 bytes each.
0.19user 1.23system 0:01.43elapsed 99%CPU (0avgtext+0avgdata 3907584maxresident)k
0inputs+0outputs (0major+976623minor)pagefaults 0swaps

# Same, with calloc. No page faults, instant completion.
$ /usr/bin/time ./mall 1000000000 4
Allocated 1000 bytes at 0x55e8257bb260
 Successfully malloc'ed 1000000000 elements of 4 bytes each.
0.00user 0.00system 0:00.00elapsed 100%CPU (0avgtext+0avgdata 1656maxresident)k
0inputs+0outputs (0major+62minor)pagefaults 0swaps

$ # Again, setting the magic malloc config variable changes everything
$ MALLOC_PERMUTE_=35 /usr/bin/time ./mall 1000000000 4
Allocated 1000 bytes at 0x5646f391e260
 Successfully malloc'ed 1000000000 elements of 4 bytes each.
0.00user 0.00system 0:00.00elapsed 100%CPU (0avgtext+0avgdata 1656maxresident)k
0inputs+0outputs (0major+62minor)pagefaults 0swaps

【讨论】:

  • 谢谢,里奇。我暂时(可能是永久)决定使用 Tom 的 /proc/meminfo 建议,正如我在上面原始问题底部所做的编辑中所述。 memfree()/2 安全系数应避免在程序执行时动态出现的任何系统资源问题。
猜你喜欢
  • 2012-12-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-26
  • 2012-09-20
  • 2021-06-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多