在C / C ++中处理内存不足的优美方式是什么?

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

希望我能够好好地pipe理我的记忆,但是我只是想着如果内存不足,该怎么办。

如果即使分配一个简单对象的调用失败,是否有可能连系统日志调用也会失败?

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

编辑2:我刚刚意识到,一个“新”的调用可以抛出一个exception。 这可能会陷入更高的层面,所以我可以优雅地退出。 在那个时候,甚至有可能根据释放多less内存来恢复。 至less我应该在这一点上,希望能够logging一些东西。 所以当我看到新的代码检查指针的值时,这是没有必要的。 在C中,你应该检查malloc的返回值。

那么,如果你在分配内存失败的情况下,你会得到一个std::bad_allocexception。 exception导致您的程序堆栈被解开。 很可能,应用程序逻辑的内部循环不会处理内存不足的情况,只有更高级别的应用程序才能这样做。 因为堆栈正在被解开,所以一大块内存将被释放 – 实际上它应该几乎是你的程序所使用的全部内存。

一个例外就是当你要求一个非常大的(例如几百MB)的内存块时,不能满足。 当发生这种情况时,通常会有足够小的内存块,这将允许您正常处理故障。

堆栈放开是你的朋友;)

编辑:只是意识到,问题也被标记为C – 如果是这样的话,那么你应该有你的function手动释放内部结构时,内存条件不足; 不这样做是内存泄漏。

编辑2:例如:

 #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 //ie 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; } } 

编辑3:好的,根据评论者的说法,这里有不符合标准的系统。 另一方面,在这样的系统上,无论如何你都会是SOL,所以我不明白他们为什么值得讨论。 但是如果你在这样一个平台上的话,应该牢记这一点。

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

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

我在Linux上没有任何特定的经验,但是我花了很多时间在游戏机上玩video游戏,这些游戏机在内存不足的情况下,以及基于Windows的工具。

在现代操作系统上,您最有可能耗尽地址空间。 记忆犹新,基本上是不可能的。 因此,只需在启动时分配一个大的缓冲区或缓冲区,以便保存所有您需要的数据,同时为操作系统留下less量的数据。 向这些区域写入随机垃圾可能是一个好主意,以迫使操作系统将内存实际分配给您的进程。 如果你的stream程在这个尝试使用每个字节的尝试中都存在,那么现在就有一些支持所有这些东西的支持,所以现在你是黄金了。

写/偷你自己的内存pipe理器,并指示它从这些缓冲区分配。 然后在您的应用程序中持续使用它,或者利用gcc的--wrap选项来正确转发来自malloc和朋友的调用。 如果你使用任何不能被定向的库来调用你的内存pipe理器,那就去垃圾它们,因为它们会阻碍你。 缺乏可重复的内存pipe理调用是深层次问题的证据; 没有这个特定的组件,你会更好。 (注意:即使你正在使用--wrap ,相信我,这仍然是一个问题的证据!生活太短了,不能使用那些不会让你的内存pipe理超载的库!

一旦你用完了记忆,好吧,你被搞砸了,但是你还有空闲的空间,所以如果释放你所要求的一些内存太难了,你可以系统调用将消息写入系统日志,然后终止,或其他。 只要确保避免调用C库,因为他们可能会尝试分配一些内存,当你最不期待它 – 与虚拟地址空间的系统工作的程序员是臭名昭着的这种事情 – 这就是首先是造成问题的事情。

这种做法可能听起来像屁股疼痛。 那么…是的。 但是这很简单,值得付出一些努力。 我认为这有一个Kernighan和/或Ritche的报价。

如果您的应用程序可能分配大块内存,并冒着每个进程或VM限制的风险,那么等待分配实际上失败是一个难以恢复的情况。 到malloc返回NULLnew std::bad_alloc ,事情可能已经不能可靠地恢复。 根据你的恢复策略,许多操作本身可能仍然需要堆分配,所以你必须非常小心你可以依赖的例程。

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

此外,根据您的内存使用模式,使用自定义分配器可能会给您比标准的内置malloc更好的结果。 例如,某些分配模式实际上可能会导致内存碎片随着时间的推移,所以即使您拥有可用内存,堆场中的可用块可能也没有合适大小的可用块。 一个很好的例子就是Firefox,它转向了dmalloc并且看到了内存效率的大幅提升。

我不认为捕获mallocnew的失败会在你的情况下获得很多。 linux通过mmapmalloc分配大块的虚拟页面。 通过这个,你可能会发现自己处于一个你分配的虚拟内存比你真实的多的状态(真实+交换)。

那么当你写入没有交换位置的第一页时,程序将只会在很晚之后才会出现段错误(SIGSEGV)。 从理论上讲,你可以通过编写一个信号处理程序来testing这种情况,然后弄脏你分配的所有页面。

但通常情况下,这也不会有什么帮助,因为在此之前,应用程序将处于非常糟糕的状态:不断交换,用硬盘进行机械计算。

在低内存条件下写入系统日志可能失败:没有办法知道每个平台都没有查看相关函数的源代码。 例如,他们可能需要dynamic内存来格式化传入的string。

但是,在内存不足之前,您就会开始将内容分页到磁盘。 发生这种情况时,可以忘记caching带来的任何性能优势。

就个人而言,我深信Varnish的devise:操作系统提供的服务可以解决许多相关的问题,使用这些服务(小编辑)是有意义的:

那么,Squid精心devise的内存pipe理会发生什么呢?它会和内核的精心devise的内存pipe理打成一片。

Squid在RAM中创build一个HTTP对象,并在创build后快速使用一段时间。 然后过了一段时间,没有更多的命中,内核注意到这一点。 然后有人试图从内核获取内存的某些内容,内核决定将这些未使用的内存页面推出来交换空间,并更为合理地使用(cache-RAM)stored procedures实际使用的某些数据。 然而,这完成没有Squid知道它。 鱿鱼仍然认为这些http对象在RAM中,它们将是第二次尝试访问它们,但在此之前,RAM用于提高生产力。 …

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

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

内核试图find一个空闲的页面,如果没有,它会从某个地方采取一点点使用的页面,可能是另一个小使用的Squid对象,将其写入磁盘上的分页…空间(“交换区域”)当写入完成时,它将从分页池中的另一个地方读取它被“翻出”到当前未使用的RAM页面中的数据,修复分页表,并重试失败的指令。 …

所以现在Squid在RAM中的一个页面中有对象,并写入磁盘两个地方:操作系统分页空间中的一个副本和文件系统中的一个副本。 …

这是如何清漆:

Varnish分配一些虚拟内存,它告诉操作系统从磁盘文件中以空间备份这个内存。 当需要将对象发送给客户端时,只需引用该虚拟内存,然后将剩余的内容留给内核。

如果/当内核决定需要使用其他的RAM时,页面将被写入备份文件,并在其他地方重用RAM页面。

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

就是这样。 Varnish并没有真正地控制RAM中caching的内容,内核有代码和硬件支持来做好这个工作,而且它做得很好。

你可能根本不需要编写caching代码。

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

我不知道为什么很多明智的答案被否决了。 在大多数服务器环境中,内存不足意味着你在某处发生了泄漏,而“释放一些内存并尝试继续”是没有意义的。 C ++特别是标准库的本质是它一直需要分配。 如果幸运的话,你可以释放一些内存并执行一次干净的closures,或者至less发出警告。

然而,更有可能的是,你将无法做一件事情,除非失败的分配是巨大的,并且还有可用于“正常”事物的记忆。

Dan Bernstein是我所知道的能够实现在内存受限情况下运行的服务器软件的极less数人之一。

对于我们大多数人来说,我们可能应该devise我们的软件,当它由于内存不足错误而退出时,它会使事物处于一个有用的状态。

除非你是某种脑外科医生,否则没有太多的事要做。

另外,很多时候你甚至不会得到一个std :: bad_alloc或者类似的东西,你只要得到一个指针返回到你的malloc / new,并且只有当你真的试图触摸所有的内存时才会死掉。 这可以通过closures操作系统中的overcommit来防止,但仍然可以。

不要指望能够处理SIGSEGV当你触摸内存,内核希望你不会..我不太确定这是如何工作在Windows的一面的东西,但我敢打赌,他们也过度承认。

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

我在写一个消耗大量内存的caching应用程序。 希望我能够好好地pipe理我的记忆,但是我只是想着如果内存不足,该怎么办。

如果您正在编写应该运行24/7/365的deamon,则不应该使用dynamic内存pipe理:预先分配所有内存,并使用一些slab分配器 /内存池机制来pipe理它。 这也将保护你的堆碎片。

如果即使分配一个简单对象的调用失败,是否有可能连系统日志调用也会失败?

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

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

我通常会尝试在这种情况下正确处理错误情况,应用一般的error handling规则。 如果在初始化期间发生错误 – 终止错误,可能是configuration错误。 如果在请求处理过程中发生错误 – 使请求失败,出现内存不足错误。

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

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

  • 错误的分配大小:在计算块大小时混合有符号和无符号types是非常常见的编码错误。 如果大小错误地被否定,那么传递给malloc() ,在size_t被预期的地方变成非常大的数字。

因此,从某种意义上讲, abort()生成可以稍后分析的核心文件也是没有问题的,看看为什么malloc()返回0 。 尽pipe我倾向于(1)在错误消息中包含尝试分配的大小,(2)尝试进一步处理。 如果应用程序由于其他内存问题(*)而崩溃,则会产生核心文件。

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