11

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

我想尝试重写代码,以便它不会在堆上分配内存。

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

4

11 回答 11

30

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

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

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

于 2009-06-22T12:34:50.790 回答
9

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

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

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

所以我可能会用清晰明了的“这取决于”来回答你的问题......

于 2009-06-22T11:51:09.023 回答
6

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

man alloca

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

于 2009-06-22T12:59:10.523 回答
3

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

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

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

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

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

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

于 2009-06-22T11:57:29.697 回答
2

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

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

于 2009-06-22T11:49:03.223 回答
2

1:是的,你可以 - 如果你不需要动态内存分配,但它可能有一个可怕的性能,这取决于你的应用程序。(即不使用堆不会给你更好的应用程序)

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

于 2009-06-22T11:50:10.350 回答
2

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

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

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

于 2009-06-22T12:03:55.190 回答
2

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

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

于 2009-06-22T13:10:43.650 回答
0

我最关心的是,废除堆真的有帮助吗?

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

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

我对处理此案的建议:

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

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

于 2009-06-22T13:23:06.560 回答
0

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

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

于 2009-06-22T18:48:26.453 回答
0

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

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

于 2009-06-23T09:22:17.973 回答