【问题标题】:Can I write a C application without using the heap?我可以在不使用堆的情况下编写 C 应用程序吗?
【发布时间】:2010-11-04 19:58:51
【问题描述】:

我在嵌入式环境中遇到了似乎是堆栈/堆冲突的情况(有关背景信息,请参阅 this question)。

我想尝试重写代码,使其不会在堆上分配内存。

我可以在不使用 C 中的堆的情况下编写应用程序吗?例如,如果我需要动态内存分配,我将如何使用堆栈?

【问题讨论】:

  • 你是问如何在栈上做动态内存分配?
  • 我的意思只是作为一个例子。我会更新问题。
  • 您的问题似乎有点含糊。越具体越好。
  • @Craig - 我已经更新了这个问题。如果您有更多反馈,我将不胜感激。谢谢。

标签: c embedded heap-memory


【解决方案1】:

如果不使用堆内存,就无法在 C 中进行动态内存分配。如果不使用堆,编写一个真实世界的应用程序将非常困难。至少,我想不出办法。

顺便说一句,你为什么要避免堆?它有什么问题?

【讨论】:

  • 我添加了一个提供一些背景的链接
  • 实际上,您可以使用底层操作系统的 API 来分配系统内存,而无需使用运行时堆。例如,在 Windows CE 中,您可以使用内存映射文件分配内存,而无需调用 malloc() 一次。
  • 但这取决于平台。对吗?
  • 啊,我知道那种感觉。研究如何配置编译器关于内存布局的决策。另外,请解释一下您使用 malloc() 的确切用途,也许我们可以对您的内存使用策略提出一些改进建议。
  • @Artelius - 见stackoverflow.com/questions/1027416/…。谢谢。
【解决方案2】:

1:是的,您可以 - 如果您不需要动态内存分配,但它的性能可能很糟糕,具体取决于您的应用程序。 (即不使用堆不会给你更好的应用程序)

2:不,我不认为你可以在堆栈上动态分配内存,因为那部分是由编译器管理的。

【讨论】:

  • 这取决于,一些平台有像 alloca(3) 这样的库调用,它在堆栈上分配动态内存。大多数编译器现在还允许您分配可变大小的数组,您可以强制它们像动态 void* 内存指针一样工作,尽管这当然不推荐。
  • 但重要的是,当函数结束时,这块内存会被释放!
  • @Jason Coco/Artelius:谢谢,不知道!
【解决方案3】:

有趣的是,我曾经看到一个完全依赖静态分配内存的数据库应用程序。此应用程序对字段和记录长度有严格的限制。即使是嵌入式文本编辑器(我仍然会这样称呼它)也无法创建超过 250 行文本的文本。这解决了我此时的一些问题:为什么每个客户只允许 40 条记录?

在严肃的应用程序中,您无法提前计算正在运行的系统的内存需求。因此,根据需要动态分配内存是个好主意。然而,在嵌入式系统中,通常会预先分配您真正需要的内存,以防止由于内存不足而导致意外故障。

您可以使用 alloca() 库调用在堆栈上分配动态内存。但是这个内存对于应用程序的执行上下文来说是紧密的,将这种类型的内存返回给调用者是个坏主意,因为它会被以后的子程序调用覆盖。

所以我可能会用清晰明了的“视情况而定”来回答你的问题......

【讨论】:

    【解决方案4】:

    可以在 main() 中从堆栈中分配大量内存,然后让您的代码对其进行子分配。这样做很愚蠢,因为这意味着您的程序正在占用它实际上并不需要的内存。

    我想不出任何理由(除了一些愚蠢的编程挑战或学习练习)想要避免堆。如果您“听说”堆分配很慢而堆栈分配很快,那仅仅是因为堆涉及动态分配。如果您要从堆栈中的保留块动态分配内存,它会一样慢。

    堆栈分配既简单又快速,因为您只能释放堆栈上“最年轻”的项。它适用于局部变量。它不适用于动态数据结构。

    编辑:看到问题的动机......

    首先,堆和栈必须竞争相同数量的可用空间。一般来说,它们会相互靠近。这意味着,如果您以某种方式将所有堆使用量移到堆栈中,那么堆栈大小将刚好超过您可用的 RAM 量,而不是堆栈与堆冲突。

    我认为您只需要注意堆和堆栈的使用情况(您可以获取指向局部变量的指针以了解当前堆栈的位置),如果它太高,请减少它。如果您有很多小的动态分配对象,请记住每次分配都有一些内存开销,因此从池中对它们进行子分配有助于减少内存需求。如果您在任何地方使用递归,请考虑将其替换为基于数组的解决方案。

    【讨论】:

    • 我在嵌入式环境中工作(请参阅链接的问题)并遇到似乎是堆/堆栈冲突的情况。该问题与性能无关。
    • 好的,不好意思跳了。你真的应该在你的问题中提供更多细节。
    • @Artelius:较小的嵌入式处理器根本不使用堆是很常见的,因此没有动态内存分配。
    • 好吧,如果您处于无操作系统或 RTOS 环境中,您可以随意使用空闲内存 - 如果需要,您可以自己实现某种 malloc()。
    【解决方案5】:

    是的,这是可行的。将您的动态需求从内存转移到磁盘(或任何可用的大容量存储)上——并遭受随之而来的性能损失。

    例如,您需要构建和引用未知大小的二叉树。指定描述树节点的记录布局,其中指向其他节点的指针实际上是树文件中的记录编号。编写例程,让您通过将附加记录写入文件来添加到树中,并通过读取记录、查找其子项作为另一个记录号、读取该记录等来遍历树。

    这种技术动态分配空间,但它是磁盘空间,而不是 RAM 空间。所有涉及的例程都可以使用静态分配的空间来编写——在堆栈上。

    【讨论】:

    • 我在嵌入式环境中工作,因此没有磁盘或任何其他存储设备...不过我可以添加一个 +1。
    【解决方案6】:

    我曾经在嵌入式环境中做过一次,当时我们正在为生物医学机器编写“超级安全”的代码。 Malloc() 被明确禁止,部分原因是资源限制和动态内存中可能出现的意外行为(查找 malloc()、VxWorks/Tornado 和碎片,你会有一个很好的例子)。

    无论如何,解决方案是提前计划所需资源,并在包含在单独模块中的向量中静态分配“动态”资源,并使用某种特殊用途的分配器给予和收回指针。这种方法完全避免了碎片问题,并有助于在资源耗尽时获得更细粒度的错误信息。

    这在大铁上听起来可能很愚蠢,但在嵌入式系统上,尤其是在安全关键系统上,最好事先很好地了解需要哪些时间和空间资源,如果只是为了调整大小硬件。

    【讨论】:

    • 这正是我在我从事的安全关键/高可用性(嵌入式)系统上所做的。缓冲池、静态分配等
    • +1 同样,我已经有一段时间没有在嵌入式系统上使用 malloc 了。
    • 在静态数组中准确分配动态对象与为其创建堆和分配器有何不同?
    【解决方案7】:

    您可以使用alloca() 函数在堆栈上分配内存 - 当您退出该函数时,该内存将被自动释放。 alloca() 是 GNU 特定的,您使用 GCC,所以它必须可用。

    man alloca

    另一种选择是使用变长数组,但你需要使用 C99 模式。

    【讨论】:

      【解决方案8】:

      嵌入式应用程序需要注意内存分配,但我不认为使用堆栈或您自己的预分配堆是答案。如果可能,在初始化时从堆中分配所有需要的内存(通常是缓冲区和大型数据结构)。这需要与我们大多数人现在习惯的不同风格的程序,但这是接近确定性行为的最佳方式。

      稍后被子分配的大堆仍然会耗尽内存,唯一要做的就是启动看门狗(或类似操作)。使用堆栈听起来很吸引人,但如果您要在堆栈上分配大型缓冲区/数据结构,则必须确保堆栈足够大以处理您的程序可以执行的所有可能的代码路径。这并不容易,最终类似于子分配的堆。

      【讨论】:

      • 大概当你说'分配所有需要的内存......在初始化时'你的意思是在堆上分配?
      • 否 - 不一定,甚至通常。您预先分配数组,然后根据需要使用(分配)其中的元素。这甚至可以节省空间,因为您可以存储 2 字节(或 4 字节)数组索引,而不是存储 8 字节指针。
      • @Jonathan Leffler:如果我使用该方案,我仍然会担心不知道运行时需要多少内存。这和堆有什么区别?
      • @dp:您在所施加的限制范围内定义系统(例如 64 KB RAM - 真的!),并且您认识到您没有虚拟内存,并且您决定如何摆脱不需要的内存当没有空间容纳最新的新数据时的数据,这确实是一门要求很高的学科。这与堆之间的区别在于,堆的一部分可以重新分配给不同的用途,而且堆通常具有占用宝贵空间的管理开销。
      • @Jonathan Leffler:也许是情境性的。在内存很少的嵌入式系统中,我个人更喜欢在启动时分配所有内容,因此我可以确保在运行时不会耗尽内存。这样,应用程序的内存预算就建立了,我可以(相对)安全地执行一些代码路径,分配比我想象的更多的内存。我已经按照您建议的方式完成了,在这些情况下,我一定会在尽可能多的情况下尽可能多地测试运行时内存使用情况。
      【解决方案9】:

      我最关心的是,取消堆真的有帮助吗?

      由于你不使用堆的愿望源于堆栈/堆冲突,假设堆栈的开始和堆的开始设置正确(例如在相同的设置下,小示例程序没有这样的碰撞问题),那么碰撞意味着硬件没有足够的内存供您的程序使用。

      不使用堆,确实可以从堆碎片中节省一些浪费空间;但是如果你的程序不使用堆进行一堆不规则的大尺寸分配,那么浪费可能并不多。我会看到你的碰撞问题更多的是内存不足问题,仅仅通过避免堆是无法解决的。

      我对处理此案的建议:

      1. 计算程序的总潜在内存使用量。如果它太接近但尚未超过您为硬件准备的内存量,那么您可以
      2. 尝试使用更少的内存(改进算法)或更有效地使用内存(例如更小和更常规的malloc() 以减少堆碎片);或
      3. 只需为硬件购买更多内存

      当然你可以尝试把所有东西都压入预定义的静态内存空间,但是这次很有可能会被堆栈覆盖到静态内存中。因此,首先改进算法以减少内存消耗,然后再购买更多内存。

      【讨论】:

        【解决方案10】:

        我会以不同的方式解决这个问题 - 如果您认为堆栈和堆发生冲突,那么通过防范它来测试它。

        例如(假设是*ix 系统)尝试mprotect()ing 最后一个堆栈页(假设一个固定大小的堆栈),因此它不可访问。或者 - 如果您的堆栈增长 - 然后 mmap 在堆栈和堆中间的页面。如果你在你的保护页面上得到一个 segv,你就知道你已经跑出了堆栈或堆的末尾;通过查看 seg 错误的地址,您可以看到堆栈和堆中的哪一个发生了冲突。

        【讨论】:

        • 如果你用 C 语言编写一个没有 / 有 / 堆的系统,你会怎么做? (如果您使用 malloc,它是使用大量堆栈实现的),并且在程序运行时您没有诊断输出? (没有屏幕,没有串口……)
        【解决方案11】:

        通常可以在不使用动态内存分配的情况下编写嵌入式应用程序。在许多嵌入式应用程序中,不推荐使用动态分配,因为堆碎片可能会出现问题。随着时间的推移,很可能没有适当大小的可用堆空间区域来允许分配内存,除非有适当的方案来处理此错误,否则应用程序将崩溃。有多种方案可以解决这个问题,一种是始终在堆上分配固定大小的对象,以便新分配始终适合已释放的内存区域。另一个用于检测分配失败并对堆上的所有对象执行碎片整理过程(留给读者练习!)

        您没有说明您使用的是什么处理器或工具集,但在许多情况下,静态、堆和堆栈被分配给链接器中单独定义的段。如果是这种情况,那么一定是您的堆栈在您为其定义的内存空间之外增长。您需要的解决方案是减少堆和/或静态变量大小(假设这两个是连续的),以便堆栈有更多可用空间。单方面减少堆是可能的,尽管这会增加碎片问题的可能性。确保没有不必要的静态变量会释放一些空间,但如果将变量设为自动,则可能会增加堆栈使用量。

        【讨论】:

          猜你喜欢
          • 2021-01-08
          • 1970-01-01
          • 2020-04-22
          • 1970-01-01
          • 1970-01-01
          • 2010-09-05
          • 1970-01-01
          • 2011-10-26
          • 1970-01-01
          相关资源
          最近更新 更多