一个多线程的C应用程序在遇到malloc()失败时该如何处理?

9
我正在开发的应用程序中,有一部分是基于pthread的简单服务器,通过TCP/IP socket进行通信。由于它将在内存受限的环境中运行,因此我选择使用C语言编写。我的问题是:如果其中一个线程遇到返回NULL的malloc(),该程序应该怎么处理?我想到了以下几种可能性:
  1. 没有特殊处理。让malloc()返回NULL,并让它被解引用以导致整个程序崩溃。
  2. 在malloc()失败时立即退出,通过调用abort()或exit(-1)。假设环境会清理所有东西。
  3. 跳出主事件循环并尝试pthread_join()所有线程,然后关闭。
第一种选项显然最简单,但似乎非常错误。第二种也不太对,因为我不知道会发生什么。第三种选项看起来很诱人,但存在两个问题:首先,在正常情况下,并不需要将所有线程重新加入到主线程中;其次,为了完成线程执行,大多数剩余的线程仍将需要再次调用malloc()。
那我该怎么办呢?

4
第一次阅读时,我认为第2点是说“假设政府会清理所有事情。” :) - Rob
1
@Rob 最近看了太多有关油污泄漏的新闻了,是吧? - Tyler McHenry
@Rob - 是的,我们只需要取消对malloc()的管制,以鼓励进程之间更多的竞争...会有什么问题呢? - Tim Post
6个回答

4

选项2没有问题。不必假设-exit()退出进程,这意味着所有线程都被关闭并且所有内容都被清理。

不要忘记尝试记录分配失败的位置。


“everything”有点过于乐观了。例如,您必须确保磁盘上的文件得到适当的清理。 - MSalters
“everything is cleaned-up” 意味着资源已经归还给操作系统;但是你最好希望这不是 你的 生命支持系统!系统非确定性地停止的后果可能并不简单,并且取决于应用程序。 - Clifford
3
如果我的生命维持系统是一个接受TCP/IP连接的服务器,那么我一定已经厌倦了生命! - caf
同意,生命支持可能只是一个极端的例子。然而,在许多其他情况下,进程的自动终止将产生超出硬件健康状况的后果。例如,对于我们所有人来说,应用程序崩溃损失数小时的工作肯定不止一次发生过,对吧?我的观点是,“没关系,因为它不会泄漏或留下开放的句柄或线程运行”是一个相当狭隘的观点“清理干净”。 - Clifford
我的判断是,OP的问题只关注于释放系统资源,这是一个非常狭隘的“清理”观点。另外,当malloc()失败时,尝试保存当前应用程序状态可能是不可能的 - 更好的策略是在一切正常运行时为应用程序创建定期检查点以保存到持久存储中。 - caf

4
这是空间/辐射硬化系统通常禁止动态内存分配的原因之一。当malloc()失败时,很难“治愈”这种失败。你有一些选择:
  • 您不需要使用内置的libc malloc()(完全不使用或通常情况下不使用)。您可以包装malloc()以在失败时执行额外的工作,例如通知其他内容。当使用类似看门狗的东西时,这很有帮助。您还可以使用一个完整的垃圾收集器,但我不建议这样做。最好识别并修复泄漏。
  • 根据存储和复杂性,不经常访问的已分配块可以映射到磁盘。但是,在这里,通常只查看物理内存中几KB的节省。
  • 您可以使用静态内存池和自己的malloc(),不会超卖它。如果您已经广泛地对堆使用进行了剖析(使用像Valgrind的massif或类似工具),则可以合理地调整池的大小。

然而,大多数建议的实质都是不信任/不使用系统malloc(),如果失败不是选项。

在您的情况下,我认为最好的做法是确保在malloc()失败时通知看门狗,以便可以重新启动您的进程(或整个系统)。您不希望它在死锁状态下仍然表现得“活着并运行”。这可能只需要取消链接文件即可。
编写非常详细的日志。故障发生在哪个文件/行/函数?
如果尝试获取仅几KB时malloc()失败,则表明您的进程无法继续可靠运行。如果尝试获取数百MB时失败,则您可能能够恢复并继续运行。因此,您采取的任何行动都应基于您要获取的内存量以及是否仍然成功分配较小大小的调用。
您永远不要只操作空指针并让其崩溃。这很随意,没有提供有用的日志记录,给人留下软件质量低/不稳定的印象。

1
哇,感谢您详细的回答。我认为您提出的通知看门狗的建议对我的情况很有效。在这个程序中,我只请求小块内存(几乎总是小于1KB,最常见的是小于100字节),但事先不知道我需要多少内存,所以如果malloc返回NULL,我无法释放内存。 - ipartola
@ipartola - 是的,如果你不能分配1k,那么肯定有严重的问题。无论如何,一个相当聪明的看门狗应该能够在你修复每个泄漏/等等并真正调整所有运行在小空间中的东西以合作的情况下完成工作。 - Tim Post

2

还有第四种选择:释放一些内存(缓存通常是不错的选择),然后再尝试。

如果您负担不起这个代价,我会选择第二种选项(记录或打印某种错误消息,显然)...唯一需要考虑的清理问题是以有序的方式关闭打开的网络连接,以便客户端知道另一端的应用程序正在关闭,而不是发现意外的连接问题。


是的,我考虑/阅读过这个选项。但在这种情况下不可行,因为没有缓存。套接字/文件描述符在程序退出时不会自动销毁吗? - ipartola
1
它们应该是这样的,因为它们由内核处理。当进程终止时,它们将自动关闭。 - Wyzard
是的,它们已经关闭了,但也许您的应用程序有一些特定的协议来断开连接,而不仅仅是关闭流。 - fortran

0

我认为这取决于你的架构。

malloc()失败是否意味着只有该线程无法继续,还是在这种情况下整个进程都会崩溃?

通常情况下,当内存非常紧张(即微处理器环境)时,为避免出现此类问题,最好避免所有动态内存分配。


0

从个人经验来看,我可以说malloc失败的频率经常被高估了。例如,在Linux中,通常的“解决方案”是2的变体,你不会得到malloc失败的提示。进程只是突然死亡。在更大的系统上,一旦交换使其无响应,应用程序往往会因为用户或看门狗而死亡。

这使得清理工作有点困难,也使得想出一个通用解决方案变得困难。


我认为这不太常见。在需要一次性分配几兆字节的情况下,我可能会有更多自由来清除缓存等。在这种情况下,我只分配非常少的内存,所以唯一可能发生的情况就是系统上的另一个进程出了问题。我这样做只是为了正确地完成而不会使源代码混乱不堪。 - ipartola

0

这个程序在操作系统上运行吗?使用pthread表明如此。您知道甚至malloc()会返回NULL吗?在某些系统上(例如Linux),故障将在malloc()内部发生,并由操作系统处理(通过终止进程),而不是malloc()返回。

我建议您在应用程序初始化时分配一个内存池,并从该内存池中进行分配,而不是在初始化后使用malloc()。这将使您控制内存分配算法和内存耗尽时的行为。如果内存池不足,则在应用程序启动之前会有一个单点故障,无法完成任何操作。

在实时和嵌入式系统中,通常使用'固定块内存分配器'。如果您的操作系统没有提供服务,则可以通过预先分配内存块并将它们的指针放置在队列上来实现。要分配块,您需要从队列中取出指针,要释放它,您需要将其放回队列中。当队列为空时,内存已耗尽,您可以选择拒绝并处理错误,或者阻塞并等待另一个线程返回一些内存。您可能希望创建具有不同大小块的多个池,甚至为特定目的创建一个池,其中包含精确所需大小的块。


这个程序将在*nix风格的系统上运行,但可能只有很少的RAM和没有交换空间。我想知道在UNIX程序中分配所有的内存池是否很常见。对于我的情况来说,这似乎有点过度,但我一定会记住它的。 - ipartola

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接