C API设计:malloc返回NULL怎么办?

Posted

技术标签:

【中文标题】C API设计:malloc返回NULL怎么办?【英文标题】:C API design: what to do when malloc returns NULL? 【发布时间】:2012-02-21 11:09:09 【问题描述】:

假设我正在用 C 语言编写一个小库——比如说一些数据结构。无法分配内存怎么办?

这可能非常重要,例如我首先需要一些内存来初始化数据结构,或者我正在插入一个键值对并希望将其包装在一个小结构中。它也可能不太重要,例如像pretty_print 函数,它构建了一个很好的内容字符串表示。但是,它通常比您的平均错误更严重 - 可能根本没有继续的意义。大量在线使用malloc 的示例,如果它返回NULL,则直接退出程序。我猜很多真实的客户端代码也会这样做——只是弹出一些错误,或者将其写入stderr,然后中止。 (而且很多真实代码可能根本不会检查malloc 的返回值。)

有时返回NULL 是有意义的,但并非总是如此。错误代码(或只是一些布尔值 success 值),无论是作为返回值还是输出参数都可以正常工作,但似乎它们可能会使 API 混乱或损害 API 的可读性(再说一次,这可能在某种语言中有点像C?)。另一种选择是让调用者随后可以查询某种内部错误状态,例如使用get_error 函数,但是你必须小心线程安全,并且很容易错过;无论如何,人们往往对检查错误感到松懈,如果它完全是一个单独的函数,他们可能不知道,或者他们可能不会打扰(但我想这是他们的问题)。

(我有时会看到 malloc 包裹在一个函数中,该函数会再次尝试直到内存可用...

void *my_malloc(size_t size)

    void *result = NULL;
    while (result == NULL)
        result = malloc(size);
    return result;

但这似乎有点愚蠢,而且可能很危险。)

什么是处理这个问题的正确方法?

【问题讨论】:

优雅退出是我的选择。 @Till:这对于库代码来说是绝对不能接受的。 @R。哎呀,我在阅读这个问题时完全跳过了第一行。在这种情况下,返回 FALSE/NULL 并添加错误状态函数以获取详细信息。 见What is the correct way to handle “out of memory”?或What are out-of-memory handling strategies in C programming? 在某些情况下,loop-until-memory-is-available 可能是一个合理的策略(尽管最好在循环中使用 sleep 来做到这一点,以便让其他东西可以释放记忆!)。这些情况通常是您控制系统上运行的所有内容并且您知道不会让所有应用程序死锁在同一个循环中等待彼此释放内存的情况。当然不是在独立的应用程序中。 【参考方案1】:

如果分配失败的方式阻止了前进,库代码唯一可接受的解决方案是撤消在部分完成的操作中已经进行的任何分配和其他更改并返回调用者的失败代码。只有调用应用程序才能知道正确的方法是什么。一些例子:

    音乐播放器可能只是中止或返回初始/停止状态并再次等待用户输入。 文字处理器可能需要将当前文档状态的紧急转储存储到恢复文件中,然后中止。 高端数据库服务器可能需要拒绝并退出整个事务并向客户端报告。

如果您遵循经常被建议但倒退的想法,即您的库应该在分配失败时中止调用者,那么您将有许多程序确定他们因此无法使用您的库,您的用户使用的程序当分配失败导致有价值的数据被丢弃时,您的图书馆会非常生气。

编辑: 一些“中止”阵营对我的回答提出的一个反对意见是,在过度提交的系统上,即使对似乎成功的malloc 的调用在内核尝试时也可能失败为分配的虚拟内存实例化物理存储。这忽略了一个事实,即任何需要高可靠性的人都将禁用过度使用,以及(至少在 32 位系统上)分配失败更可能是由于虚拟地址空间耗尽而不是物理存储耗尽这一事实。

【讨论】:

【参考方案2】:

只需以您通常使用的任何方式返回错误即可。由于我们讨论的是 API,因此您不知道从哪个环境调用您,因此只需返回 NULL 或遵循您已经使用的任何其他错误处理过程。您不想永远循环,因为调用者可能并不真正需要该内存,他们宁愿只知道您无法处理它,或者调用者可能有一个他们可以将错误发送到的用户界面。

大多数 API 都会有某种返回值来指示所有函数中的错误,其他 API 要求调用者调用特殊的“check_error”函数来确定是否存在错误。您可能还需要一个“get_error”函数来返回一个错误字符串,调用者可以选择向用户显示或包含在日志中。它应该是描述性的:“某某 API 在函数中遇到错误:无法分配内存”。管他呢。足够了,当有人收到错误时,他们知道是哪个组件抛出了它,当他通过电子邮件向您发送日志消息时,您就知道出了什么问题。

当然你也可以直接崩溃,但这会阻止调用者关闭他们可能正在做的任何其他事情并且看起来很丑,如果你的代码有在调用时死掉而不是返回错误的习惯,人们会会寻找一个不会主动尝试杀死他们的程序的库。

【讨论】:

【参考方案3】:

对于图书馆,您有两种选择。在没有应用程序合作的情况下,您几乎所能做的就是将错误传递回应用程序。

通过应用程序合作,您可以做得更多。例如,您可以让应用程序注册一个回调,当malloc 返回NULL 时您的库调用该回调。您可以向回调传递您需要的字节数以及您需要它们的紧急程度。 (例如,“将完全无法操作”,“将不得不中止操作”等。)然后应用程序作者可以决定是否给您内存。

在应用程序级别,您可以做的更多。例如,您可以malloc 一堆内存块用作“应急池”。如果malloc 失败,您可以从池中释放块并开始减载、减少缓存或任何其他减少内存消耗的选择。

但是除了通知合作的应用程序之外,您通常不能在库中做很多事情。

【讨论】:

【参考方案4】:

用于线性代数函数调用的 BLAS 标准 API 使用与此处给出的“返回错误代码”建议有些不同的方法:它调用特定的记录函数,然后返回。

现在,该库还提供了此文档化函数的实现,该函数会打印有用的错误消息和(在可能的情况下)堆栈跟踪,然后中止。这是处理事情的一种方式,这意味着普通用户不会因为忘记检查错误代码而遇到奇怪的问题。

然而,作为特定文档功能的关键点在于,它意味着用户可以选择提供他们自己的该功能的实现,这将覆盖默认实现。这个实现可以做很多事情——它可以设置一个全局错误代码供用户检查,或者它可以做一些尝试清理一些内存并继续的事情。

对于实施和用户来说,这是一种重量级的解决方案,但在明显错误代码不合适的情况下,它提供了很大的灵活性。

编辑以添加更多细节:BLAS 函数是 xerbla(或 C 接口中的 cblas_xerbla),期望在链接时会覆盖它——假设是静态链接。在this header from Apple 中也有一些关于如何为动态库进行调整的相关说明(请参阅文件底部附近 SetBLASParamErrorProc 上的 cmets)——在动态链接的情况下,需要在以下位置注册回调运行时。

另请参阅“R”的有用注释。在下面的 cmets 中,不幸的是,这种覆盖是全局的,如果用户通过第二个库直接或间接使用您的库,并且用户和第二个库都希望覆盖处理程序,这可能会导致问题。

【讨论】:

这真是个好主意。与数据结构示例保持一致,调用者可能已经在提供回调,例如释放、比较、散列或将值转换为字符串 - 添加错误处理回调是有意义的。 这实际上是一个非常糟糕的设计,因为它是有状态的(涉及全局变量/状态),除非您的所有库函数都采用“指向上下文的指针”参数。想象一个应用程序同时使用 libfoo 和间接使用 libfoo 的 libbar 的情况。你的应用程序设置了如何处理分配失败的钩子函数,然后 libbar 用它自己的钩子替换它。现在您的应用程序的处理程序不会被调用。不酷。库永远不应该有这种类型的全局状态;这是非常糟糕的设计。 啊,我没想到是这样的;我在考虑那些函数的行为或多或少类似于不透明结构上的方法的库(例如mylib_append(mylib_list* l, void *element))。在这种情况下,回调将是结构的属性,也将充当您提到的“上下文”。 是的,这是一个非常好的方法,但是调用者在它创建的每个对象上设置回调属性当然很痛苦。我认为将回调函数指针传递给每个函数会更容易,或者只使用返回值并让调用者负责错误处理。 我永远不会推荐任何人将 BLAS 用于 API 设计示例。【参考方案5】:

很难设计软件来干净地处理内存不足问题并继续进行。 很少有真正的应用程序会认真尝试这样做。作为库作者,唯一合理的做法是向调用者报告错误。 (返回失败;抛出异常;取决于语言等)

您绝对不想为通用库循环和阻塞。 @R 有一个好处,如果发生故障,请尝试将状态恢复到原始状态。

处理内存不足和磁盘空间不足问题可能需要在应用的所有部分进行协调。您可能希望预先分配应急内存。您可能会像您打算的那样循环/重试 malloc,但在超时之间会有一些延迟。它确实超出了典型库的范围。

【讨论】:

【参考方案6】:

在 Java 或 C# 等语言中,明确的答案通常是“抛出异常!”。

在 C 中,一种常见的方法是同步处理错误(例如,使用结果代码和/或像“null”这样的标志值)。

也可以生成一个异步信号(很像 Java 的“异常”……或强硬的“abort()”)。使用这种方法,您还可以允许用户安装自定义“错误处理程序”。

这是一个使用 setjmp/longjmp 的示例:

http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html

这里有一些有趣的想法:

http://blog.staila.com/?p=114

这里有一个关于使用 C 回调进行错误处理的优缺点的很好的讨论:

Using callback functions for error handling in C

【讨论】:

【参考方案7】:

您可以编写一开始就不需要malloc 的软件 - 安全关键的东西。

您只需确保在开始执行时分配、定义所需内容并确保算法不会超出这些障碍。

很难,但并非不可能。

【讨论】:

以上是关于C API设计:malloc返回NULL怎么办?的主要内容,如果未能解决你的问题,请参考以下文章

使 malloc() 返回 NULL 而不是使程序崩溃?

C语言中malloc函数的使用方法是啥?

c语言中,malloc和free是啥意思?

c语言内存分配-malloc

C语言 DrawDibDraw函数怎用?

malloc函数实现的功能是啥?