【问题标题】:What's the graceful way of handling out of memory situations in C/C++?在 C/C++ 中处理内存不足情况的优雅方式是什么?
【发布时间】:2011-04-05 13:10:57
【问题描述】:

我正在编写一个消耗大量内存的缓存应用程序。

希望我能很好地管理我的记忆,但我只是在想什么 如果我的内存用完了怎么办。

如果调用分配即使是一个简单的对象也失败了,是否有可能即使是 syslog 调用 也会失败?

编辑:好吧,也许我应该澄清这个问题。如果 malloc 或 new 返回一个 NULL 或 0L 值,那么它本质上意味着调用失败并且由于某种原因它不能给你内存。那么,在这种情况下,明智的做法是什么?

EDIT2:我刚刚意识到对“new”的调用可能会引发异常。这可能会在更高的水平上被抓住,所以我也许可以优雅地退出更远的地方。到那时,甚至可以根据释放的内存量来恢复。至少到那时我应该希望能够记录一些东西。因此,虽然我看到了在 new 之后检查指针值的代码,但这是不必要的。在 C 语言中,您应该检查 malloc 的返回值。

【问题讨论】:

  • 这是一个有趣的问题。这让我想知道是否有与 Java 的软引用 (en.wikipedia.org/wiki/Soft_reference) 等效的 C++。
  • @Daniel:这与任何类型的引用系统有什么关系? C 和 C++ 中的内存释放完全是确定性的。
  • @Billy:是的。您可以捕获std::bad_alloc 并在特定分配失败的情况下执行某些操作。然而,因为这是一个缓存应用程序,所以能够找到最旧或最近访问最少的内存块可能会很好,这些内存块可以被释放以便为新分配腾出空间,而不是忽略缓存一条信息的新请求. Matt 可能正在寻找实现软引用的内存管理子系统。

标签: c++ c linux


【解决方案1】:

我正在编写一个消耗大量内存的缓存应用程序。 希望我能很好地管理我的记忆,但我只是在考虑如果我的内存用完了该怎么办。

如果您正在编写应该运行 24/7/365 的守护程序,那么您不应该使用动态内存管理:预先分配所有内存并使用一些slab allocator/内存池机制对其进行管理。这也将再次保护您的堆碎片。

如果调用分配即使是一个简单的对象也失败了,那么即使是 syslog 调用也会失败?

不应该。这也是syslog 作为系统调用存在的部分原因:该应用程序可以独立于其内部状态报告错误。

如果 malloc 或 new 返回一个 NULL 或 0L 值,那么它本质上意味着调用失败并且由于某种原因它不能给你内存。那么,在这种情况下,明智的做法是什么?

我通常会尝试在各种情况下正确处理错误情况,应用一般错误处理规则。如果在初始化期间发生错误 - 以错误终止,可能是配置错误。如果在请求处理期间发生错误 - 请求失败并出现内存不足错误。

对于普通堆内存,malloc() 返回0 通常意味着:

  • 你已经用尽了堆,除非你的应用程序释放一些内存,否则malloc()s 不会成功。

  • 错误的分配大小:在计算块大小时混合有符号和无符号类型是很常见的编码错误。如果大小最终错误地为负数,传递给 malloc() 预计 size_t ,它会变得非常大。

所以从某种意义上说,abort() 生成核心文件也没有错,稍后可以分析该文件以了解为什么malloc() 返回0。虽然我更喜欢 (1) 在错误消息中包含尝试的分配大小,并且 (2) 尝试进一步进行。如果应用程序由于其他内存问题(*)而崩溃,无论如何它都会生成核心文件。

(*) 根据我使具有动态内存管理的软件对malloc() 错误具有弹性的经验,我发现malloc() 经常返回0 并不可靠。第一次尝试返回 0 之后是成功的 malloc() 返回有效指针。但是首先访问指向的内存会使应用程序崩溃。这是我在 Linux 和 HP-UX 上的经验——我也在 Solaris 10 上看到过类似的模式。这种行为并不是 Linux 独有的。据我所知,让应用程序 100% 适应内存问题的唯一方法是提前预分配所有内存。这对于关键任务、安全、生命支持和运营商级应用程序来说是强制性的——它们不允许在初始化阶段之后进行动态内存管理。

【讨论】:

    【解决方案2】:

    我不知道为什么许多明智的答案都被否决了。在大多数服务器环境中,内存不足意味着您在某处发生了泄漏,并且“释放一些内存并尝试继续”毫无意义。 C++,尤其是标准库的本质是它一直需要分配。如果幸运的话,您也许可以释放一些内存并执行干净的关机,或者至少发出警告。

    然而,你更有可能无法做某事,除非失败的分配是一个巨大的分配,并且仍有可用于“正常”事情的内存。

    Dan Bernstein 是我认识的极少数可以实现在内存受限情况下运行的服务器软件的人之一。

    对于我们其他大多数人来说,我们可能应该设计我们的软件,当它因为内存不足错误而退出时,它会让事情处于有用的状态。

    除非您是某种脑外科医生,否则没有什么可做的。

    另外,很多时候你甚至不会得到一个 std::bad_alloc 或类似的东西,你只会得到一个指向你的 malloc/new 的指针,并且只会在你死时死掉实际上试图触摸所有的记忆。这可以通过关闭操作系统中的过度使用来防止,但仍然如此。

    当你触摸内核希望你不会的内存时,不要指望能够处理 SIGSEGV。我不太确定这在 Windows 方面是如何工作的,但我敢打赌他们也做过度使用。

    总而言之,这不是 C++ 的强项之一。

    【讨论】:

    • 内存不足不需要内存泄漏。这可能只是意味着您分配的内存超出了地址空间允许的范围。或者您可能已达到用户限制。
    【解决方案3】:

    在内存不足的情况下,写入 syslog 可能会失败:如果不查看相关函数的源,就无法知道每个平台的情况。例如,它们可能需要动态内存来格式化传入的字符串。

    然而,在您耗尽内存之前很久,您就会开始将内容分页到磁盘。发生这种情况时,您可能会忘记缓存带来的任何性能优势。

    就我个人而言,Varnish 背后的设计让我深信不疑:操作系统提供服务来解决很多相关问题,使用这些服务是有意义的(稍作修改):

    所以 Squid 的精细内存管理会发生什么,它会与内核精细的内存管理发生冲突...

    Squid 在 RAM 中创建一个 HTTP 对象,并在创建后快速使用一段时间。然后过了一段时间,它不再受到点击,内核注意到了这一点。然后有人试图从内核获取内存,内核决定将那些未使用的内存页面推送到交换空间,并更明智地使用(缓存-RAM)来处理程序实际使用的一些数据。然而,这是在 Squid 不知道的情况下完成的。 Squid 仍然认为这些 http 对象在 RAM 中,并且它们会在它尝试访问它们的那一刻,但在那之前,RAM 被用于生产一些东西。 ...

    一段时间后,Squid 也会注意到这些对象未被使用,并决定将它们移动到磁盘上以便 RAM 可以用于更繁忙的数据。所以 Squid 出去,创建一个文件,然后将 http 对象写入文件。

    这里我们切换到高速摄像头:squid 调用 write(2),它给出的地址是一个“虚拟地址”,内核将它标记为“不在家”。 ...

    内核试图找到一个空闲页面,如果没有,它会从某个地方获取一个使用过的页面,可能是另一个使用过的 Squid 对象,将其写入磁盘上的分页...空间(“交换区域”)当该写入完成时,它将从分页池中的另一个位置读取它“分页”到现在未使用的 RAM 页中的数据,修复分页表,并重试失败的指令。 ...

    所以现在 Squid 将对象放在 RAM 的一个页面中,并将其写入磁盘的两个位置:一个副本在操作系统的分页空间中,另一个副本在文件系统中。 ...

    Varnish 是这样做的:

    Varnish 分配一些虚拟内存,它告诉操作系统用磁盘文件中的空间来支持这个内存。当它需要将对象发送给客户端时,它只是引用那块虚拟内存并将其余部分留给内核。

    如果/当内核决定它需要将 RAM 用于其他用途时,该页面将被写入支持文件,并且 RAM 页面在其他地方重用。

    当 Varnish 下次引用虚拟内存时,操作系统会找到一个 RAM 页面,可能会释放一个,然后从备份文件中读取内容。

    就是这样。 Varnish 并没有真正尝试控制哪些缓存在 RAM 中,哪些不缓存,内核有代码和硬件支持来做好这方面的工作,而且做得很好。

    您可能根本不需要编写缓存代码。

    【讨论】:

    • 这个项目与 HTTP 缓存无关,也与任何服务器已经存在的当前协议无关。所以是的,我确实需要缓存代码。
    • Varnish 恰好是 HTTP 服务器这一事实无关紧要。严重地。关键是应用程序缓存通常最终会与虚拟内存等操作系统服务发生冲突。大缓存可能会被交换到磁盘并导致您的应用程序运行速度明显变慢。相反,只需分配一个内存池,让操作系统来缓存常用对象。
    【解决方案4】:

    我认为捕获mallocnew 的失败不会对您的情况有多大帮助。 linux 通过mmapmalloc 中分配大块虚拟 页面。这样,您可能会发现自己分配的虚拟内存比您拥有的多得多(真实 + 交换)。

    然后,当您写入在交换中没有任何位置的第一页时,该程序只会在很久以后因段错误(SIGSEGV)而失败。理论上,您可以通过编写信号处理程序然后弄脏您分配的所有页面来测试这种情况。

    但通常这也无济于事,因为您的应用程序在此之前很久就会处于非常糟糕的状态:不断交换,使用您的硬盘进行机械计算......

    【讨论】:

    • 这种行为称为过度使用,是可选的(可控的)。如果你关闭它,如果没有足够的虚拟内存,malloc 将失败。
    • 交换肯定是个问题。当一切都比正常运行慢 6 或 7 个数量级时,即使是最完美的恢复逻辑也几乎毫无用处。
    【解决方案5】:

    好吧,如果你在分配内存失败的情况下,你会得到一个std::bad_alloc 异常。该异常会导致程序的堆栈被展开。很可能,应用程序逻辑的内部循环不会处理内存不足的情况,只有更高级别的应用程序应该这样做。因为堆栈正在展开,很大一部分内存将被释放——实际上这几乎应该是您的程序使用的所有内存。

    唯一的例外是当您请求无法满足的非常大(例如几百 MB)的内存块时。但是,当这种情况发生时,通常会剩余足够小的内存块,让您可以优雅地处理故障。

    堆栈展开是你的朋友;)

    编辑:刚刚意识到这个问题也用 C 标记——如果是这种情况,那么当发现内存不足的情况时,你应该让你的函数手动释放它们的内部结构;不这样做是内存泄漏。

    EDIT2:示例:

    #include <iostream>
    #include <vector>
    
    void DoStuff()
    {
        std::vector<int> data;
        //insert a whole crapload of stuff into data here.
        //Assume std::vector::push_back does the actual throwing
        //i.e. data.resize(SOME_LARGE_VALUE_HERE);
    }
    
    int main()
    {
        try
        {
            DoStuff();
            return 0;
        }
        catch (const std::bad_alloc& ex)
        {   //Observe that the local variable `data` no longer exists here.
            std::cerr << "Oops. Looks like you need to use a 64 bit system (or "
                         "get a bigger hard disk) for that calculation!";
            return -1;
        }
    }
    

    EDIT3: 好的,根据评论者的说法,有些系统在这方面不符合标准。另一方面,在这样的系统上,无论如何你都将成为 SOL,所以我不明白为什么它们值得讨论。但如果您在这样的平台上,请记住这一点。

    【讨论】:

    • 如果您要对答案投反对票,请... 评论您投反对票的原因。我厌倦了无法改进答案,因为人们不会告诉我他们不喜欢什么。
    • 不保证会抛出 bad_alloc。 malloc impl 可以只是 mmap 匿名(事实上它确实如此),并且尝试写入分配的区域将导致 segv,而不是异常。
    • @Billy:Linux 不是因为它的 OOM 杀手而臭名昭著吗?实现不仅仅是编译器,它是库和主机环境。通常我同意我们不应该迎合非标准的实现,但是内存不足很少按照标准来处理,我们别无选择。
    • @stepancheg:你把完整的图片弄糊涂了。 Linux 可以遵循“乐观”内存分配方案,在该方案中,它将分配给在分配时可能没有的进程内存。分配仍然可以被拒绝,并且在malloc 的情况下返回NULL,或者在C++ 的new 的情况下抛出异常,正如标准所说。如果以后不能实现乐观分配,Linux 将通过 OOM 杀手终止进程。我非常怀疑它是否通过发送 SIGSEGV 来做到这一点 - 我预计会发生更严重的事情。
    【解决方案6】:

    如前所述,耗尽内存意味着所有赌注都结束了。恕我直言,处理这种情况的最佳方法是优雅地失败(而不是简单地崩溃!)。您的缓存可以在实例化时分配合理数量的内存。该内存的大小将等于释放时允许程序合理终止的数量。当您的缓存检测到内存变低时,它应该释放此内存并启动正常关闭。

    【讨论】:

    • 1) 总是很好地解释为什么一个答案被否决 2) @Chris,标记你的答案,因为你不高兴你被否决是对审核系统的滥用。如果您有疑问或想对机器发火,可以这么说,请访问 meta.stackoverflow.com,您将被拒绝状态。
    • 对不起,我没有意识到这是对审核系统的滥用,而且我当时不知道 meta.stackoverflow.com。请注意,虽然我并没有那么不高兴,但我只是想了解为什么我被否决了,以便我可以本着改进 SO 内容的精神改进答案。感谢您的回复。
    【解决方案7】:

    这个问题不是对过度使用的内存做出假设吗?

    即,内存不足的情况可能无法恢复!即使您没有剩余内存,对malloc 和其他分配器的调用仍可能成功,直到程序尝试使用内存。然后,BAM!,一些进程被内核杀死以满足内存负载。

    【讨论】:

    • @Daniel:请查看“OOM 杀手”。这是在低(虚拟)内存条件下杀死低优先级进程的东西。并非所有操作系统都有,但一些非常常见的操作系统有。
    • @Billy @Daniel 实际上它可能,所以不应该投反对票。至少在很多 Linux 发行版中,内存过度提交是默认开启的。当内存过度提交打开时,内核将始终在所有分配上返回成功,并且只会在进程尝试写入内存时尝试实际保留内存。如果没有可用的,则调用臭名昭著的 OOM 杀手并随机进程终止(根据其 OOM 分数)。我认为它被严重破坏了,但很多人(包括 Linus)为它辩护并说在用户应用程序中优雅地处理 OOM 是浪费时间。
    • @Alex:我认为“可能”有点夸大其词——这就是他们所做的,但据我所知,这是标准不允许的。但是编译器和操作系统做的很多事情都违反了标准——只要它们有一个兼容的模式,那么它们就是合法的。如果您想要一个兼容的环境,则可以禁用过度提交,尽管这需要程序员和用户之间进行不切实际的通信量。在实践中,您可以忽略 Linus 并正确处理 OOM,但必须接受操作系统可以随时以任何原因杀死您的应用程序,包括 OOM。
    • @Billy(第一条评论):如果分配地址空间失败,malloc 将返回 NULL。但是地址空间的可用性和内存的可用性是完全不同的事情。
    • @Billy:那么标准需要不间断电源以及完美无误的内存。实际上,由于 C 标准库无法控制的各种原因,如果没有显式释放内存,内存可能会变得不可访问。
    【解决方案8】:

    如果您的应用程序可能会分配大块内存并且有可能达到每个进程或虚拟机的限制,那么等到分配实际失败是很难恢复的情况。当malloc 返回NULLnew 抛出std::bad_alloc 时,事情可能已经过去太远了,无法可靠地恢复。根据您的恢复策略,许多操作本身可能仍需要堆分配,因此您必须非常小心可以依赖哪些例程。

    您可能希望考虑的另一个策略是查询操作系统并监控可用内存,主动管理您的分配。这样,如果您知道大块可能会失败,就可以避免分配大块,从而有更好的恢复机会。

    此外,根据您的内存使用模式,使用自定义分配器可能会比标准内置 malloc 提供更好的结果。例如,某些分配模式实际上会随着时间的推移导致内存碎片,因此即使您有空闲内存,堆区域中的可用块也可能没有合适大小的可用块。一个很好的例子是 Firefox,它切换到 dmalloc 并看到内存效率大大提高。

    【讨论】:

    • 同意 - 与其尝试一直使用内存直到没有剩余内存,您应该提供一个配置选项来设置您的进程将用于缓存的最大内存量。当你去缓存一个新对象时,检查你不会超过内存限制——如果你愿意,扔掉一些旧对象。换句话说,将整个问题交给系统管理员。
    • 谢谢,这实际上是我打算做的,但后来我认为这可能是一个很好的 stackoverflow 问题
    • "A good example of this is Firefox, which switched to dmalloc and saw a great increase in memory efficiency." 你知道我们可以在哪里了解更多相关信息吗?
    【解决方案9】:

    我在 Linux 方面没有任何具体经验,但我花了很多时间在游戏机上的视频游戏(内存耗尽是禁止的)和基于 Windows 的工具上工作。

    在现代操作系统上,您很可能会用完地址空间。因此,内存不足基本上是不可能的。因此,只需在启动时分配一个或多个大缓冲区,以保存您需要的所有数据,同时为操作系统留下少量数据。将随机垃圾写入这些区域可能是一个好主意,以强制操作系统将内存实际分配给您的进程。如果您的进程在尝试使用它所要求的每个字节的尝试中幸存下来,那么现在为所有这些东西保留了某种支持,所以现在您是黄金。

    编写/窃取您自己的内存管理器,并指示它从这些缓冲区中分配。然后在您的应用程序中始终如一地使用它,或者利用 gcc 的 --wrap 选项适当地转发来自 malloc 和朋友的呼叫。如果您使用任何无法定向调用内存管理器的库,请将它们丢弃,因为它们只会妨碍您。缺乏可覆盖的内存管理调用是更深层次问题的证据;你最好没有这个特定的组件。 (注意:即使你使用--wrap,相信我,这仍然是一个问题的证据!寿命太短了,不能使用那些不会让你超载内存管理的库!)

    一旦你的内存用完了,好吧,你搞砸了,但你仍然有你之前留下的空间,所以如果释放你要求的一些内存太难了,你可以(用关心)调用系统调用以将消息写入系统日志然后终止,或者其他。只要确保避免调用 C 库,因为它们可能会在您最不期望的时候尝试分配一些内存——使用具有虚拟化地址空间的系统的程序员因这种事情而臭名昭著——这就是正是导致问题的原因。

    这种方法听起来可能让人头疼。嗯……是的。但这很简单,值得为此付出一些努力。我认为有一个 Kernighan 和/或 Ritche 的名言。

    【讨论】:

    • 顺便说一句,我在 Linux 上做了一些编码,但它正在处理一个从未分配超过 10MB 的长期运行缓冲区的程序,其中没有一个超过 720K。所以我没有考虑地址空间不足的问题;根据我的口味,这种可能性几乎为零。
    • @brone:如果堆变得碎片化(当您的所有分配都很小时,这种情况会很快发生),您可能会在用完后备存储之前用完地址空间。现在的大多数系统在物理 RAM 和交换空间之间都有超过 4GB 的 RAM。
    • 确实-如答案中所述。因此,请提前获取您的内存,自己管理地址,并为系统留出一点空闲空间。如果你无法避免达到内存限制,你至少可以确保当你这样做时,有一些地址肯定没有被你的程序使用,这样系统至少可以在某种程度上工作。 (您可以分配一个在开始用完时释放的缓冲区,但如果您现在有多个线程,则必须停止它们,等等 - 只进行自己的内存管理会更安全。)
    • 这是针对 OOM 问题的唯一理智、实用和真实的建议。只是有OOM的情况就是bug,防止你的程序接触不到OOM。
    • 只有当您在 64 位硬件上使用 32 位操作系统时,才会出现关于“地址空间不足但内存不足”的说法。无论如何,答案中描述的预分配方法(这是管理自己内存的好主意)并不能解决地址空间不足的问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-09
    相关资源
    最近更新 更多