我们正在编译一个嵌入式C++应用程序,它部署在电离辐射轰击的环境中的屏蔽设备中。我们正在为ARM使用GCC和交叉编译。在部署时,我们的应用程序会生成一些错误的数据,并且比我们希望的更频繁地崩溃。硬件是为这个环境而设计的,我们的应用程序已经在这个平台上运行了几年。
我们是否可以对代码进行更改,或对编译时进行改进,以识别/纠正由单个事件扰乱导致的软错误和内存损坏?是否有其他开发人员成功地减少了软错误对长时间运行的应用程序的有害影响?
我们正在编译一个嵌入式C++应用程序,它部署在电离辐射轰击的环境中的屏蔽设备中。我们正在为ARM使用GCC和交叉编译。在部署时,我们的应用程序会生成一些错误的数据,并且比我们希望的更频繁地崩溃。硬件是为这个环境而设计的,我们的应用程序已经在这个平台上运行了几年。
我们是否可以对代码进行更改,或对编译时进行改进,以识别/纠正由单个事件扰乱导致的软错误和内存损坏?是否有其他开发人员成功地减少了软错误对长时间运行的应用程序的有害影响?
当前回答
也许了解一下硬件“为这种环境而设计”意味着什么会有所帮助。它如何纠正和/或指示SEU错误的存在?
在一个与空间探索相关的项目中,我们有一个自定义MCU,它会在SEU错误时引发异常/中断,但会有一些延迟,即在导致SEU异常的insn之后可能会通过一些循环/执行一些指令。
数据缓存尤其容易受到攻击,因此处理程序会使有问题的缓存行无效并重新启动程序。只是,由于异常的不精确性,以引发异常的insn为首的insn序列可能无法重新启动。
我们确定了危险的(不可重启的)序列(如lw$3,0x0($2),然后是insn,它修改了$2,数据不依赖于$3),我对GCC进行了修改,所以这样的序列不会发生(例如,作为最后的手段,用nop分隔两个insn)。
只是需要考虑的事情。。。
其他回答
既然您专门要求软件解决方案,而且您使用的是C++,为什么不使用运算符重载来创建自己的安全数据类型呢?例如:
不要使用uint32_t(以及double、int64_t等),而是制作自己的SAFE_uint32-t,其中包含uint32/t的倍数(最小值为3)。重载您想要执行的所有操作(*+-/<<>>==!=等),并使重载的操作对每个内部值独立执行,即不要执行一次并复制结果。在之前和之后,检查所有内部值是否匹配。如果值不匹配,可以将错误的值更新为最常见的值。如果没有最常见的值,您可以安全地通知存在错误。
这样,即使ALU、寄存器、RAM或总线上发生损坏也无所谓,您仍然可以多次尝试并很好地捕获错误。然而,请注意,这只适用于您可以替换的变量-例如,堆栈指针仍然是易受影响的。
附带故事:我遇到了一个类似的问题,也是在一个旧的ARM芯片上。结果发现,这是一个使用旧版本GCC的工具链,与我们使用的特定芯片一起,在某些边缘情况下触发了一个错误,这会(有时)破坏传递到函数中的值。在将设备归咎于无线电活动之前,确保设备没有任何问题,是的,有时是编译器错误=)
使用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编译器中却不安全。这种编译器在这种环境中可能非常危险,因为它们努力只包含通过某种定义良好的机制可能出现的情况下相关的代码,并且其结果也将得到很好的定义。在某些情况下,旨在检测和清理故障的代码可能会使情况变得更糟。如果编译器确定尝试的恢复在某些情况下会调用未定义的行为,则可能推断在这种情况下不可能出现需要恢复的条件,从而消除了检查这些条件的代码。
这个答案假设你关心的是一个工作正常的系统,而不是一个成本最低或速度快的系统;大多数玩放射性物品的人都看重正确性/安全性而不是速度/成本
有几个人建议您可以进行硬件更改(很好,答案中已经有很多好东西,我不打算重复所有内容),还有一些人建议冗余(原则上很好),但我认为没有人建议冗余在实践中如何工作。你怎么会失败?你怎么知道什么时候出了问题?许多技术都是在一切都会成功的基础上工作的,因此失败是一件棘手的事情。然而,一些为规模而设计的分布式计算技术预计会出现故障(毕竟,规模足够大,多个节点中的一个节点的故障是不可避免的,单个节点的平均无故障时间为MTBF);你可以利用它来保护你的环境。
以下是一些想法:
确保整个硬件复制n次(其中n大于2,最好是奇数),并且每个硬件元素可以与其他硬件元素通信。以太网是实现这一点的一种明显方式,但还有许多其他更简单的路由可以提供更好的保护(例如CAN)。尽量减少常见组件(甚至电源)。例如,这可能意味着在多个地方对ADC输入进行采样。确保应用程序状态在一个地方,例如在有限状态机中。这可以完全基于RAM,但并不排除稳定的存储。因此,它将存储在几个地方。对状态变化采用仲裁协议。例如,请参见RAFT。当您在C++中工作时,有一些众所周知的库可以实现这一点。只有当大多数节点同意时,才能对FSM进行更改。为协议堆栈和仲裁协议使用一个已知的好库,而不是自己滚动一个,否则当仲裁协议挂断时,您在冗余方面的所有好工作都将被浪费。确保您对FSM进行校验和(例如,CRC/SHA),并将CRC/CHA存储在FSM本身中(以及在消息中传输,并对消息本身进行校验和)。让节点定期对照这些校验和、传入消息的校验和检查其FSM,并检查其校验和是否与仲裁的校验和匹配。在系统中构建尽可能多的其他内部检查,使检测到自身故障的节点重新启动(这比在有足够节点的情况下继续半工作要好)。尝试让他们在重新启动过程中彻底退出仲裁,以防他们再次出现。在重新启动时,让他们检查软件映像(以及他们加载的任何其他内容),并在重新引入仲裁之前进行完整的RAM测试。使用硬件支持您,但要小心操作。例如,您可以获取ECC RAM,并定期对其进行读/写,以纠正ECC错误(如果错误无法纠正,则会死机)。然而(从内存来看)静态RAM比DRAM更能耐受电离辐射,因此最好使用静态DRAM。请参见“我不会做的事情”下的第一点。
假设您在一天内任何给定节点都有1%的失败机会,假设您可以使失败完全独立。如果有5个节点,一天内需要3个节点失败,这是0.00001%的概率。有了更多,你就明白了。
我不会做的事情:
低估了一开始没有问题的价值。除非重量是一个问题,否则你的设备周围的一大块金属将是一个比程序员团队所能想到的更便宜、更可靠的解决方案。同样,EMI输入的光学耦合也是一个问题,等等。无论怎样,在采购部件时,都要尽量选择那些抗电离辐射性能最好的部件。使用自己的算法。人们以前也做过这种事。利用他们的工作。容错和分布式算法很难。尽可能利用他人的工作。使用复杂的编译器设置,天真地希望您检测到更多失败。如果你运气好,你可能会发现更多的失败。更有可能的是,您将在编译器中使用一个测试较少的代码路径,特别是如果您自己滚动的话。使用在您的环境中未经测试的技术。大多数编写高可用性软件的人必须模拟故障模式,以检查其HA是否正常工作,并因此错过了许多故障模式。你处于“幸运”的境地,经常按需出现故障。因此,测试每种技术,并确保其应用程序实际提高MTBF的数量超过引入它的复杂性(复杂性带来了bug)。特别是将此应用于我的建议重新仲裁算法等。
免责声明:我不是放射性专业人员,也不是这类应用的工作人员。但我致力于关键数据的长期归档的软错误和冗余,这有点联系(相同的问题,不同的目标)。
在我看来,放射性的主要问题是放射性可以切换位,因此放射性可以/将篡改任何数字存储器。这些错误通常被称为软错误、比特腐烂等。
问题是:当你的内存不可靠时,如何可靠地计算?
要显著降低软错误率(以计算开销为代价,因为大多数情况下都是基于软件的解决方案),您可以:
依靠好的旧冗余方案,更具体地说,是更有效的纠错码(目的相同,但算法更聪明,这样可以用更少的冗余恢复更多的比特)。这有时(错误地)也称为校验和。使用这种解决方案,您必须随时将程序的完整状态存储在主变量/类(或结构?)中,计算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之间自动复制计算,并在最后以机械方式进行多数表决(例如,使用“与/或”门)。这通常在飞机和任务关键设备中实现(参见三模块冗余)。这样,你就不会有任何计算开销(因为额外的计算将并行进行),并且你有另一层防止软错误的保护(因为计算重复和多数表决将由硬件直接管理,而不是由软件管理——因为程序只是存储在内存中的位……)。
为放射性环境编写代码实际上与为任何任务关键型应用程序编写代码没有什么不同。
除了已经提到的内容外,还有一些杂项提示:
使用任何半专业嵌入式系统都应具备的日常“面包和黄油”安全措施:内部看门狗、内部低电压检测、内部时钟监视器。这些事情在2016年甚至不需要提及,它们几乎是每个现代微控制器的标准。如果您有一个面向安全和/或汽车的MCU,它将具有某些看门狗功能,例如给定的时间窗口,您需要在其中刷新看门狗。如果您有任务关键型实时系统,则首选此选项。一般来说,使用适用于这类系统的MCU,而不是在一包玉米片中收到的普通主流绒毛。现在几乎每个MCU制造商都有专门为安全应用设计的MCU(TI、Freescale、Renesas、ST、Infineon等)。它们有很多内置的安全功能,包括锁步内核:这意味着有两个CPU内核执行相同的代码,它们必须彼此一致。重要事项:您必须确保内部MCU寄存器的完整性。硬件外设的所有可写控制和状态寄存器可能位于RAM内存中,因此易受攻击。为了保护自己免受寄存器损坏,最好选择具有内置寄存器“一次写入”功能的微控制器。此外,您需要在NVM中存储所有硬件寄存器的默认值,并定期将这些值复制到寄存器中。您可以以同样的方式确保重要变量的完整性。注意:始终使用防御性编程。这意味着您必须在MCU中设置所有寄存器,而不仅仅是应用程序使用的寄存器。你不希望一些随机的硬件外设突然醒来。有各种各样的方法来检查RAM或NVM中的错误:校验和、“行走模式”、软件ECC等。现在最好的解决方案是不使用任何这些,而是使用内置ECC和类似检查的MCU。因为在软件中这样做很复杂,因此错误检查本身可能会引入错误和意外问题。使用冗余。您可以将易失性和非易失性内存存储在两个相同的“镜像”段中,这两个段必须始终相等。每个段可以附加CRC校验和。避免使用MCU外部的外部存储器。为所有可能的中断/异常实现默认中断服务例程/默认异常处理程序。即使是你不使用的。默认例程除了关闭自己的中断源之外,不应该做任何事情。理解并接受防御性编程的概念。这意味着您的程序需要处理所有可能的情况,即使是理论上无法发生的情况。示例。高质量的任务关键型固件检测到尽可能多的错误,然后以安全的方式处理或忽略它们。不要编写依赖于指定不良行为的程序。这种行为可能会因辐射或EMI引起的意外硬件变化而发生剧烈变化。确保您的程序没有此类垃圾的最佳方法是使用像MISRA这样的编码标准,以及静态分析器工具。这也有助于防御编程和消除bug(为什么您不想在任何类型的应用程序中检测bug?)。重要提示:不要依赖静态存储持续时间变量的默认值。也就是说,不要信任.data或.bss的默认内容。从初始化点到实际使用变量的点之间可能有任何时间,RAM可能有足够的时间损坏。相反,编写程序,以便在运行时从NVM中设置所有此类变量,就在首次使用此类变量之前。在实践中,这意味着如果变量在文件范围内声明或声明为静态,则永远不应该使用=来初始化它(或者可以,但这是没有意义的,因为无论如何都不能依赖于值)。始终在运行时设置,就在使用之前。如果可以从NVM中重复更新这些变量,那么就这样做。类似地,在C++中,对于静态存储持续时间变量,不要依赖构造函数。让构造函数调用公共的“设置”例程,您也可以稍后在运行时直接从调用方应用程序调用该例程。如果可能的话,请完全删除初始化.data和.bss(并调用C++构造函数)的“向下复制”启动代码,这样在编写依赖于这些的代码时就会出现链接器错误。许多编译器都可以选择跳过这一步,通常称为“最小/快速启动”或类似操作。这意味着必须检查任何外部库,以便它们不包含任何此类依赖。实现并定义程序的安全状态,以便在发生严重错误时恢复到该状态。实施错误报告/错误日志系统总是有帮助的。