我们正在编译一个嵌入式C++应用程序,它部署在电离辐射轰击的环境中的屏蔽设备中。我们正在为ARM使用GCC和交叉编译。在部署时,我们的应用程序会生成一些错误的数据,并且比我们希望的更频繁地崩溃。硬件是为这个环境而设计的,我们的应用程序已经在这个平台上运行了几年。
我们是否可以对代码进行更改,或对编译时进行改进,以识别/纠正由单个事件扰乱导致的软错误和内存损坏?是否有其他开发人员成功地减少了软错误对长时间运行的应用程序的有害影响?
我们正在编译一个嵌入式C++应用程序,它部署在电离辐射轰击的环境中的屏蔽设备中。我们正在为ARM使用GCC和交叉编译。在部署时,我们的应用程序会生成一些错误的数据,并且比我们希望的更频繁地崩溃。硬件是为这个环境而设计的,我们的应用程序已经在这个平台上运行了几年。
我们是否可以对代码进行更改,或对编译时进行改进,以识别/纠正由单个事件扰乱导致的软错误和内存损坏?是否有其他开发人员成功地减少了软错误对长时间运行的应用程序的有害影响?
当前回答
有人提到使用较慢的芯片来防止离子同样容易地翻转比特。以类似的方式,可能使用专门的cpu/ram,它实际上使用多个位来存储单个位。因此,提供了硬件容错,因为不太可能所有的位都被翻转。所以1=1111,但需要被击中4次才能真正翻转。(4可能是一个坏数字,因为如果2位被翻转,它就已经不明确了)。因此,如果您使用8,您得到的ram将减少8倍,访问时间也会慢一些,但数据表示更可靠。您可能可以在软件级别使用专门的编译器(为所有内容分配更多的空间)或语言实现(为以这种方式分配内容的数据结构编写包装器)来实现这一点。或具有相同逻辑结构但在固件中执行此操作的专用硬件。
其他回答
免责声明:我不是放射性专业人员,也不是这类应用的工作人员。但我致力于关键数据的长期归档的软错误和冗余,这有点联系(相同的问题,不同的目标)。
在我看来,放射性的主要问题是放射性可以切换位,因此放射性可以/将篡改任何数字存储器。这些错误通常被称为软错误、比特腐烂等。
问题是:当你的内存不可靠时,如何可靠地计算?
要显著降低软错误率(以计算开销为代价,因为大多数情况下都是基于软件的解决方案),您可以:
依靠好的旧冗余方案,更具体地说,是更有效的纠错码(目的相同,但算法更聪明,这样可以用更少的冗余恢复更多的比特)。这有时(错误地)也称为校验和。使用这种解决方案,您必须随时将程序的完整状态存储在主变量/类(或结构?)中,计算ECC,并在执行任何操作之前检查ECC是否正确,如果不正确,则修复字段。然而,此解决方案不能保证您的软件能够正常工作(简单地说,它可以正常工作,否则停止工作,因为ECC可以告诉您是否有问题,在这种情况下,您可以停止软件,这样您就不会得到假结果)。或者,您可以使用弹性算法数据结构,这在一定程度上保证您的程序即使在存在软错误的情况下仍能给出正确的结果。这些算法可以看作是普通算法结构与ECC方案的混合,但这比这更具弹性,因为弹性方案与结构紧密结合,因此不需要编码额外的过程来检查ECC,而且通常速度更快。这些结构提供了一种方法,可以确保您的程序在任何条件下都能工作,直到软错误的理论范围。您还可以将这些弹性结构与冗余/ECC方案混合使用,以提高安全性(或将最重要的数据结构编码为弹性数据结构,其余的是可从主数据结构重新计算的消耗性数据,作为具有ECC或奇偶校验的正常数据结构,计算速度非常快)。
如果您对弹性数据结构感兴趣(这是一个最近但令人兴奋的算法和冗余工程领域),我建议您阅读以下文档:
罗马大学Giuseppe F.Italiano“Tor Vergata”介绍的弹性算法数据结构Christiano,P.、Demaine,E.D.和Kishore,S.(2011)。具有附加开销的无损容错数据结构。《算法和数据结构》(第243-254页)。施普林格柏林海德堡。Ferraro Petrillo,U.、Grandoni,F.和Italiano,G.F.(2013)。数据结构对记忆故障的恢复能力:词典的实验研究。实验算法杂志(JEA),18,1-6。意大利,G.F.(2010)。弹性算法和数据结构。《算法与复杂性》(第13-24页)。施普林格柏林海德堡。
如果您有兴趣了解弹性数据结构领域的更多信息,您可以查看Giuseppe F.Italiano的作品(并通过参考文献)和Fault RAM模型(在Finocchi等人2005;Finocchi和Italiano 2008中介绍)。
/编辑:我说明了主要针对RAM内存和数据存储的软错误的预防/恢复,但我没有谈到计算(CPU)错误。其他答案已经指出了在数据库中使用原子事务,所以我将提出另一个更简单的方案:冗余和多数投票。
其思想是,您只需对需要进行的每一次计算进行x次相同的计算,并将结果存储在x个不同的变量中(x>=3)。然后可以比较x变量:
如果他们都同意,那么根本就没有计算错误。如果他们不同意,那么您可以使用多数票来获得正确的值,因为这意味着计算部分损坏,您还可以触发系统/程序状态扫描以检查其余部分是否正常。如果多数投票无法确定获胜者(所有x值都不同),那么这是触发故障保护程序(重新启动、向用户发出警报等)的完美信号。
与ECC相比,这种冗余方案非常快(实际上是O(1)),当您需要故障保护时,它为您提供了清晰的信号。多数表决也(几乎)保证不会产生损坏的输出,并从较小的计算错误中恢复,因为x计算给出相同输出的概率是无穷小的(因为有大量可能的输出,所以几乎不可能随机获得3倍相同的结果,如果x>3,则可能性更小)。
因此,通过多数表决,您可以避免损坏的输出,并且通过冗余x==3,您可以恢复1个错误(如果x==4,则可以恢复2个错误,等等——确切的公式是nb_error_recoverable==(x-2),其中x是计算重复次数,因为您需要至少2个一致的计算才能使用多数表决进行恢复)。
缺点是你需要计算x次而不是一次,所以你有额外的计算成本,但是它的线性复杂性是渐进的,所以你不会因为你获得的好处而损失太多。进行多数表决的快速方法是计算阵列上的模式,但也可以使用中值滤波器。
此外,如果您想确保计算正确进行,如果您可以制作自己的硬件,您可以用x个CPU构建设备,并将系统连接起来,以便在x个CPU之间自动复制计算,并在最后以机械方式进行多数表决(例如,使用“与/或”门)。这通常在飞机和任务关键设备中实现(参见三模块冗余)。这样,你就不会有任何计算开销(因为额外的计算将并行进行),并且你有另一层防止软错误的保护(因为计算重复和多数表决将由硬件直接管理,而不是由软件管理——因为程序只是存储在内存中的位……)。
首先,围绕失败设计应用程序。确保作为正常流程操作的一部分,它需要重置(取决于您的应用程序和软或硬故障类型)。这很难做到完美:需要某种程度的事务性的关键操作可能需要在组装级别进行检查和调整,以便关键点的中断不会导致不一致的外部命令。一旦检测到任何不可恢复的内存损坏或控制流偏差,就立即失败。如果可能,记录故障。
第二,如果可能,纠正腐败并继续下去。这意味着经常检查和修复常量表(如果可以的话,还包括程序代码);可能在每个主要操作之前或在定时中断上,并将变量存储在自动校正的结构中(同样在每个主要运算之前或在计时中断上,从3中获得多数票,如果是单个偏差,则进行校正)。如果可能,记录更正。
第三,测试失败。设置一个可重复的测试环境,随机翻转内存中的位。这将允许您复制损坏情况,并帮助围绕它们设计应用程序。
我真的读了很多很棒的答案!
这是我的2美分:通过编写软件检查内存或执行频繁的寄存器比较,建立内存/寄存器异常的统计模型。此外,以虚拟机的形式创建一个仿真器,您可以在其中试验该问题。我想,如果你改变结尺寸、时钟频率、供应商、外壳等,你会观察到不同的行为。
即使我们的台式电脑内存也有一定的故障率,但这不会影响日常工作。
使用C语言编写在这种环境中表现稳健的程序是可能的,但前提是大多数形式的编译器优化都被禁用。优化编译器旨在用“更高效”的编码模式替换许多看似冗余的编码模式,并且可能不知道当编译器知道x不可能保持任何其他值时,程序员测试x==42的原因是因为程序员想要阻止执行某些代码,而x保持某个其他值——即使在这样的情况下,它保持该值的唯一方法是系统接收到某种电气故障。
将变量声明为易失性通常很有用,但可能不是万能药。特别重要的是,注意安全编码通常需要操作具有需要多个步骤来激活的硬件联锁,并且使用以下模式编写代码:
... code that checks system state
if (system_state_favors_activation)
{
prepare_for_activation();
... code that checks system state again
if (system_state_is_valid)
{
if (system_state_favors_activation)
trigger_activation();
}
else
perform_safety_shutdown_and_restart();
}
cancel_preparations();
如果编译器以相对文字的方式翻译代码,并且如果全部在prepare_for_activation()之后重复对系统状态的检查,系统可以对几乎任何可能的单一故障事件具有鲁棒性,甚至那些会任意破坏程序计数器和堆栈的程序。如果在调用prepare_for_activation()之后发生了一个小故障,这意味着激活是合适的(因为没有其他原因prepare_for_activation()将在故障发生之前被调用)。如果故障导致代码不正确地到达prepare_for_activation(),但如果没有后续故障事件,则代码将无法在未通过验证检查或先调用cancel_preparies的情况下到达trigger_activation()[如果堆栈出现问题,则在调用prepare_for_activation()的上下文返回后,执行可能会继续到trigger_active()之前的某个位置,但调用cancel_preparations(从而使后者的调用无害。
这样的代码在传统的C语言中可能是安全的,但在现代的C编译器中却不安全。这种编译器在这种环境中可能非常危险,因为它们努力只包含通过某种定义良好的机制可能出现的情况下相关的代码,并且其结果也将得到很好的定义。在某些情况下,旨在检测和清理故障的代码可能会使情况变得更糟。如果编译器确定尝试的恢复在某些情况下会调用未定义的行为,则可能推断在这种情况下不可能出现需要恢复的条件,从而消除了检查这些条件的代码。
以下是一些想法和想法:
更创造性地使用ROM。
在ROM中存储任何可以存储的东西。不要计算东西,而是将查找表存储在ROM中。(确保编译器将查找表输出到只读部分!在运行时打印内存地址以进行检查!)将中断向量表存储在RAM中。当然,运行一些测试以查看ROM与RAM相比的可靠性。
为堆栈使用最好的RAM。
堆栈中的SEU可能是最可能的崩溃源,因为它是索引变量、状态变量、返回地址和各种类型的指针通常存在的地方。
执行计时器滴答声和看门狗计时器例程。
您可以在每一次计时器计时时运行一个“健全性检查”例程,以及一个看门狗例程来处理系统锁定。您的主代码还可以周期性地增加一个计数器以指示进度,而健全性检查例程可以确保发生了这种情况。
在软件中执行纠错代码。
您可以为数据添加冗余,以便能够检测和/或纠正错误。这将增加处理时间,可能会使处理器长时间暴露在辐射中,从而增加出错的机会,因此您必须考虑权衡。
记住缓存。
检查CPU缓存的大小。您最近访问或修改的数据可能位于缓存中。我相信您可以禁用至少一些缓存(以较大的性能代价);你应该试试看缓存对SEU的敏感性。如果缓存比RAM更硬,那么您可以定期读取和重新写入关键数据,以确保它保留在缓存中并使RAM恢复正常。
巧妙地使用页面错误处理程序。
如果将内存页标记为不存在,CPU将在您尝试访问它时发出页面错误。您可以创建一个页面错误处理程序,在处理读取请求之前进行一些检查。(PC操作系统使用此功能透明地加载已交换到磁盘的页面。)
对关键的事情使用汇编语言(可能是所有事情)。
使用汇编语言,您知道寄存器中的内容和RAM中的内容;你知道CPU使用的是什么特殊的RAM表,你可以用迂回的方式来设计,以降低风险。
使用objdump实际查看生成的汇编语言,并计算每个例程占用的代码量。
如果你使用的是像Linux这样的大型操作系统,那么你就是在自找麻烦;有太多的复杂性和太多的事情要出错。
记住这是一场概率游戏。
一位评论者说
你为捕捉错误而编写的每一个例程都会因同样的原因而失败。
虽然这是真的,但检查例程正确运行所需的(例如)100字节代码和数据中发生错误的机会要比其他地方发生错误的几率小得多。如果你的ROM非常可靠,并且几乎所有的代码/数据都在ROM中,那么你的可能性就更大了。
使用冗余硬件。
使用具有相同代码的两个或更多相同硬件设置。如果结果不同,应触发重置。对于3个或更多设备,您可以使用“投票”系统来尝试确定哪一个已被破坏。