我是否应该在我的C代码中检测OOM(内存不足)错误?

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

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

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

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

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

如果用户或系统管理员限制(参见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软件可能很有启发性。

看一下问题的另一面:如果你使用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); } 

并获得“解析器的OOM:447”。

你选。

更新

关于优雅回归的好问题。 确保优雅回报的困难在于,一般来说,你真的无法建立你如何做到的范式或模式,特别是在C中,这毕竟是一种奇特的汇编语言。 在垃圾收集环境中,您可以强制GC; 在具有exception的语言中,您可以抛出exception并展开事物。 在C中你必须自己做,所以你必须决定你想要投入多少努力。

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

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

在中间,您可以提出一个更复杂的内存管理方案,比如管理自己的动态内存池 – 毕竟,如果其他人可以编写malloc,那么你也可以。

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

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

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

编程实践 (Brian W. Kernighan和Rob Pike,1999)定义了像emalloc()这样的函数,如果没有内存,它就会退出并显示错误消息。

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

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

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

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

我建议进行一项实验 – 编写一个小程序,在不释放内存的情况下继续分配内存,然后在分配失败时打印一条小的(固定的)消息。 运行此程序时,您在系统上注意到哪些效果? 消息是否打印出来?

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

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

关注Linux OOM行为 – 这实际上是可取的,也是大多数操作系统工作的方式。 重要的是要意识到当你使用malloc()某些内存时,你不是直接从操作系统获取它,而是从C运行时库中获取它。 这通常会在操作系统之前(或在第一次请求时)向操作系统请求大块内存,然后通过malloc / free接口进行管理。 由于许多程序根本不使用动态内存,因此操作系统将“真实”内存交给C运行时是不可取的 – 相反,它可以实现在执行malloc调用时实际调用的操作系统。

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

一旦操作系统启动分页内存,整个系统就会越来越慢,并且在应用程序实际上从malloc看到NULL之前可能还需要一段时间(如果有的话)。

由于当今系统可用的内存量很大,“内存不足”错误更可能意味着代码中的错误会尝试分配任意数量的内存。 在这种情况下,您的流程中没有任何释放和重试可以解决问题。

你必须权衡一下对你来说更好还是更糟:把所有工作都放在检查OOM或让程序在意外时间失败

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

除非您的代码是使用资源限制运行,或者是在内存不足且没有交换的系统上运行,否则我认为可以假设malloc(3)成功进行编程。 如果您不确定,只需制作一个虚拟包装器,可能有一天会进行检查并直接退出。 错误状态返回值没有意义,因为您的程序需要已分配的内存。 如果您的malloc(3)失败并且您没有检查NULL,那么当它开始访问它获得的(NULL)指针时,您的进程将会死亡。

在大多数情况下,malloc(3)的问题不会出现在内存不足的情况下,而是来自程序中的逻辑错误,这会导致对malloc和free的行为exception。 通过检查malloc成功,将无法检测到此常见问题。

好。 一切都取决于情况。

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

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

好。 即使它不使用malloc,它也可以分配缓冲区。 如果它是一个GUI应用程序,那么太糟糕了 – 您的用户不太可能发现它。 如果您的用户“聪明地”从控制台运行应用程序以检查错误,他可能会看到有些东西吃掉了他的整个记忆。 好。 那么可能会显示一个对话框? 但是显示对话框可能会占用资源 – 通常会这样做。

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

  1. 其他软件是错误的。 你不能用它做任何事情
  2. 你的程序有问题。 在这种情况下,它是eighter GUI程序,您不可能以任何方式通知用户(没有提到99%的用户没有阅读消息,并且会说软件崩溃而没有进一步的细节)。 如果不是,用户可能无论如何都会发现它(选择系统监视器或使用更专业的软件)。
  3. 要释放一些缓存等。您应该检查系统但是要警告它可能不起作用。 你只能处理自己的sbrk / mmap / etc。 在Linux中,无论如何你都会得到OOM

是的,如果你坚持一贯的做法,我相信它是。 这对于用C语言编写的大型程序来说可能是不切实际的,因为这可能需要手工劳动的程度,但是在更现代的语言中,大部分工作都是为您完成的,因为内存不足会导致抛出exception。

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

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

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