是否使用堆内存(malloc / new)创build一个非确定性的程序?
几个月前我开始用C开发空间应用程序的软件,也用C ++开发微控制器。 在这样的系统中有一个经验法则,那就是不应该创build堆对象 (所以不需要malloc / new),因为它使得程序不确定 。 当人们告诉我时,我无法validation这个声明的正确性。 那么这是一个正确的说法吗?
对我来说,困惑是,就我所知,决定论意味着运行一个程序两次将导致完全相同的执行path。 从我的理解,这是一个multithreading系统的问题,因为多次运行相同的程序可能会有不同的线程在不同的次序运行。
在实时系统的情况下,确定性比可重复的“执行path”更多。 另一个要求的属性是关键事件的时间是有界的。 在硬实时系统中,在允许的时间间隔之外发生的事件(在该时间间隔开始之前或在结束之后)表示系统故障。
在这种情况下,使用dynamic内存分配会导致非确定性,特别是如果程序具有不同的分配,释放和重新分配模式。 分配,取消分配和重新分配的时间可能随着时间的推移而变化,因此整个系统的时间安排是不可预测的。
如上所述,评论是不正确的。
使用具有非确定性行为的堆pipe理器会创build一个具有非确定性行为的程序。 但是这是显而易见的。
具有决定性行为的堆pipe理者的存在略微不太明显。 也许最着名的例子是池分配器。 它有一个N * M字节的数组,和一个N位的available[]
掩码。 为了分配,它检查第一个可用入口(位testing,O(N),确定性上界)。 为了释放,它设置可用位(O(1))。 malloc(X)
会将malloc(X)
向上取整到M的下一个最大值以select正确的池。
这可能不是很有效率,特别是如果你的N和M的select太高。 如果你select太低,你的程序可能会失败。 但是N和M的限制可以低于没有dynamic内存分配的等效程序。
C11标准或n1570中没有一个表示malloc
是确定性的(或不是); 也没有一些其他的文档像Linux上的malloc(3) 。 顺便说一句,许多malloc
实现都是免费软件 。
但是, malloc
可以(而且确实)失败,其性能还不知道(在我的桌面上malloc
的典型调用实际上只需要不到一微秒,但我可以想象奇怪的情况下,可能需要更多,也许几毫秒非常负载的电脑;阅读关于颠簸 )。 而我的Linux桌面具有ASLR (地址空间布局随机化),所以两次运行相同的程序会给出不同的malloc
-ed地址(在进程的虚拟地址空间中)。 顺便说一句, 这是一个确定性的(根据具体的假设,你需要详细说明),但实际上无用的malloc
实现。
决定论意味着运行一个程序两次将导致完全相同的执行path
这在大多数embedded式系统中实际上是错误的,因为物理环境在变化。 例如,驱动火箭发动机的软件不能期望从一次发射到下一次发射的推力,或阻力,或风速等是完全相同的。
(所以我很惊讶你相信或希望实时系统是确定性的,它们从来都不是!也许你关心的是WCET ,由于高速caching越来越难以预测)
顺便一些“实时”或“embedded式”系统正在实现自己的malloc
(或它的一些变种)。 C ++程序可以有它们的分配器 -s,可以被标准容器使用。 另见这个和那个等等…..
而embedded式软件的高层(想象一下自主汽车及其规划软件)肯定会使用堆分配,甚至可能是垃圾收集技术(其中一些是“实时”的),但通常不被认为是安全的。
tl; dr:dynamic内存分配不是固有的非确定性的 (正如你用相同的执行path定义的那样)。 这是它通常使你的程序不可预知的 。 具体而言,您无法预测分配器是否可能会面对任意input序列而失败。
你可以有一个非确定性的分配器。 这实际上在您的实时世界之外是常见的,操作系统使用诸如地址布局随机化之类的东西。 当然,这会使你的程序不确定。
但是这不是一个有趣的例子,所以让我们假设一个完全确定性的分配器:相同的分配和释放顺序总是在相同的位置产生相同的块,并且这些分配和释放总是有一个有限的运行时间。
现在你的程序可以是确定性的:同一组input将导致完全相同的执行path。
问题是,如果你分配和释放内存来响应input,你不能预测分配是否会失败(并且失败不是一个选项)。
首先,你的程序可能会泄漏内存。 所以如果需要无限期地运行,最终分配将会失败。
但是,即使你能certificate没有泄漏,你也需要知道,从来没有一个input序列可能需要更多的内存。
但是,即使你能certificate程序永远不会需要比可用内存更多的内存,分配器可能会根据分配和释放的顺序来分割内存,从而最终无法find连续的块来满足分配,即使总体上有足够的空闲内存。
要certificate没有一系列的input会导致病理性分裂是非常困难的。
您可以devise分配器来保证不会出现碎片(例如,通过分配只有一个大小的块),但这会给调用者带来很大的限制,并且可能会由于浪费而增加所需的内存量。 而且调用者还必须certificate没有泄漏,并且无论input的顺序如何,都需要总内存的上限。 这个负担很重,devise系统实际上比较简单,因此不使用dynamic内存分配。
与实时系统的交易是,程序必须严格地满足一定的计算和内存限制,而不pipe执行path如何(这依赖于input可能仍然相当大)。 那么在这种情况下,使用通用dynamic内存分配(如malloc / new)是什么意思呢? 这意味着开发人员在某些时候不能确定精确的内存消耗,也不可能知道生成的程序是否能满足内存和计算能力的要求。
是的,这是正确的。 对于你提到的那种应用,所有可能发生的事情都必须详细说明。 程序必须根据规范处理最坏的情况,并准确地留出那么多的内存,不要多less,不要less。 “我们不知道我们得到多less投入”的情况并不存在。 最坏的情况是用固定数字指定的。
您的程序必须是确定性的,因为它可以处理所有情况,甚至是最坏的情况。
堆的目的是允许几个不相关的应用程序共享RAM内存,例如在PC中,程序/进程/线程运行的数量是不确定的。 这种情况在实时系统中不存在。
此外,堆的本质是非确定性的,随着时间段的增加或删除。
更多信息在这里: https : //electronics.stackexchange.com/a/171581/6102
即使你的堆分配器具有可重复的行为(相同的分配序列和空闲的调用产生相同的块序列,因此(希望)也是相同的内部堆状态),如果调用序列改变,堆的状态可能会剧烈变化,可能导致碎片化,以不可预知的方式导致内存分配失败。
堆分配的原因在embedded式系统中被彻底禁止了,尤其是在embedded式系统中。 如航空器或航天器指导或生命支持系统等关键任务系统是没有办法testing响应本质上asynchronous事件可能发生的malloc / free调用序列中所有可能的变化。
解决方法是每个处理程序为其目的而留出一个内存,并且这些处理程序被调用的顺序不再重要(至less就内存使用而言)。
在硬实时软件中使用堆的问题是堆分配可能会失败。 你什么时候你堆完了?
你在谈论太空应用。 你有非常难的没有失败的要求。 您必须不可能泄漏内存,因此至less安全模式代码无法运行。 你不能倒下 你不能抛出没有catch块的exception。 你可能没有一个带有保护内存的操作系统,所以一个崩溃的应用程序理论上可以把所有东西都拿出来
你可能根本不想使用堆。 好处不会超过整个计划的成本。
非决定性通常意味着别的东西,但在这种情况下,最好的阅读是他们希望整个程序的行为完全可以预测。
从GHS引入完整性RTOS:
https://www.ghs.com/products/rtos/integrity.html
和LynxOS:
LynxOS和Integrity RTOS是空间应用,导弹,飞机等使用的软件之一,因为其他许多软件未经当局批准或authentication(如FAA)。
https://www.ghs.com/news/230210r.html
为了满足空间应用的严格标准,Integrity RTOS实际上提供了formsvalidation,即mathcertificate的逻辑,即他们的软件按照规范行事。
在这些标准中,从这里引用:
https://en.wikipedia.org/wiki/Integrity_(operating_system);
和这里:
青山诚信dynamic内存分配
这是:
我不是正规方法的专家,但也许这个validation的要求之一是消除内存分配所需时间的不确定性。 在实时操作系统中,所有的事件都是精确地计划在毫秒之间。 dynamic内存分配总是有一个需要时间的问题。
在math上,你真的需要certificate从关于时间和内存数量的最基本的假设中得到的一切。
如果你想到堆内存的替代方法: 静态内存 。 地址是固定的,分配的大小是固定的。 内存中的位置是固定的。 因此,关于内存充足性,可靠性,可用性等是很容易的。
简短的回答
例如,第一级或第二级触发闪烁体装置的数据值或其统计不确定度分布会有一些影响,这些闪烁体装置可能来自不可重现的时间量,您可能不得不等待malloc/free
。
最糟糕的是,它们与硬件无关,而与记忆的状态(及其历史)无关。
在这种情况下,您的目标是根据受这些错误影响的数据重build事件的原始顺序。 重构/猜测的序列也会受到错误的影响。 并不总是这个迭代将收敛在一个稳定的解决scheme; 这不是说它会是正确的; 你的数据不再是独立的…你冒险的逻辑短路 …
较长的答案
你说: “当人们告诉我这些话时,我无法validation这个声明的正确性” 。
我会尽力给你一个纯粹的假设情况/案例研究。
让我们来想象一下,在一个必须节约资源的系统(你在太空中)处理一个CCD或者一些闪烁体触发器和一些闪烁体触发器。
采集率将被设置为背景将在MAXBINCOUNT
x%
。
-
有一个爆炸,你有一个尖峰的计数和在柜台溢出。
我想要这一切:你切换到最大采集率,你完成你的缓冲区。
在完成额外的缓冲区的同时,您可以释放/分配更多的内存。
你会怎么做?- 你会保持抵抗冒险溢出 (第二级将尽量正确地计数数据包的时间),但在这种情况下,你会低估那个时期的计数?
- 你会停止在时间序列中引入一个洞 ?
注意:
- 等待分配,你将失去瞬态 (或至less是它的开始)。
- 无论你做什么取决于你的记忆状态,它是不可重现的。
-
现在,信号在硬件允许的最大采集速率下可以在
maxbincount
周围maxbincount
,事件比平常长。
你完成的空间,并要求更多…与此同时,你遇到了上述同样的问题。
溢出和系统性的峰值计数时间序列中的低估或漏洞?
让我们移动一个二级(也可以在一级触发器上)。
从您的硬件,您收到更多的数据比您可以储存或传输。
您必须在时间或空间上对数据进行聚类(2×2,4×4,… 16×16 … 256×256 …像素缩放…)。
前一个问题的不确定性可能会影响误差分布 。
有CCD设置,你有边界像素的计数接近maxbincount
(这取决于“哪里”你想看到更好)。
现在,您可以在您的CCD上有一个淋浴,也可以有一个统计总数相同的统计不确定性(由等待时间引入的部分)。
因此,例如,如果您期待Lorentzianconfiguration文件,您可以使用高斯函数(Voigt)来获得它的卷积,或者如果第二个实际上是用一个肮脏的高斯函数来支配它的话。
总是有一个权衡。 这是程序的运行环境,它所执行的任务应该是决定是否使用HEAP的基础。
当你想在多个函数调用之间共享数据时,堆对象是有效的。 您只需要传递指针,因为堆是全局访问的。 也有缺点。 一些函数可能释放这个内存,但是在其他地方也可能存在一些引用。
如果堆内存在工作完成后没有被释放,并且程序继续分配更多的内存,那么在某个时刻HEAP将耗尽内存并影响程序的确定性。