24

我已经将大量的 C 代码行用于清理标签/条件以用于失败的内存分配(由alloc返回的系列表示NULL)。我被告知这是一种很好的做法,因此,在内存故障时,可以标记适当的错误状态,并且调用者可以潜在地执行“优雅的内存清理”并重试。我现在对这种哲学有一些疑问,我希望能澄清一下。

我想调用者可能会释放过多的缓冲区空间或剥离其数据的关系对象,但发现调用者很少有能力(或处于适当的抽象级别)这样做。此外,从被调用函数中提前返回而没有副作用通常很重要。

我还刚刚发现了 Linux OOM 杀手,这似乎使这些努力在我的主要开发平台上毫无意义。

默认情况下,Linux 遵循乐观的内存分配策略。这意味着当 malloc() 返回非 NULL 时,不能保证内存确实可用。这是一个非常糟糕的错误。如果发现系统内存不足,一个或多个进程将被臭名昭著的 OOM 杀手杀死。

我认为可能还有其他平台遵循相同的原则。是否有一些实用的东西使得检查 OOM 条件值得?

4

11 回答 11

21

如果用户或系统管理员限制(参见 ulimit)进程的内存空间,或者操作系统支持每个用户的内存分配限制,即使在具有大量内存的现代计算机上也可能发生内存不足的情况。在病态的情况下,碎片化使这很有可能,甚至。

但是,由于现代程序中普遍使用动态分配的内存,因此处理内存不足错误变得非常麻烦。检查和处理此类错误必须在任何地方进行,而且复杂性很高。

我发现最好把程序设计成可以随时崩溃。例如,确保用户创建的数据一直保存在磁盘上,即使用户没有明确保存。(例如,请参见 vi -r。)这样,您可以创建一个函数来分配内存,该函数在出现错误时终止程序。由于您的应用程序旨在随时处理崩溃,因此崩溃是可以的。用户会感到惊讶,但不会失去(很多)工作。

永不失败的分配函数可能是这样的(未经测试、未编译的代码,仅用于演示目的):

/* Callback function so application can do some emergency saving if it wants to. */
static void (*safe_malloc_callback)(int error_number, size_t requested);

void safe_malloc_set_callback(void (*callback)(int, size_t))
{
    safe_malloc_callback = callback;
}

void *safe_malloc(size_t n)
{
    void *p;

    if (n == 0)
        n = 1; /* malloc(0) is not well defined. */
    p = malloc(n);
    if (p == NULL) {
        if (safe_malloc_callback)
            safe_malloc_callback(errno, n);
        exit(EXIT_FAILURE);
    }
    return p;
}

Valerie Aurora 的文章Crash-only software可能很有启发性。

于 2009-04-18T09:18:03.560 回答
14

看看问题的另一面:如果你 malloc 内存,它失败了,而你在 malloc 处没有 检测到它,你什么时候检测到它?

显然,当您尝试取消引用指针时。

你将如何检测它?通过获得一个Bus error或类似的东西,在 malloc 之后的某个地方,您必须使用核心转储和调试器进行跟踪。

另一方面,你可以写

  #define OOM 42 /* just some number */

  /* ... */

  if((ptr=malloc(size))==NULL){
      /* a well-behaved fprintf should NOT malloc, so it can be used
       * in this sort of context
       */
      fprintf(stderr,"OOM at %s: %s\n", __FILE__, __LINE__);
      exit(OOM);
   }

并获得“parser.c:447 处的 OOM”。

你选。

更新

关于优雅回报的好问题。确保优雅返回的困难在于,通常你真的无法建立一个范例或模式来说明你如何做到这一点,尤其是在 C 中,它毕竟是一种花哨的汇编语言。在垃圾收集环境中,您可以强制进行 GC;在带有异常的语言中,您可以抛出异常并展开事情。在 C 中,你必须自己做,所以你必须决定要投入多少精力。

大多数程序中,异常终止是您能做的最好的事情。在这个方案中,您(希望)在 stderr 上得到一个有用的消息——当然它也可能是一个记录器或类似的东西——以及一个作为返回码的已知值。

具有较短恢复时间的高可靠性程序会将您推向类似恢复块的状态,您可以在其中编写代码以尝试使系统恢复到可生存状态。这些很棒,但很复杂;我链接到的论文详细讨论了它们。

中间,你可以想出一个更复杂的内存管理方案,比如管理你自己的动态内存池——毕竟,如果别人可以写 malloc,你也可以。

但是没有一般的模式(无论如何我都知道)进行足够的清理以能够可靠地返回并让周围的程序继续。

于 2009-04-18T10:05:01.217 回答
8

无论平台如何(可能是嵌入式系统除外),检查NULL然后直接退出而不手动进行任何(或大量)清理是一个好主意。

内存不足不是一个简单的错误。这对当今的系统来说是一场灾难。

The Practice of Programming(Brian W. Kernighan 和 Rob Pike,1999 年)一书定义了类似emalloc()这样的函数,如果没有剩余内存,就会退出并显示错误消息。

于 2009-04-18T08:56:05.703 回答
6

这取决于你在写什么。它是一个通用库吗?如果是这样,您希望尽可能优雅地处理内存不足问题,特别是如果可以合理预期它将用于 el-cheapo 系统或嵌入式设备。

考虑一下:程序员正在使用您的库。他的程序中有一个错误(可能是未初始化的变量),它向您的代码传递了一个愚蠢的参数,因此试图分配一个 3.6GB 的内存块。显然malloc()返回NULL。他宁愿在库代码的某处生成无法解释的段错误,还是返回值来指示错误?

为了避免对代码进行错误检查,一种方法是在开始时分配合理的内存量,并根据需要对其进行子分配。

关于 Linux OOM 杀手,我听说现在主要发行版默认禁用此行为。即使它已启用,也不要误解:malloc() 可以返回 NULL,如果您的程序的总内存使用量超过 4GiB(在 32 位系统上),它肯定会返回。换句话说,即使malloc()实际上并不能确保您获得一些 RAM/交换空间,它也会保留您的部分地址空间。

于 2009-04-18T10:09:58.363 回答
4

我建议做一个实验 - 编写一个小程序,它不断分配内存而不释放它,然后在分配失败时打印一条小(固定)消息。当你运行这个程序时,你注意到你的系统有什么影响?消息是否会被打印?

如果系统运行正常并且在显示错误之前保持响应,那么我会说是的,值得检查。OTOH,如果系统在显示消息之前变得缓慢、无响应并且最终无法使用(如果有的话),那么 II 会说不,不值得检查。

重要提示:在运行此测试之前,请保存所有重要工作。不要在生产服务器上运行它。

关于 Linux OOM 行为 - 这实际上是可取的,并且是大多数操作系统的工作方式。重要的是要意识到,当您 malloc() 一些内存时,您不是直接从操作系统获取它,而是从 C 运行时库获取它。这通常会要求操作系统预先(或在第一次请求时)提供一大块内存,然后通过 malloc/free 接口进行管理。由于许多程序根本不使用动态内存,因此操作系统不希望将“真实”内存交给 C 运行时 - 相反,它会交给一些未提交的虚拟机,这些虚拟机实际上会在您进行 malloc 调用时被提交。

于 2009-04-18T09:17:43.013 回答
2

对于当今的计算机和通常安装的 RAM 量,到处检查内存分配错误可能过于详细。正如您所看到的,通常很难或不可能对要解除分配的内容做出合理的决定。随着您的进程分配越来越多的内存,操作系统将相应地减少可用于磁盘缓冲区的内存量。当它低于某个阈值时,操作系统将开始将内存分页到磁盘。(这是一种简化,因为内存管理中有很多因素。)

一旦操作系统开始分页内存,整个系统就会变得越来越慢,并且可能需要很长时间才能让您的应用程序真正从 malloc 中看到 NULL(如果有的话)。

在当今系统上可用的大量内存中,“内存不足”错误更有可能意味着代码中的错误试图分配任意数量的内存。在这种情况下,您的过程中再多的释放和重试都无法解决问题。

于 2009-04-18T08:59:49.017 回答
1

您必须权衡哪个对您来说更好或更坏:将所有工作都用于检查 OOM 或让您的程序在意外时间失败

于 2009-04-18T08:55:57.717 回答
1

进程通常在堆栈大小上以资源限制(参见 ulimit (3))运行,而不是在堆大小上。malloc (3) 将从操作系统逐页管理其堆区域的内存增加,并且操作系统将安排此页面以某种方式物理分配并对应于您的进程的堆。如果您的计算机中没有更多的 RAM,那么大多数操作系统都有类似磁盘上的交换分区之类的东西。当您的系统开始需要使用交换时,事情会逐渐变慢。如果一个过程导致了这种情况,它可以很容易地用 ps (1) 之类的实用程序来识别。

除非您的代码要在资源限制的情况下运行,或者在内存容量较小且没有交换的系统上运行,否则我认为可以假设 malloc (3) 成功进行编程。如果您不确定,只需制作一个虚拟包装器,有朝一日可能会进行检查并简单地退出。错误状态返回值没有意义,因为您的程序需要它已经分配的内存。如果你的 malloc (3) 失败并且你不检查 NULL,那么当它开始访问它获得的 (NULL) 指针时,你的进程无论如何都会死掉。

malloc (3) 的问题在大多数情况下不是由内存不足引起的,而是由于程序中的逻辑错误导致对 malloc 和 free 的不当调用。通过检查 malloc 成功不会检测到这个常见的问题。

于 2009-04-18T11:46:58.177 回答
1

好。一切视情况而定。

首先。如果您检测到内存不足以满足您的需求 - 您会怎么做?最常见的用法是:

if (ptr == NULL) {
    fprintf(log /* stderr or anything */, "Cannot allocate memory");
    exit(2);
}

好。即使它不使用 malloc,它也可以分配缓冲区。如果它是一个 GUI 应用程序,那就太糟糕了——您的用户不太可能发现它。如果您的用户“足够聪明”从控制台运行应用程序来检查错误,他可能会看到某些东西吞噬了他的全部记忆。行。那么可能会显示一个对话框吗?但是显示对话框可能会吃掉资源——而且通常会。

其次——为什么需要OOM的信息?它发生在两种情况下:

  1. 其他软件有问题。你不能用它做任何事情
  2. 你的程序有问题。在这种情况下,它是一个 GUI 程序,您不太可能以任何方式通知用户(更不用说 99% 的用户没有阅读消息,并且会说软件崩溃而没有进一步的细节)。如果不是,用户很可能会发现它(观察系统监视器或使用更专业的软件)。
  3. 要释放一些缓存等。您应该签入系统,但请注意它可能无法正常工作。您只能处理自己的 sbrk/mmap/etc。调用,在 Linux 中你无论如何都会得到 OOM
于 2009-04-18T20:01:57.007 回答
0

是的,我相信它是,如果您始终如一地遵循这种做法。这对于用 C 编写的大型程序可能不切实际,因为这可能需要大量的体力劳动,但在更现代的语言中,大部分工作都是为您完成的,因为内存不足会导致抛出异常。

始终这样做的好处是程序不会由于内存不足导致缓冲区溢出而进入未定义状态(这显然会由于提前退出函数而导致未定义状态的可能性,尽管这是不同类别的错误)。这样做后,您的程序可以始终如一地处理错误情况,或者如果失败是严重的,则决定以优雅的方式退出。

于 2009-04-18T08:57:18.070 回答
0

如果您设计错误的软件,检查 OOM 条件并采取适当的措施可能会很困难。您是否真的需要检查这种情况取决于您想要获得的软件的可靠性。

例如,VirtualBox 虚拟机管理程序将检测内存不足错误并优雅地暂停虚拟机,允许用户关闭一些应用程序以释放内存。我在 Windows 下观察到了这种行为。实际上,VirtualBox 中几乎所有的调用都有成功指示符作为返回值,您可以只返回VERR_NO_MEMORY表示内存分配失败。这引入了一些额外的检查,但在这种情况下它是值得的。

于 2009-04-18T09:32:53.897 回答