BMRTECH2017 发表于 2022-12-14 14:07:01

陷阱:中断中分配内存

现在的C++编程非常的抽象,这对程序员来说非常好。当然,这是在代码运行一切都正常的情况下。不幸的是,C++在嵌入式中引入了一个稳定性问题,许多程序员都还没有意识到这个问题。本文解释了C++编程时中断内存分配的问题,并提出了解决方案。这里谈到的内存分配在主程序和中断服务例程中都被使用。在很多情况下,这个灾难性的问题会影响到最终产品,使系统变得不稳定、崩溃或故障是随机的,很难发现,但又没有罕见到可以忽略。事实上,现在可能有成千上万的系统存在这个问题。
背景 //
在早期,嵌入式设备的程序是用汇编语言编写的。很繁琐,容易出错,而且非常依赖目标CPU,一旦嵌入式系统的C编译器可用,开发人员就会切换到更高级别的语言。高级语言编程的第一个广泛选择是C语言。在C语言中,程序员不必考虑机器级别,即针对目标CPU的个别指令。这样做的好处是开发人员可以专注于要解决的实际算法或问题。与汇编语言相比,C源代码更容易阅读、更紧凑,也更容易维护。但是放弃控制是有代价的,因为程序员与机器和目标系统中正在发生的事情之间的距离自然会越来越远。当然,代码容易编写,但是对于编译器如何将源代码转换为要执行的指令的控制要少得多。从C迁移到C++又向前迈进了一步。抽象级别更高,程序源代码变得更加紧凑。与此同时,目标系统中究竟发生了什么变得更加不清楚。在许多情况下,被调用的函数对程序员来说不再可见。
内存碎片 //

在C程序中,对于应用程序的编程来说,是否使用堆,在哪里以及如何使用是很清晰的。这基本上是一个内存块的分配和释放,如alloc()和free()。堆存在一些问题,这就是许多应用程序避免使用它们的原因。最大的问题之一是碎片化。只有在内存不多的情况下,这才会成为一个问题,所以在开发PC应用程序时,这也不是一个问题。但是在嵌入式系统中,碎片可能导致系统内存耗尽的情况,不是因为没有可用的内存,而是由于碎片没有足够大小的可用块。虽然这可能是一个严重的问题,但大多数程序员都意识到了这一点,这不是本文的重点。
可重入代码 //
堆管理例程不是可重入的,因为它们管理全局内存池。这意味着需要对堆管理例程进行序列化,以确保对一个例程的调用不会被对同一个或另一个堆管理例程的调用中断(在临界段)。对于多任务环境(RTOS)中的线程,可以使用信号量或互斥量来保证这一点。
中断 //
在编写桌面C++应用程序时,这存在问题,因为应用程序不处理中断。这些由底层操作系统处理。嵌入式系统开发人员没有这样的待遇,中断服务例程(ISR)是应用程序的一部分。这也是让事情变得棘手和出错的地方。ISR中断正常的程序执行。如果它中断堆操作,然后使用堆操作本身,它可能会造成系统不稳定,例如将同一块内存两次分配给请求它的ISR和请求它的正常程序。这种类型的冲突会产生不可预测的行为,包括系统不稳定和崩溃,因此可能是灾难性的行为。这个问题很少在测试中出现。然后,一个bug就会进入到最终产品中,使系统变得不稳定,导致随机崩溃,几乎不可能重现。即使问题已经很明显了,要找出原因就像大海捞针。
解决方法 //
麻烦一点的方法是确保应用程序不会从ISR中调用堆操作函数。这可以通过只在ISR中使用C来实现,同时确保没有调用alloc()或free()函数。不幸的是,这并不容易,特别是ISR很可能会调用其他函数,这些函数现在也需要保证是没有堆操作的。另一个问题是,即使您的代码能够做到这一点,另一个团队成员可能会引入C++,从而产生问题。所以这说起来容易做起来难。但如果你决定走这条路,有一个建议:通过在“Lock”操作中使用一段代码查看CPU的中断状态,应该确保在调试版本中,你的内存管理不是从中断上下文(ISR)中调用的。这样就不能避免关键问题,而是可以在开发过程中发现,而不会进入最终产品。更简单的一种办法是确保内存分配函数是中断安全的简单方法是在堆操作的关键部分禁用中断,这需要工具链的配合。在Embedded Studio中,这是默认的选项。换句话说,这个问题在使用Embedded Studio时不存在。Embedded Studio还提供了另一个锁定选项,称为“User”,由用户负责控制堆锁定,以便针对多核cpu或高度时间关键的应用程序调优系统。

页: [1]
查看完整版本: 陷阱:中断中分配内存