关于C++:编译应用于高辐射环境中的应用程序

Compiling an application for use in highly radioactive environments

我们正在编译一个嵌入式C/C++应用程序,它被部署在屏蔽设备中,在电离辐射轰击的环境中。我们正在使用gcc和arm的交叉编译。部署时,我们的应用程序会生成一些错误的数据,并比我们希望的更频繁地崩溃。硬件是为这个环境设计的,我们的应用程序已经在这个平台上运行了几年。

我们是否可以对代码进行更改,或者对编译时进行改进,以识别/纠正由单个事件混乱导致的软错误和内存损坏?是否有其他开发人员成功地减少了软错误对长时间运行的应用程序的有害影响?


在小型卫星的软件/固件开发和环境测试方面工作了4-5年,我想在此分享我的经验。好的。

*(小型化卫星比大型卫星更容易发生单事件混乱,因为其电子元件的尺寸相对较小且有限)好的。

To be very concise and direct: there is no mechanism to recover from detectable, erroneous
situation by the software/firmware itself without, at least, one
copy of minimum working version of the software/firmware somewhere for recovery purpose - and with the hardware supporting the recovery (functional).

Ok.

现在,这种情况通常在硬件和软件级别处理。在这里,按照您的要求,我将分享我们在软件级别所能做的。好的。

  • …恢复目的….提供在真实环境中更新/重新编译/刷新软件/固件的能力。对于高度电离环境中的任何软件/固件,这几乎是必须具备的功能。如果不这样做,您可以拥有尽可能多的冗余软件/硬件,但在某一点上,它们都将爆炸。所以,准备这个特性!好的。

  • …最低工作版本…在代码中具有响应性、多个副本、最低版本的软件/固件。这就像Windows中的安全模式。与其只有一个软件的完整功能版本,不如多个软件/固件最低版本的副本。最小副本的大小通常比完整副本小得多,并且几乎总是只有以下两个或三个功能:好的。

  • 能够听取外部系统的指令,
  • 能够更新当前软件/固件,
  • 能够监控基本操作的内务数据。
  • …复制…某处…在某处有冗余软件/固件。好的。

  • 无论是否有冗余硬件,您都可以尝试在ARM UC中使用冗余软件/固件。这通常是通过将两个或多个相同的软件/固件放在不同的地址中来完成的,这些地址相互发送心跳信号——但一次只有一个处于活动状态。如果已知一个或多个软件/固件没有响应,请切换到其他软件/固件。使用这种方法的好处是,在发生错误后,我们可以立即进行功能替换,而无需与负责检测和修复错误的任何外部系统/方进行任何接触(在卫星情况下,通常是任务控制中心(MCC))。好的。

    严格来说,如果没有冗余硬件,这样做的缺点是您实际上无法消除所有单点故障。至少,您仍然会有一个单一的故障点,即开关本身(或者通常是代码的开头)。然而,对于高度电离环境中尺寸有限的设备(如pico/femto卫星),在没有额外硬件的情况下,将单点故障减少到一点仍然值得考虑。另外,用于切换的代码段肯定要比整个程序的代码少得多,这显著降低了在其中发生单个事件的风险。好的。

  • 但是,如果您不这样做,您的外部系统中应该至少有一个副本,可以与设备联系并更新软件/固件(在卫星情况下,它还是任务控制中心)。好的。

  • 您也可以将副本保存在设备的永久内存中,然后触发该副本以恢复正在运行的系统的软件/固件。
  • …可检测到的错误情况..误差必须是可检测的,通常通过硬件误差校正/检测电路或一小段代码进行误差校正/检测。最好将此类代码设置为小代码、多代码和独立于主软件/固件。它的主要任务只是检查/纠正。如果硬件电路/固件是可靠的(例如,它比其余的更抗辐射-或具有多个电路/逻辑),那么您可以考虑使用它进行错误更正。但如果不是,最好将其作为错误检测。可以通过外部系统/设备进行校正。对于纠错,您可以考虑使用诸如Hamming/Golay23之类的基本纠错算法,因为它们在电路/软件中都可以更容易地实现。但这最终取决于你的团队的能力。对于错误检测,通常使用CRC。好的。

  • …现在支持恢复的硬件是这个问题最困难的方面。最终,恢复需要负责恢复的硬件至少能够正常工作。如果硬件永久性损坏(通常在总电离剂量达到一定水平后发生),那么(遗憾的是)软件无法帮助恢复。因此,对于暴露在高辐射水平下的设备(如卫星),硬件是最重要的。好的。

  • 除上述建议外,我还建议您:好的。

  • 子系统间通信协议中的错误检测和/或纠错算法。为了避免从其他系统接收到不完整/错误的信号,这几乎是另一个必须具备的功能。好的。

  • 过滤你的ADC读数。不要直接使用ADC读数。用中值过滤器、均值过滤器或任何其他过滤器过滤——不要相信单个读数。多取样,而不是少取样。好的。

  • 好啊。


    美国国家航空航天局有一篇关于抗辐射软件的论文。它描述了三个主要任务:

  • 定期监控内存中的错误,然后清除这些错误,
  • 强大的错误恢复机制,以及
  • 重新配置的能力,如果某物不再工作。
  • 请注意,内存扫描速率应该足够频繁,很少发生多位错误,因为大多数ECC内存可以从单位错误而不是多位错误中恢复。

    健壮的错误恢复包括控制流传输(通常在错误发生之前的某个点重新启动进程)、资源释放和数据恢复。

    他们对数据恢复的主要建议是,通过将中间数据视为临时数据来避免对它的需要,这样在错误发生之前重新启动也会将数据回滚到可靠的状态。这听起来类似于数据库中的"事务"概念。

    他们讨论了特别适合面向对象语言(如C++)的技术。例如

  • 用于连续内存对象的基于软件的ECC
  • 契约式编程:验证前提条件和后置条件,然后检查对象以验证它是否仍处于有效状态。
  • 而且,正是如此,NASA已经使用C++作为火星探测器的主要项目。

    C++ class abstraction and encapsulation enabled rapid development and testing among multiple projects and developers.

    它们避免了某些可能产生问题的C++特征:

  • 例外情况
  • 模板
  • iostream(无控制台)
  • 多重继承
  • 运算符重载(除newdelete之外)
  • 动态分配(使用专用内存池并放置new以避免系统堆损坏的可能性)。

  • 以下是一些想法和想法:

    更具创造性地使用ROM。

    把你能做的任何事情都存储在只读存储器中。不要计算,而是把查找表存储在只读存储器中。(确保你的编译器正在把查找表输出到只读区。)在运行时打印出内存地址以进行检查!)将中断向量表存储在ROM中。当然,运行一些测试,看看您的ROM与RAM相比有多可靠。

    使用您最好的RAM进行堆栈。

    堆栈中的SEU可能是崩溃的最可能来源,因为索引变量、状态变量、返回地址和各种类型的指针通常都存在。

    执行定时器计时和看门狗定时器程序。

    您可以在每个计时器的滴答声中运行"健全性检查"例程,也可以运行看门狗例程来处理系统锁定。您的主代码还可以周期性地增加一个计数器来指示进度,并且健全性检查例程可以确保已经发生了这种情况。

    在软件中执行纠错代码。

    您可以为数据添加冗余,以便能够检测和/或纠正错误。这将增加处理时间,可能使处理器暴露在辐射下的时间更长,从而增加出错的可能性,因此必须考虑权衡。

    记住缓存。

    检查CPU缓存的大小。您最近访问或修改的数据可能在缓存中。我相信您至少可以禁用一些缓存(性能成本很高);您应该试试看缓存对SEU有多敏感。如果缓存比RAM更硬,那么您可以定期读写关键数据,以确保它保持在缓存中并使RAM恢复正常。

    巧妙地使用页面错误处理程序。

    如果您将内存页标记为不存在,则当您尝试访问它时,CPU将发出一个页面错误。您可以创建一个页面错误处理程序,在处理读取请求之前进行一些检查。(PC操作系统使用它透明地加载已交换到磁盘的页面。)

    使用汇编语言处理关键的事情(可能是所有的事情)。

    使用汇编语言,您知道寄存器中是什么,RAM中是什么;您知道CPU正在使用什么特殊的RAM表,并且您可以以一种迂回的方式设计事物以降低风险。

    使用objdump来实际查看生成的汇编语言,并计算出每个例程占用的代码量。

    如果你使用的是像Linux这样的大型操作系统,那么你就是在自找麻烦;有太多的复杂性和太多的事情要出错。

    记住这是一个概率游戏。

    一位评论家说

    Every routine you write to catch errors will be subject to failing itself from the same cause.

    虽然这是真的,但是检查例程正常运行所需的(比如)100字节的代码和数据中发生错误的几率要比其他地方发生错误的几率小得多。如果你的只读存储器非常可靠,几乎所有的代码/数据都在只读存储器中,那么你的可能性就更大了。

    使用冗余硬件。

    使用具有相同代码的2个或更多相同硬件设置。如果结果不同,应触发重置。使用3个或更多的设备,您可以使用"投票"系统来尝试识别哪个设备已被破坏。


    您也可能对有关算法容错的丰富文献感兴趣。这包括旧的分配:编写一个排序,当常量比较失败时(或者,更糟糕的是,当失败比较的渐近数按log(n)n比较)进行缩放时)对输入进行正确排序。

    开始阅读的地方是黄和亚伯拉罕1984年的论文"基于算法的矩阵运算容错"。他们的想法与同态加密计算有点相似(但实际上并不相同,因为他们正尝试在操作级别进行错误检测/更正)。

    该论文最新的一个分支是Bosilca、Delmas、Dongarra和Langou的"应用于高性能计算的基于算法的容错"。


    为放射性环境编写代码与为任何关键任务应用程序编写代码没有什么不同。好的。

    除了已经提到的以外,这里还有一些其他提示:好的。

    • 使用任何半专业嵌入式系统都应具备的日常"面包和黄油"安全措施:内部看门狗、内部低压检测、内部时钟监视器。这些东西甚至不需要在2016年被提及,它们几乎是所有现代微控制器的标准。
    • 如果您有一个安全和/或面向汽车的MCU,它将具有某些看门狗功能,例如给定的时间窗口,您需要在其中刷新看门狗。如果您有一个任务关键型实时系统,这是首选。
    • 一般来说,使用适合这些系统的单片机,而不是你在一包玉米片中收到的普通主流绒毛。如今,几乎所有的微控制器制造商都有专门为安全应用(TI、Freescale、Renesas、ST、Infineon等)设计的微控制器。它们有许多内置的安全特性,包括锁步核心:意味着有两个CPU核心执行相同的代码,它们必须彼此一致。
    • 重要事项:必须确保内部MCU寄存器的完整性。可写硬件外围设备的所有控制和状态寄存器可能位于RAM内存中,因此容易受到攻击。好的。

      为了防止寄存器损坏,最好选择一个内置寄存器"只写一次"功能的微控制器。此外,您需要在NVM中存储所有硬件寄存器的默认值,并定期将这些值复制到寄存器中。您可以以相同的方式确保重要变量的完整性。好的。

      注意:总是使用防御性编程。这意味着您必须在MCU中设置所有寄存器,而不仅仅是应用程序使用的寄存器。你不希望一些随机的硬件外围设备突然醒来。好的。

    • 在RAM或NVM中有各种检查错误的方法:校验和、"行走模式"、软件ECC等。目前最好的解决方案是不要使用这些方法中的任何一种,而是使用内置ECC和类似检查的MCU。因为在软件中这样做是复杂的,错误检查本身可能会导致错误和意外的问题。好的。

    • 使用冗余。您可以将易失性和非易失性内存存储在两个相同的"镜像"段中,这两个段必须始终相等。每个段可以附加一个CRC校验和。
    • 避免在MCU外部使用外部存储器。
    • 为所有可能的中断/异常实现默认中断服务例程/默认异常处理程序。即使是你不使用的。除了关闭自己的中断源之外,默认例程什么也不做。
    • 理解并接受防御编程的概念。这意味着你的程序需要处理所有可能的情况,甚至那些理论上不能发生的情况。例子。好的。

      高质量的关键任务固件检测尽可能多的错误,然后以安全的方式忽略它们。好的。

    • 不要编写依赖于错误指定行为的程序。这种行为很可能会随着辐射或电磁干扰引起的意外硬件变化而发生剧烈变化。确保您的程序没有这种垃圾的最佳方法是使用像misra这样的编码标准和静态分析器工具。这也将有助于防御编程和消除bug(为什么您不想在任何类型的应用程序中检测bug?).
    • 重要提示:不要实现对静态存储持续时间变量默认值的任何依赖。也就是说,不要信任.data.bss的默认内容。在初始化点到实际使用变量点之间可能有足够的时间,RAM可能有足够的时间损坏。相反,编写程序,使所有这些变量都在运行时从NVM设置,就在第一次使用此类变量之前。好的。

      在实践中,这意味着,如果变量在文件范围或作为static声明,则不应使用=来初始化它(也可以,但这是毫无意义的,因为您无论如何都不能依赖该值)。总是在运行时设置,就在使用之前。如果可以从NVM中重复更新这些变量,则执行此操作。好的。

      同样,在C++中,不要依赖构造函数来实现静态存储持续时间变量。让构造函数调用一个公共的"设置"例程,稍后您还可以直接从调用方应用程序在运行时调用该例程。好的。

      如果可能的话,删除"初始化"代码,它初始化EDCOX1 2和EDCOX1×3(并且调用C++构造函数),这样,如果您依赖于这样编写代码,就会获得链接错误。许多编译器都可以选择跳过这个过程,通常称为"最小/快速启动"或类似的过程。好的。

      这意味着必须检查任何外部库,以便它们不包含任何此类依赖。好的。

    • 实现并定义程序的安全状态,以便在出现严重错误时恢复到该状态。好的。

    • 实现错误报告/错误日志系统总是有帮助的。

    好啊。


    也许可以使用C来编写在这种环境中运行稳定的程序,但前提是大多数形式的编译器优化都被禁用。优化编译器的目的是用"更有效"的代码模式替换许多看似多余的代码模式,并且可能不知道当编译器知道x不可能保存任何其他内容时,程序员测试x==42的原因是因为程序员想阻止某些COD的执行。e当x持有一些其他值时——即使在只有系统收到某种电气故障时,它才能持有该值的情况下也是如此。

    将变量声明为EDOCX1[3]通常是有用的,但可能不是万能的。特别重要的是,注意安全编码通常要求操作具有需要多个步骤激活的硬件联锁,而该代码是使用以下模式编写的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    ... 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(),但是如果没有后续的故障事件,代码将无法随后在未通过验证检查或先调用取消准备的情况下到达trigger_activation()[如果堆栈出现故障,执行可能会在调用prepare_for_activation()返回的上下文之后的trigger_activation()之前进行,但调用cancel_preparations()会在调用prepare_for_activation()trigger_activation()之间发生,从而使后者无害。

    在传统的C语言中,这样的代码可能是安全的,但在现代的C编译器中则不然。在这种环境中,这样的编译器可能非常危险,因为它们努力只包含与可能通过某种定义良好的机制产生的情况相关的代码,并且其结果也将得到很好的定义。在某些情况下,其目的是在失败后检测和清理的代码可能会使情况变得更糟。如果编译器确定尝试的恢复在某些情况下会调用未定义的行为,则可能推断在此类情况下不可能发生需要进行此类恢复的条件,从而消除可能检查它们的代码。


    这是一个非常广泛的主题。基本上,你不能真正从内存损坏中恢复过来,但你至少可以尝试立即失败。您可以使用以下几种技术:

    • 校验和常量数据。如果有任何配置数据长时间保持不变(包括已配置的硬件寄存器),请在初始化时计算其校验和并定期验证。当您看到不匹配时,是时候重新初始化或重置了。

    • 用冗余存储变量。如果你有一个重要的变量x,把它的值写在x1x2x3中,读作(x1 == x2) ? x2 : x3

    • 实施程序流监控。XOR在从主循环调用的重要函数/分支中具有唯一值的全局标志。在接近100%测试覆盖率的无辐射环境中运行程序,应该在循环结束时为您提供标志的可接受值列表。如果看到偏差,请重置。

    • 监视堆栈指针。在主循环的开头,将堆栈指针与其预期值进行比较。偏差复位。


    能帮你的是看门狗。在20世纪80年代,看门狗在工业计算中被广泛使用。当时硬件故障更为常见——另一个答案也提到了那个时期。

    看门狗是一种硬件/软件组合功能。硬件是一个简单的计数器,从一个数字(比如1023)倒计时到零。可以使用TTL或其他逻辑。

    软件的设计是这样的:一个程序监控所有基本系统的正确运行。如果此例程正确完成=发现计算机运行正常,则将计数器设置回1023。

    总体设计是在正常情况下,软件防止硬件计数器达到零。如果计数器达到零,则计数器的硬件将执行其唯一的任务并重置整个系统。从计数器的角度来看,零等于1024,计数器继续向下计数。

    这个看门狗可以确保连接的计算机在许多许多故障情况下重新启动。我必须承认,我不熟悉能够在今天的计算机上执行这种功能的硬件。到外部硬件的接口现在比以前复杂得多。

    看门狗的一个固有缺点是,在看门狗计数器达到零+重新启动时间之前,系统从故障时起不可用。虽然这一时间通常比任何外部或人为干预都要短得多,但受支持的设备将需要能够在没有计算机控制的情况下继续运行。


    这个答案假设你所关心的是一个工作正常的系统,超过或超过这个系统的最低成本或速度;大多数人玩放射性物品的价值正确性/安全性超过速度/成本。好的。

    有几个人建议对硬件进行修改(很好,答案中已经有很多好的东西,我不打算重复所有的),还有一些人建议冗余(原则上很好),但我认为没有人建议冗余在实践中如何工作。你怎么会失败呢?你怎么知道什么时候出了问题?许多技术都是在一切正常的基础上工作的,因此失败是一件棘手的事情。但是,一些针对规模设计的分布式计算技术预计会出现故障(毕竟,只要有足够的规模,多个节点中的一个节点的故障是不可避免的,对于单个节点,任何MTBF都是不可避免的);您可以为您的环境利用这一点。好的。

    以下是一些想法:好的。

    • 确保您的整个硬件都复制了n次(其中n大于2次,最好是奇数次),并且每个硬件元素都可以与其他硬件元素通信。以太网是实现这一点的一种显而易见的方法,但还有许多其他更简单的路由可以提供更好的保护(如CAN)。尽量减少常见部件(甚至电源)。例如,这可能意味着在多个地方对ADC输入进行采样。好的。

    • 确保您的应用程序状态位于单个位置,例如在有限状态机中。这可以完全基于RAM,但不排除稳定的存储。因此,它将存放在多个地方。好的。

    • 通过法定协议来改变国家。如筏板。当您在C++中工作时,这里有一些众所周知的库。只有当大多数节点同意时,才会对FSM进行更改。为协议栈和仲裁协议使用一个已知良好的库,而不是自己滚动一个,否则当仲裁协议挂起时,您在冗余方面的所有良好工作都将被浪费。好的。

    • 确保校验和(例如crc/sha)您的fsm,并将crc/sha存储在fsm本身(以及在消息中传输和校验消息本身)。让节点根据这些校验和定期检查它们的FSM,校验和传入消息,并检查它们的校验和与仲裁的校验和匹配。好的。

    • 尽可能多地在系统中构建其他内部检查,使检测到自身故障的节点重新启动(如果您有足够的节点,这比执行一半工作要好)。在重新启动时,尝试让它们干净地从仲裁中删除自己,以防它们再次出现。在重新启动时,让他们对软件映像(以及他们加载的其他任何映像)进行校验和,并在将自己重新引入仲裁之前进行完整的RAM测试。好的。

    • 使用硬件来支持您,但要小心。例如,您可以获取ECC RAM,并定期对其进行读/写以更正ECC错误(如果错误无法更正,则会出现恐慌)。但是(从内存中)静态RAM对电离辐射的耐受性远远高于DRAM,因此最好使用静态DRAM。在"我不会做的事情"下面也看到第一点。好的。

    假设在一天内任何给定节点发生故障的概率为1%,我们假设可以使故障完全独立。有了5个节点,你需要三个节点在一天内失败,这是0.00001%的机会。有了更多,你就明白了。好的。

    我不会做的事情:好的。

    • 低估没有问题的价值。除非考虑到重量问题,否则设备周围的一大块金属将是一个比程序员团队能想到的更便宜、更可靠的解决方案。同样,EMI输入的光学耦合也是一个问题,等等。不管怎样,在采购组件时,尝试采购那些最适合电离辐射的组件。好的。

    • 滚动你自己的算法。人们以前做过这种事。利用他们的工作。容错和分布式算法很难实现。尽可能利用他人的工作。好的。

    • 使用复杂的编译器设置,希望您检测到更多的失败。如果幸运的话,你可能会发现更多的故障。更可能的是,您将在编译器中使用一个测试较少的代码路径,特别是如果您自己滚动它的话。好的。

    • 使用未在您的环境中测试的技术。大多数编写高可用性软件的人必须模拟故障模式,以检查他们的HA是否正常工作,并因此错过许多故障模式。你处于"幸运"的位置,可以根据需要经常失败。因此,测试每一种技术,并确保其应用程序实际提高了MTBF的数量,超过了引入它的复杂性(复杂性带来了错误)。尤其适用于我的建议重新仲裁算法等。好的。

    好啊。


    既然你特别要求软件解决方案,而你在使用C++,为什么不使用运算符重载来创建自己的安全数据类型呢?例如:

    不要使用uint32_t(和doubleint64_t等),而是自己制作SAFE_uint32_t,它包含一个uint32的倍数(至少3个)。过载您想要的所有操作(+-/<<>>==!=etc)对每个内部值执行并使重载操作独立执行,即不要执行一次并复制结果。前后检查所有内部值是否匹配。如果值不匹配,可以将错误的值更新为最常见的值。如果没有最常见的值,可以安全地通知存在错误。

    这样,如果在ALU、寄存器、RAM或总线上发生损坏,则无需担心,您仍然会有多次尝试,并且很有可能捕获错误。但是请注意,尽管这只适用于您可以替换的变量-例如,您的堆栈指针仍然是易受影响的。

    旁白:我遇到了一个类似的问题,也在一个旧的手臂芯片上。原来是一个工具链,它使用了一个旧版本的gcc,与我们使用的特定芯片一起,在某些边缘情况下触发了一个bug,该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的作品(并通过参考文献工作)和故障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个以上的从机,在辐射环境之外有一个主机。所有I/O都通过包含投票和/或重试机制的主机。从系统必须每个都有一个硬件看门狗,并且对它们进行碰撞的调用应该被CRC等包围,以减少非自愿碰撞的可能性。碰撞应该由主机控制,因此与主机的连接丢失等于在几秒钟内重新启动。

    这个解决方案的一个优点是,您可以对主设备和从设备使用相同的API,因此冗余成为一个透明的特性。

    编辑:从评论中,我觉得有必要澄清"CRC理念"。如果您用CRC或对来自主服务器的随机数据进行摘要检查来包围缓冲区,那么从机缓冲区自己的看门狗的可能性接近于零。只有当被监视的从设备与其他设备对齐时,才从主设备发送随机数据。每次通气后立即清除随机数据和CRC/摘要。主从通气频率应大于看门狗超时的两倍。每次从主服务器发送的数据都是唯一生成的。


    有一点似乎无人提及。你说你在GCC中开发并在ARM上交叉编译。你怎么知道你没有代码来假设自由RAM、整数大小、指针大小、执行某个操作需要多长时间、系统将持续运行多长时间,或者诸如此类的事情?这是一个很常见的问题。

    答案通常是自动单元测试。编写在开发系统上运行代码的测试工具,然后在目标系统上运行相同的测试工具。寻找差异!

    同时检查嵌入式设备上的勘误表。您可能会发现"不要这样做,因为它会崩溃,所以启用编译器选项,编译器就会解决这个问题"。

    简而言之,最可能的崩溃源是代码中的错误。在你确定情况并非如此之前,不要担心更深奥的故障模式。


    你问的是一个非常复杂的话题——不容易回答。其他的答案是可以的,但是它们只涵盖了你需要做的所有事情的一小部分。

    正如评论中所看到的,不可能100%地解决硬件问题,但是使用各种技术减少或捕获这些问题的可能性很高。

    如果我是你,我会创建最高安全完整性级别(SIL-4)的软件。获取IEC 61513文件(核工业)并遵照执行。


    有人提到使用较慢的芯片来防止离子轻易地翻转比特。以类似的方式,可能使用专门的CPU/RAM,它实际使用多个位来存储单个位。因此,提供硬件容错,因为所有位都不太可能翻转。所以1=1111,但需要被击中4次才能真正翻转。(4可能是一个错误的数字,因为如果2位翻转,它就已经不明确了)。因此,如果使用8,RAM会减少8倍,访问时间也会慢一些,但数据表示更可靠。您可以在软件级别使用专门的编译器(为所有内容分配x个更多的空间)或语言实现(为以这种方式分配内容的数据结构编写包装器)来实现这一点。或具有相同逻辑结构但在固件中执行此操作的专用硬件。


    运行应用程序的多个实例如何?如果崩溃是由于随机的内存位变化造成的,那么您的一些应用程序实例很可能会通过并产生准确的结果。对于具有统计背景的人来说,计算给定的位故障概率需要多少实例才能实现您希望的最小总体错误可能相当容易。


    也许这有助于了解硬件是否意味着"为这个环境设计"。它如何纠正和/或指示SEU错误的存在?

    在一个与空间探索相关的项目中,我们有一个定制的MCU,它会在SEU错误时引发异常/中断,但会有一些延迟,即在导致SEU异常的一个INSN之后,可能会有一些周期通过/执行指令。

    特别容易受到攻击的是数据缓存,因此处理程序会使有问题的缓存线无效并重新启动程序。只有这样,由于异常的不精确性,由异常引发的insn引导的insn序列可能无法重新启动。

    我们确定了危险的(不可重启的)序列(如lw $3, 0x0($2),接着是insn,它修改$2,而不是依赖于$3的数据),我对gcc进行了修改,这样的序列就不会发生(例如,作为最后手段,用nop将两个insn分开)。

    只是考虑一下…


    如果您的硬件出现故障,那么您可以使用机械存储来恢复它。如果您的代码库很小并且有一些物理空间,那么您可以使用机械数据存储。

    Enter image description here

    材料表面不会受到辐射的影响。会有多个档位。一个机械读卡器可以在所有的齿轮上运行,并且可以灵活地上下移动。向下表示0,向上表示1。从0和1可以生成代码库。


    使用循环调度程序。这使您能够添加定期维护时间来检查关键数据的正确性。最常遇到的问题是堆栈损坏。如果您的软件是周期性的,您可以在周期之间重新初始化堆栈。不要为中断调用重用堆栈,请为每个重要的中断调用设置单独的堆栈。

    类似于看门狗的概念是期限计时器。在调用函数之前启动硬件计时器。如果函数在截止时间计时器中断之前未返回,则重新加载堆栈并重试。如果3/5次尝试后仍然失败,则需要从ROM重新加载。

    将软件分成多个部分,并将这些部分隔离开来,以使用单独的内存区域和执行时间(尤其是在控制环境中)。示例:信号采集、预处理数据、主要算法和结果实现/传输。这意味着一个部分的失败不会导致程序其余部分的失败。因此,当我们修复信号采集时,其余的任务仍在使用陈旧的数据。

    一切都需要CRC。如果在RAM外执行,即使.text也需要CRC。如果使用循环调度程序,请定期检查CRC。有些编译器(不是GCC)可以为每个部分生成CRC,有些处理器有专门的硬件来进行CRC计算,但我猜这可能超出了您的问题范围。检查CRC也会提示内存上的ECC控制器在出现问题之前修复单个位错误。


    首先,围绕失败设计应用程序。确保作为正常流程操作的一部分,它期望重置(取决于您的应用程序和故障类型,软故障或硬故障)。这很难做到完美:需要某种程度的事务性的关键操作可能需要在程序集级别进行检查和调整,以便关键点的中断不会导致不一致的外部命令。一旦检测到任何不可恢复的内存损坏或控制流偏差,就会快速失败。如果可能,记录故障。

    第二,在可能的情况下,纠正腐败并继续下去。这意味着经常校验和修复常量表(如果可以的话,还包括程序代码);可能是在每次主要操作之前或在定时中断时,将变量存储在自动更正的结构中(在每次主要操作或定时中断之前,从3中获得多数票,如果是单个偏差,则更正)。如果可能,记录更正。

    第三,测试失败。设置一个可重复的测试环境,随机翻转内存psuedo中的位。这将允许您复制损坏情况,并帮助围绕这些情况设计应用程序。


    考虑到Supercat的评论、现代编译器的倾向,以及其他一些东西,我很想回到古代,在汇编和静态内存分配中编写整个代码。对于这种绝对的可靠性,我认为装配不再会产生很大比例的成本差异。


    这里有大量的回复,但我会试着总结一下我的想法。

    某些东西崩溃或不能正常工作可能是由于你自己的错误-那么当你找到问题的时候应该很容易解决。但也有可能出现硬件故障——这很难,如果不是不可能的话,也很难整体修复。

    我建议首先尝试通过日志记录(堆栈、寄存器、函数调用)来捕获问题情况,或者将它们记录到文件中的某个位置,或者直接传输它们("噢,不,我崩溃了")。

    从这种错误情况中恢复是重新启动(如果软件仍在运行并启动)或硬件重置(例如硬件看门狗)。更容易从第一个开始。

    如果问题与硬件相关,那么日志记录应该帮助您确定哪些函数调用出现了问题,这可以让您了解哪些函数调用不起作用,哪些函数调用出现了问题。

    另外,如果代码相对复杂,"分而治之"是有意义的,这意味着你删除/禁用了一些你怀疑有问题的函数调用——通常是禁用一半代码,再启用另一半——你可以得到"可以工作"/"不能工作"之类的决定,然后你可以把注意力集中到另一半代码上。(问题所在)

    如果问题发生在一段时间之后——那么可以怀疑堆栈溢出——那么最好监视堆栈点寄存器——如果它们不断增长的话。

    如果你设法将你的代码完全最小化,直到"hello world"类型的应用程序——它仍然随机失败——那么硬件问题是可以预料的——需要"硬件升级"——也就是说发明这样的CPU/RAM/。-能够更好地承受辐射的硬件组合。

    最重要的事情可能是,如果机器完全停止/重新设置/不工作(可能是bootstap应该做的第一件事),您将如何取回日志?如果遇到问题,您将如何回家?

    如果在您的环境中也可以发送信号和接收响应-您可以尝试构建某种在线远程调试环境,但是您必须至少有通信介质工作,并且有一些处理器/某些RAM处于工作状态。远程调试指的是gdb/gdb存根(stub)方法,或者您自己实现从应用程序中获得的内容(例如,下载日志文件、下载调用堆栈、下载RAM、重新启动)


    我读了很多很好的答案!

    下面是我的2分:通过编写软件检查内存或执行频繁的寄存器比较,建立内存/寄存器异常的统计模型。此外,以虚拟机的样式创建一个模拟器,您可以在其中试验这个问题。我猜如果你改变结的尺寸,时钟频率,厂商,外壳等会观察到不同的行为。

    即使我们的台式电脑内存也有一定的故障率,但这不会影响日常工作。